Skip to content

Instantly share code, notes, and snippets.

@ollieatkinson
Last active January 7, 2024 03:30
Show Gist options
  • Save ollieatkinson/be337cf83c0cc0b4ed3fa1ccb7e09300 to your computer and use it in GitHub Desktop.
Save ollieatkinson/be337cf83c0cc0b4ed3fa1ccb7e09300 to your computer and use it in GitHub Desktop.
KeyPath to get/set from a Swift JSON object ([String: Any] or [Any])
import Foundation
extension Dictionary where Key == String, Value == Any {
public subscript(keyPath: JSON.Path.Index...) -> Value? {
get { self[keyPath: .init(path: keyPath)] }
set { self[keyPath: .init(path: keyPath)] = newValue }
}
public subscript(keyPath keyPath: JSON.Path) -> Value? {
get {
switch keyPath.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 as [Value]:
return array[keyPath: remaining]
case let dictionary as [Key: Value]:
return dictionary[keyPath: remaining]
default: return nil
}
}
}
set {
switch keyPath.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 as [Value]:
array[keyPath: remaining] = newValue
self[key] = array
case var nested as [Key: Value]:
nested[keyPath: remaining] = newValue
self[key] = nested
default:
guard let next = remaining.path.first else {
return (self[key] = newValue)
}
if let idx = next.intValue, idx >= 0 {
var array = [Value]()
array[keyPath: remaining] = newValue
self[key] = array
} else {
var dictionary = [String: Value]()
dictionary[keyPath: remaining] = newValue
self[key] = dictionary
}
}
}
}
}
}
extension Array where Element == Any {
public subscript(keyPath: JSON.Path.Index...) -> Element? {
get { self[keyPath: .init(path: keyPath)] }
set { self[keyPath: .init(path: keyPath)] = newValue }
}
public subscript(keyPath 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 as [Element]:
return array[keyPath: remaining]
case let dictionary as [String: Element]:
return dictionary[keyPath: 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: NSNull())
switch (idx, remaining) {
case nil: return
case let (idx, remaining) where remaining.isEmpty:
self[idx] = newValue ?? NSNull()
case let (idx, remaining):
let value = indices.contains(idx) ? self[idx] : nil
switch value {
case var .some(array as [Element]):
array[keyPath: remaining] = newValue
self[idx] = array
case var .some(dictionary as [String: Element]):
dictionary[keyPath: remaining] = newValue
self[idx] = dictionary
default:
guard let next = remaining.path.first else {
return self[idx] = newValue ?? NSNull()
}
if let idx = next.intValue, idx >= 0 {
var array = [Element]()
array[keyPath: remaining] = newValue
self[idx] = array
} else {
var dictionary = [String: Element]()
dictionary[keyPath: remaining] = newValue
self[idx] = dictionary
}
}
}
}
}
}
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() })
}
}
// TODO
public enum JSON { }
extension JSON {
public struct Path {
public enum Index {
case int(Int)
case string(String)
}
public var path: [CodingKey]
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(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)
}
}
@ollieatkinson
Copy link
Author

var dictionary: [String: Any] = [
    "string": "hello world",
    "int": 1,
    "structure": [
        "is": [
            "good": [
                true,
                [
                    "and": [
                        "i": [
                            "like": [
                                "pie",
                                "programming",
                                "dogs"
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ]
]

dictionary["structure", "is", "good", 0] // true
dictionary["structure", "is", "good", 1, "and", "i", "like", 3] // nil

dictionary["structure", "is", "good", 5, "and", "i", "like", 3] = [ "noodles", "chicken" ]
dictionary["structure", "is", "good", 5, "and", "i", "like", 3] // ["noodles", "chicken"]

dictionary
/*
[
    "structure": [
        "is": [
            "good": [
                true, [
                    "and": [
                        "i": [
                            "like": [
                                "pie",
                                "programming",
                                "dogs"
                            ]
                        ]
                    ]
                ],
                <null>,
                <null>,
                <null>,
                [
                    "and": [
                        "i": [
                            "like": [
                                <null>,
                                <null>,
                                <null>,
                                [
                                    "noodles",
                                    "chicken"
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ],
    "string": "hello world",
    "int": 1
]
*/

@ollieatkinson
Copy link
Author

ollieatkinson commented Oct 1, 2020

Combine:

import Combine

extension Publisher {
    
    subscript<T>(path: JSON.Path.Index..., as type: T.Type = T.self) -> AnyPublisher<T, Failure> where T: Equatable {
        self[.init(path: path)]
    }
    
    subscript<T>(path: JSON.Path, as type: T.Type = T.self) -> AnyPublisher<T, Failure> where T: Equatable {
        compactMap{
            switch $0 {
            case let fragment as T: return fragment
            case let array as [Any]: return array[keyPath: path] as? T
            case let dictionary as [String: Any]: return dictionary[keyPath: path] as? T
            default: return nil
            }
        }
        .scanNewValues()
        .eraseToAnyPublisher()
    }
    
}

extension Publisher {
    subscript<Value>(keyPath: KeyPath<Output, Value>) -> AnyPublisher<Value, Failure> where Value: Equatable {
        map(keyPath)
            .scanNewValues()
            .eraseToAnyPublisher()
    }
}

extension Publisher where Output: Equatable {
    func scanNewValues() -> AnyPublisher<Output, Failure> {
        scan((nil, nil), { ($1, $0.0) })
            .compactMap{ old, new -> (newValue: Output, oldValue: Output)? in
                guard let a = old else { return nil }
                guard let b = new else { return (a, a) }
                guard a != b else { return nil }
                return (a, b)
            }
            .map(\.newValue)
            .eraseToAnyPublisher()
    }
}
class Test {
    @Published var json: [String: Any] = [
        "string": "hello world",
        "int": 1,
        "structure": [
            "is": [
                "good": [
                    true,
                    [
                        "and": [
                            "i": [
                                "like": [
                                    "pie",
                                    "programming",
                                    "dogs"
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ]
}

let test = Test()
var bag = Set<AnyCancellable>()

test.$json["structure", "is", "good", 0, as: Bool.self].sink { any in
    print("🌍", any)
}.store(in: &bag)

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

// 🌍 true
// 🌍 false
// 🌍 true

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