Created
May 22, 2024 20:16
-
-
Save ollieatkinson/98e39f0c6a2799d4e06b112237dfd936 to your computer and use it in GitHub Desktop.
Decoder for decoding URL into a concrete type
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 | |
public final class URLDecoder: Decoder { | |
public var codingPath: [CodingKey] = [] | |
public var userInfo: [CodingUserInfoKey: Any] = [:] | |
let parseParametersFromURL: (URL) throws -> [String: Any] | |
public init<Output>(_ regex: Regex<Output>) { | |
self.parseParametersFromURL = { url in | |
guard let match = try regex.firstMatch(in: url.absoluteString) else { | |
throw URLDecodingError.noMatch | |
} | |
return AnyRegexOutput(match).reduce(into: [String: Any]()) { sum, next in | |
if let name = next.name { | |
sum[name] = next.value | |
} | |
} | |
} | |
} | |
public init(_ parseParametersFromURL: @escaping (URL) throws -> [String: Any]) { | |
self.parseParametersFromURL = parseParametersFromURL | |
} | |
public init() { | |
self.parseParametersFromURL = { _ in [:] } | |
} | |
private(set) var dictionary: [String: Any] = [:] | |
public func decode<T>(_: T.Type = T.self, from url: URL) throws -> T where T: Decodable { | |
let old = dictionary | |
dictionary = try url.dictionary() | |
.merging(url.queryDictionary() as [String: Any], uniquingKeysWith: { $1 }) | |
.merging(parseParametersFromURL(url.sanitized()), uniquingKeysWith: { $1 }) | |
defer { dictionary = old } | |
return try T(from: self) | |
} | |
func convert<T>(_ any: Any, to: T.Type) throws -> Any? { | |
switch (any, T.self) { | |
case (let value as T, _): | |
return value | |
case (let string as String, is Bool.Type): | |
switch string.lowercased() { | |
case "1", "yes", "true": | |
return true | |
case "0", "no", "false": | |
return false | |
default: | |
break | |
} | |
fallthrough | |
case (let string as String, is UUID.Type): | |
return UUID(uuidString: string) | |
case (let timeInterval as TimeInterval, is Date.Type): | |
return Date(timeIntervalSince1970: timeInterval) | |
case (let string as CustomStringConvertible, is String.Type): | |
return string.description | |
case (let string as any StringProtocol, _): | |
switch Witness<T>.self { | |
case let convertible as URLDecoderLosslessStringConvertible.Type: | |
return convertible.value(from: String(string)) | |
default: | |
break | |
} | |
fallthrough | |
default: | |
switch Witness<T>.self { | |
case let rawRepresentable as URLDecoderRawRepresentable.Type: | |
return rawRepresentable.value(from: any, using: self) | |
default: | |
return nil | |
} | |
} | |
} | |
} | |
enum URLDecodingError: Swift.Error { | |
case noMatch | |
case unsupportedContainer(Any.Type) | |
case unsupportedOperaton(String) | |
case conversionFailed(String, to: Any.Type) | |
} | |
extension URLDecoder { | |
public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey { | |
try KeyedDecodingContainer(KeyedContainer<Key>(decoder: self)) | |
} | |
} | |
extension URLDecoder { | |
public struct KeyedContainer<Key> where Key: CodingKey { | |
let decoder: URLDecoder | |
let dictionary: [String: Any] | |
public var codingPath: [CodingKey] { decoder.codingPath } | |
public var userInfo: [CodingUserInfoKey: Any] { decoder.userInfo } | |
public init(decoder: URLDecoder) throws { | |
self.decoder = decoder | |
self.dictionary = decoder.dictionary | |
} | |
func value(for key: Key) throws -> Any { | |
guard let value = dictionary[key.stringValue] else { | |
throw DecodingError.valueNotFound( | |
String.self, | |
DecodingError.Context( | |
codingPath: codingPath, | |
debugDescription: "No value found for key '\(key.stringValue)'" | |
) | |
) | |
} | |
return value | |
} | |
} | |
} | |
extension URLDecoder.KeyedContainer: KeyedDecodingContainerProtocol { | |
public var allKeys: [Key] { | |
dictionary.keys.compactMap(Key.init) | |
} | |
public func contains(_ key: Key) -> Bool { | |
dictionary[key.stringValue] != nil | |
} | |
public func decodeNil(forKey key: Key) throws -> Bool { | |
dictionary[key.stringValue] == nil | |
} | |
public func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { | |
let value = try value(for: key) | |
decoder.codingPath.append(key) | |
defer { decoder.codingPath.removeLast() } | |
guard let result = try decoder.convert(value, to: T.self) as? T else { | |
throw DecodingError.typeMismatch( | |
T.self, | |
DecodingError.Context( | |
codingPath: codingPath, | |
debugDescription: "Unable to convert \(value) to \(T.self)" | |
) | |
) | |
} | |
return result | |
} | |
public func decodeIfPresent<T>(_ type: T.Type, forKey key: Key) throws -> T? where T: Decodable { | |
try? decode(type, forKey: key) | |
} | |
} | |
private protocol URLDecoderRawRepresentable { | |
static func value(from any: Any, using decoder: URLDecoder) -> Any? | |
} | |
private protocol URLDecoderLosslessStringConvertible { | |
static func value(from string: String) -> Any? | |
} | |
private enum Witness<T> {} | |
extension Witness: URLDecoderRawRepresentable where T: RawRepresentable, T.RawValue: Decodable { | |
static func value(from any: Any, using decoder: URLDecoder) -> Any? { | |
guard let value = any as? T.RawValue ?? (try? decoder.convert(any, to: T.RawValue.self) as? T.RawValue) else { return nil } | |
return T(rawValue: value) | |
} | |
} | |
extension Witness: URLDecoderLosslessStringConvertible where T: LosslessStringConvertible { | |
static func value(from string: String) -> Any? { T(string) } | |
} | |
private extension URL { | |
func dictionary() -> [String: Any] { | |
let components = URLComponents(url: self, resolvingAgainstBaseURL: false) | |
return [ | |
"fragment": components?.fragment as Any, | |
"host": components?.host as Any, | |
"password": components?.password as Any, | |
"path": components?.path as Any, | |
"port": components?.port as Any, | |
"query": components?.query as Any, | |
"scheme": components?.scheme as Any, | |
"user": components?.user as Any, | |
] | |
} | |
func sanitized() -> URL { | |
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)! | |
components.query = nil | |
return components.url! | |
} | |
func queryDictionary() -> [String: String] { | |
guard let items = URLComponents(url: self, resolvingAgainstBaseURL: false)?.queryItems else { | |
return [:] | |
} | |
return items.reduce(into: [String: String]()) { query, item in | |
guard let value = item.value else { return } | |
query[item.name] = value | |
} | |
} | |
} | |
// Unsupported stuff | |
extension URLDecoder { | |
public func unkeyedContainer() throws -> UnkeyedDecodingContainer { throw URLDecodingError.unsupportedContainer(UnkeyedDecodingContainer.self) } | |
public func singleValueContainer() throws -> SingleValueDecodingContainer { throw URLDecodingError.unsupportedContainer(SingleValueDecodingContainer.self) } | |
} | |
extension URLDecoder.KeyedContainer { | |
public func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey { throw URLDecodingError.unsupportedOperaton(#function) } | |
public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { throw URLDecodingError.unsupportedOperaton(#function) } | |
public func superDecoder() throws -> Decoder { throw URLDecodingError.unsupportedOperaton(#function) } | |
public func superDecoder(forKey key: Key) throws -> Decoder { throw URLDecodingError.unsupportedOperaton(#function) } | |
} |
Author
ollieatkinson
commented
May 22, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment