{"id":16868749,"url":"https://github.com/ilyapuchka/urlformat","last_synced_at":"2025-09-24T13:31:49.054Z","repository":{"id":63912169,"uuid":"237803705","full_name":"ilyapuchka/URLFormat","owner":"ilyapuchka","description":"Type safe url pattern matching without regular expressions and arguments type mismatches based on parser combinators.","archived":false,"fork":false,"pushed_at":"2020-02-20T20:37:03.000Z","size":55,"stargazers_count":212,"open_issues_count":0,"forks_count":4,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-01-11T21:11:15.434Z","etag":null,"topics":["parser-combinators","swift","url-parsing"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ilyapuchka.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-02-02T16:55:05.000Z","updated_at":"2024-06-16T05:03:50.000Z","dependencies_parsed_at":"2023-01-14T13:16:04.958Z","dependency_job_id":null,"html_url":"https://github.com/ilyapuchka/URLFormat","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilyapuchka%2FURLFormat","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilyapuchka%2FURLFormat/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilyapuchka%2FURLFormat/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilyapuchka%2FURLFormat/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ilyapuchka","download_url":"https://codeload.github.com/ilyapuchka/URLFormat/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234086786,"owners_count":18777639,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["parser-combinators","swift","url-parsing"],"created_at":"2024-10-13T14:59:19.750Z","updated_at":"2025-09-24T13:31:48.685Z","avatar_url":"https://github.com/ilyapuchka.png","language":"Swift","readme":"# URLFormat\n\nType safe url pattern matching without regular expressions and argument type mismatches based on parser combinators.\n\nExample:\n\n```swift\nlet format: URLFormat = \"\"/.users/.string/.repos/?.filter(.string)\u0026.page(.int)\nlet url = URLComponents(string: \"/users/apple/repos/?filter=swift\u0026page=2\")!\nlet request = URLRequestComponents(urlComponents: url)\nlet parameters = try format.parse(request)\n\n_ = flatten(parameters) // (\"apple\", \"swift\", 2)\ntry format.print(parameters) // \"users/apple/repos?filter=swift\u0026page=2\"\ntry format.template(parameters) // \"users/:String/repos?filter=:String\u0026page=:Int\"\n```\n\nThis library is based on [CommonParsers](https://github.com/ilyapuchka/common-parsers) which provides a common foundation for parser combinators and is heavily inspired by [swift-parser-printer](https://github.com/pointfreeco/swift-parser-printer) from [pointfreeco](https://twitter.com/pointfreeco). If you want to learn more about [parser combinators](https://www.pointfree.co/episodes/ep62-parser-combinators-part-1) and application of functional concepts in every day iOS development check out their [blog](https://www.pointfree.co).\n\nURLFormat is used in [SwiftNIOMock](https://github.com/ilyapuchka/SwiftNIOMock) to implement URL router.\n\nTo use URLFormat with Vapor use a dedicated \"vapor\" branch ([Read more](#Vapor)) \n\nAlso checkout [Interplate](https://github.com/ilyapuchka/Interplate) which provides a foundation for string templates using parser combinators and string interpolation together.\n\n## Usage\n\nURLFormat is a URL builder that allows you to describe URL in a natural manner and allows you to pattern match it in a type safe way.\n\nThe conventional way of representing URL patterns, i.e. for web server API routes, is using some kind of string placeholder for parameters, i.e. `/user/:name`. This is then parsed, and path and query parameters are aggregated into a collection. The issue is that this approach is error-prone (what if `:` is missed) and access to the parameters is not type safe - it's possible to access parameters as a wrong type or conversion must be implemented by the client, and it's possible to access parameter by the wrong key or index.\n\nAnother approach that Swift allows is to use enums pattern matching, as described in [this post](https://alisoftware.github.io/swift/pattern-matching/2015/08/23/urls-and-pattern-matching/) and implemented in [URLPatterns](https://github.com/johnpatrickmorgan/URLPatterns). While this approach allows type-safe access to parameters it's not very ergonomic and nice to read:\n\n```swift\nif case .n4(\"user\", let userId, \"profile\", _) ~= url.countedPathElements() { ... }\n```\n\nAnother downside of this approach is that it only allows to extract parameters of the same type, so most of the time you would extract all of them as `String` and convert to other types:\n\n```swift\ncase chat(room: String, membersCount: Int)\n\ncase .n3(\"chat\", let room, let membersCount):\n  self = .chat(room: room, membersCount: number) // Cannot convert value of type 'String' to expected argument type 'Int'\n```\n\nIn Vapor routes are defined as a collection of path components:\n\n```swift\nrouter.get(\"users\", String.parameter) { req in\n    let name = try req.parameters.next(String.self)\n    return \"User #\\(name)\"\n}\n```\n\nYou can as well use string placeholders for parameters:\n\n```swift\nrouter.get(\"users\", \":name\") { request in\n    guard let userName = request.parameters[\"name\"]?.string else {\n        throw Abort.badRequest\n    }\n\n    return \"You requested User #\\(userName)\"\n}\n```\n\nThis is nicer to write and read, but it's even less type safe - the parameters must be fetched in the order they appear in the path and their types should match but the compiler won't ensure that and you would need to make sure that the pattern definition and parameter access are always in sync.\n\nYou also can't describe query parameters in the route, they are instead accessed in the route handler either via `request.data[\"key\"]?.string` or `request.query?[\"key\"]?.stirng` which is also not type safe.\n\nWith URLFormat you would describe URLs as follows:\n\n```swift\nlet urlFormat: URLFormat = \"\"/.users/.string/.repos/?.filter(.string)\u0026.page(.int)\nlet url = URLComponents(string: \"/users/apple/repos/?filter=swift\u0026page=2\")!\nlet request = URLRequestComponents(urlComponents: url)\nlet parameters = urlFormat.parse(request)\nprint(flatten(parameters)) // (\"apple\", \"swift\", 2)\n```\n\nThis pattern will match URL with path like `/users/apple/repos/?filter=swift\u0026page=1` (first and last `/` are optional). The fully qualified type of `urlFormat` in this case would be  `ClosedQueryFormat\u003c((String, String), Int)\u003e` (most of the time using base class type `URLFormat` is sufficient). The type of generic parameter describes the types of all captured parameters. To extract them from the actual URL you'd use `parse` method and one of `flatten` functions to \"flatten\" nested tuples, i.e. `((A, B), C) -\u003e (A, B, C)` which makes it more convenient to access parameters.\n\nNote that it's not necessary to specify a generic type parameter manually as the compiler can infer it from the declaration\u003csup id=\"a1\"\u003e[1](#f1)\u003c/sup\u003e. And the compiler ensures that pattern and types of captured parameters are always in sync.\n\nA nice caveat is that `URLFormat` can be used to print actual URLs and their readable templates if you provide it values for its parameters (again the compiler makes sure that they are always in sync):\n\n```swift\nlet parameters = parenthesize(\"apple\", \"swift\", 2)\nurlFormat.print(parameters) // \"users/apple/repos?filter=swift\u0026page=2\"\nurlFormat.template(parameters) // \"users/:String/repos?filter=:String\u0026page=:Int\"\n```\n\nNote that there are no string literals involved in declaring this URL except the first one. This is because under the hood `URLFormat` implements `@dynamicMemberLookup`, so an expression like `.users` is converted to the parser that parses `\"users\"` string from the path components.\n\nYou can either leave the first string component empty\u003csup id=\"a2\"\u003e[2](#f2)\u003c/sup\u003e or use it to specify the HTTP method of the request if you use URLFormat with HTTP requests and not just URLs:\n\n```swift\nlet urlFormat: URLFormat = \"GET\"/.users/.string/.repos/?.filter(.string)\u0026.page(.int)\nlet url = URLComponents(string: \"/users/apple/repos/?filter=swift\u0026page=2\")!\nlet request = URLRequestComponents(method: \"GET\", urlComponents: url)\nlet parameters = urlFormat.parse(request)\nurlFormat.print(parameters) // \"GET users/apple/repos?filter=swift\u0026page=2\"\n```\n\nPath parameters are parsed using `.string` and `.int` operators. Query parameters are parsed with a combination of these operators and dynamic member lookup, so `.filter(.string)` will parse a string query parameter named `\"filter\"`, `.page(.int)` will parse an integer query parameter named `\"page\"`.\n\nURLFormat also makes sure that URL is composed of path and query components correctly by allowing usage of `/`, `/?`, `\u0026`, `*` and `*?` operators only in the correct places. This is done by using different subclasses of `URLFormat` to keep track of the builder state. It is similar to using phantom generic type parameters but allows to implement dynamic member lookup only for specific states of the builder.\n\n\u003ca name=\"f1\"\u003e1\u003c/a\u003e: an exeption here is when pattern does not capture any parameters, i.e. `_ = URLFormat\u003cPrelude.Unit\u003e = \"\"/.helloworld` . `Prelude.Unit` here is a type, similar to `Void`, but unlike `Void` it is an actual empty struct type. [↩](#a1)\n\n\u003ca name=\"f2\"\u003e2\u003c/a\u003e: String in the beginning of the pattern is needed because static `dynamicMemberLookup` subscript calls can't be infered without explicitly specifying type in the beginning of expression (see [this discussion](https://forums.swift.org/t/static-dynamicmemberlookup/33310/5) for details) [↩](#a2)\n\n## Parameters types\n\nFollowing parameters types are supported:\n\n- `String` with `.string` operator \n- `Character` with `.char` operator\n- `Int` with `.int` operator\n- `Double` with `.double` operator\n- `Bool` with `.bool` operator\n- `UUID` with `.uuid` operator\n- `Any` with `.any` operator (unlike `*` this will match only single path component, `*` will capture all trailing path components into one string)\n- `LosslessStringConvertible` types with `lossless(MyType.self)` operator\n- `RawRepresentable` with `String`, `Character`, `Int` and `Double` raw value types with `raw(MyType.self)` operator\n\nIn rare cases where your URL path components collide with these operator names you can use a `.const` operator to define path component as a string literal and not a typed parameter:\n\n```swift\n/.users/.const(\"uuid\")/.uuid\n```\n\nYou can add support for your own types by implementing `PartialIso\u003cString, MyType\u003e`:\n\n```swift\nimport CommonParsers\n\nextension URLPartialIso where A == String, B == MyType {\n    static var myType: URLPartialIso { ... }\n}\nextension OpenPathFormat where A == Prelude.Unit {\n    var myType: ClosedPathFormat\u003cMyType\u003e {\n        return ClosedPathFormat(parser %\u003e path(.myType))\n    }\n}\nextension OpenPathFormat {\n    var myType: ClosedPathFormat\u003c(A, MyType)\u003e {\n        return ClosedPathFormat(parser \u003c%\u003e path(.myType))\n    }\n}\n```\n\nWith that you can use your type as a path or a query parameter:\n\n`\"\"/.users/.myType/.repos/?.filter(.myType)\u0026.page(.int)`\n\n## Operators\n\n`/` - concatenates two path components\n`/?` - concatenates path with a query component\n`\u0026` - concatenates two query components\n`*` - allows any trailing path components\n`*?` - concatenates path with any trailing path components and a query component\n\n## Vapor\n\nTo use URLFormat with Vapor you need to install it from the \"vapor\" branch . Then you can use `import VaporURLFormat` instead of `import URLFormat` and register routes using `router` method instead of `get`, `post`, `put` etc.:\n\n```swift\nrouter.route(GET/.hello/.string) { (request, string) in\n    print(string) // \"vapor\"\n    ...\n}\ntry app.client(.GET, \"/hello/vapor\")\n```\n\nWith Swift 5.2 you can use router as a function directly (using Swift static callable feature):\n\n```swift\nrouter(GET/.hello/.string) { (request, string) in\n    print(string) // \"vapor\"\n    ...\n}\ntry app.client(.GET, \"/hello/vapor\")\n``` \n\n## Installation\n\n```swift\nimport PackageDescription\n\nlet package = Package(\n    dependencies: [\n        .package(url: \"https://github.com/ilyapuchka/URLFormat.git\", .branch(\"master\")),\n    ]\n)\n```\n\nFor using URLFormat with Vapor:\n\n```swift\nimport PackageDescription\n\nlet package = Package(\n    dependencies: [\n        .package(url: \"https://github.com/ilyapuchka/URLFormat.git\", .branch(\"vapor\")),\n    ]\n)\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Filyapuchka%2Furlformat","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Filyapuchka%2Furlformat","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Filyapuchka%2Furlformat/lists"}