124 | var role: Role
125 | }
126 | ```
127 |
128 | ## Installation
129 | **Using the Swift Package Manager**
130 |
131 | Add **DefaultCodable** as a dependency to your `Package.swift` file. For more information, see the [Swift Package Manager documentation](https://github.com/apple/swift-package-manager/tree/master/Documentation).
132 |
133 | ```
134 | .package(url: "https://github.com/gonzalezreal/DefaultCodable", from: "1.0.0")
135 | ```
136 |
137 | ## Help & Feedback
138 | - [Open an issue](https://github.com/gonzalezreal/DefaultCodable/issues/new) if you need help, if you found a bug, or if you want to discuss a feature request.
139 | - [Open a PR](https://github.com/gonzalezreal/DefaultCodable/pull/new/master) if you want to make some change to `DefaultCodable`.
140 | - Contact [@gonzalezreal](https://twitter.com/gonzalezreal) on Twitter.
141 |
--------------------------------------------------------------------------------
/Sources/DefaultCodable/Default.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | @propertyWrapper
4 | public struct Default: Codable {
5 | public var wrappedValue: Provider.Value
6 |
7 | public init() {
8 | wrappedValue = Provider.default
9 | }
10 |
11 | public init(wrappedValue: Provider.Value) {
12 | self.wrappedValue = wrappedValue
13 | }
14 |
15 | public init(from decoder: Decoder) throws {
16 | let container = try decoder.singleValueContainer()
17 |
18 | if container.decodeNil() {
19 | wrappedValue = Provider.default
20 | } else {
21 | wrappedValue = try container.decode(Provider.Value.self)
22 | }
23 | }
24 | }
25 |
26 | extension Default: Equatable where Provider.Value: Equatable {}
27 | extension Default: Hashable where Provider.Value: Hashable {}
28 |
29 | public extension KeyedDecodingContainer {
30 | func decode(_: Default
.Type, forKey key: Key) throws -> Default
{
31 | if let value = try decodeIfPresent(Default
.self, forKey: key) {
32 | return value
33 | } else {
34 | return Default()
35 | }
36 | }
37 | }
38 |
39 | public extension KeyedEncodingContainer {
40 | mutating func encode
(_ value: Default
, forKey key: Key) throws {
41 | guard value.wrappedValue != P.default else { return }
42 | try encode(value.wrappedValue, forKey: key)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/DefaultCodable/DefaultValueProvider.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol DefaultValueProvider {
4 | associatedtype Value: Equatable & Codable
5 |
6 | static var `default`: Value { get }
7 | }
8 |
9 | public enum False: DefaultValueProvider {
10 | public static let `default` = false
11 | }
12 |
13 | public enum True: DefaultValueProvider {
14 | public static let `default` = true
15 | }
16 |
17 | public enum Empty: DefaultValueProvider where A: Codable, A: Equatable, A: RangeReplaceableCollection {
18 | public static var `default`: A { A() }
19 | }
20 |
21 | public enum EmptyDictionary: DefaultValueProvider where K: Hashable & Codable, V: Equatable & Codable {
22 | public static var `default`: [K: V] { Dictionary() }
23 | }
24 |
25 | public enum FirstCase: DefaultValueProvider where A: Codable, A: Equatable, A: CaseIterable {
26 | public static var `default`: A { A.allCases.first! }
27 | }
28 |
29 | public enum Zero: DefaultValueProvider {
30 | public static let `default` = 0
31 | }
32 |
33 | public enum One: DefaultValueProvider {
34 | public static let `default` = 1
35 | }
36 |
37 | public enum ZeroDouble: DefaultValueProvider {
38 | public static let `default`: Double = 0
39 | }
40 |
--------------------------------------------------------------------------------
/Tests/DefaultCodableTests/DefaultTests.swift:
--------------------------------------------------------------------------------
1 | import DefaultCodable
2 | import XCTest
3 |
4 | final class DefaultTests: XCTestCase {
5 | private enum ThingType: String, Codable, CaseIterable {
6 | case foo, bar, baz
7 | }
8 |
9 | private struct Thing: Codable, Hashable {
10 | var name: String
11 |
12 | @Default var description: String
13 | @Default var entities: [String: String]
14 | @Default var isFoo: Bool
15 | @Default var type: ThingType
16 | @Default var floatingPoint: Double
17 |
18 | init(
19 | name: String,
20 | description: String = "",
21 | entities: [String: String] = [:],
22 | isFoo: Bool = true,
23 | type: ThingType = .foo,
24 | floatingPoint: Double = 0
25 | ) {
26 | self.name = name
27 | self.description = description
28 | self.entities = entities
29 | self.isFoo = isFoo
30 | self.type = type
31 | self.floatingPoint = floatingPoint
32 | }
33 | }
34 |
35 | func testValueDecodesToActualValue() throws {
36 | // given
37 | let json = """
38 | {
39 | "name": "Any name",
40 | "description": "Any description",
41 | "entities": {
42 | "foo": "bar"
43 | },
44 | "isFoo": false,
45 | "type": "baz",
46 | "floatingPoint": 12.34
47 | }
48 | """.data(using: .utf8)!
49 |
50 | // when
51 | let result = try JSONDecoder().decode(Thing.self, from: json)
52 |
53 | // then
54 | XCTAssertEqual("Any description", result.description)
55 | XCTAssertEqual(["foo": "bar"], result.entities)
56 | XCTAssertFalse(result.isFoo)
57 | XCTAssertEqual(ThingType.baz, result.type)
58 | XCTAssertEqual(result.floatingPoint, 12.34)
59 | }
60 |
61 | func testNullDecodesToDefaultValue() throws {
62 | // given
63 | let json = """
64 | {
65 | "name": "Any name",
66 | "description": null,
67 | "entities": null,
68 | "isFoo": null,
69 | "type": null,
70 | "floatingPoint": null
71 | }
72 | """.data(using: .utf8)!
73 |
74 | // when
75 | let result = try JSONDecoder().decode(Thing.self, from: json)
76 |
77 | // then
78 | XCTAssertEqual("", result.description)
79 | XCTAssertEqual([:], result.entities)
80 | XCTAssertTrue(result.isFoo)
81 | XCTAssertEqual(ThingType.foo, result.type)
82 | XCTAssertEqual(result.floatingPoint, 0)
83 | }
84 |
85 | func testNotPresentValueDecodesToDefaultValue() throws {
86 | // given
87 | let json = """
88 | {
89 | "name": "Any name"
90 | }
91 | """.data(using: .utf8)!
92 |
93 | // when
94 | let result = try JSONDecoder().decode(Thing.self, from: json)
95 |
96 | // then
97 | XCTAssertEqual("", result.description)
98 | XCTAssertEqual([:], result.entities)
99 | XCTAssertTrue(result.isFoo)
100 | XCTAssertEqual(ThingType.foo, result.type)
101 | XCTAssertEqual(result.floatingPoint, 0)
102 | }
103 |
104 | func testTypeMismatchThrows() {
105 | // given
106 | let json = """
107 | {
108 | "name": "Any name",
109 | "description": ["nope"],
110 | "isFoo": 5500,
111 | "type": [1, 2, 3],
112 | "floatingPoint": "point"
113 | }
114 | """.data(using: .utf8)!
115 |
116 | // then
117 | XCTAssertThrowsError(try JSONDecoder().decode(Thing.self, from: json))
118 | }
119 |
120 | @available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *)
121 | func testValueEncodesToActualValue() throws {
122 | // given
123 | let thing = Thing(
124 | name: "Any name",
125 | description: "Any description",
126 | entities: ["foo": "bar"],
127 | isFoo: false,
128 | type: .baz,
129 | floatingPoint: 12.34
130 | )
131 | let expected = """
132 | {
133 | "description" : "Any description",
134 | "entities" : {
135 | "foo" : "bar"
136 | },
137 | "floatingPoint" : 12.34,
138 | "isFoo" : false,
139 | "name" : "Any name",
140 | "type" : "baz"
141 | }
142 | """.data(using: .utf8)!
143 | let encoder = JSONEncoder()
144 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
145 |
146 | // when
147 | let result = try encoder.encode(thing)
148 |
149 | // then
150 | XCTAssertEqual(expected, result)
151 | }
152 |
153 | func testDefaultValueEncodesToNothing() throws {
154 | // given
155 | let thing = Thing(name: "Any name")
156 | let expected = """
157 | {
158 | "name" : "Any name"
159 | }
160 | """.data(using: .utf8)!
161 | let encoder = JSONEncoder()
162 | encoder.outputFormatting = [.prettyPrinted]
163 |
164 | // when
165 | let result = try encoder.encode(thing)
166 |
167 | // then
168 | XCTAssertEqual(expected, result)
169 | }
170 |
171 | static var allTests = [
172 | ("testValueDecodesToActualValue", testValueDecodesToActualValue),
173 | ("testNullDecodesToDefaultValue", testNullDecodesToDefaultValue),
174 | ("testNotPresentValueDecodesToDefaultValue", testNotPresentValueDecodesToDefaultValue),
175 | ("testTypeMismatchThrows", testTypeMismatchThrows),
176 | ("testDefaultValueEncodesToNothing", testDefaultValueEncodesToNothing),
177 | ]
178 | }
179 |
--------------------------------------------------------------------------------
/Tests/DefaultCodableTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | [
6 | testCase(DefaultTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import DefaultCodableTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += DefaultCodableTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------