Skip to content

Commit

Permalink
Merge pull request #629 from devxoul/map-nested-key-delimiter
Browse files Browse the repository at this point in the history
Add support to customize nested key delimiter
  • Loading branch information
tristanhimmelman authored Oct 22, 2016
2 parents 77036d8 + c21bf08 commit 4553dff
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 30 deletions.
33 changes: 17 additions & 16 deletions Sources/ImmutableMappable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,25 @@ public extension ImmutableMappable {

public extension Map {

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

// MARK: Basic

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

/// Returns a transformed value or throws an error.
public func value<Transform: TransformType>(_ key: String, using transform: Transform, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> Transform.Object {
let currentValue = self.currentValue(for: key)
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 {
let currentValue = self.currentValue(for: key, nested: nested, delimiter: delimiter)
guard let value = transform.transformFromJSON(currentValue) else {
throw MapError(key: key, currentValue: currentValue, reason: "Cannot transform to '\(Transform.Object.self)' using \(transform)", file: file, function: function, line: line)
}
Expand All @@ -81,16 +82,16 @@ public extension Map {
// MARK: BaseMappable

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

// MARK: [BaseMappable]

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

/// Returns a `[BaseMappable]` using transform or throws an error.
public func value<Transform: TransformType>(_ key: String, using transform: Transform, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> [Transform.Object] {
let currentValue = self.currentValue(for: key)
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] {
let currentValue = self.currentValue(for: key, nested: nested, delimiter: delimiter)
guard let jsonArray = currentValue as? [Any] else {
throw MapError(key: key, currentValue: currentValue, reason: "Cannot cast to '[Any]'", file: file, function: function, line: line)
}
Expand All @@ -116,8 +117,8 @@ public extension Map {
// MARK: [String: BaseMappable]

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

/// Returns a `[String: BaseMappable]` using transform or throws an error.
public func value<Transform: TransformType>(_ key: String, using transform: Transform, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) throws -> [String: Transform.Object] {
let currentValue = self.currentValue(for: key)
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] {
let currentValue = self.currentValue(for: key, nested: nested, delimiter: delimiter)
guard let jsonDictionary = currentValue as? [String: Any] else {
throw MapError(key: key, currentValue: currentValue, reason: "Cannot cast to '[String: Any]'", file: file, function: function, line: line)
}
Expand Down
36 changes: 25 additions & 11 deletions Sources/Map.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public final class Map {
public internal(set) var currentValue: Any?
public internal(set) var currentKey: String?
var keyIsNested = false
public internal(set) var nestedKeyDelimiter: String = "."
public var context: MapContext?

let toObject: Bool // indicates whether the mapping is being applied to an existing object
Expand All @@ -58,23 +59,36 @@ public final class Map {
/// The Key paramater can be a period separated string (ex. "distance.value") to access sub objects.
public subscript(key: String) -> Map {
// save key and value associated to it
let nested = key.contains(".")
return self[key, nested: nested, ignoreNil: false]
return self[key, delimiter: ".", ignoreNil: false]
}

public subscript(key: String, delimiter delimiter: String) -> Map {
let nested = key.contains(delimiter)
return self[key, nested: nested, delimiter: delimiter, ignoreNil: false]
}

public subscript(key: String, nested nested: Bool) -> Map {
return self[key, nested: nested, ignoreNil: false]
return self[key, nested: nested, delimiter: ".", ignoreNil: false]
}

public subscript(key: String, ignoreNil ignoreNil: Bool) -> Map {
let nested = key.contains(".")
return self[key, nested: nested, ignoreNil: ignoreNil]
public subscript(key: String, nested nested: Bool, delimiter delimiter: String) -> Map {
return self[key, nested: nested, delimiter: delimiter, ignoreNil: false]
}

public subscript(key: String, ignoreNil ignoreNil: Bool) -> Map {
return self[key, delimiter: ".", ignoreNil: ignoreNil]
}
public subscript(key: String, delimiter delimiter: String, ignoreNil ignoreNil: Bool) -> Map {
let nested = key.contains(delimiter)
return self[key, nested: nested, delimiter: delimiter, ignoreNil: ignoreNil]
}

public subscript(key: String, nested nested: Bool, ignoreNil ignoreNil: Bool) -> Map {

public subscript(key: String, nested nested: Bool, ignoreNil ignoreNil: Bool) -> Map {
return self[key, nested: nested, delimiter: ".", ignoreNil: ignoreNil]
}
public subscript(key: String, nested nested: Bool, delimiter delimiter: String, ignoreNil ignoreNil: Bool) -> Map {
// save key and value associated to it
currentKey = key
keyIsNested = nested
nestedKeyDelimiter = delimiter

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

// update isKeyPresent if ignoreNil is true
Expand Down
6 changes: 3 additions & 3 deletions Sources/ToJSON.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@
import class Foundation.NSNumber

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

private func setValue(_ value: Any, key: String, checkForNestedKeys: Bool, dictionary: inout [String : Any]) {
private func setValue(_ value: Any, key: String, checkForNestedKeys: Bool, delimiter: String, dictionary: inout [String : Any]) {
if checkForNestedKeys {
let keyComponents = ArraySlice(key.characters.split { $0 == "." })
let keyComponents = ArraySlice(key.components(separatedBy: delimiter).filter { !$0.isEmpty }.map { $0.characters })
setValue(value, forKeyPathComponents: keyComponents, dictionary: &dictionary)
} else {
dictionary[key] = value
Expand Down
61 changes: 61 additions & 0 deletions Tests/ImmutableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ class ImmutableObjectTests: XCTestCase {
"prop24": 255,
"prop25": true,
"prop26": 255.0,

"non.nested->key": "string",
"nested": [
"int": 123,
"string": "hello",
"array": ["a", "b", "c"],
"dictionary": ["a": 10, "b": 20, "c": 30],
],
"com.hearst.ObjectMapper.nested": [
"com.hearst.ObjectMapper.int": 123,
"com.hearst.ObjectMapper.string": "hello",
"array": ["a", "b", "c"],
"dictionary": ["a": 10, "b": 20, "c": 30],
]
]

func testImmutableMappable() {
Expand Down Expand Up @@ -115,6 +129,18 @@ class ImmutableObjectTests: XCTestCase {
XCTAssertEqual(immutable.prop24!, 255)
XCTAssertEqual(immutable.prop25!, true)
XCTAssertEqual(immutable.prop26!, 255.0)

XCTAssertEqual(immutable.nonnestedString, "string")

XCTAssertEqual(immutable.nestedInt, 123)
XCTAssertEqual(immutable.nestedString, "hello")
XCTAssertEqual(immutable.nestedArray, ["a", "b", "c"])
XCTAssertEqual(immutable.nestedDictionary, ["a": 10, "b": 20, "c": 30])

XCTAssertEqual(immutable.delimiterNestedInt, 123)
XCTAssertEqual(immutable.delimiterNestedString, "hello")
XCTAssertEqual(immutable.delimiterNestedArray, ["a", "b", "c"])
XCTAssertEqual(immutable.delimiterNestedDictionary, ["a": 10, "b": 20, "c": 30])

let JSON2: [String: Any] = [ "prop1": "prop1", "prop2": NSNull() ]
let immutable2 = try? mapper.map(JSON: JSON2)
Expand Down Expand Up @@ -259,6 +285,17 @@ struct Struct {
var prop24: Int?
var prop25: Bool?
var prop26: Double?

var nonnestedString: String
var nestedInt: Int
var nestedString: String
var nestedArray: [String]
var nestedDictionary: [String: Int]

var delimiterNestedInt: Int
var delimiterNestedString: String
var delimiterNestedArray: [String]
var delimiterNestedDictionary: [String: Int]
}

extension Struct: ImmutableMappable {
Expand Down Expand Up @@ -291,6 +328,18 @@ extension Struct: ImmutableMappable {
prop20 = try map.value("prop20")
prop21 = try? map.value("prop21")
prop22 = try? map.value("prop22")

nonnestedString = try map.value("non.nested->key", nested: false)

nestedInt = try map.value("nested.int")
nestedString = try map.value("nested.string")
nestedArray = try map.value("nested.array")
nestedDictionary = try map.value("nested.dictionary")

delimiterNestedInt = try map.value("com.hearst.ObjectMapper.nested->com.hearst.ObjectMapper.int", delimiter: "->")
delimiterNestedString = try map.value("com.hearst.ObjectMapper.nested->com.hearst.ObjectMapper.string", delimiter: "->")
delimiterNestedArray = try map.value("com.hearst.ObjectMapper.nested->array", delimiter: "->")
delimiterNestedDictionary = try map.value("com.hearst.ObjectMapper.nested->dictionary", delimiter: "->")
}

mutating func mapping(map: Map) {
Expand Down Expand Up @@ -327,6 +376,18 @@ extension Struct: ImmutableMappable {
prop20 >>> map["prop20"]
prop21 >>> map["prop21"]
prop22 >>> map["prop22"]

nonnestedString >>> map["non.nested->key", nested: false]

nestedInt >>> map["nested.int"]
nestedString >>> map["nested.string"]
nestedArray >>> map["nested.array"]
nestedDictionary >>> map["nested.dictionary"]

delimiterNestedInt >>> map["com.hearst.ObjectMapper.nested->com.hearst.ObjectMapper.int", delimiter: "->"]
delimiterNestedString >>> map["com.hearst.ObjectMapper.nested->com.hearst.ObjectMapper.string", delimiter: "->"]
delimiterNestedArray >>> map["com.hearst.ObjectMapper.nested->array", delimiter: "->"]
delimiterNestedDictionary >>> map["com.hearst.ObjectMapper.nested->dictionary", delimiter: "->"]
}
}

Expand Down
Loading

0 comments on commit 4553dff

Please sign in to comment.