Skip to content

Instantly share code, notes, and snippets.

@P0ed
Last active March 28, 2022 14:39
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 P0ed/31f402b578770f737a6cf2e835e19d63 to your computer and use it in GitHub Desktop.
Save P0ed/31f402b578770f737a6cf2e835e19d63 to your computer and use it in GitHub Desktop.
import Foundation
public protocol JSONDecodable {
init(json: JSON) throws
}
public protocol JSONContainer {
func get() throws -> JSON
}
public struct AnyJSONContainer {
public var json: () throws -> JSON
}
extension AnyJSONContainer: JSONContainer {
public func get() throws -> JSON { try json() }
}
public struct JSON {
public var object: Any
public var codingPath: [CodingKey]
}
public extension JSON {
enum Key {
case field(String)
case index(Int)
}
}
public extension JSON {
init(_ object: Any) { self = JSON(object: object, codingPath: []) }
func nestedContainer(forKey key: String) throws -> JSON {
if let object = (try convert() as NSDictionary)[key] {
return JSON(object: object, codingPath: codingPath + [Key.field(key)])
} else {
throw DecodingError.keyNotFound(Key.field(key), .path(codingPath))
}
}
func array() throws -> [JSON] {
try convert(NSArray.self).enumerated().map { JSON(object: $1, codingPath: codingPath + [Key.index($0)]) }
}
func decode<A>(_ transform: (JSON) throws -> A) throws -> A {
try decodeOptional(transform)
?? { throw DecodingError.valueNotFound(A.self, .path(codingPath)) }()
}
func decodeOptional<A>(_ transform: (JSON) throws -> A) throws -> A? {
object is NSNull ? nil : try transform(self)
}
func convert<A>(_ type: A.Type = A.self) throws -> A {
try decode { json in try (json.object as? A)
?? { throw DecodingError.typeMismatch(A.self, .path(json.codingPath)) }()
}
}
}
extension JSON.Key: CodingKey {
public var stringValue: String {
switch self {
case .field(let value): return value
case .index(let value): return "\(value)"
}
}
public var intValue: Int? {
switch self {
case .field: return nil
case .index(let value): return value
}
}
public init?(stringValue: String) { self = .field(stringValue) }
public init?(intValue: Int) { self = .index(intValue) }
}
extension JSON: JSONContainer {
public func get() throws -> JSON { self }
}
public extension JSONContainer {
subscript(_ field: String) -> JSONContainer { AnyJSONContainer { try self.get().nestedContainer(forKey: field) } }
var ifPresent: JSON? { try? get() }
func array() throws -> [JSON] { try get().array() }
func convert<A>(_ type: A.Type = A.self) throws -> A { try get().convert() }
func decode<A>(_ transform: (JSON) throws -> A) throws -> A { try get().decode(transform) }
func decode<A: JSONDecodable>(_ type: A.Type = A.self) throws -> A { try decode(A.init(json:)) }
func decodeOptional<A>(_ transform: (JSON) throws -> A) throws -> A? { try get().decodeOptional(transform) }
func decodeOptional<A: JSONDecodable>(_ type: A.Type = A.self) throws -> A? { try decodeOptional(A.init(json:)) }
}
extension Array: JSONDecodable where Element: JSONDecodable {
public init(json: JSON) throws { self = try json.array().map { try $0.decode() } }
}
// MARK: Types convertible from `Any` with `as?` operator
public protocol JSONConvertible: JSONDecodable {}
public extension JSONConvertible {
init(json: JSON) throws { self = try json.convert() }
}
extension NSDictionary: JSONConvertible {}
extension NSArray: JSONConvertible {}
extension NSNumber: JSONConvertible {}
extension String: JSONConvertible {}
extension Int: JSONConvertible {}
extension Int8: JSONConvertible {}
extension Int16: JSONConvertible {}
extension Int32: JSONConvertible {}
extension Int64: JSONConvertible {}
extension UInt: JSONConvertible {}
extension UInt8: JSONConvertible {}
extension UInt16: JSONConvertible {}
extension UInt32: JSONConvertible {}
extension UInt64: JSONConvertible {}
extension Float: JSONConvertible {}
extension Double: JSONConvertible {}
extension Bool: JSONConvertible {}
private extension DecodingError.Context {
static func path(_ codingPath: [CodingKey]) -> DecodingError.Context {
DecodingError.Context(
codingPath: codingPath,
debugDescription: codingPath.map { $0.stringValue }.debugDescription
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment