Skip to content

Instantly share code, notes, and snippets.

@dhiraj
Created May 1, 2017 07:14
Show Gist options
  • Save dhiraj/cf8666bbc1ca7efefeb99112cd40c5ad to your computer and use it in GitHub Desktop.
Save dhiraj/cf8666bbc1ca7efefeb99112cd40c5ad to your computer and use it in GitHub Desktop.
//
// KeyPath.swift
// GIFMosCore
//
// Copied liberally by Dhiraj Gupta on 4/27/17 from https://oleb.net/blog/2017/01/dictionary-key-paths/
//
import Foundation
public struct KeyPath {
var segments: [String]
var isEmpty: Bool { return segments.isEmpty }
var path: String {
return segments.joined(separator: ".")
}
/// Strips off the first segment and returns a pair
/// consisting of the first segment and the remaining key path.
/// Returns nil if the key path has no segments.
func headAndTail() -> (head: String, tail: KeyPath)? {
guard !isEmpty else { return nil }
var tail = segments
let head = tail.removeFirst()
return (head, KeyPath(segments: tail))
}
}
/// Initializes a KeyPath with a string of the form "this.is.a.keypath"
extension KeyPath {
init(_ string: String) {
segments = string.components(separatedBy: ".")
}
}
extension KeyPath: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.init(value)
}
public init(unicodeScalarLiteral value: String) {
self.init(value)
}
public init(extendedGraphemeClusterLiteral value: String) {
self.init(value)
}
}
// Needed because Swift 3.0 doesn't support extensions with concrete
// same-type requirements (extension Dictionary where Key == String).
public protocol StringProtocol {
init(string s: String)
}
extension String: StringProtocol {
public init(string s: String) {
self = s
}
}
public extension Dictionary where Key: StringProtocol {
subscript(keyPath keyPath: KeyPath) -> Any? {
get {
switch keyPath.headAndTail() {
case nil:
// key path is empty.
return nil
case let (head, remainingKeyPath)? where remainingKeyPath.isEmpty:
// Reached the end of the key path.
let key = Key(string: head)
return self[key]
case let (head, remainingKeyPath)?:
// Key path has a tail we need to traverse.
let key = Key(string: head)
switch self[key] {
case let nestedDict as [Key: Any]:
// Next nest level is a dictionary.
// Start over with remaining key path.
return nestedDict[keyPath: remainingKeyPath]
default:
// Next nest level isn't a dictionary.
// Invalid key path, abort.
return nil
}
}
}
set {
switch keyPath.headAndTail() {
case nil:
// key path is empty.
return
case let (head, remainingKeyPath)? where remainingKeyPath.isEmpty:
// Reached the end of the key path.
let key = Key(string: head)
self[key] = newValue as? Value
case let (head, remainingKeyPath)?:
let key = Key(string: head)
let value = self[key]
switch value {
case var nestedDict as [Key: Any]:
// Key path has a tail we need to traverse
nestedDict[keyPath: remainingKeyPath] = newValue
self[key] = nestedDict as? Value
default:
// Invalid keyPath
return
}
}
}
}
}
@gmckenzi
Copy link

gmckenzi commented Sep 28, 2017

The current implementation won't automatically create the nested dictionaries for you. So, attempting to store the value "1" at the key path "a.b.c" in an empty dictionary won't work, because the last "default" block in the last case statement at line 105 simply returns with the Invalid keyPath comment. To make this possible, that default block should be modified as so:

default:
    // Store a new empty dictionary and continue
    var nestedDict = [Key: Any]()
    nestedDict[keyPath: remainingKeyPath] = newValue
    self[key] = nestedDict as? Value

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