|
| 1 | +# URI Templating |
| 2 | + |
| 3 | +* Proposal: [SF-00020](00020-uri-templating.md) |
| 4 | +* Authors: [Daniel Eggert](https://github.com/danieleggert) |
| 5 | +* Review Manager: [Tina L](https://github.com/itingliu) |
| 6 | +* Status: **Review: March 14, 2025...March 21, 2025** |
| 7 | +* Implementation: [swiftlang/swift-foundation#1198](https://github.com/swiftlang/swift-foundation/pull/1198) |
| 8 | +* Review: ([pitch](https://forums.swift.org/t/pitch-uri-templating/78030)) |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +This proposal adds support for [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) _URI templates_ to the Swift URL type. |
| 13 | + |
| 14 | +Although there are multiple levels of expansion, the core concept is that you can define a _template_ such as |
| 15 | +``` |
| 16 | +http://example.com/~{username}/ |
| 17 | +http://example.com/dictionary/{term:1}/{term} |
| 18 | +http://example.com/search{?q,lang} |
| 19 | +``` |
| 20 | + |
| 21 | +and then _expand_ these using named values (i.e. a dictionary) into a `URL`. |
| 22 | + |
| 23 | +The templating has a rich set of options for substituting various parts of URLs. [RFC 6570 section 1.2](https://datatracker.ietf.org/doc/html/rfc6570#section-1.2) lists all 4 levels of increasing complexity. |
| 24 | + |
| 25 | +## Motivation |
| 26 | + |
| 27 | +[RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) provides a simple, yet powerful way to allow for variable expansion in URLs. |
| 28 | + |
| 29 | +This provides a mechanism for a server to convey to clients how to construct URLs for specific resources. In the [RFC 8620 JMAP protocol](https://datatracker.ietf.org/doc/html/rfc8620) for example, the server sends it client a template such as |
| 30 | +``` |
| 31 | +https://jmap.example.com/download/{accountId}/{blobId}/{name}?accept={type} |
| 32 | +``` |
| 33 | +and the client can then use variable expansion to construct a URL for resources. The API contract between the server and the client defines which variables this specific template has, and which ones are optional. |
| 34 | + |
| 35 | +Since URI templates provide a powerful way to define URL patterns with placeholders, they are adopted in various standards. |
| 36 | + |
| 37 | +## Proposed solution |
| 38 | + |
| 39 | +```swift |
| 40 | +guard |
| 41 | + let template = URL.Template("http://www.example.com/foo{?query,number}"), |
| 42 | + let url = URL( |
| 43 | + template: template, |
| 44 | + variables: [ |
| 45 | + "query": "bar baz", |
| 46 | + "number": "234", |
| 47 | + ] |
| 48 | + ) |
| 49 | +else { return } |
| 50 | +``` |
| 51 | + |
| 52 | +The RFC 6570 template gets parsed as part of the `URL.Template(_:)` initializer. It will return `nil` if the passed in string is not a valid template. |
| 53 | + |
| 54 | +The `Template` can then be expanded with _variables_ to create a URL: |
| 55 | +```swift |
| 56 | +extension URL { |
| 57 | + public init?( |
| 58 | + template: URL.Template, |
| 59 | + variables: [URL.Template.VariableName: URL.Template.Value] |
| 60 | + ) |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +## Detailed design |
| 65 | + |
| 66 | +### Templates and Expansion |
| 67 | + |
| 68 | +[RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) defines 8 different kinds of expansions: |
| 69 | + * Simple String Expansion: `{var}` |
| 70 | + * Reserved Expansion: `{+var}` |
| 71 | + * Fragment Expansion: `{#var}` |
| 72 | + * Label Expansion with Dot-Prefix: `{.var}` |
| 73 | + * Path Segment Expansion: `{/var}` |
| 74 | + * Path-Style Parameter Expansion: `{;var}` |
| 75 | + * Form-Style Query Expansion: `{?var}` |
| 76 | + * Form-Style Query Continuation: `{&var}` |
| 77 | + |
| 78 | +Additionally, RFC 6570 allows for _prefix values_ and _composite values_. |
| 79 | + |
| 80 | +Prefix values allow for e.g. `{var:3}` which would result in (up to) the first 3 characters of `var`. |
| 81 | + |
| 82 | +Composite values allow `/mapper{?address*}` to e.g. expand into `/mapper?city=Newport%20Beach&state=CA`. |
| 83 | + |
| 84 | +This implementation covers all levels and expression types defined in the RFC. |
| 85 | + |
| 86 | +### API Details |
| 87 | + |
| 88 | +There are 3 new types: |
| 89 | + * `URL.Template` |
| 90 | + * `URL.Template.VariableName` |
| 91 | + * `URL.Template.Value` |
| 92 | + |
| 93 | +All new API is guarded by `@available(FoundationPreview 6.2, *)`. |
| 94 | + |
| 95 | +#### `Template` |
| 96 | + |
| 97 | +`URL.Template` represents a parsed template that can be used to create a `URL` from it by _expanding_ variables according to RFC 6570. |
| 98 | + |
| 99 | +Its sole API is its initializer: |
| 100 | +```swift |
| 101 | +extension URL { |
| 102 | + /// A template for constructing a URL from variable expansions. |
| 103 | + /// |
| 104 | + /// This is an template that can be expanded into |
| 105 | + /// a ``URL`` by calling ``URL(template:variables:)``. |
| 106 | + /// |
| 107 | + /// Templating has a rich set of options for substituting various parts of URLs. See |
| 108 | + /// [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) for |
| 109 | + /// details. |
| 110 | + public struct Template: Sendable, Hashable {} |
| 111 | +} |
| 112 | + |
| 113 | +extension URL.Template { |
| 114 | + /// Creates a new template from its text form. |
| 115 | + /// |
| 116 | + /// The template string needs to be a valid RFC 6570 template. |
| 117 | + /// |
| 118 | + /// If parsing the template fails, this will return `nil`. |
| 119 | + public init?(_ template: String) |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +It will return `nil` if the provided string can not be parsed as a valid template. |
| 124 | + |
| 125 | +#### Variables |
| 126 | + |
| 127 | +Variables are represented as a `[URL.Template.VariableName: URL.Template.Value]`. |
| 128 | + |
| 129 | +#### `VariableName` |
| 130 | + |
| 131 | +The `URL.Template.VariableName` type is a type-safe wrapper around `String` |
| 132 | +```swift |
| 133 | +extension URL.Template { |
| 134 | + /// The name of a variable used for expanding a template. |
| 135 | + public struct VariableName: Sendable, Hashable { |
| 136 | + public init(_ key: String) |
| 137 | + } |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +The following extensions and conformances make it easy to convert between `VariableName` and `String`: |
| 142 | +```swift |
| 143 | +extension String { |
| 144 | + public init(_ key: URL.Template.VariableName) |
| 145 | +} |
| 146 | + |
| 147 | +extension URL.Template.VariableName: CustomStringConvertible { |
| 148 | + public var description: String |
| 149 | +} |
| 150 | + |
| 151 | +extension URL.Template.VariableName: ExpressibleByStringLiteral { |
| 152 | + public init(stringLiteral value: String) |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +#### `Value` |
| 157 | + |
| 158 | +The `URL.Template.Value` type can represent the 3 different kinds of values that RFC 6570 supports: |
| 159 | +```swift |
| 160 | +extension URL.Template { |
| 161 | + /// The value of a variable used for expanding a template. |
| 162 | + /// |
| 163 | + /// A ``Value`` can be one of 3 kinds: |
| 164 | + /// 1. "text": a single `String` |
| 165 | + /// 2. "list": an array of `String` |
| 166 | + /// 3. "associative list": an ordered array of key-value pairs of `String` (similar to `Dictionary`, but ordered). |
| 167 | + public struct Value: Sendable, Hashable {} |
| 168 | +} |
| 169 | + |
| 170 | +extension URL.Template.Value { |
| 171 | + /// A text value to be used with a ``URL.Template``. |
| 172 | + public static func text(_ text: String) -> URL.Template.Value |
| 173 | + |
| 174 | + /// A list value (an array of `String`s) to be used with a ``URL.Template``. |
| 175 | + public static func list(_ list: some Sequence<String>) -> URL.Template.Value |
| 176 | + |
| 177 | + /// An associative list value (ordered key-value pairs) to be used with a ``URL.Template``. |
| 178 | + public static func associativeList(_ list: some Sequence<(key: String, value: String)>) -> URL.Template.Value |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +To make it easier to use hard-coded values, the following `ExpressibleBy…` conformances are provided: |
| 183 | +```swift |
| 184 | +extension URL.Template.Value: ExpressibleByStringLiteral { |
| 185 | + public init(stringLiteral value: String) |
| 186 | +} |
| 187 | + |
| 188 | +extension URL.Template.Value: ExpressibleByArrayLiteral { |
| 189 | + public init(arrayLiteral elements: String...) |
| 190 | +} |
| 191 | + |
| 192 | +extension URL.Template.Value: ExpressibleByDictionaryLiteral { |
| 193 | + public init(dictionaryLiteral elements: (String, String)...) |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +#### Expansion to `URL` |
| 198 | + |
| 199 | +Finally, `URL.Template` has this factory method: |
| 200 | +```swift |
| 201 | +extension URL { |
| 202 | + /// Creates a new `URL` by expanding the RFC 6570 template and variables. |
| 203 | + /// |
| 204 | + /// This will fail if variable expansion does not produce a valid, |
| 205 | + /// well-formed URL. |
| 206 | + /// |
| 207 | + /// All text will be converted to NFC (Unicode Normalization Form C) and UTF-8 |
| 208 | + /// before being percent-encoded if needed. |
| 209 | + /// |
| 210 | + /// - Parameters: |
| 211 | + /// - template: The RFC 6570 template to be expanded. |
| 212 | + /// - variables: Variables to expand in the template. |
| 213 | + public init?( |
| 214 | + template: URL.Template, |
| 215 | + variables: [URL.Template.VariableName: URL.Template.Value] |
| 216 | + ) |
| 217 | +} |
| 218 | +``` |
| 219 | + |
| 220 | +This will only fail (return `nil`) if `URL.init?(string:)` fails. |
| 221 | + |
| 222 | +It may seem counterintuitive when and how this could fail, but a string such as `http://example.com:bad%port/` would cause `URL.init?(string:)` to fail, and URI Templates do not provide a way to prevent this. It is also worth noting that it is valid to not provide values for all variables in the template. Expansion will still succeed, generating a string. If this string is a valid URL, depends on the exact details of the template. Determining which variables exist in a template, which are required for expansion, and whether the resulting URL is valid is part of the API contract between the server providing the template and the client generating the URL. |
| 223 | + |
| 224 | +Additionally, the new types `URL.Template`, `URL.Template.VariableName`, and `URL.Template.Value` all conform to `CustomStringConvertible`. |
| 225 | + |
| 226 | +### Unicode |
| 227 | + |
| 228 | +The _expansion_ that happens as part of calling `URL(template:variables:)` will |
| 229 | + * convert text to NFC (Unicode Normalization Form C) |
| 230 | + * convert text to UTF-8 before being percent-encoded (if needed). |
| 231 | + |
| 232 | +as per [RFC 6570 section 1.6](https://datatracker.ietf.org/doc/html/rfc6570#section-1.6). |
| 233 | + |
| 234 | +## Source compatibility |
| 235 | + |
| 236 | +These changes are additive only and are not expected to have an impact on source compatibility. |
| 237 | + |
| 238 | +## Implications on adoption |
| 239 | + |
| 240 | +This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source compatibility. |
| 241 | + |
| 242 | +## Future directions |
| 243 | + |
| 244 | +Since this proposal covers all of RFC 6570, the current expectation is for it to not be extended further. |
| 245 | + |
| 246 | +## Alternatives considered |
| 247 | + |
| 248 | +Instead of `URL.init?(template:variables)`, the API could have used a method on `URL.Template`, e.g. `URL.Template.makeURL(variables:)`. The `URL.init?` approach would be less discoverable. There was some feedback to the initial pitch, though, that preferred the `URL.init?` method which aligns with the existing `URL.init?(string:)` initializer. This initializer approach aligns more with existing `URL.init?(string:)` and `String.init(format:)`. |
| 249 | + |
| 250 | +Additionally, the API _could_ expose a (non-failing!) `URL.Template.expand(variables:)` (or other naming) that returns a `String`. But since the purpose is very clearly to create URLs, it feels like that would just add noise. |
| 251 | + |
| 252 | +Using a DSL (domain-specific language) for `URL.Template` could improve type safety. However, because servers typically send templates as strings for client-side processing and request generation, the added complexity of a DSL outweighs its benefits. The proposed implementation is string-based (_stringly typed_) because that is what the RFC 6570 mandates. |
| 253 | + |
| 254 | +There was a lot of interest during the pitch to have an API that lends itself to _routing_, providing a way to go back-and-forth between a route and its variables. But that’s a very different use case than the RFC 6570 templates provide, and it would be better suited to have a web server routing specific API, either in Foundation or in a web server specific package. [pointfreeco/swift-url-routing](https://github.com/pointfreeco/swift-url-routing) is one such example. |
| 255 | + |
| 256 | +Instead of using the `text`, `list` and `associativeList` names (which are _terms of art_ in RFC 6570), the names `string`, `array`, and `orderedDictioary` would align better with normal Swift naming conventions. The proposal favors the _terms of art_, but there was some interest in using changing this. |
0 commit comments