Skip to content

Instantly share code, notes, and snippets.

@ollieatkinson
Created March 15, 2021 09:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ollieatkinson/79557c23eb4486a30b9f125e9eb21a51 to your computer and use it in GitHub Desktop.
Save ollieatkinson/79557c23eb4486a30b9f125e9eb21a51 to your computer and use it in GitHub Desktop.
Another JSON box implementation over `Any` type
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 { }
@ollieatkinson
Copy link
Author

var json: JSON = [
    "hello": [
        "world": "GitHub"
    ]
]

json.get("hello", "world").as(String.self) // GitHub
json.set("oliver", at: "hello", "world")
json.get("hello", "world").as(String.self) // oliver
json.set("oliver", at: ["hello"])
json.get("hello").as(String.self) // oliver
json.set("oliver", at: "hello", 0, "world", 1)
json.get("hello", 0, "world", 1).as(String.self) // oliver
json.string(prettyPrinted: true)
/*
{
  "hello" : [
    {
      "world" : [
        null,
        "oliver"
      ]
    }
  ]
}
*/

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