Created
May 1, 2017 07:14
-
-
Save dhiraj/cf8666bbc1ca7efefeb99112cd40c5ad to your computer and use it in GitHub Desktop.
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
// | |
// 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 | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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: