Created
August 2, 2020 09:29
Custom URL Schemeをstructにマッピングする。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
// URLQueryValue | |
public enum URLQueryValueCompatibleError: Error { | |
case none // nil | |
case empty // isEmpty | |
case format // 期待した書式ではない | |
} | |
public protocol URLQueryValueCompatible { | |
static func compatible(urlQueryValue: String?) -> Bool | |
init(urlQueryValue: String?) throws | |
} | |
extension URLQueryValueCompatible { | |
public static func compatible(urlQueryValue: String?) -> Bool { | |
if let _ = try? Self(urlQueryValue: urlQueryValue) { | |
return true | |
} else { | |
return false | |
} | |
} | |
} | |
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 Double: URLQueryValueCompatible { | |
public init(urlQueryValue: String?) throws { | |
guard let urlQueryValue = urlQueryValue else { throw URLQueryValueCompatibleError.none } | |
guard !urlQueryValue.isEmpty else { throw URLQueryValueCompatibleError.empty } | |
guard let double = Double(urlQueryValue) else { throw URLQueryValueCompatibleError.format } | |
self = double | |
} | |
} | |
extension Float: URLQueryValueCompatible { | |
public init(urlQueryValue: String?) throws { | |
guard let urlQueryValue = urlQueryValue else { throw URLQueryValueCompatibleError.none } | |
guard !urlQueryValue.isEmpty else { throw URLQueryValueCompatibleError.empty } | |
guard let float = Float(urlQueryValue) else { throw URLQueryValueCompatibleError.format } | |
self = float | |
} | |
} | |
extension Bool: URLQueryValueCompatible { | |
public init(urlQueryValue: String?) throws { | |
guard let urlQueryValue = urlQueryValue else { throw URLQueryValueCompatibleError.none } | |
guard !urlQueryValue.isEmpty else { throw URLQueryValueCompatibleError.empty } | |
guard let bool = Bool(urlQueryValue) else { throw URLQueryValueCompatibleError.format } | |
self = bool | |
} | |
} | |
extension String: URLQueryValueCompatible { | |
public init(urlQueryValue: String?) throws { | |
guard let urlQueryValue = urlQueryValue else { throw URLQueryValueCompatibleError.none } | |
self = urlQueryValue | |
} | |
} | |
extension URL: URLQueryValueCompatible { | |
public init(urlQueryValue: String?) throws { | |
guard let urlQueryValue = urlQueryValue else { throw URLQueryValueCompatibleError.none } | |
guard !urlQueryValue.isEmpty else { throw URLQueryValueCompatibleError.empty } | |
guard let url = URL(string: urlQueryValue) else { throw URLQueryValueCompatibleError.format } | |
self = url | |
} | |
} | |
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) | |
} | |
} | |
// 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() | |
} | |
} | |
extension URLQueryKey where Value: URLQueryValueCompatible { | |
public convenience init(_ name: String) { | |
self.init(name, converter: { source in try Value(urlQueryValue: source) }) | |
} | |
} | |
// 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 | |
} | |
} | |
} | |
extension URLComponents { | |
public func queryValue<Value: URLQueryValueCompatible>(key name: String) throws -> Value { | |
if let queryItem = queryItems?.first(where: { $0.name == name }) { | |
do { | |
return try Value(urlQueryValue: queryItem.value) | |
} catch { | |
throw URLComponentsCompatibleError.queryValue(name: name, error: error) | |
} | |
} else { | |
throw URLComponentsCompatibleError.queryItemNotFound(name: name) | |
} | |
} | |
public func queryValue<Value: URLQueryValueCompatible>(ifContainsKey name: String) throws -> Value? { | |
do { | |
return try queryValue(key: name) | |
} catch URLComponentsCompatibleError.queryItemNotFound(_) { | |
return nil | |
} catch { | |
throw error | |
} | |
} | |
} | |
// URLComponentsCompatible | |
public protocol URLComponentsCompatible { | |
static func compatible(urlComponents: URLComponents) -> Bool | |
init(urlComponents: URLComponents) throws | |
static func compatible(url: URL, resolvingAgainstBaseURL resolve: Bool) -> Bool | |
init(url: URL, resolvingAgainstBaseURL resolve: Bool) throws | |
} | |
extension URLComponentsCompatible { | |
public static func compatible(urlComponents: URLComponents) -> Bool { | |
if let _ = try? Self(urlComponents: urlComponents) { | |
return true | |
} else { | |
return false | |
} | |
} | |
static func compatible(url: URL, resolvingAgainstBaseURL resolve: Bool) -> Bool { | |
if let _ = try? Self(url: url, resolvingAgainstBaseURL: resolve) { | |
return true | |
} else { | |
return false | |
} | |
} | |
public init(url: URL, resolvingAgainstBaseURL resolve: Bool) throws { | |
if let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: resolve) { | |
try self.init(urlComponents: urlComponents) | |
} else { | |
throw URLComponentsCompatibleError.notURL | |
} | |
} | |
} | |
// for test | |
var positiveIntConverter: URLQueryKey<Int>.Converter = { urlQueryValue throws -> Int in | |
guard let urlQueryValue = urlQueryValue else { throw URLQueryValueCompatibleError.none } | |
guard !urlQueryValue.isEmpty else { throw URLQueryValueCompatibleError.empty } | |
guard let int = Int(urlQueryValue) else { throw URLQueryValueCompatibleError.format } | |
guard int > 0 else { throw URLQueryValueCompatibleError.format } | |
return int | |
} | |
extension URLQueryKeys { | |
static let name: URLQueryKey<String> = .init("name") | |
static let int: URLQueryKey<Int> = .init("int") | |
static let double: URLQueryKey<Double> = .init("double") | |
static let float: URLQueryKey<Float> = .init("float") | |
static let string: URLQueryKey<String> = .init("string") | |
static let url: URLQueryKey<URL> = .init("url") | |
static let mode: URLQueryKey<Mode> = .init("mode") | |
static let intOptional: URLQueryKey<Int?> = .init("intOptional") | |
static let positiveInt: URLQueryKey<Int> = .init("positiveInt", converter: positiveIntConverter) | |
} | |
struct Something: URLComponentsCompatible { | |
var name: String | |
init(urlComponents: URLComponents) throws { | |
name = try urlComponents.queryValue(key: .name) | |
} | |
} | |
enum Mode: Int, Codable, URLQueryValueCompatible { | |
case a = 0 | |
case b = 1 | |
} | |
struct Scheme: URLComponentsCompatible { | |
var int: Int | |
var double: Double | |
var float: Float | |
var string: String | |
var url: URL | |
var something: Something | |
var mode: Mode | |
var intOptional: Int? | |
var positiveInt: Int | |
init(urlComponents: URLComponents) throws { | |
int = try urlComponents.queryValue(key: .int) | |
double = try urlComponents.queryValue(key: .double) | |
float = try urlComponents.queryValue(key: .float) | |
string = try urlComponents.queryValue(key: .string) | |
url = try urlComponents.queryValue(key: .url) | |
something = try Something(urlComponents: urlComponents) | |
mode = try urlComponents.queryValue(key: .mode) | |
intOptional = try urlComponents.queryValue(ifContainsKey: "intOptional") | |
positiveInt = try urlComponents.queryValue(key: .positiveInt) | |
} | |
} | |
// URLComponentsをstructにマッピングしたい。 | |
// 特に大事なのは、何が原因でマッピングに失敗したか簡単にわかること(開発初期だと特に)。 | |
// | |
// URLQueryItemが必須/任意の区別をしたい。 | |
// また任意であっても、URLQueryItemが存在して `.value` の書式が異なる場合はエラーにしたい。 | |
// 生成 | |
do { | |
var urlComponents = URLComponents() | |
urlComponents.scheme = "myscheme" | |
urlComponents.host = "host" | |
urlComponents.path = "/path" | |
urlComponents.queryItems = [URLQueryItem(name: "int", value: "1"), | |
URLQueryItem(name: "double", value: "2.3"), | |
URLQueryItem(name: "float", value: "4.5"), | |
URLQueryItem(name: "string", value: "string value"), | |
URLQueryItem(name: "url", value: "https://google.com/"), | |
URLQueryItem(name: "name", value: "name value"), | |
URLQueryItem(name: "mode", value: "1"), | |
URLQueryItem(name: "intOptional", value: "6"), | |
URLQueryItem(name: "positiveInt", value: "1")] | |
Scheme.compatible(urlComponents: urlComponents) | |
try Scheme(urlComponents: urlComponents) | |
} catch { | |
print(error) | |
} | |
// optionalはなくても生成可能 | |
do { | |
var urlComponents = URLComponents() | |
urlComponents.scheme = "myscheme" | |
urlComponents.host = "host" | |
urlComponents.path = "/path" | |
urlComponents.queryItems = [URLQueryItem(name: "int", value: "1"), | |
URLQueryItem(name: "double", value: "2.3"), | |
URLQueryItem(name: "float", value: "4.5"), | |
URLQueryItem(name: "string", value: "string value"), | |
URLQueryItem(name: "url", value: "https://google.com/"), | |
URLQueryItem(name: "name", value: "name value"), | |
URLQueryItem(name: "mode", value: "1"), | |
URLQueryItem(name: "positiveInt", value: "1")] | |
Scheme.compatible(urlComponents: urlComponents) | |
try Scheme(urlComponents: urlComponents) | |
} catch { | |
print(error) | |
} | |
// 必須のキーなし | |
do { | |
var urlComponents = URLComponents() | |
urlComponents.scheme = "myscheme" | |
urlComponents.host = "host" | |
urlComponents.path = "/path" | |
urlComponents.queryItems = [URLQueryItem(name: "int", value: "1"), | |
URLQueryItem(name: "double", value: "2.3"), | |
URLQueryItem(name: "float", value: "4.5"), | |
URLQueryItem(name: "string", value: "string value"), | |
URLQueryItem(name: "url", value: "https://google.com/"), | |
URLQueryItem(name: "name", value: "name value"), | |
URLQueryItem(name: "intOptional", value: "6"), | |
URLQueryItem(name: "positiveInt", value: "1")] | |
Scheme.compatible(urlComponents: urlComponents) | |
try Scheme(urlComponents: urlComponents) | |
} catch { | |
print(error) // queryItemNotFound(name: "mode") | |
} | |
// 文字列フォーマットが違う | |
do { | |
var urlComponents = URLComponents() | |
urlComponents.scheme = "myscheme" | |
urlComponents.host = "host" | |
urlComponents.path = "/path" | |
urlComponents.queryItems = [URLQueryItem(name: "int", value: "a"), | |
URLQueryItem(name: "double", value: "2.3"), | |
URLQueryItem(name: "float", value: "4.5"), | |
URLQueryItem(name: "string", value: "string value"), | |
URLQueryItem(name: "url", value: "https://google.com/"), | |
URLQueryItem(name: "name", value: "name value"), | |
URLQueryItem(name: "mode", value: "1"), | |
URLQueryItem(name: "intOptional", value: "6"), | |
URLQueryItem(name: "positiveInt", value: "1")] | |
Scheme.compatible(urlComponents: urlComponents) | |
try Scheme(urlComponents: urlComponents) | |
} catch { | |
print(error) // queryValue(name: "int", error: URLQueryValueCompatibleError.format) | |
} | |
// codableエラー | |
do { | |
var urlComponents = URLComponents() | |
urlComponents.scheme = "myscheme" | |
urlComponents.host = "host" | |
urlComponents.path = "/path" | |
urlComponents.queryItems = [URLQueryItem(name: "int", value: "1"), | |
URLQueryItem(name: "double", value: "2.3"), | |
URLQueryItem(name: "float", value: "4.5"), | |
URLQueryItem(name: "string", value: "string value"), | |
URLQueryItem(name: "url", value: "https://google.com/"), | |
URLQueryItem(name: "name", value: "name value"), | |
URLQueryItem(name: "mode", value: "-1"), | |
URLQueryItem(name: "intOptional", value: "6"), | |
URLQueryItem(name: "positiveInt", value: "1")] | |
Scheme.compatible(urlComponents: urlComponents) | |
try Scheme(urlComponents: urlComponents) | |
} catch { | |
print(error) // queryValue(name: "mode", error: Swift.DecodingError.dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "Cannot initialize Mode from invalid Int value -1", underlyingError: nil))) | |
} | |
// フォーマットエラーならoptionalであってもエラーとする | |
do { | |
var urlComponents = URLComponents() | |
urlComponents.scheme = "myscheme" | |
urlComponents.host = "host" | |
urlComponents.path = "/path" | |
urlComponents.queryItems = [URLQueryItem(name: "int", value: "1"), | |
URLQueryItem(name: "double", value: "2.3"), | |
URLQueryItem(name: "float", value: "4.5"), | |
URLQueryItem(name: "string", value: "string value"), | |
URLQueryItem(name: "url", value: "https://google.com/"), | |
URLQueryItem(name: "name", value: "name value"), | |
URLQueryItem(name: "mode", value: "1"), | |
URLQueryItem(name: "intOptional", value: "abc"), | |
URLQueryItem(name: "positiveInt", value: "1")] | |
Scheme.compatible(urlComponents: urlComponents) | |
try Scheme(urlComponents: urlComponents) | |
} catch { | |
print(error) // queryValue(name: "intOptional", error: URLQueryValueCompatibleError.format) | |
} | |
// converterでエラー | |
do { | |
var urlComponents = URLComponents() | |
urlComponents.scheme = "myscheme" | |
urlComponents.host = "host" | |
urlComponents.path = "/path" | |
urlComponents.queryItems = [URLQueryItem(name: "int", value: "1"), | |
URLQueryItem(name: "double", value: "2.3"), | |
URLQueryItem(name: "float", value: "4.5"), | |
URLQueryItem(name: "string", value: "string value"), | |
URLQueryItem(name: "url", value: "https://google.com/"), | |
URLQueryItem(name: "name", value: "name value"), | |
URLQueryItem(name: "mode", value: "1"), | |
URLQueryItem(name: "intOptional", value: "6"), | |
URLQueryItem(name: "positiveInt", value: "-1")] | |
Scheme.compatible(urlComponents: urlComponents) | |
try Scheme(urlComponents: urlComponents) | |
} catch { | |
print(error) // queryValue(name: "positiveInt", error: URLQueryValueCompatibleError.format) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment