Skip to content

Commit

Permalink
Refined Dictionary get/set extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
Alkenso committed Apr 3, 2022
1 parent f154fce commit 456c341
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 35 deletions.
59 changes: 24 additions & 35 deletions Sources/SwiftConvenience/Common/Extensions - Collections.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,56 +25,45 @@ import Foundation
// MARK: - Dictionary

extension Dictionary {
/// Get and set value in nested dictionary
/// 'Set' notes:
/// 1. if key path refers to nonexistent nested dictionary, it will be created as [AhyHashable: Any]
/// 2. if key path refers to type that cannot be casted to [AnyHashable: Any], 'set' will do nothing
/// 3. 'insert(value:at:)' is failable replacement of 'set'
/// Get the value in nested dictionary
public subscript(keyPath keyPath: [AnyHashable]) -> Any? {
get {
guard let lastKey = keyPath.last else { return nil }

var lastDict: [AnyHashable: Any] = self
for keyPathComponent in keyPath.dropLast() {
guard let nestedDict = lastDict[keyPathComponent] as? [AnyHashable: Any] else {
return nil
}
lastDict = nestedDict
guard let lastKey = keyPath.last else { return nil }

var lastDict: [AnyHashable: Any] = self
for keyPathComponent in keyPath.dropLast() {
guard let nestedDict = lastDict[keyPathComponent] as? [AnyHashable: Any] else {
return nil
}

return lastDict[lastKey]
}
set {
try? insert(value: newValue, at: keyPath)
lastDict = nestedDict
}

return lastDict[lastKey]
}

/// Inserts value into nested dictionary at key path
/// If nested dictionary(one or multiple) does not exist, they are created
/// If nested dictionary(one or multiple) does not exist, they are created as [AnyHashable: Any]
/// If value at any nested level according to key path has unappropriate type, the error is thrown
public mutating func insert(value: Any?, at keyPath: [AnyHashable]) throws {
guard let nextKey = keyPath.first as? Key else { return }

let nestedKeyPath = Array(keyPath.dropFirst())
guard !nestedKeyPath.isEmpty else {
let typedValue = try (value as? Value)
.get(ifNil: CommonError.cast(
value,
to: Value.self,
description: "Failed to insert value of unappropriate type"
))
let typedValue = try (value as? Value).get(ifNil: CommonError.cast(
value,
to: Value.self,
description: "Failed to insert value of unappropriate type"
))
self[nextKey] = typedValue
return
}

var nested = try nestedDict(for: nextKey)
try nested.insert(value: value, at: nestedKeyPath)
self[nextKey] = try (nested as? Value)
.get(ifNil: CommonError.cast(
value,
to: Value.self,
description: "Failed to insert value of unappropriate type as nested dictionary"
))
self[nextKey] = try (nested as? Value).get(ifNil: CommonError.cast(
value,
to: Value.self,
description: "Failed to insert value of unappropriate type as nested dictionary"
))
}

private func nestedDict(for key: Key) throws -> [AnyHashable: Any] {
Expand All @@ -90,10 +79,10 @@ extension Dictionary {
}

extension Dictionary {
/// Get and set value in nested dictionary using dot-separated key path
/// Get value in nested dictionary using dot-separated key path.
/// Keys in dictionary at keyPath componenets must be of String type
public subscript(dotPath dotPath: String) -> Any? {
get { self[keyPath: dotPath.components(separatedBy: ".")] }
set { try? insert(value: newValue, at: dotPath) }
self[keyPath: dotPath.components(separatedBy: ".")]
}

/// Inserts value in nested dictionary using dot-separated key path
Expand Down
56 changes: 56 additions & 0 deletions Tests/SwiftConvenienceTests/Extensions Tests/DictionaryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import SwiftConvenience

import XCTest

class DictionaryTests: XCTestCase {
func test_keyPath() {
let dict: [String : Any] = [
"lv1_key1": "lv1_val1",
"lv1_key2": [
2: "lv2_val2",
"lv2_key1": 21,
"lv2_key2": [
"lv3_key1": "lv3_val1"
]
]
]

XCTAssertEqual(dict[keyPath: ["lv1_key1"]] as? String, "lv1_val1")
XCTAssertEqual(dict[keyPath: ["lv1_key2", 2]] as? String, "lv2_val2")
XCTAssertEqual(dict[keyPath: ["lv1_key2", "lv2_key1"]] as? Int, 21)
XCTAssertEqual(dict[keyPath: ["lv1_key2", "lv2_key2", "lv3_key1"]] as? String, "lv3_val1")

XCTAssertEqual(dict[dotPath: "lv1_key1"] as? String, "lv1_val1")
XCTAssertNil(dict[dotPath: "lv1_key2.2"])
XCTAssertEqual(dict[dotPath: "lv1_key2.lv2_key1"] as? Int, 21)
XCTAssertEqual(dict[dotPath: "lv1_key2.lv2_key2.lv3_key1"] as? String, "lv3_val1")
}

func test_insertKeyPath() throws {
var dict: [String : Any] = [:]

try dict.insert(value: "lv1_val1", at: ["lv1_key1"])
try dict.insert(value: "lv2_val2", at: ["lv1_key2", 2])
try dict.insert(value: 21, at: ["lv1_key2", "lv2_key1"])
try dict.insert(value: "lv3_val1", at: ["lv1_key2", "lv2_key2", "lv3_key1"])

XCTAssertEqual(dict[keyPath: ["lv1_key1"]] as? String, "lv1_val1")
XCTAssertEqual(dict[keyPath: ["lv1_key2", 2]] as? String, "lv2_val2")
XCTAssertEqual(dict[keyPath: ["lv1_key2", "lv2_key1"]] as? Int, 21)
XCTAssertEqual(dict[keyPath: ["lv1_key2", "lv2_key2", "lv3_key1"]] as? String, "lv3_val1")
}

func test_insertDotPath() throws {
var dict: [String : Any] = [:]

try dict.insert(value: "lv1_val1", at: ["lv1_key1"])
try dict.insert(value: "lv2_val2", at: "lv1_key2.2")
try dict.insert(value: 21, at: "lv1_key2.lv2_key1")
try dict.insert(value: "lv3_val1", at: "lv1_key2.lv2_key2.lv3_key1")

XCTAssertEqual(dict[keyPath: ["lv1_key1"]] as? String, "lv1_val1")
XCTAssertEqual(dict[keyPath: ["lv1_key2", "2"]] as? String, "lv2_val2")
XCTAssertEqual(dict[keyPath: ["lv1_key2", "lv2_key1"]] as? Int, 21)
XCTAssertEqual(dict[keyPath: ["lv1_key2", "lv2_key2", "lv3_key1"]] as? String, "lv3_val1")
}
}

0 comments on commit 456c341

Please sign in to comment.