Created
May 18, 2020 10:20
-
-
Save valeriomazzeo/28c996ae9e6cd95456a3687b40edc5d6 to your computer and use it in GitHub Desktop.
A JSON data structure conforming to Codable
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
// | |
// JSON.swift | |
// | |
// Created by Valerio Mazzeo on 26/03/2018. | |
// | |
import Foundation | |
public enum JSON { | |
case object(JSON.Object) | |
case array(JSON.Array) | |
case string(String) | |
case number(Double) | |
case bool(Bool) | |
case null | |
case unencoded(Encodable) | |
} | |
public extension JSON { | |
typealias Array = [JSON] | |
typealias Object = [String: JSON] | |
} | |
public extension JSON { | |
init(from value: Encodable) { | |
self = .unencoded(value) | |
} | |
init(_ value: JSON.Object) { | |
self = .object(value) | |
} | |
init(_ value: JSON.Array) { | |
self = .array(value) | |
} | |
init(_ value: String) { | |
self = .string(value) | |
} | |
init(_ value: Int) { | |
self = .number(Double(value)) | |
} | |
init(_ value: Float) { | |
self = .number(Double(value)) | |
} | |
init(_ value: Double) { | |
self = .number(value) | |
} | |
init(_ value: Bool) { | |
self = .bool(value) | |
} | |
init(jsonObject: Any) throws { | |
switch jsonObject { | |
case let value as String: | |
self = .string(value) | |
case let value as Bool where type(of: jsonObject) is Bool.Type: | |
self = .bool(value) | |
case let value as Int: | |
self = .number(Double(value)) | |
case let value as Int8: | |
self = .number(Double(value)) | |
case let value as Int16: | |
self = .number(Double(value)) | |
case let value as Int32: | |
self = .number(Double(value)) | |
case let value as Int64: | |
self = .number(Double(value)) | |
case let value as UInt: | |
self = .number(Double(value)) | |
case let value as UInt8: | |
self = .number(Double(value)) | |
case let value as UInt16: | |
self = .number(Double(value)) | |
case let value as UInt32: | |
self = .number(Double(value)) | |
case let value as UInt64: | |
self = .number(Double(value)) | |
case let value as [Any?]: | |
self = .array(try value.map { try JSON(jsonObject: $0 ?? NSNull()) }) | |
case let value as [String: Any?]: | |
self = .object(try value.mapValues { try JSON(jsonObject: $0 ?? NSNull()) }) | |
case let value as Float: | |
self = .number(Double(value)) | |
case let value as Double: | |
self = .number(value) | |
case let value as Decimal: | |
self = .number(NSDecimalNumber(decimal: value).doubleValue) | |
case let value as NSDecimalNumber: | |
self = .number(value.doubleValue) | |
case is NSNull: | |
self = .null | |
case let value as NSNumber: | |
#if os(Linux) | |
self = .number(value.doubleValue) | |
#else | |
if value === kCFBooleanTrue as NSNumber { | |
self = .bool(true) | |
} else if value === kCFBooleanFalse as NSNumber { | |
self = .bool(false) | |
} else { | |
self = .number(value.doubleValue) | |
} | |
#endif | |
default: | |
throw DecodingError.typeMismatch( | |
type(of: jsonObject), | |
DecodingError.Context(codingPath: [], debugDescription: "Invalid value cannot be decoded") | |
) | |
} | |
} | |
} | |
extension JSON: ExpressibleByDictionaryLiteral { | |
public init(dictionaryLiteral elements: (String, JSON)...) { | |
var dictionary: JSON.Object = [:] | |
for (key, value) in elements { | |
dictionary[key] = value | |
} | |
self.init(dictionary) | |
} | |
} | |
extension JSON: ExpressibleByArrayLiteral { | |
public init(arrayLiteral elements: JSON...) { | |
self.init(elements) | |
} | |
} | |
extension JSON: ExpressibleByStringLiteral { | |
public init(stringLiteral value: String) { | |
self.init(value) | |
} | |
} | |
extension JSON: ExpressibleByBooleanLiteral { | |
public init(booleanLiteral value: Bool) { | |
self.init(value) | |
} | |
} | |
extension JSON: ExpressibleByIntegerLiteral { | |
public init(integerLiteral value: Int) { | |
self.init(value) | |
} | |
} | |
extension JSON: ExpressibleByFloatLiteral { | |
public init(floatLiteral value: Float) { | |
self.init(value) | |
} | |
} | |
public extension JSON { | |
static let defaultEncoder: JSONEncoder = { | |
let encoder = JSONEncoder() | |
encoder.dateEncodingStrategy = .iso8601 | |
return encoder | |
}() | |
static let defaultDecoder: JSONDecoder = { | |
let encoder = JSONDecoder() | |
encoder.dateDecodingStrategy = .iso8601 | |
return encoder | |
}() | |
func encode(encoder: JSONEncoder = JSON.defaultEncoder) throws -> Foundation.Data { | |
return try encoder.encode(self) | |
} | |
func decode<T: Decodable>(_ type: T.Type, decoder: JSONDecoder = JSON.defaultDecoder) throws -> T { | |
return try decoder.decode(type, from: self.encode()) | |
} | |
} | |
public extension JSON { | |
subscript(key: String) -> JSON? { | |
return self.object?[key] | |
} | |
subscript(index: Int) -> JSON? { | |
return self.array?[index] | |
} | |
} | |
public extension JSON { | |
var object: JSON.Object? { | |
switch self { | |
case .object(let value): | |
return value | |
default: | |
return nil | |
} | |
} | |
var array: JSON.Array? { | |
switch self { | |
case .array(let value): | |
return value | |
default: | |
return nil | |
} | |
} | |
var string: String? { | |
switch self { | |
case .string(let value): | |
return value | |
default: | |
return nil | |
} | |
} | |
var int: Int? { | |
switch self { | |
case .number(let value) where value == value.rounded(): | |
return Int(value) | |
default: | |
return nil | |
} | |
} | |
var double: Double? { | |
switch self { | |
case .number(let value): | |
return value | |
default: | |
return nil | |
} | |
} | |
var bool: Bool? { | |
switch self { | |
case .bool(let value): | |
return value | |
default: | |
return nil | |
} | |
} | |
var isUnencoded: Bool { | |
switch self { | |
case .unencoded: | |
return true | |
default: | |
return false | |
} | |
} | |
var isNull: Bool { | |
switch self { | |
case .null: | |
return true | |
default: | |
return false | |
} | |
} | |
} | |
extension JSON: Codable { | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if let value = try? container.decode(String.self) { | |
self = .string(value) | |
} else if let value = try? container.decode(Bool.self) { | |
self = .bool(value) | |
} else if container.decodeNil() { | |
self = .null | |
} else if let value = try? container.decode(JSON.Object.self) { | |
self = .object(value) | |
} else if let value = try? container.decode(JSON.Array.self) { | |
self = .array(value) | |
} else if let value = try? container.decode(Double.self) { | |
self = .number(value) | |
} else { | |
throw DecodingError.dataCorruptedError( | |
in: container, | |
debugDescription: "Invalid value cannot be decoded" | |
) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .object(let value): | |
try container.encode(value) | |
case .array(let value): | |
try container.encode(value) | |
case .string(let value): | |
try container.encode(value) | |
case .number(let value): | |
try container.encode(value) | |
case .bool(let value): | |
try container.encode(value) | |
case .null: | |
try container.encodeNil() | |
case .unencoded(let value): | |
try value.encode(to: encoder) | |
} | |
} | |
} | |
extension JSON: Equatable { | |
public static func == (lhs: JSON, rhs: JSON) -> Bool { | |
switch (lhs, rhs) { | |
case (.object(let lhs), .object(let rhs)): | |
return lhs == rhs | |
case (.array(let lhs), .array(let rhs)): | |
return lhs == rhs | |
case (.string(let lhs), .string(let rhs)): | |
return lhs == rhs | |
case (.number(let lhs), .number(let rhs)) where lhs == lhs.rounded() && rhs == rhs.rounded(): | |
return lhs == rhs | |
case (.number(let lhs), .number(let rhs)): | |
return lhs.distance(to: rhs) <= 0.001 | |
case (.bool(let lhs), .bool(let rhs)): | |
return lhs == rhs | |
case (.null, .null): | |
return true | |
case (.unencoded, .unencoded): | |
return false | |
default: | |
return false | |
} | |
} | |
} | |
public extension Dictionary where Key == String, Value == JSON { | |
init(from value: Encodable) throws { | |
guard let object = try JSON.unencoded(value).decode(JSON.self).object else { | |
throw DecodingError.typeMismatch( | |
JSON.Object.self, | |
DecodingError.Context(codingPath: [], debugDescription: "Value is not representable as a JSON.Object") | |
) | |
} | |
self = object | |
} | |
} | |
public extension Array where Element == JSON { | |
init(from value: Encodable) throws { | |
guard let array = try JSON.unencoded(value).decode(JSON.self).array else { | |
throw DecodingError.typeMismatch( | |
JSON.Object.self, | |
DecodingError.Context(codingPath: [], debugDescription: "Value is not representable as a JSON.Array") | |
) | |
} | |
self = array | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment