Skip to content

Instantly share code, notes, and snippets.

@ollieatkinson
Last active December 29, 2020 17:09
Show Gist options
  • Save ollieatkinson/bd7e33123789a7eb7246196142f00b1d to your computer and use it in GitHub Desktop.
Save ollieatkinson/bd7e33123789a7eb7246196142f00b1d to your computer and use it in GitHub Desktop.
Hacking around with JSON represented as an indirect enum
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)
}
}
@ollieatkinson
Copy link
Author

ollieatkinson commented Oct 2, 2020

var json: JSON = [
    "string": "hello world",
    "int": 1,
    "structure": [
        "is": [
            "good": [
                true,
                false
            ],
            "and": [
                "i": [
                    "like": [
                        "pie",
                        "programming",
                        "dogs"
                    ]
                ]
            ]
        ]
    ]
]
test.json["structure", "is", "good"] // [true, false]
test.$json["structure", "is", "good", 0].as(Bool.self).sink { c in
    print(#"test.$json["structure", "is", "good", 0]"#, "\(c)")
} receiveValue: { any in
    print(#"test.$json["structure", "is", "good", 0]"#, "📀", any)
}.store(in: &bag)

test.json["structure", "is", "good", 0] = false
test.json["structure", "is", "good", 0] = true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment