Swift Digest

URI テンプレート

URI Templating

Proposal
SF-0020
Authors
Daniel Eggert
Review Manager
Tina L
Status
Accepted

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

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.TemplateString を渡して初期化します。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.VariableNameString の型安全なラッパで、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
}

textlistassociativeList という名前は 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.TemplateURL.Template.VariableNameURL.Template.Value はいずれも CustomStringConvertible に適合します。

03 今後の見通し

この提案は RFC 6570 の全レベル・全展開タイプをカバーしているため、現時点ではこの API をさらに拡張していく構想は示されていません。

なお、ここで述べたのは将来の方向性の示唆であり、その実現を約束するものではありません。今後 RFC 6570 を超える機能が必要になった場合は、別の Proposal として議論されることになります。