Skip to content

Commit 4553dff

Browse files
Merge pull request #629 from devxoul/map-nested-key-delimiter
Add support to customize nested key delimiter
2 parents 77036d8 + c21bf08 commit 4553dff

File tree

5 files changed

+262
-30
lines changed

5 files changed

+262
-30
lines changed

Sources/ImmutableMappable.swift

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,25 @@ public extension ImmutableMappable {
5454

5555
public extension Map {
5656

57-
fileprivate func currentValue(for key: String) -> Any? {
58-
return self[key].currentValue
57+
fileprivate func currentValue(for key: String, nested: Bool? = nil, delimiter: String = ".") -> Any? {
58+
let isNested = nested ?? key.contains(delimiter)
59+
return self[key, nested: isNested, delimiter: delimiter].currentValue
5960
}
6061

6162
// MARK: Basic
6263

6364
/// Returns a value or throws an error.
64-
public func value<T>(_ key: String, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> T {
65-
let currentValue = self.currentValue(for: key)
65+
public func value<T>(_ key: String, nested: Bool? = nil, delimiter: String = ".", file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> T {
66+
let currentValue = self.currentValue(for: key, nested: nested, delimiter: delimiter)
6667
guard let value = currentValue as? T else {
6768
throw MapError(key: key, currentValue: currentValue, reason: "Cannot cast to '\(T.self)'", file: file, function: function, line: line)
6869
}
6970
return value
7071
}
7172

7273
/// Returns a transformed value or throws an error.
73-
public func value<Transform: TransformType>(_ key: String, using transform: Transform, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> Transform.Object {
74-
let currentValue = self.currentValue(for: key)
74+
public func value<Transform: TransformType>(_ key: String, nested: Bool? = nil, delimiter: String = ".", using transform: Transform, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> Transform.Object {
75+
let currentValue = self.currentValue(for: key, nested: nested, delimiter: delimiter)
7576
guard let value = transform.transformFromJSON(currentValue) else {
7677
throw MapError(key: key, currentValue: currentValue, reason: "Cannot transform to '\(Transform.Object.self)' using \(transform)", file: file, function: function, line: line)
7778
}
@@ -81,16 +82,16 @@ public extension Map {
8182
// MARK: BaseMappable
8283

8384
/// Returns a `BaseMappable` object or throws an error.
84-
public func value<T: BaseMappable>(_ key: String) throws -> T {
85-
let currentValue = self.currentValue(for: key)
85+
public func value<T: BaseMappable>(_ key: String, nested: Bool? = nil, delimiter: String = ".") throws -> T {
86+
let currentValue = self.currentValue(for: key, nested: nested, delimiter: delimiter)
8687
return try Mapper<T>().mapOrFail(JSONObject: currentValue)
8788
}
8889

8990
// MARK: [BaseMappable]
9091

9192
/// Returns a `[BaseMappable]` or throws an error.
92-
public func value<T: BaseMappable>(_ key: String, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> [T] {
93-
let currentValue = self.currentValue(for: key)
93+
public func value<T: BaseMappable>(_ key: String, nested: Bool? = nil, delimiter: String = ".", file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> [T] {
94+
let currentValue = self.currentValue(for: key, nested: nested, delimiter: delimiter)
9495
guard let jsonArray = currentValue as? [Any] else {
9596
throw MapError(key: key, currentValue: currentValue, reason: "Cannot cast to '[Any]'", file: file, function: function, line: line)
9697
}
@@ -100,8 +101,8 @@ public extension Map {
100101
}
101102

102103
/// Returns a `[BaseMappable]` using transform or throws an error.
103-
public func value<Transform: TransformType>(_ key: String, using transform: Transform, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> [Transform.Object] {
104-
let currentValue = self.currentValue(for: key)
104+
public func value<Transform: TransformType>(_ key: String, nested: Bool? = nil, delimiter: String = ".", using transform: Transform, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> [Transform.Object] {
105+
let currentValue = self.currentValue(for: key, nested: nested, delimiter: delimiter)
105106
guard let jsonArray = currentValue as? [Any] else {
106107
throw MapError(key: key, currentValue: currentValue, reason: "Cannot cast to '[Any]'", file: file, function: function, line: line)
107108
}
@@ -116,8 +117,8 @@ public extension Map {
116117
// MARK: [String: BaseMappable]
117118

118119
/// Returns a `[String: BaseMappable]` or throws an error.
119-
public func value<T: BaseMappable>(_ key: String, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> [String: T] {
120-
let currentValue = self.currentValue(for: key)
120+
public func value<T: BaseMappable>(_ key: String, nested: Bool? = nil, delimiter: String = ".", file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> [String: T] {
121+
let currentValue = self.currentValue(for: key, nested: nested, delimiter: delimiter)
121122
guard let jsonDictionary = currentValue as? [String: Any] else {
122123
throw MapError(key: key, currentValue: currentValue, reason: "Cannot cast to '[String: Any]'", file: file, function: function, line: line)
123124
}
@@ -129,8 +130,8 @@ public extension Map {
129130
}
130131

131132
/// Returns a `[String: BaseMappable]` using transform or throws an error.
132-
public func value<Transform: TransformType>(_ key: String, using transform: Transform, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> [String: Transform.Object] {
133-
let currentValue = self.currentValue(for: key)
133+
public func value<Transform: TransformType>(_ key: String, nested: Bool? = nil, delimiter: String = ".", using transform: Transform, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> [String: Transform.Object] {
134+
let currentValue = self.currentValue(for: key, nested: nested, delimiter: delimiter)
134135
guard let jsonDictionary = currentValue as? [String: Any] else {
135136
throw MapError(key: key, currentValue: currentValue, reason: "Cannot cast to '[String: Any]'", file: file, function: function, line: line)
136137
}

Sources/Map.swift

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public final class Map {
4343
public internal(set) var currentValue: Any?
4444
public internal(set) var currentKey: String?
4545
var keyIsNested = false
46+
public internal(set) var nestedKeyDelimiter: String = "."
4647
public var context: MapContext?
4748

4849
let toObject: Bool // indicates whether the mapping is being applied to an existing object
@@ -58,23 +59,36 @@ public final class Map {
5859
/// The Key paramater can be a period separated string (ex. "distance.value") to access sub objects.
5960
public subscript(key: String) -> Map {
6061
// save key and value associated to it
61-
let nested = key.contains(".")
62-
return self[key, nested: nested, ignoreNil: false]
62+
return self[key, delimiter: ".", ignoreNil: false]
6363
}
64-
64+
public subscript(key: String, delimiter delimiter: String) -> Map {
65+
let nested = key.contains(delimiter)
66+
return self[key, nested: nested, delimiter: delimiter, ignoreNil: false]
67+
}
68+
6569
public subscript(key: String, nested nested: Bool) -> Map {
66-
return self[key, nested: nested, ignoreNil: false]
70+
return self[key, nested: nested, delimiter: ".", ignoreNil: false]
6771
}
68-
69-
public subscript(key: String, ignoreNil ignoreNil: Bool) -> Map {
70-
let nested = key.contains(".")
71-
return self[key, nested: nested, ignoreNil: ignoreNil]
72+
public subscript(key: String, nested nested: Bool, delimiter delimiter: String) -> Map {
73+
return self[key, nested: nested, delimiter: delimiter, ignoreNil: false]
74+
}
75+
76+
public subscript(key: String, ignoreNil ignoreNil: Bool) -> Map {
77+
return self[key, delimiter: ".", ignoreNil: ignoreNil]
78+
}
79+
public subscript(key: String, delimiter delimiter: String, ignoreNil ignoreNil: Bool) -> Map {
80+
let nested = key.contains(delimiter)
81+
return self[key, nested: nested, delimiter: delimiter, ignoreNil: ignoreNil]
7282
}
73-
74-
public subscript(key: String, nested nested: Bool, ignoreNil ignoreNil: Bool) -> Map {
83+
84+
public subscript(key: String, nested nested: Bool, ignoreNil ignoreNil: Bool) -> Map {
85+
return self[key, nested: nested, delimiter: ".", ignoreNil: ignoreNil]
86+
}
87+
public subscript(key: String, nested nested: Bool, delimiter delimiter: String, ignoreNil ignoreNil: Bool) -> Map {
7588
// save key and value associated to it
7689
currentKey = key
7790
keyIsNested = nested
91+
nestedKeyDelimiter = delimiter
7892

7993
// check if a value exists for the current key
8094
// do this pre-check for performance reasons
@@ -85,7 +99,7 @@ public final class Map {
8599
currentValue = isNSNull ? nil : object
86100
} else {
87101
// break down the components of the key that are separated by .
88-
(isKeyPresent, currentValue) = valueFor(ArraySlice(key.components(separatedBy: ".")), dictionary: JSON)
102+
(isKeyPresent, currentValue) = valueFor(ArraySlice(key.components(separatedBy: delimiter)), dictionary: JSON)
89103
}
90104

91105
// update isKeyPresent if ignoreNil is true

Sources/ToJSON.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@
2929
import class Foundation.NSNumber
3030

3131
private func setValue(_ value: Any, map: Map) {
32-
setValue(value, key: map.currentKey!, checkForNestedKeys: map.keyIsNested, dictionary: &map.JSON)
32+
setValue(value, key: map.currentKey!, checkForNestedKeys: map.keyIsNested, delimiter: map.nestedKeyDelimiter, dictionary: &map.JSON)
3333
}
3434

35-
private func setValue(_ value: Any, key: String, checkForNestedKeys: Bool, dictionary: inout [String : Any]) {
35+
private func setValue(_ value: Any, key: String, checkForNestedKeys: Bool, delimiter: String, dictionary: inout [String : Any]) {
3636
if checkForNestedKeys {
37-
let keyComponents = ArraySlice(key.characters.split { $0 == "." })
37+
let keyComponents = ArraySlice(key.components(separatedBy: delimiter).filter { !$0.isEmpty }.map { $0.characters })
3838
setValue(value, forKeyPathComponents: keyComponents, dictionary: &dictionary)
3939
} else {
4040
dictionary[key] = value

Tests/ImmutableTests.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ class ImmutableObjectTests: XCTestCase {
7474
"prop24": 255,
7575
"prop25": true,
7676
"prop26": 255.0,
77+
78+
"non.nested->key": "string",
79+
"nested": [
80+
"int": 123,
81+
"string": "hello",
82+
"array": ["a", "b", "c"],
83+
"dictionary": ["a": 10, "b": 20, "c": 30],
84+
],
85+
"com.hearst.ObjectMapper.nested": [
86+
"com.hearst.ObjectMapper.int": 123,
87+
"com.hearst.ObjectMapper.string": "hello",
88+
"array": ["a", "b", "c"],
89+
"dictionary": ["a": 10, "b": 20, "c": 30],
90+
]
7791
]
7892

7993
func testImmutableMappable() {
@@ -115,6 +129,18 @@ class ImmutableObjectTests: XCTestCase {
115129
XCTAssertEqual(immutable.prop24!, 255)
116130
XCTAssertEqual(immutable.prop25!, true)
117131
XCTAssertEqual(immutable.prop26!, 255.0)
132+
133+
XCTAssertEqual(immutable.nonnestedString, "string")
134+
135+
XCTAssertEqual(immutable.nestedInt, 123)
136+
XCTAssertEqual(immutable.nestedString, "hello")
137+
XCTAssertEqual(immutable.nestedArray, ["a", "b", "c"])
138+
XCTAssertEqual(immutable.nestedDictionary, ["a": 10, "b": 20, "c": 30])
139+
140+
XCTAssertEqual(immutable.delimiterNestedInt, 123)
141+
XCTAssertEqual(immutable.delimiterNestedString, "hello")
142+
XCTAssertEqual(immutable.delimiterNestedArray, ["a", "b", "c"])
143+
XCTAssertEqual(immutable.delimiterNestedDictionary, ["a": 10, "b": 20, "c": 30])
118144

119145
let JSON2: [String: Any] = [ "prop1": "prop1", "prop2": NSNull() ]
120146
let immutable2 = try? mapper.map(JSON: JSON2)
@@ -259,6 +285,17 @@ struct Struct {
259285
var prop24: Int?
260286
var prop25: Bool?
261287
var prop26: Double?
288+
289+
var nonnestedString: String
290+
var nestedInt: Int
291+
var nestedString: String
292+
var nestedArray: [String]
293+
var nestedDictionary: [String: Int]
294+
295+
var delimiterNestedInt: Int
296+
var delimiterNestedString: String
297+
var delimiterNestedArray: [String]
298+
var delimiterNestedDictionary: [String: Int]
262299
}
263300

264301
extension Struct: ImmutableMappable {
@@ -291,6 +328,18 @@ extension Struct: ImmutableMappable {
291328
prop20 = try map.value("prop20")
292329
prop21 = try? map.value("prop21")
293330
prop22 = try? map.value("prop22")
331+
332+
nonnestedString = try map.value("non.nested->key", nested: false)
333+
334+
nestedInt = try map.value("nested.int")
335+
nestedString = try map.value("nested.string")
336+
nestedArray = try map.value("nested.array")
337+
nestedDictionary = try map.value("nested.dictionary")
338+
339+
delimiterNestedInt = try map.value("com.hearst.ObjectMapper.nested->com.hearst.ObjectMapper.int", delimiter: "->")
340+
delimiterNestedString = try map.value("com.hearst.ObjectMapper.nested->com.hearst.ObjectMapper.string", delimiter: "->")
341+
delimiterNestedArray = try map.value("com.hearst.ObjectMapper.nested->array", delimiter: "->")
342+
delimiterNestedDictionary = try map.value("com.hearst.ObjectMapper.nested->dictionary", delimiter: "->")
294343
}
295344

296345
mutating func mapping(map: Map) {
@@ -327,6 +376,18 @@ extension Struct: ImmutableMappable {
327376
prop20 >>> map["prop20"]
328377
prop21 >>> map["prop21"]
329378
prop22 >>> map["prop22"]
379+
380+
nonnestedString >>> map["non.nested->key", nested: false]
381+
382+
nestedInt >>> map["nested.int"]
383+
nestedString >>> map["nested.string"]
384+
nestedArray >>> map["nested.array"]
385+
nestedDictionary >>> map["nested.dictionary"]
386+
387+
delimiterNestedInt >>> map["com.hearst.ObjectMapper.nested->com.hearst.ObjectMapper.int", delimiter: "->"]
388+
delimiterNestedString >>> map["com.hearst.ObjectMapper.nested->com.hearst.ObjectMapper.string", delimiter: "->"]
389+
delimiterNestedArray >>> map["com.hearst.ObjectMapper.nested->array", delimiter: "->"]
390+
delimiterNestedDictionary >>> map["com.hearst.ObjectMapper.nested->dictionary", delimiter: "->"]
330391
}
331392
}
332393

0 commit comments

Comments
 (0)