Skip to content

Instantly share code, notes, and snippets.

@ryotapoi
Created August 2, 2020 09:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryotapoi/5c5d3bd4bc26c0473b5d85818c93f255 to your computer and use it in GitHub Desktop.
Save ryotapoi/5c5d3bd4bc26c0473b5d85818c93f255 to your computer and use it in GitHub Desktop.
Custom URL Schemeをstructにマッピングする。
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