Last active
December 29, 2020 17:09
-
-
Save ollieatkinson/bd7e33123789a7eb7246196142f00b1d to your computer and use it in GitHub Desktop.
Hacking around with JSON represented as an indirect enum
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 indirect enum JSON: Hashable { | |
public enum Leaf: Hashable { | |
case null | |
case boolean(Bool) | |
case number(NSNumber) | |
case string(String) | |
case error(Error) | |
} | |
case leaf(Leaf) | |
case array([JSON]) | |
case dictionary([String: JSON]) | |
} | |
extension JSON { | |
init(_ any: Any?) { | |
switch any { | |
case nil: self = ^.null | |
case let boolean as Bool: self = ^.boolean(boolean) | |
case let number as NSNumber: self = ^.number(number) | |
case let string as String: self = ^.string(string) | |
case let array as [Any]: self = .array(array.map(JSON.init)) | |
case let dictionary as [String: Any]: self = .dictionary(dictionary.mapValues(JSON.init)) | |
default: self = ^.error(Error("\(any ?? "nil") is not a JSON object")) | |
} | |
} | |
var any: Any { | |
switch self { | |
case let .leaf(value): | |
switch value { | |
case .null: return Optional<Any>.none as Any | |
case let .boolean(bool): return bool | |
case let .number(number): return number | |
case let .string(string): return string | |
case let .error(error): return error | |
} | |
case let .array(array): return array.map(\.any) | |
case let .dictionary(dictionary): return dictionary.mapValues(\.any) | |
} | |
} | |
} | |
extension JSON { | |
init(file: URL) throws { | |
try self.init(data: Data(contentsOf: file)) | |
} | |
init(data: Data, options: JSONSerialization.ReadingOptions = [.fragmentsAllowed]) throws { | |
try self.init(JSONSerialization.jsonObject(with: data, options: options)) | |
} | |
} | |
extension JSON: ExpressibleByNilLiteral { | |
public init(nilLiteral: ()) { self = ^.null } | |
public init() { self = ^.null } | |
} | |
extension JSON: ExpressibleByArrayLiteral { | |
public init(arrayLiteral elements: JSON...) { self = .array(elements) } | |
} | |
extension JSON: ExpressibleByDictionaryLiteral { | |
public init(dictionaryLiteral elements: (String, JSON)...) { self = .dictionary(.init(uniqueKeysWithValues: elements)) } | |
} | |
extension JSON: ExpressibleByBooleanLiteral { | |
public init(booleanLiteral value: Bool) { self = ^.boolean(value) } | |
} | |
extension JSON: ExpressibleByIntegerLiteral { | |
public init(integerLiteral value: Int) { self = ^.number(value as NSNumber) } | |
} | |
extension JSON: ExpressibleByFloatLiteral { | |
public init(floatLiteral value: Double) { self = ^.number(value as NSNumber) } | |
} | |
extension JSON: ExpressibleByStringLiteral { | |
public init(stringLiteral value: String) { self = ^.string(value) } | |
} | |
extension JSON: ExpressibleByUnicodeScalarLiteral { | |
public init(unicodeScalarLiteral value: String) { self = ^.string(value) } | |
} | |
extension JSON: ExpressibleByExtendedGraphemeClusterLiteral { | |
public init(extendedGraphemeClusterLiteral value: String) { self = ^.string(value) } | |
} | |
extension JSON { | |
public struct Path { | |
public enum Index { | |
case int(Int) | |
case string(String) | |
} | |
public var path: [CodingKey] | |
init(_ path: [CodingKey]) { | |
self.path = path | |
} | |
public var isEmpty: Bool { path.isEmpty } | |
func headAndTail() -> (head: Index, tail: Path)? { | |
guard !isEmpty else { return nil } | |
var tail = path | |
let head = tail.removeFirst() | |
return (head.intValue.map(Index.int) ?? .string(head.stringValue), Path(tail)) | |
} | |
} | |
} | |
extension JSON.Path.Index: CodingKey, ExpressibleByStringLiteral, ExpressibleByIntegerLiteral { | |
public init(stringLiteral value: String) { | |
self = .string(value) | |
} | |
public init(integerLiteral value: Int) { | |
self = .int(value) | |
} | |
public var stringValue: String { | |
switch self { | |
case let .int(o): return "\(o)" | |
case let .string(o): return o | |
} | |
} | |
public init?(stringValue: String) { | |
self = .string(stringValue) | |
} | |
public var intValue: Int? { | |
switch self { | |
case let .int(o): return o | |
case .string: return nil | |
} | |
} | |
public init?(intValue: Int) { | |
self = .int(intValue) | |
} | |
} | |
prefix operator ^ | |
prefix func ^ (_ value: JSON.Leaf) -> JSON { .leaf(value) } | |
extension JSON { | |
public subscript(path: JSON.Path.Index...) -> JSON { | |
get { self[.init(path)] } | |
set { self[.init(path)] = newValue } | |
} | |
public subscript(path: JSON.Path) -> JSON { | |
get { | |
switch self { | |
case _ where path.isEmpty: return self | |
case let .array(array): return array[path] | |
case let .dictionary(dictionary): return dictionary[path] | |
default: return ^.error(Error("Cannot traverse value \(self) with \(path)")) | |
} | |
} | |
set { | |
switch self { | |
case _ where path.isEmpty: self = newValue | |
case var .array(array): | |
array[path] = newValue | |
self = .array(array) | |
case var .dictionary(dictionary): | |
dictionary[path] = newValue | |
self = .dictionary(dictionary) | |
default: self = ^.error(Error("Cannot traverse value \(self) with \(path)")) | |
} | |
} | |
} | |
} | |
extension Dictionary where Key == String, Value == JSON { | |
public subscript(path: JSON.Path.Index...) -> Value { | |
get { self[.init(path)] } | |
set { self[.init(path)] = newValue } | |
} | |
public subscript(path: JSON.Path) -> Value { | |
get { | |
switch path.headAndTail() { | |
case nil: return nil | |
case let (head, remaining)? where remaining.isEmpty: | |
return self[head.stringValue] ?? nil | |
case let (head, remaining)?: | |
switch self[head] { | |
case let .array(array): | |
return array[remaining] | |
case let .dictionary(dictionary): | |
return dictionary[remaining] | |
default: return nil | |
} | |
} | |
} | |
set { | |
switch path.headAndTail() { | |
case nil: return | |
case let (head, remaining)? where remaining.isEmpty: | |
self[head.stringValue] = newValue | |
case let (head, remaining)?: | |
let key = head.stringValue | |
let value = self[key] | |
switch value { | |
case var .array(array): | |
array[remaining] = newValue | |
self[key] = .array(array) | |
case var .dictionary(dictionary): | |
dictionary[remaining] = newValue | |
self[key] = .dictionary(dictionary) | |
default: | |
guard let next = remaining.path.first else { | |
return (self[key] = newValue) | |
} | |
if let idx = next.intValue, idx >= 0 { | |
var array = [Value]() | |
array[remaining] = newValue | |
self[key] = .array(array) | |
} else { | |
var dictionary = [String: Value]() | |
dictionary[remaining] = newValue | |
self[key] = .dictionary(dictionary) | |
} | |
} | |
} | |
} | |
} | |
} | |
extension Array where Element == JSON { | |
public subscript(keyPath: JSON.Path.Index...) -> Element { | |
get { self[.init(keyPath)] } | |
set { self[.init(keyPath)] = newValue } | |
} | |
public subscript(keyPath: JSON.Path) -> Element { | |
get { | |
guard let (head, remaining) = keyPath.headAndTail() else { return nil } | |
guard let idx = head.intValue else { return nil } | |
switch (idx, remaining) { | |
case nil: return nil | |
case let (idx, remaining) where remaining.isEmpty: | |
return indices.contains(idx) ? self[idx] : nil | |
case let (idx, remaining): | |
switch self[idx] { | |
case let .array(array): | |
return array[remaining] | |
case let .dictionary(dictionary): | |
return dictionary[remaining] | |
default: return nil | |
} | |
} | |
} | |
set { | |
guard let (head, remaining) = keyPath.headAndTail() else { return } | |
guard let idx = head.intValue, idx >= 0 else { return } | |
padded(to: idx, with: ^.null) | |
switch (idx, remaining) { | |
case nil: return | |
case let (idx, remaining) where remaining.isEmpty: | |
self[idx] = newValue | |
case let (idx, remaining): | |
let value = indices.contains(idx) ? self[idx] : nil | |
switch value { | |
case var .array(array): | |
array[remaining] = newValue | |
self[idx] = .array(array) | |
case var .dictionary(dictionary): | |
dictionary[remaining] = newValue | |
self[idx] = .dictionary(dictionary) | |
default: | |
guard let next = remaining.path.first else { | |
return self[idx] = newValue | |
} | |
if let idx = next.intValue, idx >= 0 { | |
var array = [Element]() | |
array[remaining] = newValue | |
self[idx] = .array(array) | |
} else { | |
var dictionary = [String: Element]() | |
dictionary[remaining] = newValue | |
self[idx] = .dictionary(dictionary) | |
} | |
} | |
} | |
} | |
} | |
} | |
extension JSON { | |
public struct Error: Swift.Error, Hashable { | |
public let message: String | |
public var function: String | |
public var file: String | |
public var line: Int | |
public init( | |
_ message: Any, | |
_ function: String = #function, | |
_ file: String = #file, | |
_ line: Int = #line | |
) { | |
self.message = String(describing: message) | |
self.function = function | |
self.file = file | |
self.line = line | |
} | |
} | |
} | |
extension RangeReplaceableCollection where Self: BidirectionalCollection { | |
public mutating func padded(to size: Int, with value: @autoclosure () -> Element) { | |
guard !indices.contains(index(startIndex, offsetBy: size)) else { return } | |
append(contentsOf: (0..<(1 + size - count)).map { _ in value() }) | |
} | |
} | |
extension JSON { | |
typealias Decoder = JSONDecoder | |
typealias Encoder = JSONEncoder | |
func data(_ options: JSONSerialization.WritingOptions = [.fragmentsAllowed]) throws -> Data { | |
try JSONSerialization.data(withJSONObject: any, options: options) | |
} | |
} | |
extension JSON.Decoder { | |
func decode<T>(_ type: T.Type = T.self, from json: JSON) throws -> T where T: Decodable { | |
try decode(T.self, from: json.data()) | |
} | |
} | |
extension JSON.Encoder { | |
func encode<T>(_ value: T) throws -> JSON where T: Encodable { | |
try JSON(data: encode(value)) | |
} | |
} | |
import Combine | |
extension Publisher where Output == JSON { | |
subscript(path: JSON.Path.Index...) -> AnyPublisher<JSON, Failure> { | |
self[.init(path)] | |
} | |
subscript(path: JSON.Path) -> AnyPublisher<JSON, Failure> { | |
compactMap { $0[path] }.eraseToAnyPublisher() | |
} | |
func `as`<A>( | |
_ type: A.Type = A.self, | |
_ function: String = #function, | |
_ file: String = #file, | |
_ line: Int = #line | |
) -> AnyPublisher<A, Error> where A: Decodable { | |
tryMap { | |
do { return try JSON.Decoder().decode(A.self, from: $0) } | |
catch { throw JSON.Error(error, function, file, line) } | |
}.eraseToAnyPublisher() | |
} | |
} | |
// Thanks to @iankeen for this implementation | |
public struct AnyCodable: Codable { | |
public let value: Any? | |
public init(_ value: Any?) { | |
self.value = value | |
} | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if container.decodeNil() { | |
self.value = nil | |
} else if let value = try? container.decode([String: AnyCodable].self) { | |
self.value = value.compactMapValues(\.value) | |
} else if let value = try? container.decode([AnyCodable].self) { | |
self.value = value.compactMap(\.value) | |
} else if let value = try? container.decode(Bool.self) { | |
self.value = value | |
} else if let value = try? container.decode(String.self) { | |
self.value = value | |
} else if let value = try? container.decode(Int.self) { | |
self.value = value | |
} else if let value = try? container.decode(Int8.self) { | |
self.value = value | |
} else if let value = try? container.decode(Int16.self) { | |
self.value = value | |
} else if let value = try? container.decode(Int32.self) { | |
self.value = value | |
} else if let value = try? container.decode(Int64.self) { | |
self.value = value | |
} else if let value = try? container.decode(UInt.self) { | |
self.value = value | |
} else if let value = try? container.decode(UInt8.self) { | |
self.value = value | |
} else if let value = try? container.decode(UInt16.self) { | |
self.value = value | |
} else if let value = try? container.decode(UInt32.self) { | |
self.value = value | |
} else if let value = try? container.decode(UInt64.self) { | |
self.value = value | |
} else if let value = try? container.decode(Double.self) { | |
self.value = value | |
} else if let value = try? container.decode(Float.self) { | |
self.value = value | |
} else { | |
throw DecodingError.dataCorruptedError(in: container, debugDescription: "tbc") | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch value { | |
case nil: try container.encodeNil() | |
case let value as [String: AnyCodable]: try container.encode(value) | |
case let value as [String: Any]: try container.encode(value.mapValues(AnyCodable.init)) | |
case let value as [AnyCodable]: try container.encode(value) | |
case let value as [Any]: try container.encode(value.map(AnyCodable.init)) | |
case let value as Bool: try container.encode(value) | |
case let value as String: try container.encode(value) | |
case let value as Int: try container.encode(value) | |
case let value as Int8: try container.encode(value) | |
case let value as Int16: try container.encode(value) | |
case let value as Int32: try container.encode(value) | |
case let value as Int64: try container.encode(value) | |
case let value as UInt: try container.encode(value) | |
case let value as UInt8: try container.encode(value) | |
case let value as UInt16: try container.encode(value) | |
case let value as UInt32: try container.encode(value) | |
case let value as UInt64: try container.encode(value) | |
case let value as Double: try container.encode(value) | |
case let value as Float: try container.encode(value) | |
default: throw EncodingError.invalidValue(value ?? "nil", .init(codingPath: encoder.codingPath, debugDescription: "tbc")) | |
} | |
} | |
} | |
extension JSON: Codable { | |
public init(from decoder: Swift.Decoder) throws { | |
self = try JSON(AnyCodable(from: decoder).value) | |
} | |
public func encode(to encoder: Swift.Encoder) throws { | |
try AnyCodable(any).encode(to: encoder) | |
} | |
} |
Author
ollieatkinson
commented
Oct 2, 2020
•
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment