Created
March 15, 2021 09:42
-
-
Save ollieatkinson/79557c23eb4486a30b9f125e9eb21a51 to your computer and use it in GitHub Desktop.
Another JSON box implementation over `Any` type
This file contains hidden or 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
struct JSON { | |
enum Index { | |
case ordinal(Int), key(String) | |
} | |
private(set) var any: Any? | |
init(_ any: Any? = nil) { | |
self.any = any | |
} | |
} | |
extension JSON { | |
func array() throws -> [Any] { | |
guard let array = any as? [Any] else { throw "\(type(of: any)) is not an array" } | |
return array | |
} | |
func dictionary() throws -> [String: Any] { | |
guard let dictionary = any as? [String: Any] else { throw "\(type(of: any)) is not an array" } | |
return dictionary | |
} | |
} | |
extension JSON { | |
func get(_ path: Index...) throws -> JSON { | |
try get(path) | |
} | |
func get<Path>(_ path: Path) throws -> JSON where Path: Collection, Path.Element == Index { | |
guard let (head, remaining) = path.headAndTail() else { return self } | |
switch (head, any) { | |
case let (.key(key), dictionary as [Key: Value]): | |
guard let value = dictionary[key] else { throw "Value does not exist at \(path)" } | |
return try JSON(value).get(remaining) | |
case let (.ordinal(idx), array as [Value]): | |
guard array.indices.contains(idx) else { throw "Path indexing into array is out of bounds: \(path)" } | |
return try JSON(array[idx]).get(remaining) | |
default: | |
throw "Cannot path into \(String(describing: any)) value with \(head)" | |
} | |
} | |
} | |
extension JSON { | |
mutating func set(_ value: Value?, at path: Index...) throws { | |
try set(value, at: path) | |
} | |
mutating func set<Path>(_ value: Value?, at path: Path) throws where Path: Collection, Path.Element == Index { | |
try JSON.setting(value, on: &any, at: path) | |
} | |
func setting(_ value: Value?, at path: Index...) throws -> JSON { | |
try setting(value, at: path) | |
} | |
func setting<Path>(_ value: Value?, at path: Path) throws -> JSON where Path: Collection, Path.Element == Index { | |
var o = any | |
try JSON.setting(value, on: &o, at: path) | |
return JSON(o) | |
} | |
private static func setting<Path>(_ value: Value?, on object: inout Any?, at path: Path) throws where Path: Collection, Path.Element == Index { | |
guard let (head, remaining) = path.headAndTail() else { object = value; return } | |
switch head { | |
case let .key(key): | |
var dictionary = object as? [Key: Any] ?? [:] | |
try JSON.setting(value, on: &dictionary[key], at: remaining) | |
object = dictionary | |
case let .ordinal(idx): | |
precondition(idx >= 0) | |
var array = object as? [Any?] ?? [] | |
array.padded(to: idx, with: nil) | |
try JSON.setting(value, on: &array[idx], at: remaining) | |
object = array | |
} | |
} | |
} | |
extension JSON { | |
func `as`<T>(_: T.Type = T.self) throws -> T { | |
guard let o = any as? T else { throw "\(String(describing: any)) is not \(T.self)" } | |
return o | |
} | |
} | |
extension JSON { | |
func string(prettyPrinted: Bool = false) throws -> String { | |
guard let any = any else { return "null" } | |
guard let string = try String( | |
data: JSONSerialization.data(withJSONObject: any, options: [.fragmentsAllowed, .sortedKeys, .prettyPrinted, .withoutEscapingSlashes]), | |
encoding: .utf8 | |
) else { | |
return "<INVALID UTF8>" | |
} | |
return string | |
} | |
} | |
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 Collection { | |
func headAndTail() -> (head: Element, tail: SubSequence)? { | |
guard let first = first else { return nil } | |
return (first, dropFirst()) | |
} | |
} | |
extension JSON.Index: ExpressibleByStringLiteral, ExpressibleByUnicodeScalarLiteral, ExpressibleByExtendedGraphemeClusterLiteral { | |
init(stringLiteral value: String) { | |
self = .key(value) | |
} | |
init(unicodeScalarLiteral value: Unicode.Scalar) { | |
self = .key(String(value)) | |
} | |
init(extendedGraphemeClusterLiteral value: String.ExtendedGraphemeClusterLiteralType) { | |
self = .key(String(extendedGraphemeClusterLiteral: value)) | |
} | |
} | |
extension JSON.Index: ExpressibleByIntegerLiteral { | |
init(integerLiteral value: Int) { | |
self = .ordinal(.init(integerLiteral: value)) | |
} | |
} | |
extension JSON.Index { | |
var key: String? { | |
switch self { | |
case .ordinal: return nil | |
case let .key(s): return s | |
} | |
} | |
var ordinal: Int? { | |
switch self { | |
case let .ordinal(i): return i | |
case .key: return nil | |
} | |
} | |
} | |
extension JSON: ExpressibleByDictionaryLiteral { | |
init(dictionaryLiteral elements: (String, Any)...) { | |
self.init(Dictionary(uniqueKeysWithValues: elements)) | |
} | |
} | |
extension JSON: ExpressibleByArrayLiteral { | |
init(arrayLiteral elements: Any...) { | |
self.init(elements) | |
} | |
} | |
extension String: Error { } |
Author
ollieatkinson
commented
Mar 15, 2021
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment