01 何が問題だったのか
サーバとクライアントの間では、リソースの URL をプレースホルダ付きの「テンプレート」として受け渡したいことがあります。たとえば JMAP プロトコル(RFC 8620)では、サーバがクライアントに次のような文字列を送り、クライアント側で変数を埋めて URL を組み立てます。
https://jmap.example.com/download/{accountId}/{blobId}/{name}?accept={type}
このような URL のテンプレート表現には RFC 6570 という仕様があり、{var}、{+var}、{?var}、{/var}、{var:3}、{?address*} のように、単純な値の埋め込みからクエリ文字列・パスセグメント・prefix 値・composite 値まで、4 段階のレベルにわたって表現力豊かな展開を定義しています。複数の標準仕様で採用されているにもかかわらず、Swift の URL には RFC 6570 形式のテンプレートを解析・展開する標準的な手段がなく、利用するにはアプリ側で実装するか別パッケージに頼る必要がありました。
02 どのように解決されるのか
URL に RFC 6570 形式のテンプレートを扱うための型と初期化子が追加されます。新しい API は @available(FoundationPreview 6.2, *) でガードされ、FoundationPreview 6.2 以降で利用できます。
導入されるのは次の 3 つの型です。
URL.Template: 解析済みのテンプレートを表す型URL.Template.VariableName: テンプレート中の変数名を表す型URL.Template.Value: 展開時に変数へ与える値を表す型
テンプレートを解析する
URL.Template は String を渡して初期化します。URL.init?(string:) と同様に、解析できなかった場合は nil を返します。
extension URL {
public struct Template: Sendable, Hashable {}
}
extension URL.Template {
public init?(_ template: String)
}
変数を渡して URL に展開する
テンプレートと変数の辞書を URL の初期化子に渡すと、展開された URL が得られます。展開時、テキストは NFC(Unicode Normalization Form C)に正規化されたうえで UTF-8 として、必要に応じてパーセントエンコードされます。
extension URL {
public init?(
template: URL.Template,
variables: [URL.Template.VariableName: URL.Template.Value]
)
}
実際の使い方は次のようになります。
guard
let template = URL.Template("http://www.example.com/foo{?query,number}"),
let url = URL(
template: template,
variables: [
"query": "bar baz",
"number": "234",
]
)
else { return }
この初期化子が nil を返すのは、展開後の文字列が URL.init?(string:) でも有効な URL とならない場合に限られます。たとえば http://example.com:bad%port/ のような結果になるテンプレートと変数の組み合わせでは失敗します。なお、テンプレートで参照されているすべての変数に値を与える必要はなく、値が無い変数は単に展開されません。どの変数が必須で、どこまでが許容される結果かはテンプレートを提供するサーバとクライアントの間の取り決めに従います。
変数名
URL.Template.VariableName は String の型安全なラッパで、String リテラルから直接書けるようになっています。
extension URL.Template {
public struct VariableName: Sendable, Hashable {
public init(_ key: String)
}
}
extension String {
public init(_ key: URL.Template.VariableName)
}
extension URL.Template.VariableName: CustomStringConvertible {
public var description: String
}
extension URL.Template.VariableName: ExpressibleByStringLiteral {
public init(stringLiteral value: String)
}
変数の値
URL.Template.Value は RFC 6570 が定める 3 種類の値、すなわち単一の文字列(text)、文字列の配列(list)、順序付きキー値ペア(associative list)を表現します。
extension URL.Template {
public struct Value: Sendable, Hashable {}
}
extension URL.Template.Value {
public static func text(_ text: String) -> URL.Template.Value
public static func list(_ list: some Sequence<String>) -> URL.Template.Value
public static func associativeList(
_ list: some Sequence<(key: String, value: String)>
) -> URL.Template.Value
}
text、list、associativeList という名前は RFC 6570 の用語にそろえたもので、Swift 的には string / array / orderedDictionary のほうが自然ですが、仕様書との対応関係を優先しています。
リテラルから直接 Value を作れるよう、ExpressibleBy… への適合も用意されています。
extension URL.Template.Value: ExpressibleByStringLiteral {
public init(stringLiteral value: String)
}
extension URL.Template.Value: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: String...)
}
extension URL.Template.Value: ExpressibleByDictionaryLiteral {
public init(dictionaryLiteral elements: (String, String)...)
}
これらの適合により、辞書リテラルでまとめて変数を書く際に、文字列・配列・順序付きキー値ペアを混在させて自然に渡せます。
let template = URL.Template("http://example.com/mapper{?address*}")!
let url = URL(
template: template,
variables: [
"address": ["city": "Newport Beach", "state": "CA"],
]
)
// http://example.com/mapper?city=Newport%20Beach&state=CA
URL.Template、URL.Template.VariableName、URL.Template.Value はいずれも CustomStringConvertible に適合します。
03 今後の見通し
この提案は RFC 6570 の全レベル・全展開タイプをカバーしているため、現時点ではこの API をさらに拡張していく構想は示されていません。
なお、ここで述べたのは将来の方向性の示唆であり、その実現を約束するものではありません。今後 RFC 6570 を超える機能が必要になった場合は、別の Proposal として議論されることになります。