From 444d8cb36964535871ddd4953e9e6a3e327786df Mon Sep 17 00:00:00 2001 From: xinting Date: Thu, 14 Dec 2023 14:55:37 +0800 Subject: [PATCH] Add Multi-Keys to single property support --- ObjectMapper.xcodeproj/project.pbxproj | 8 + README.md | 9 + Sources/Map.swift | 44 +++++ Tests/ObjectMapperTests/MultiKeysTests.swift | 173 +++++++++++++++++++ 4 files changed, 234 insertions(+) create mode 100644 Tests/ObjectMapperTests/MultiKeysTests.swift diff --git a/ObjectMapper.xcodeproj/project.pbxproj b/ObjectMapper.xcodeproj/project.pbxproj index 24b928f3..f50657eb 100644 --- a/ObjectMapper.xcodeproj/project.pbxproj +++ b/ObjectMapper.xcodeproj/project.pbxproj @@ -30,6 +30,9 @@ 491D799D216FBA9A006DB931 /* CodableTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D7998216FBA93006DB931 /* CodableTransform.swift */; }; 491D799E216FBA9B006DB931 /* CodableTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D7998216FBA93006DB931 /* CodableTransform.swift */; }; 491D799F216FBA9B006DB931 /* CodableTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D7998216FBA93006DB931 /* CodableTransform.swift */; }; + 563845552B2ADE3A004AB0AE /* MultiKeysTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563845542B2ADE3A004AB0AE /* MultiKeysTests.swift */; }; + 563845562B2ADE3A004AB0AE /* MultiKeysTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563845542B2ADE3A004AB0AE /* MultiKeysTests.swift */; }; + 563845572B2ADE3A004AB0AE /* MultiKeysTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563845542B2ADE3A004AB0AE /* MultiKeysTests.swift */; }; 6100B1C01BD76A030011114A /* NestedArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6100B1BF1BD76A020011114A /* NestedArrayTests.swift */; }; 6A05B7B01BE274BE00F19B53 /* ObjectMapper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A05B7A61BE274BE00F19B53 /* ObjectMapper.framework */; }; 6A05B7BD1BE274E800F19B53 /* ObjectMapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 6AAC8F7B19F03C2900E7A677 /* ObjectMapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -261,6 +264,7 @@ 3BAD2C0F1BDDC0B000E6B203 /* MappableExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MappableExtensionsTests.swift; path = ObjectMapperTests/MappableExtensionsTests.swift; sourceTree = ""; }; 491D7994216FB8B2006DB931 /* CodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CodableTests.swift; path = ObjectMapperTests/CodableTests.swift; sourceTree = ""; }; 491D7998216FBA93006DB931 /* CodableTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableTransform.swift; sourceTree = ""; }; + 563845542B2ADE3A004AB0AE /* MultiKeysTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MultiKeysTests.swift; path = ObjectMapperTests/MultiKeysTests.swift; sourceTree = ""; }; 6100B1BF1BD76A020011114A /* NestedArrayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = NestedArrayTests.swift; path = ObjectMapperTests/NestedArrayTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 6A05B7A61BE274BE00F19B53 /* ObjectMapper.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ObjectMapper.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6A05B7AF1BE274BE00F19B53 /* ObjectMapper-tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ObjectMapper-tvOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -435,6 +439,7 @@ 6AAC8F8219F03C2900E7A677 /* ObjectMapperTests */ = { isa = PBXGroup; children = ( + 563845542B2ADE3A004AB0AE /* MultiKeysTests.swift */, 491D7994216FB8B2006DB931 /* CodableTests.swift */, 6A6AEB971A9387D0002573D3 /* BasicTypes.swift */, 6A3774331A31427F00CC0AB5 /* BasicTypesTestsToJSON.swift */, @@ -885,6 +890,7 @@ 6AC692461BE3FD45004C119A /* NestedArrayTests.swift in Sources */, DC99C8CE1CA261AE005C788C /* NullableKeysFromJSONTests.swift in Sources */, 6AC692471BE3FD45004C119A /* ObjectMapperTests.swift in Sources */, + 563845572B2ADE3A004AB0AE /* MultiKeysTests.swift in Sources */, 030114AE1D951A5600FBFD4F /* ImmutableTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -976,6 +982,7 @@ 6AAC8F8619F03C2900E7A677 /* ObjectMapperTests.swift in Sources */, DC99C8CC1CA261A8005C788C /* NullableKeysFromJSONTests.swift in Sources */, 6100B1C01BD76A030011114A /* NestedArrayTests.swift in Sources */, + 563845552B2ADE3A004AB0AE /* MultiKeysTests.swift in Sources */, 6AC53CAC1F03FA1B008BDDCD /* ImmutableTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1036,6 +1043,7 @@ 6A412A251BB0DA26001C3F67 /* PerformanceTests.swift in Sources */, DC99C8CD1CA261AD005C788C /* NullableKeysFromJSONTests.swift in Sources */, CD1603281AC02480000CD69A /* CustomTransformTests.swift in Sources */, + 563845562B2ADE3A004AB0AE /* MultiKeysTests.swift in Sources */, 030114AD1D951A5300FBFD4F /* ImmutableTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/README.md b/README.md index 2aacbd8e..f5b39c6a 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,15 @@ struct Temperature: Mappable { } ``` +If the `username` has alternative value from key `nickname`, you can use `or` like: +``` +func mapping(map: Map) { + //... + username <- map["username"].or["nickname"] + //... +} +``` + Once your class implements `Mappable`, ObjectMapper allows you to easily convert to and from JSON. Convert a JSON string to a model object: diff --git a/Sources/Map.swift b/Sources/Map.swift index 1ca9903a..09800a99 100644 --- a/Sources/Map.swift +++ b/Sources/Map.swift @@ -244,3 +244,47 @@ public extension Map { } } } + +// MARK: - Multi Key Support for Map +public extension Map { + + /// If can't get value from primary choice, use this instead + /// first json data like: + /// ```json + /// { + /// username: "John Doe" + /// } + /// ``` + /// second json data like: + /// ```json + /// { + /// nickname: "John" + /// } + /// Usage: + /// ```swift + /// func mapping(map: Map) { + /// //... + /// name <- map["username"].or["nickname"] + /// //... + /// } + /// ``` + /// You can get valid name either the first or the second json data. + var or: MapOr { .init(self) } +} + +public class MapOr { + var map: Map + init(_ map: Map) { + self.map = map + } + + public subscript(key: String, nested nested: Bool? = nil, delimiter delimiter: String = ".", ignoreNil ignoreNil: Bool = false) -> Map { + guard map.currentValue == nil else { + return map + } + + _ = map[key, nested: nested, delimiter: delimiter, ignoreNil: ignoreNil] + + return map + } +} diff --git a/Tests/ObjectMapperTests/MultiKeysTests.swift b/Tests/ObjectMapperTests/MultiKeysTests.swift new file mode 100644 index 00000000..283d63c2 --- /dev/null +++ b/Tests/ObjectMapperTests/MultiKeysTests.swift @@ -0,0 +1,173 @@ +// +// MultiKeysTests.swift +// ObjectMapper +// +// Created by xinting on 2023/12/14. +// Copyright © 2023 hearst. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2014-2018 Tristan Himmelman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +import ObjectMapper + +class MultiKeysTests: ObjectMapperTests { + let multiKeyUserMapper = Mapper() + + func testBasicMultiParsing() { + let username = "John Doe" + let nickname = "John" + let identifier = "user8723" + let photoCount = 13 + let age = 1227 + let weight = 123.23 + let weightInKG = 180.0 + let float: Float = 123.231 + let drinker = true + let smoker = false + let sex: Sex = .Female + let gender: Sex = .Male + let canDrive = true + let subUserJSON: [String: Any] = [ + "identifier": "user8723", + "drinker": true, + "age": 17, + "username": "sub user", + "canDrive": canDrive + ] + let classMate: [String: Any] = [ + "identifier": "classmate8723", + "drinker": false, + "age": 18, + "username": "sub user classmate", + "canDrive": canDrive + ] + + var userJson: [String: Any] = [ + "username": username, + "nickname": nickname, + "identifier": identifier, + "photoCount": photoCount, + "age": age, + "drinker": drinker, + "smoker": smoker, + "sex": sex.rawValue, + "gender": gender.rawValue, + "canDrive": canDrive, + "arr": [ + "bla", true, 42 + ], + "dict": [ + "key1": "value1", + "key2": false, + "key3": 142 + ], + "arrOpt": [ + "bla", true, 42 + ], + "dictOpt": [ + "key1": "value1", + "key2": false, + "key3": 142 + ], + "weight": weight, + "weightInKG": weightInKG, + "float": float, + "friend": subUserJSON, + "classmate": classMate, + "friendDictionary": [ + "bestFriend": subUserJSON + ], + "friends": [subUserJSON, subUserJSON] + ] + + guard var user = multiKeyUserMapper.map(JSON: userJson) else { + XCTFail() + return + } + + XCTAssertNotNil(user) + XCTAssertEqual(username, user.username) + XCTAssertEqual(identifier, user.identifier) + XCTAssertEqual(photoCount, user.photoCount) + XCTAssertEqual(age, user.age) + XCTAssertEqual(weight, user.weight) + XCTAssertEqual(float, user.float) + XCTAssertEqual(drinker, user.drinker) + XCTAssertEqual(smoker, user.smoker) + XCTAssertEqual(sex, user.sex) + XCTAssertEqual(canDrive, user.canDrive) + XCTAssertNotNil(user.friends) + XCTAssertEqual(user.friends?.count, 2) + XCTAssertEqual(user.friends?[1].canDrive, canDrive) + XCTAssertEqual(user.friend?.age, 17) + + userJson.removeValue(forKey: "username") + userJson.removeValue(forKey: "weight") + userJson.removeValue(forKey: "sex") + userJson.removeValue(forKey: "friend") + + guard let nickUser = multiKeyUserMapper.map(JSON: userJson) else { + XCTFail() + return + } + user = nickUser + XCTAssertNotNil(user) + XCTAssertEqual(nickname, user.username) + XCTAssertEqual(identifier, user.identifier) + XCTAssertEqual(photoCount, user.photoCount) + XCTAssertEqual(age, user.age) + XCTAssertEqual(weightInKG, user.weight) + XCTAssertEqual(float, user.float) + XCTAssertEqual(drinker, user.drinker) + XCTAssertEqual(smoker, user.smoker) + XCTAssertEqual(gender, user.sex) + XCTAssertEqual(canDrive, user.canDrive) + XCTAssertNotNil(user.friends) + XCTAssertEqual(user.friends?.count, 2) + XCTAssertEqual(user.friends?[1].canDrive, canDrive) + XCTAssertEqual(user.friend?.age, 18) + } + + class MultiKeyUser: User { + override func mapping(map: Map) { + username <- map["username"].or["nickname"] + identifier <- map["identifier"] + photoCount <- map["photoCount"] + age <- map["age"] + weight <- map["weight"].or["weightInKG"] + float <- map["float"] + drinker <- map["drinker"] + smoker <- map["smoker"] + sex <- map["sex"].or["gender"] + canDrive <- map["canDrive"] + arr <- map["arr"] + arrOptional <- map["arrOpt"] + dict <- map["dict"] + dictOptional <- map["dictOpt"] + friend <- map["friend"].or["classmate"] + friends <- map["friends"] + friendDictionary <- map["friendDictionary"] + dictString <- map["dictString"] + } + } +}