Last active
January 7, 2024 03:30
-
-
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])
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 | |
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) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Combine: