Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to customize nested key delimiter #629

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -171,6 +197,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 @@ -203,6 +240,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 @@ -239,6 +288,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