基于解析器组合子,提供类型安全的 URL 模式匹配,无需正则表达式,避免参数类型不匹配。
示例
let format: URLFormat = ""/.users/.string/.repos/?.filter(.string)&.page(.int)
let url = URLComponents(string: "/users/apple/repos/?filter=swift&page=2")!
let request = URLRequestComponents(urlComponents: url)
let parameters = try format.parse(request)
_ = flatten(parameters) // ("apple", "swift", 2)
try format.print(parameters) // "users/apple/repos?filter=swift&page=2"
try format.template(parameters) // "users/:String/repos?filter=:String&page=:Int"
此库基于 CommonParsers,后者为解析器组合子提供了一个通用基础,并深受来自 pointfreeco 的 swift-parser-printer 的启发。 如果你想了解更多关于 解析器组合子以及在日常 iOS 开发中应用函数式概念的信息,请查看他们的博客。
URLFormat 用于 SwiftNIOMock 中以实现 URL 路由器。
要在 Vapor 中使用 URLFormat,请使用专用的 "vapor" 分支(阅读更多)
另外,请查看 Interplate,它为使用解析器组合子和字符串插值的字符串模板提供了基础。
URLFormat 是一个 URL 构建器,允许你以自然的方式描述 URL,并以类型安全的方式进行模式匹配。
表示 URL 模式的传统方式,例如用于 Web 服务器 API 路由,是使用某种字符串占位符表示参数,例如 /user/:name
。 然后对其进行解析,并将路径和查询参数聚合到一个集合中。 问题是这种方法容易出错(如果错过了 :
怎么办),并且对参数的访问不是类型安全的 - 可能会以错误的类型访问参数,或者必须由客户端实现转换,并且可能通过错误的键或索引访问参数。
Swift 允许的另一种方法是使用枚举模式匹配,如 这篇文章所述,并在 URLPatterns 中实现。 虽然这种方法允许对参数进行类型安全的访问,但它不是很符合人体工程学,并且不便于阅读
if case .n4("user", let userId, "profile", _) ~= url.countedPathElements() { ... }
这种方法的另一个缺点是它只允许提取相同类型的参数,因此大多数情况下,你会将它们全部提取为 String
并转换为其他类型
case chat(room: String, membersCount: Int)
case .n3("chat", let room, let membersCount):
self = .chat(room: room, membersCount: number) // Cannot convert value of type 'String' to expected argument type 'Int'
在 Vapor 中,路由被定义为路径组件的集合
router.get("users", String.parameter) { req in
let name = try req.parameters.next(String.self)
return "User #\(name)"
}
你也可以使用字符串占位符表示参数
router.get("users", ":name") { request in
guard let userName = request.parameters["name"]?.string else {
throw Abort.badRequest
}
return "You requested User #\(userName)"
}
这种方式更易于编写和阅读,但它的类型安全性更差 - 必须按照它们在路径中出现的顺序获取参数,并且它们的类型应该匹配,但编译器不会确保这一点,你需要确保模式定义和参数访问始终保持同步。
你也不能在路由中描述查询参数,而是通过 request.data["key"]?.string
或 request.query?["key"]?.stirng
在路由处理程序中访问它们,这也是不类型安全的。
使用 URLFormat,你可以如下描述 URL
let urlFormat: URLFormat = ""/.users/.string/.repos/?.filter(.string)&.page(.int)
let url = URLComponents(string: "/users/apple/repos/?filter=swift&page=2")!
let request = URLRequestComponents(urlComponents: url)
let parameters = urlFormat.parse(request)
print(flatten(parameters)) // ("apple", "swift", 2)
此模式将匹配路径类似于 /users/apple/repos/?filter=swift&page=1
的 URL (第一个和最后一个 /
是可选的)。 在这种情况下,urlFormat
的完全限定类型将是 ClosedQueryFormat<((String, String), Int)>
(大多数时候使用基类类型 URLFormat
就足够了)。 泛型参数的类型描述了所有捕获的参数的类型。 要从实际 URL 中提取它们,你可以使用 parse
方法和其中一个 flatten
函数来“扁平化”嵌套元组,例如 ((A, B), C) -> (A, B, C)
,这使得访问参数更加方便。
请注意,没有必要手动指定泛型类型参数,因为编译器可以从声明中推断出来1。 并且编译器确保模式和捕获参数的类型始终保持同步。
一个不错的优点是,如果你为 URLFormat
的参数提供值,则可以使用它来打印实际 URL 及其可读的模板(同样,编译器确保它们始终保持同步)
let parameters = parenthesize("apple", "swift", 2)
urlFormat.print(parameters) // "users/apple/repos?filter=swift&page=2"
urlFormat.template(parameters) // "users/:String/repos?filter=:String&page=:Int"
请注意,除了第一个字符串字面量之外,在声明此 URL 时没有涉及任何字符串字面量。 这是因为在底层,URLFormat
实现了 @dynamicMemberLookup
,因此像 .users
这样的表达式会转换为解析路径组件中 "users"
字符串的解析器。
你可以将第一个字符串组件留空2,或者如果你将 URLFormat 与 HTTP 请求一起使用而不仅仅是 URL,则可以使用它来指定请求的 HTTP 方法
let urlFormat: URLFormat = "GET"/.users/.string/.repos/?.filter(.string)&.page(.int)
let url = URLComponents(string: "/users/apple/repos/?filter=swift&page=2")!
let request = URLRequestComponents(method: "GET", urlComponents: url)
let parameters = urlFormat.parse(request)
urlFormat.print(parameters) // "GET users/apple/repos?filter=swift&page=2"
路径参数使用 .string
和 .int
运算符进行解析。 查询参数通过这些运算符和动态成员查找的组合进行解析,因此 .filter(.string)
将解析一个名为 "filter"
的字符串查询参数,.page(.int)
将解析一个名为 "page"
的整数查询参数。
URLFormat 还通过仅允许在正确的位置使用 /
、/?
、&
、*
和 *?
运算符来确保 URL 正确地由路径和查询组件组成。 这是通过使用 URLFormat
的不同子类来跟踪构建器状态来完成的。 它类似于使用幻像泛型类型参数,但允许仅为构建器的特定状态实现动态成员查找。
1: 这里的例外是当模式不捕获任何参数时,例如 _ = URLFormat<Prelude.Unit> = ""/.helloworld
。 Prelude.Unit
在这里是一个类型,类似于 Void
,但与 Void
不同,它是一个实际的空结构体类型。 ↩
2: 需要模式开头的字符串,因为如果没有在表达式开头显式指定类型,则无法推断静态 dynamicMemberLookup
下标调用 (有关详细信息,请参阅 此讨论) ↩
支持以下参数类型
.string
运算符的 String
.char
运算符的 Character
.int
运算符的 Int
.double
运算符的 Double
.bool
运算符的 Bool
.uuid
运算符的 UUID
.any
运算符的 Any
(与 *
不同,这只会匹配单个路径组件,*
会将所有尾随路径组件捕获到一个字符串中)lossless(MyType.self)
运算符的 LosslessStringConvertible
类型raw(MyType.self)
运算符的,且具有 String
、Character
、Int
和 Double
原始值类型的 RawRepresentable
在极少数情况下,你的 URL 路径组件与这些运算符名称冲突,你可以使用 .const
运算符将路径组件定义为字符串字面量,而不是类型化参数
/.users/.const("uuid")/.uuid
你可以通过实现 PartialIso<String, MyType>
来添加对你自己的类型的支持
import CommonParsers
extension URLPartialIso where A == String, B == MyType {
static var myType: URLPartialIso { ... }
}
extension OpenPathFormat where A == Prelude.Unit {
var myType: ClosedPathFormat<MyType> {
return ClosedPathFormat(parser %> path(.myType))
}
}
extension OpenPathFormat {
var myType: ClosedPathFormat<(A, MyType)> {
return ClosedPathFormat(parser <%> path(.myType))
}
}
这样你就可以将你的类型用作路径或查询参数
""/.users/.myType/.repos/?.filter(.myType)&.page(.int)
/
- 连接两个路径组件 /?
- 将路径与查询组件连接 &
- 连接两个查询组件 *
- 允许任何尾随路径组件 *?
- 将路径与任何尾随路径组件和查询组件连接
要在 Vapor 中使用 URLFormat,你需要从 "vapor" 分支安装它。 然后,你可以使用 import VaporURLFormat
而不是 import URLFormat
,并使用 router
方法而不是 get
、post
、put
等来注册路由。
router.route(GET/.hello/.string) { (request, string) in
print(string) // "vapor"
...
}
try app.client(.GET, "/hello/vapor")
使用 Swift 5.2,你可以直接将路由器用作函数(使用 Swift 静态可调用特性)
router(GET/.hello/.string) { (request, string) in
print(string) // "vapor"
...
}
try app.client(.GET, "/hello/vapor")
import PackageDescription
let package = Package(
dependencies: [
.package(url: "https://github.com/ilyapuchka/URLFormat.git", .branch("master")),
]
)
对于在 Vapor 中使用 URLFormat
import PackageDescription
let package = Package(
dependencies: [
.package(url: "https://github.com/ilyapuchka/URLFormat.git", .branch("vapor")),
]
)