├── .gitignore ├── .swift-version ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── DefaultCodable │ ├── Default.swift │ └── DefaultValueProvider.swift └── Tests ├── DefaultCodableTests ├── DefaultTests.swift └── XCTestManifests.swift └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.1 2 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Guillermo Gonzalez 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 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "DefaultCodable", 7 | products: [ 8 | .library(name: "DefaultCodable", targets: ["DefaultCodable"]), 9 | ], 10 | dependencies: [], 11 | targets: [ 12 | .target(name: "DefaultCodable", dependencies: []), 13 | .testTarget(name: "DefaultCodableTests", dependencies: ["DefaultCodable"]), 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DefaultCodable 2 | ![Swift 5.1](https://img.shields.io/badge/Swift-5.1-orange.svg) 3 | [![Swift Package Manager](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager) 4 | [![Twitter: @gonzalezreal](https://img.shields.io/badge/twitter-@gonzalezreal-blue.svg?style=flat)](https://twitter.com/gonzalezreal) 5 | 6 | **DefaultCodable** is a Swift µpackage that provides a convenient way to define default values in `Codable` types for properties that are **not present or have a `nil` value**. 7 | 8 | ## Usage 9 | Consider a hypothetical model for Apple products., in which only the property `name` is *required*. 10 | 11 | ```swift 12 | enum ProductType: String, Codable, CaseIterable { 13 | case phone, pad, mac, accesory 14 | } 15 | 16 | struct Product: Codable { 17 | var name: String 18 | var description: String? 19 | var isAvailable: Bool? 20 | var type: ProductType? 21 | } 22 | ``` 23 | 24 | Using the `@Default` property wrapper, we can provide default values for the properties not required and thus get rid of the optionals in our model. 25 | 26 | ```swift 27 | struct Product: Codable { 28 | var name: String 29 | 30 | @Default 31 | var description: String 32 | 33 | @Default 34 | var isAvailable: Bool 35 | 36 | @Default 37 | var type: ProductType 38 | } 39 | ``` 40 | 41 | With that in place, we can safely decode the following JSON into a `Product` type. 42 | 43 | ```json 44 | { 45 | "name": "iPhone 11 Pro" 46 | } 47 | ``` 48 | 49 | The resulting `Product` instance is using the default values for those properties not present in the JSON. 50 | 51 | ``` 52 | ▿ Product 53 | - name : "iPhone 11 Pro" 54 | - description : "" 55 | - isAvailable : true 56 | - type : ProductType.phone 57 | ``` 58 | 59 | If you encode the result back, the resulting JSON will be the same as the one we started with. The `@Default` property wrapper will not encode the value if it is equal to the default value. 60 | 61 | The `@Default` property wrapper takes a `DefaultValueProvider` as a parameter. This type provides the default value when a value is not present or is `nil`. 62 | 63 | ```swift 64 | protocol DefaultValueProvider { 65 | associatedtype Value: Equatable & Codable 66 | 67 | static var `default`: Value { get } 68 | } 69 | ``` 70 | 71 | **DefaultCodable** provides the following implementations for your convenience: 72 | 73 | ### `Empty` 74 | It provides an empty instance of a `String`, `Array` or any type that implements `RangeReplaceableCollection`. 75 | 76 | ### `EmptyDictionary` 77 | It provides an empty instance of a `Dictionary`. 78 | 79 | ### `True` and `False` 80 | Provide `true` and `false` respectively for `Bool` properties. 81 | 82 | ### `Zero` and `One` 83 | Provide `0` and `1` respectively for `Int` properties. 84 | 85 | ### `FirstCase` 86 | It provides the first case of an `enum` type as the default value. The `enum` must implement the `CaseIterable` protocol. 87 | 88 | ### `ZeroDouble` 89 | Provide `0` for `Double` properties. 90 | 91 | ## Default values for custom types 92 | Your custom type must implement the `DefaultValueProvider` protocol to be compatible with the `@Default` property wrapper. 93 | 94 | Consider the following type that models a role in a conversation: 95 | 96 | ```swift 97 | struct Role: Codable, Equatable, Hashable, RawRepresentable { 98 | let rawValue: String 99 | 100 | init?(rawValue: String) { 101 | self.rawValue = rawValue 102 | } 103 | 104 | static let user = Role(rawValue: "user")! 105 | static let bot = Role(rawValue: "bot")! 106 | } 107 | ``` 108 | 109 | If we want the default role to be `user`, we can implement `DefaultValueProvider` as follows: 110 | 111 | ```swift 112 | extension Role: DefaultValueProvider { 113 | static let `default` = user 114 | } 115 | ``` 116 | 117 | With that in place, we can use the `@Default` property wrapper in any type that has a property of type `Role`: 118 | 119 | ```swift 120 | struct ChannelAccount: Codable { 121 | var name: String 122 | 123 | @Default 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 | --------------------------------------------------------------------------------