├── keyedCodable.gif
├── KeyedCodable.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcshareddata
│ └── xcschemes
│ │ ├── KeyedCodable-watchOS.xcscheme
│ │ ├── KeyedCodable-iOS.xcscheme
│ │ ├── KeyedCodable-tvOS.xcscheme
│ │ └── KeyedCodable-macOS.xcscheme
└── project.pbxproj
├── .gitignore
├── KeyedCodable
├── KeyedCodable.h
├── Info.plist
└── Sources
│ ├── KeyedConfig.swift
│ ├── AnyKey.swift
│ ├── Helpers.swift
│ ├── KeyedKey.swift
│ ├── Transformers.swift
│ ├── ZeroDecodable.swift
│ ├── KeyedJSONDecoder.swift
│ └── KeyedJSONEncoder.swift
├── KeyedCodableTests
├── Info.plist
├── KeyedCodableTests.swift
├── ZeroDecodableTests.swift
├── FlatTests.swift
├── DecodePathTest.swift
├── OptionalArrayElementTests.swift
├── ExtensionTests.swift
├── AllKeysTests.swift
├── TransformersTests.swift
├── KeyOptionsTests.swift
└── InnerTests.swift
├── Package.swift
├── LICENSE
├── KeyedCodable.podspec
├── CHANGELOG.md
└── README.md
/keyedCodable.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgrzeszczak/KeyedCodable/HEAD/keyedCodable.gif
--------------------------------------------------------------------------------
/KeyedCodable.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/KeyedCodable.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | build/
3 | *.pbxuser
4 | !default.pbxuser
5 | *.mode1v3
6 | !default.mode1v3
7 | *.mode2v3
8 | !default.mode2v3
9 | *.perspectivev3
10 | !default.perspectivev3
11 | xcuserdata
12 | *.xccheckout
13 | *.moved-aside
14 | DerivedData
15 | *.xcuserstate
16 | # AppCode etc.
17 | .idea/
18 | # Desktop Servies
19 | .DS_Store
20 |
21 | # Carthage
22 | Carthage/Build
23 |
24 | # Swift package manager
25 | .build/
26 | Packages/
27 |
--------------------------------------------------------------------------------
/KeyedCodable/KeyedCodable.h:
--------------------------------------------------------------------------------
1 | //
2 | // KeyedCodable.h
3 | // KeyedCodable
4 | //
5 | // Created by Dariusz Grzeszczak on 26/03/2018.
6 | // Copyright © 2018 Dariusz Grzeszczak. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for KeyedCodable.
12 | FOUNDATION_EXPORT double KeyedCodableVersionNumber;
13 |
14 | //! Project version string for KeyedCodable.
15 | FOUNDATION_EXPORT const unsigned char KeyedCodableVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/KeyedCodableTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/KeyedCodable/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 2.5.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/KeyedCodable/Sources/KeyedConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyedConfig.swift
3 | // KeyedCodable
4 | //
5 | // Created by Dariusz Grzeszczak on 26/03/2018.
6 | // Copyright © 2018 Dariusz Grzeszczak. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct KeyedConfig {
12 |
13 | public static var `default` = KeyedConfig()
14 |
15 | public var keyOptions: KeyOptions
16 | public var defaultJSONDecoder: () -> KeyedJSONDecoder
17 | public var defaultJSONEncoder: () -> KeyedJSONEncoder
18 |
19 | public init(keyOptions: KeyOptions = KeyOptions(delimiter: .character("."), flat: .emptyOrWhitespace),
20 | keyedJSONDecoder: @escaping () -> KeyedJSONDecoder = KeyedJSONDecoder.init,
21 | keyedJSONEncoder: @escaping () -> KeyedJSONEncoder = KeyedJSONEncoder.init) {
22 |
23 | self.keyOptions = keyOptions
24 | defaultJSONDecoder = keyedJSONDecoder
25 | defaultJSONEncoder = keyedJSONEncoder
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | // TODO: resolve problem with sources shared to all targets
7 |
8 | let package = Package(
9 | name: "KeyedCodable",
10 | // platforms: [.macOS(.v10_10), .iOS(.v9), .tvOS(.v9), .watchOS(.v2)],
11 | platforms: [.iOS(.v9)],
12 | products: [
13 | .library(
14 | name: "KeyedCodable",
15 | // targets: ["KeyedCodable-iOS", "KeyedCodable-watchOS", "KeyedCodable-tvOS", "KeyedCodable-macOS"]
16 | targets: ["KeyedCodable"]
17 | ),
18 | ],
19 | targets: [
20 | .target(name: "KeyedCodable", path: "KeyedCodable/Sources"),
21 | // .target(name: "KeyedCodable-watchOS", path: "KeyedCodable/Sources"),
22 | // .target(name: "KeyedCodable-tvOS", path: "KeyedCodable/Sources"),
23 | // .target(name: "KeyedCodable-macOS", path: "KeyedCodable/Sources")
24 | ]
25 | )
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Dariusz Grzeszczak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/KeyedCodable.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'KeyedCodable'
3 | s.version = '3.0.0'
4 | s.license = 'MIT'
5 | s.summary = 'Easy nested key mappings for swift Codable'
6 | s.description = <<-DESC
7 | KeyedCodable is an addition to swift's Codable and it's designed for automatic nested key mappings. The goal it to avoid manual implementation of Encodable/Decodable and make encoding/decoding easier, more readable, less boilerplate and what is the most important fully compatible with 'standard' Codable.
8 | DESC
9 |
10 | s.homepage = 'https://github.com/dgrzeszczak/KeyedCodable'
11 | s.authors = { 'Dariusz Grzeszczak' => 'dariusz.grzeszczak@interia.pl' }
12 | s.source = { :git => 'https://github.com/dgrzeszczak/KeyedCodable.git', :tag => s.version }
13 |
14 | s.watchos.deployment_target = '2.0'
15 | s.ios.deployment_target = '8.0'
16 | s.osx.deployment_target = '10.9'
17 | s.tvos.deployment_target = '9.0'
18 |
19 | s.swift_version = '4.1'
20 |
21 | s.pod_target_xcconfig = {
22 | 'SWIFT_VERSION' => '4.1',
23 | }
24 |
25 | s.requires_arc = true
26 | s.source_files = 'KeyedCodable/Sources/**/*.swift'
27 | end
28 |
--------------------------------------------------------------------------------
/KeyedCodable/Sources/AnyKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnyKey.swift
3 | // KeyedCodable
4 | //
5 | // Created by Dariusz Grzeszczak on 26/03/2018.
6 | // Copyright © 2018 Dariusz Grzeszczak. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct AnyKey: CodingKey, Hashable, AnyKeyedKey {
12 |
13 | public let stringValue: String
14 | public let intValue: Int?
15 |
16 | public let options: KeyOptions?
17 |
18 | public init(stringValue: String, options: KeyOptions) {
19 | self.stringValue = stringValue
20 | intValue = nil
21 | self.options = options
22 | }
23 |
24 | public init(intValue: Int, options: KeyOptions) {
25 | stringValue = String(intValue)
26 | self.intValue = intValue
27 | self.options = options
28 | }
29 |
30 | public init(stringValue: String) {
31 | self.stringValue = stringValue
32 | intValue = nil
33 | options = nil
34 | }
35 |
36 | public init(intValue: Int) {
37 | stringValue = String(intValue)
38 | self.intValue = intValue
39 | options = nil
40 | }
41 |
42 | public init(key: CodingKey, options: KeyOptions? = nil) {
43 | if let intValue = key.intValue { self.init(intValue: intValue) }
44 | else { self.init(stringValue: key.stringValue )}
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [3.1.0](https://github.com/dgrzeszczak/KeyedCodable/releases/tag/3.1.0)
2 |
3 | #### New
4 | -SPM integration added
5 |
6 | ## [3.0.0](https://github.com/dgrzeszczak/KeyedCodable/releases/tag/3.0.0)
7 |
8 | #### New
9 | - @CodedBy, @EncodedBy, @DecodedBy transformers added
10 | - @Zero fill implementation added
11 | - @Flat properties and arrays added
12 |
13 |
14 | ## [2.6.0](https://github.com/dgrzeszczak/KeyedCodable/releases/tag/2.6.0)
15 |
16 | #### New
17 | - added .keyed to Codable extensions to increase readibility
18 | - deprecate Codable extensions from 2.5.0
19 |
20 | ## [2.5.0](https://github.com/dgrzeszczak/KeyedCodable/releases/tag/2.5.0)
21 |
22 | #### New
23 | - added support for manual coding with keyed AnyKey
24 | - Keyed<> wrapper added
25 | - Codable extensions from/toString
26 | - Default JSON coders
27 | - valuetype support for json coding
28 |
29 | ## [2.0.0](https://github.com/dgrzeszczak/KeyedCodable/releases/tag/2.0.0)
30 |
31 | #### New
32 | - redesigned way of encoding and decoding the keyed codables
33 |
34 |
35 | ## [1.2.0](https://github.com/dgrzeszczak/KeyedCodable/releases/tag/1.2.0)
36 |
37 | #### New
38 | - ifPresent operators added
39 | - swift 5 migration
40 |
41 | ## [1.1.0](https://github.com/dgrzeszczak/KeyedCodable/releases/tag/v1.1.0)
42 |
43 | #### Fixed
44 | - Encoder fixes for conficting inner keys
45 | - decodeIfPresent in decoder
46 | - improved UnitTests
47 |
48 | ## [1.0.0](https://github.com/dgrzeszczak/KeyedCodable/releases/tag/v1.0.0)
49 |
50 | #### Added
51 | - Initial release of KeyedCodable.
52 |
--------------------------------------------------------------------------------
/KeyedCodableTests/KeyedCodableTests.swift:
--------------------------------------------------------------------------------
1 | ////
2 | //// KeyedCodableTests.swift
3 | //// KeyedCodableTests
4 | ////
5 | //// Created by Dariusz Grzeszczak on 26/03/2018.
6 | ////
7 |
8 | import KeyedCodable
9 | import XCTest
10 |
11 | struct KeyedCodableTestHelper {
12 |
13 | static func checkEncode(data: Data, checkString: Bool = true, testObject: (_ codable: Type) -> Void) {
14 |
15 | let origString = String(data: data, encoding: .utf8)
16 |
17 | let codable: Type
18 |
19 | do {
20 | codable = try Type.keyed.fromJSON(data)
21 | } catch {
22 | XCTFail("\(Type.self) cannot be parsed with error: \(error)")
23 | return
24 | }
25 |
26 | testObject(codable)
27 |
28 | guard let jsonString = try? codable.keyed.jsonString() else {
29 | XCTFail("\(Type.self) cannot be encoded")
30 | return
31 | }
32 |
33 | if checkString {
34 | XCTAssert(jsonString.removedWhiteSpaces == origString?.removedWhiteSpaces,
35 | "Result JSON string is different") // check the string are the same
36 | }
37 |
38 | guard let object = try? Type.keyed.fromJSON(jsonString) else {
39 | XCTFail("\(Type.self) cannot be parsed second time")
40 | return
41 | }
42 |
43 | testObject(object)
44 |
45 | guard let jsonString1 = try? object.keyed.jsonString() else {
46 | XCTFail("\(Type.self) cannot be encoded second time")
47 | return
48 | }
49 |
50 | XCTAssert(jsonString == jsonString1)
51 | }
52 | }
53 |
54 | extension String {
55 | var removedWhiteSpaces: String {
56 | let characters = compactMap {
57 | $0 == " " || $0 == "\t" || $0 == "\n" ? nil : $0
58 | }
59 | return String(characters.sorted())
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/KeyedCodableTests/ZeroDecodableTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ZeroDecodableTests.swift
3 | // KeyedCodable
4 | //
5 | // Created by Dariusz Grzeszczak on 06/10/2019.
6 | //
7 |
8 | import KeyedCodable
9 | import XCTest
10 |
11 | struct ZeroTestCodable: Codable {
12 | @Zero var property: Int
13 | }
14 |
15 | struct ZeroTestOptionalCodable: Codable {
16 | @Zero var property: Int?
17 | }
18 |
19 | struct ZeroTestImplicitCodable: Decodable {
20 | @Zero var property: Int?
21 | }
22 |
23 | class ZeroDecodableTests: XCTestCase {
24 |
25 | override func setUp() {
26 | // Put setup code here. This method is called before the invocation of each test method in the class.
27 | }
28 |
29 | override func tearDown() {
30 | // Put teardown code here. This method is called after the invocation of each test method in the class.
31 | }
32 |
33 | func testStandardExample() throws {
34 | let zero = try StandardExample.keyed.zero()
35 | XCTAssert(zero.inner.details.description == "")
36 | XCTAssert(zero.inner.greeting == "")
37 | }
38 |
39 | #if swift(>=5.1)
40 | func testStandardExampleWrapper() throws {
41 | let zero = try ZeroTestCodable.keyed.fromJSON("{ }")
42 | XCTAssert(zero.property == 0)
43 | }
44 |
45 | func testNilZeroWrapper() throws {
46 | let zero = try ZeroTestOptionalCodable.keyed.fromJSON("{ }")
47 | XCTAssert(zero.property == nil)
48 | }
49 |
50 | func testNilInpicitZeroWrapper() throws {
51 | let zero = try ZeroTestImplicitCodable.keyed.fromJSON("{ }")
52 | XCTAssert(zero.property == nil)
53 | }
54 |
55 | func testEncoding() throws {
56 | let jsonData = "{}".data(using: .utf8)!
57 | KeyedCodableTestHelper.checkEncode(data: jsonData, checkString: false) { (test: ZeroTestCodable) in
58 | XCTAssert(test.property == 0)
59 | }
60 | }
61 |
62 | func testNilEncoding() throws {
63 | let zero = try ZeroTestOptionalCodable.keyed.fromJSON("{}")
64 | let string = try zero.keyed.jsonString()
65 | XCTAssert(string == "{}")
66 | }
67 | #endif
68 | }
69 |
--------------------------------------------------------------------------------
/KeyedCodableTests/FlatTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FlatTests.swift
3 | // KeyedCodableTests
4 | //
5 | // Created by Dariusz Grzeszczak on 11/05/2018.
6 | //
7 |
8 | import KeyedCodable
9 | import XCTest
10 |
11 | private let jsonString = """
12 | {
13 | "inner": {
14 | "greeting": "hallo"
15 | },
16 | "longitude": 3.2,
17 | "latitude": 3.4
18 | }
19 | """
20 |
21 | struct Location: Codable {
22 | let latitude: Double
23 | let longitude: Double?
24 | }
25 |
26 | struct InnerWithFlatExample: Codable {
27 | let greeting: String
28 | let location: Location?
29 |
30 | enum CodingKeys: String, KeyedKey {
31 | case greeting = "inner.greeting"
32 | case location = ""
33 | }
34 | }
35 |
36 | #if swift(>=5.1)
37 | struct InnerWithFlatWrapperExample: Codable {
38 | let greeting: String
39 | @Flat private(set) var location: Location?
40 |
41 | enum CodingKeys: String, KeyedKey {
42 | case greeting = "inner.greeting"
43 | case location
44 | }
45 | }
46 | #endif
47 |
48 | class FlatTests: XCTestCase {
49 |
50 | override func setUp() {
51 | super.setUp()
52 | // Put setup code here. This method is called before the invocation of each test method in the class.
53 | }
54 |
55 | override func tearDown() {
56 | // Put teardown code here. This method is called after the invocation of each test method in the class.
57 | super.tearDown()
58 | }
59 |
60 | func testFlat() {
61 | let jsonData = jsonString.data(using: .utf8)!
62 |
63 | KeyedCodableTestHelper.checkEncode(data: jsonData, checkString: false) { (test: InnerWithFlatExample) in
64 | XCTAssert(test.greeting == "hallo")
65 | XCTAssert(test.location?.latitude == 3.4)
66 | XCTAssert(test.location?.longitude == 3.2)
67 | }
68 | }
69 |
70 | #if swift(>=5.1)
71 | func testFlatWrapper() {
72 | let jsonData = jsonString.data(using: .utf8)!
73 |
74 | KeyedCodableTestHelper.checkEncode(data: jsonData, checkString: false) { (test: InnerWithFlatWrapperExample) in
75 | XCTAssert(test.greeting == "hallo")
76 | XCTAssert(test.location?.latitude == 3.4)
77 | XCTAssert(test.location?.longitude == 3.2)
78 | }
79 | }
80 | #endif
81 | }
82 |
--------------------------------------------------------------------------------
/KeyedCodableTests/DecodePathTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DecodePathTest.swift
3 | // KeyedCodable
4 | //
5 | // Created by Dariusz Grzeszczak on 25/07/2019.
6 | //
7 |
8 | import KeyedCodable
9 | import XCTest
10 |
11 | struct Token: Codable {
12 |
13 | static var keyPath: String = "data"
14 | static var key: AnyKey {
15 | return AnyKey(stringValue: keyPath)
16 | }
17 |
18 | public let accessToken: String
19 | public let expiresIn: Int
20 | public let refreshToken: String
21 |
22 | public enum CodingKeys: String, CodingKey {
23 | case accessToken = "access.token"
24 | case expiresIn = "expires_in"
25 | case refreshToken = "refresh_token"
26 |
27 | static var container = "data"
28 | }
29 | }
30 |
31 | struct TokenContainer: Codable {
32 | public private(set) var token: Token
33 |
34 | public init(from decoder: Decoder) throws {
35 |
36 | let container = try decoder.container(keyedBy: AnyKey.self)
37 | token = try container.decode(Token.self, forKey: Token.key)
38 | }
39 |
40 | func encode(to encoder: Encoder) throws {
41 | var container = encoder.container(keyedBy: AnyKey.self)
42 | try container.encodeIfPresent(token, forKey: Token.key)
43 | }
44 | }
45 |
46 | class DecodePathTests: XCTestCase {
47 |
48 | func testTokenMapping() {
49 |
50 | let innerDataTokenJSON = """
51 | {
52 | "data": {
53 | "access.token": "access",
54 | "expires_in": 2,
55 | "refresh_token": "refresh"
56 | }
57 | }
58 | """.data(using: .utf8)!
59 |
60 | KeyedCodableTestHelper.checkEncode(data: innerDataTokenJSON, checkString: false) { (test: TokenContainer) in
61 |
62 | XCTAssert(test.token.accessToken == "access")
63 | XCTAssert(test.token.expiresIn == 2)
64 | XCTAssert(test.token.refreshToken == "refresh")
65 | }
66 |
67 | let tokenJSONString = """
68 | {
69 | "access.token": "access",
70 | "expires_in": 2,
71 | "refresh_token": "refresh"
72 | }
73 | """.data(using: .utf8)!
74 |
75 | Token.keyPath = ""
76 |
77 | KeyedCodableTestHelper.checkEncode(data: tokenJSONString, checkString: false) { (test: TokenContainer) in
78 |
79 | XCTAssert(test.token.accessToken == "access")
80 | XCTAssert(test.token.expiresIn == 2)
81 | XCTAssert(test.token.refreshToken == "refresh")
82 | }
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/KeyedCodableTests/OptionalArrayElementTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OptionalArrayElementTests.swift
3 | // KeyedCodableTests
4 | //
5 | // Created by Dariusz Grzeszczak on 11/05/2018.
6 | //
7 |
8 | import XCTest
9 | import KeyedCodable
10 |
11 | // by default parsing all array will fail when any of element fail
12 | // you can mark array an optional - so it will ommit element that couldn't be parsed (eg. some field missing)
13 |
14 | private let jsonString = """
15 | {
16 | "array": [
17 | {
18 | "element": 1
19 | },
20 | {},
21 | {
22 | "element": 3
23 | },
24 | {
25 | "element": 4
26 | }
27 | ]
28 | }
29 | """
30 |
31 | struct ArrayElement: Codable {
32 | let element: Int
33 | }
34 |
35 | struct OptionalArrayElementsExample: Codable {
36 | let array: [ArrayElement]
37 |
38 | enum CodingKeys: String, KeyedKey {
39 | case array = ".array"
40 | }
41 | }
42 |
43 | #if swift(>=5.1)
44 | struct OptionalArrayElementsWrapperExample: Codable {
45 |
46 | @Flat private(set) var array: [ArrayElement]
47 |
48 | }
49 | #endif
50 |
51 | class OptionalArrayElementTests: XCTestCase {
52 |
53 | override func setUp() {
54 | super.setUp()
55 | // Put setup code here. This method is called before the invocation of each test method in the class.
56 | }
57 |
58 | override func tearDown() {
59 | // Put teardown code here. This method is called after the invocation of each test method in the class.
60 | super.tearDown()
61 | }
62 |
63 | func testOptionalArrayElement() {
64 | let jsonData = jsonString.data(using: .utf8)!
65 |
66 | KeyedCodableTestHelper.checkEncode(data: jsonData, checkString: false) { (test: OptionalArrayElementsExample) in
67 | // returns array with 3 elements, empty element will be omitted
68 | XCTAssert(test.array.count == 3)
69 | XCTAssert(test.array[0].element == 1)
70 | XCTAssert(test.array[1].element == 3)
71 | XCTAssert(test.array[2].element == 4)
72 | }
73 | }
74 |
75 | #if swift(>=5.1)
76 | func testOptionalArrayElementWrapper() {
77 | let jsonData = jsonString.data(using: .utf8)!
78 |
79 | KeyedCodableTestHelper.checkEncode(data: jsonData, checkString: false) { (test: OptionalArrayElementsWrapperExample) in
80 | // returns array with 3 elements, empty element will be omitted
81 | XCTAssert(test.array.count == 3)
82 | XCTAssert(test.array[0].element == 1)
83 | XCTAssert(test.array[1].element == 3)
84 | XCTAssert(test.array[2].element == 4)
85 | }
86 | }
87 | #endif
88 | }
89 |
--------------------------------------------------------------------------------
/KeyedCodableTests/ExtensionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExtensionTests.swift
3 | // KeyedCodableTests-iOS
4 | //
5 | // Created by Dariusz Grzeszczak on 12/09/2019.
6 | //
7 |
8 | import KeyedCodable
9 | import XCTest
10 |
11 | struct Model: Codable {
12 | var property: Int
13 | }
14 |
15 | let singleJson = """
16 | {
17 | "property":3
18 | }
19 | """
20 |
21 | let arrayJson = """
22 | [
23 | {"property":3},
24 | {"property":4},
25 | {"property":5}
26 | ]
27 | """
28 |
29 | let valueArrayJson = """
30 | [1, 2, 3]
31 | """
32 |
33 | let intJson = "3"
34 | let floatJson = "3.3"
35 | let stringJson = "domek"
36 |
37 | class ExtensionTests: XCTestCase {
38 |
39 | override func setUp() {
40 | super.setUp()
41 | // Put setup code here. This method is called before the invocation of each test method in the class.
42 | }
43 |
44 | override func tearDown() {
45 | // Put teardown code here. This method is called after the invocation of each test method in the class.
46 | super.tearDown()
47 | }
48 |
49 | func testSingle() throws {
50 | var test = try? Model.keyed.fromJSON(singleJson)
51 | XCTAssert(test?.property == 3)
52 |
53 | let data = try test.keyed.jsonData()
54 | test = try? Model.keyed.fromJSON(data)
55 | XCTAssert(test?.property == 3)
56 | }
57 |
58 | func testArray() throws {
59 | var test = try! [Model].keyed.fromJSON(arrayJson)
60 | XCTAssert(test[0].property == 3)
61 |
62 | let data = try test.keyed.jsonData()
63 | test = try [Model].keyed.fromJSON(data)
64 | XCTAssert(test[0].property == 3)
65 | }
66 |
67 | func testValueArray() throws {
68 | var test = try! [Int].keyed.fromJSON(valueArrayJson)
69 | XCTAssert(test[1] == 2)
70 |
71 | let data = try test.keyed.jsonData()
72 | test = try [Int].keyed.fromJSON(data)
73 | XCTAssert(test[1] == 2)
74 | }
75 |
76 | func testIntValue() throws {
77 | var test = try! Int.keyed.fromJSON(intJson)
78 | XCTAssert(test == 3)
79 |
80 | let data = try test.keyed.jsonData()
81 | test = try Int.keyed.fromJSON(data)
82 | XCTAssert(test == 3)
83 | }
84 |
85 | func testFloatValue() throws {
86 | var test = try! Float.keyed.fromJSON(floatJson)
87 | XCTAssert(test == 3.3)
88 |
89 | let data = try test.keyed.jsonData()
90 | test = try Float.keyed.fromJSON(data)
91 | XCTAssert(test == 3.3)
92 | }
93 |
94 | func testStringValue() throws {
95 | var test = try! String.keyed.fromJSON(stringJson)
96 | XCTAssert(test == "domek")
97 |
98 | let data = try test.keyed.jsonData()
99 | test = try String.keyed.fromJSON(data)
100 | XCTAssert(test == "domek")
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/KeyedCodable/Sources/Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Helpers.swift
3 | // KeyedCodable
4 | //
5 | // Created by Dariusz Grzeszczak on 26/03/2018.
6 | // Copyright © 2018 Dariusz Grzeszczak. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AnyKey {
12 | static var superKey: AnyKey { return AnyKey(stringValue: "super") }
13 | }
14 |
15 | extension CodingKey {
16 |
17 | init?(_ key: AnyKey) {
18 | if let intValue = key.intValue, let zelf = Self(intValue: intValue) {
19 | self = zelf
20 | } else if let zelf = Self(stringValue: key.stringValue) {
21 | self = zelf
22 | } else {
23 | return nil
24 | }
25 | }
26 |
27 | var isFlat: Bool {
28 | guard let key = self as? AnyKeyedKey else { return false }
29 | return (key.options?.flat ?? KeyedConfig.default.keyOptions.flat).isFlat(key: key)
30 | }
31 |
32 | var isFirstFlat: Bool {
33 | guard let key = self as? AnyKeyedKey else { return false }
34 | let keyed = key.keyed
35 | guard keyed.count > 1 else { return false }
36 | return (key.options?.flat ?? KeyedConfig.default.keyOptions.flat).isFlat(key: keyed[0])
37 | }
38 |
39 | var keyed: [AnyKey] {
40 | guard let key = self as? AnyKeyedKey else { return [AnyKey(key: self)] }
41 | guard case .character(let character) = key.options?.delimiter ?? KeyedConfig.default.keyOptions.delimiter else { return [AnyKey(key: key)] }
42 |
43 | return stringValue //TODO: indexes ??
44 | .components(separatedBy: String(character))
45 | .compactMap(AnyKey.init)
46 | }
47 | }
48 |
49 | protocol _Array {
50 | static func optionalDecode(unkeyedContainer: UnkeyedDecodingContainer) -> T
51 | }
52 |
53 | extension Array: _Array where Element: Decodable {
54 | static func optionalDecode(unkeyedContainer: UnkeyedDecodingContainer) -> T {
55 | var unkeyedContainer = unkeyedContainer
56 | var newObject = [Element]()
57 | while !unkeyedContainer.isAtEnd {
58 | if let value = try? unkeyedContainer.decode(Element.self) {
59 | newObject.append(value)
60 | }
61 | // swift 4 ?
62 | // else {
63 | // _ = try? unkeyedContainer.decode(EmptyCodable.self)
64 | // }
65 | }
66 | return newObject as! T
67 | }
68 | }
69 |
70 | extension Data {
71 | static func from(string: String, encoding: String.Encoding = .utf8) throws -> Data {
72 | guard let data = string.data(using: encoding) else { throw KeyedCodableError.stringParseFailed }
73 | return data
74 | }
75 | }
76 |
77 | extension String {
78 | static func from(data: Data, encoding: String.Encoding = .utf8) throws -> String {
79 | guard let string = String(data: data, encoding: encoding) else { throw KeyedCodableError.stringParseFailed }
80 | return string
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/KeyedCodableTests/AllKeysTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AllKeysTests.swift
3 | // KeyedCodableTests
4 | //
5 | // Created by Dariusz Grzeszczak on 09/04/2018.
6 | //
7 |
8 | import KeyedCodable
9 | import XCTest
10 |
11 | private let jsonString = """
12 | {
13 | "vault": {
14 | "0": {
15 | "type": "Braintree_CreditCard",
16 | "_attributes": {
17 | "default": false
18 | }
19 | },
20 | "1": {
21 | "type": "Braintree_CreditCard",
22 | "_attributes": {
23 | "default": true
24 | }
25 | },
26 | "2": {
27 | "type": "Braintree_PayPalAccount",
28 | "_attributes": {
29 | "default": false
30 | }
31 | },
32 | "autoTopup": "9969dc"
33 | }
34 | }
35 | """
36 |
37 | struct PaymentMethod: Decodable {
38 |
39 | let type: String
40 | let isDefault: Bool
41 |
42 | public enum CodingKeys: String, KeyedKey {
43 | case type
44 | case isDefault = "_attributes.default"
45 | }
46 | }
47 |
48 | struct PaymentMethods: Decodable {
49 |
50 | let userPaymentMethods: [PaymentMethod]
51 | let autoTopUpToken: String?
52 |
53 | enum CodingKeys: String, KeyedKey {
54 | case autoTopUpToken = "vault.autoTopup"
55 | case vault
56 | }
57 |
58 | public init(from decoder: Decoder) throws {
59 |
60 | let container = try decoder.container(keyedBy: CodingKeys.self)
61 | autoTopUpToken = try container.decode(String.self, forKey: .autoTopUpToken)
62 |
63 | let universalContainer = try container.nestedContainer(keyedBy: AnyKey.self, forKey: .vault)
64 | userPaymentMethods = universalContainer.allKeys
65 | .sorted { $0.stringValue < $1.stringValue }
66 | .compactMap { try? universalContainer.decode(PaymentMethod.self, forKey: $0) }
67 | }
68 | }
69 |
70 | class AllKeysTests: XCTestCase {
71 |
72 | override func setUp() {
73 | super.setUp()
74 | // Put setup code here. This method is called before the invocation of each test method in the class.
75 | }
76 |
77 | override func tearDown() {
78 | // Put teardown code here. This method is called after the invocation of each test method in the class.
79 | super.tearDown()
80 | }
81 |
82 | func testAllKeys() {
83 | let jsonData = jsonString.data(using: .utf8)!
84 |
85 | var test: PaymentMethods!
86 | do {
87 | test = try KeyedJSONDecoder().decode(PaymentMethods.self, from: jsonData)
88 | } catch ( let error ) {
89 | XCTFail("PaymentMethods cannot be parsed")
90 | print(error.localizedDescription)
91 | return
92 | }
93 |
94 | XCTAssert(test.userPaymentMethods.count == 3)
95 | XCTAssert(test.userPaymentMethods[1].type == "Braintree_CreditCard")
96 | XCTAssert(test.userPaymentMethods[1].isDefault == true)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/KeyedCodableTests/TransformersTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransformersTests.swift
3 | // KeyedCodable
4 | //
5 | // Created by Dariusz Grzeszczak on 06/10/2019.
6 | //
7 |
8 | import KeyedCodable
9 | import XCTest
10 |
11 | #if swift(>=5.1)
12 |
13 | let notCodable = """
14 | {
15 | "some": 3
16 | }
17 | """
18 | struct NotCodable {
19 | let id: Int
20 | }
21 |
22 | enum NonCodableTransformer