Custom URL Schemeをstructにマッピングしたい＆どこが悪いかすぐ知りたい

# URLをstructにマッピングしたい

Custom URL SchemeをURLで受け取った場合、そのままでは使いにくいため、structなどにマッピングしたくなります。そのときの以下の要望を叶える部品が欲しくなったので作ってみました。

* 簡単かつできるだけType Safeにマッピングしたい
* URLに問題がありマッピングに失敗した場合、その原因を知りたい

# 部品を使ったマッピングと生成の例

`myscheme://host/openArticle?title=title-string&url=https://google.com/&mode=0`
を以下にマッピングする場合を考えます。

```swift
struct Article {
    var title: String
    var url: URL
}

enum Mode: Int {
    case safari = 0
    case webView = 1
}

struct OpenArticle {
    var article: Article
    var mode: Mode
}
```

以下のように`URLQueryKeys`を用意し`URLComponentsCompatible`に対応させて…

```swift
extension URLQueryKeys {
    static let title: URLQueryKey<String> = .init("title")
    static let url: URLQueryKey<URL> = .init("url")
    static let mode: URLQueryKey<Mode> = .init("mode")
}

struct Article: URLComponentsCompatible {
    var title: String
    var url: URL

    init(urlComponents: URLComponents) throws {
        title = try urlComponents.queryValue(key: .title)
        url = try urlComponents.queryValue(key: .url)
    }
}

enum Mode: Int, Codable, URLQueryValueCompatible {
    case safari = 0
    case webView = 1
}

struct OpenArticle: URLComponentsCompatible {
    var article: Article
    var mode: Mode

    init(urlComponents: URLComponents) throws {
        guard urlComponents.path == "/openArticle" else {
            throw URLComponentsCompatibleError.incompatible(description: nil)
        }
        article = try Article(urlComponents: urlComponents)
        mode = try urlComponents.queryValue(key: .mode)
    }
}
```

実際に生成するときは以下のように呼び出します。

```swift
// urlComponentsは
// myscheme://host/openArticle?title=title-string&url=https://google.com/&mode=0
do {
    try OpenArticle(urlComponents: urlComponents)
} catch {
    print(error)
}
```

例えば`url`が存在しないURLだった場合

```swift
// urlComponentsは
// myscheme://host/openArticle?title=title-string&mode=0
do {
    try OpenArticle(urlComponents: urlComponents)
} catch {
    print(error) // => queryItemNotFound(name: "url")
}
```

 `queryItemNotFound(name: "url")` が投げられてurlが足りないことがすぐにわかります。

JSONをDecodableでstructにマッピングするくらい簡単にできると一番嬉しいのですが、これでも十分に役に立つかなと思います。

上記例では一部機能しか使っていませんが、以下の処理に対応しています。

* queryの型として `Int` `Double` `Float` `Bool` `String` `Codable` `Optional`に対応
* queryの型を追加可能
* `URLQueryKey`に`(String?) throws -> Value`を渡し独自の変換処理が可能（100以下の`Int`など）
* queryが存在しない場合エラーを投げるか投げないか選択可能
* queryの文字列のフォーマットが異なる場合エラーを投げる

# マッピングのための部品のコード
省略なしのコードは[Custom URL Schemeをstructにマッピングする。 · GitHub](https://gist.github.com/ryotapoi/5c5d3bd4bc26c0473b5d85818c93f255)を参照してください。

## URLQueryItemのvalueを特定の型に変換する
URLQueryItemのvalueは `String?`型です。まず `String?`を各種型に変換します。

```swift
// URLQueryValue

public enum URLQueryValueCompatibleError: Error {
    case none // nil
    case empty // isEmpty
    case format // 期待した書式ではない
}

public protocol URLQueryValueCompatible {
    init(urlQueryValue: String?) throws
}

extension Int: URLQueryValueCompatible {
    public init(urlQueryValue: String?) throws {
        guard let urlQueryValue = urlQueryValue else { throw URLQueryValueCompatibleError.none }
        guard !urlQueryValue.isEmpty else { throw URLQueryValueCompatibleError.empty }
        guard let int = Int(urlQueryValue) else { throw URLQueryValueCompatibleError.format }
        self = int
    }
}

extension Optional: URLQueryValueCompatible where Wrapped: URLQueryValueCompatible {
    public init(urlQueryValue: String?) throws {
        do {
            self = try Wrapped(urlQueryValue: urlQueryValue)
        } catch let error as URLQueryValueCompatibleError {
            switch error {
            case .none, .empty:
                self = .none
            case .format:
                throw error
            }
        }
    }
}

extension URLQueryValueCompatible where Self: Codable {
    public init(urlQueryValue: String?) throws {
        guard let urlQueryValue = urlQueryValue else { throw URLQueryValueCompatibleError.none }
        guard !urlQueryValue.isEmpty else { throw URLQueryValueCompatibleError.empty }
        guard let urlQueryData = urlQueryValue.data(using: .utf8) else { throw URLQueryValueCompatibleError.format }
        self = try JSONDecoder().decode(Self.self, from: urlQueryData)
    }
}
```

独自の型に`URLQueryValueCompatible`を実装することもできます。

## URLQueryItemのnameと型を紐付ける
URLQueryItemの`var name: String`と実際に使用したい型を紐付ける`URLQueryKeys`を用意します。

```swift
// URLQueryKey

public class URLQueryKeys {
    public init() {}
}

public class URLQueryKey<Value>: URLQueryKeys {
    public typealias Converter = (String?) throws -> Value
    public var name: String
    public var converter: Converter
    public init(_ name: String, converter: @escaping Converter) {
        self.name = name
        self.converter = converter
        super.init()
    }
}
```

`Value`が`URLQueryValueCompatible`である場合は簡単に生成できるようにします。

```swift
extension URLQueryKey where Value: URLQueryValueCompatible {
    public convenience init(_ name: String) {
        self.init(name, converter: { source in try Value(urlQueryValue: source) })
    }
}
```

## queryの値を取り出す
おそらく一番面倒なのが`[URLQueryItem]`から値を取り出す部分です。
これをできるだけ簡単に取り出せるようにしましょう。

```swift
// URLComponents

public enum URLComponentsCompatibleError: Error {
    case notURL
    case incompatible(description: String?)
    case queryItemNotFound(name: String)
    case queryValue(name: String, error: Error)
}

extension URLComponents {
    public func queryValue<Value>(key: URLQueryKey<Value>) throws -> Value {
        if let queryItem = queryItems?.first(where: { $0.name == key.name }) {
            do {
                return try key.converter(queryItem.value)
            } catch {
                throw URLComponentsCompatibleError.queryValue(name: key.name, error: error)
            }
        } else {
            throw URLComponentsCompatibleError.queryItemNotFound(name: key.name)
        }
    }

    public func queryValue<Value>(ifContainsKey key: URLQueryKey<Value>) throws -> Value? {
        do {
            return try queryValue(key: key)
        } catch URLComponentsCompatibleError.queryItemNotFound(_) {
            return nil
        } catch {
            throw error
        }
    }
    
    public func queryValue<Value>(ifContainsKey key: URLQueryKey<Value?>) throws -> Value? {
        do {
            return try queryValue(key: key)
        } catch URLComponentsCompatibleError.queryItemNotFound(_) {
            return nil
        } catch {
            throw error
        }
    }
}
```

QueryItemが必ず必要な`key`版とと、QueryItemがなくてもいい`ifContainsKey`版があります。
またOptionalが二重にならないようにする対策で `ifContainsKey` は2種類メソッドがあります。

## URLComponentsからマッピングできる場合の目印
```swift
// URLComponentsCompatible

public protocol URLComponentsCompatible {
    init(urlComponents: URLComponents) throws
}
```

これは主にわかりやすさのためにつけます。

# おわりに
当初この記事におわりにはなかったのですが、自分で読んでも解説が終わっているのか不明だったので付け足しました。
もう少しわかりやすい記事に書き換えたいですが、とりあえず全体のコードを読めばなんとかなると思うので……どこかのタイミングでリニューアルできたらとおもいます。