├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── BetterCodable.podspec ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── BetterCodable │ ├── Base64Strategy.swift │ ├── DataValue.swift │ ├── DateValue.swift │ ├── DefaultCodable.swift │ ├── DefaultEmptyArray.swift │ ├── DefaultEmptyDictionary.swift │ ├── DefaultFalse.swift │ ├── DefaultTrue.swift │ ├── ISO8601Strategy.swift │ ├── ISO8601WithFractionalSecondsStrategy.swift │ ├── LosslessArray.swift │ ├── LosslessValue.swift │ ├── LossyArray.swift │ ├── LossyDictionary.swift │ ├── LossyOptional.swift │ ├── RFC2822Strategy.swift │ ├── RFC3339Strategy.swift │ ├── TimestampStrategy.swift │ └── YearMonthDayStrategy.swift └── Tests ├── BetterCodableTests ├── DataValueTests.swift ├── DateValueTests.swift ├── DefaulEmptyDictionaryTests.swift ├── DefaultCodableTests.swift ├── DefaultEmptyArrayTests.swift ├── DefaultFalseTests.swift ├── DefaultTrueTests.swift ├── LosslessArrayTests.swift ├── LosslessCustomValueTests.swift ├── LosslessValueTests.swift ├── LossyArrayTests.swift ├── LossyDictionaryTests.swift ├── LossyOptionalTests.swift ├── RawRepresentableTests.swift └── XCTestManifests.swift └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /BetterCodable.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'BetterCodable' 3 | s.version = '0.4.0' 4 | s.swift_versions = ['5.1'] 5 | s.summary = 'Better Codable through Property Wrappers' 6 | s.homepage = 'https://github.com/marksands/BetterCodable' 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.author = { 'Mark Sands' => 'http://marksands.github.io/' } 9 | s.social_media_url = 'https://twitter.com/marksands' 10 | 11 | s.ios.deployment_target = '10.0' 12 | s.osx.deployment_target = '10.12' 13 | s.tvos.deployment_target = '10.0' 14 | s.watchos.deployment_target = '3.0' 15 | 16 | s.source = { :git => 'https://github.com/marksands/BetterCodable.git', :tag => s.version.to_s } 17 | s.source_files = 'Sources/**/*.swift' 18 | s.frameworks = 'Foundation' 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mark Sands 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "BetterCodable", 7 | platforms: [ 8 | .iOS(.v10), 9 | .macOS(.v10_13), 10 | .tvOS(.v10), 11 | .watchOS(.v3) 12 | ], 13 | products: [ 14 | .library( 15 | name: "BetterCodable", 16 | targets: ["BetterCodable"]), 17 | ], 18 | dependencies: [], 19 | targets: [ 20 | .target( 21 | name: "BetterCodable", 22 | dependencies: []), 23 | .testTarget( 24 | name: "BetterCodableTests", 25 | dependencies: ["BetterCodable"]), 26 | ], 27 | swiftLanguageVersions: [ 28 | .version("5") 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Better Codable through Property Wrappers 2 | 3 | Level up your `Codable` structs through property wrappers. The goal of these property wrappers is to avoid implementing a custom `init(from decoder: Decoder) throws` and suffer through boilerplate. 4 | 5 | ## @LossyArray 6 | 7 | `@LossyArray` decodes Arrays and filters invalid values if the Decoder is unable to decode the value. This is useful when the Array contains non-optional types and your API serves elements that are either null or fail to decode within the container. 8 | 9 | ### Usage 10 | 11 | Easily filter nulls from primitive containers 12 | 13 | ```Swift 14 | struct Response: Codable { 15 | @LossyArray var values: [Int] 16 | } 17 | 18 | let json = #"{ "values": [1, 2, null, 4, 5, null] }"#.data(using: .utf8)! 19 | let result = try JSONDecoder().decode(Response.self, from: json) 20 | 21 | print(result) // [1, 2, 4, 5] 22 | ``` 23 | 24 | Or silently exclude failable entities 25 | 26 | ```Swift 27 | struct Failable: Codable { 28 | let value: String 29 | } 30 | 31 | struct Response: Codable { 32 | @LossyArray var values: [Failable] 33 | } 34 | 35 | let json = #"{ "values": [{"value": 4}, {"value": "fish"}] }"#.data(using: .utf8)! 36 | let result = try JSONDecoder().decode(Response.self, from: json) 37 | 38 | print(result) // [Failable(value: "fish")] 39 | ``` 40 | 41 | ## @LossyDictionary 42 | 43 | `@LossyDictionary` decodes Dictionaries and filters invalid key-value pairs if the Decoder is unable to decode the value. This is useful if the Dictionary is intended to contain non-optional values and your API serves values that are either null or fail to decode within the container. 44 | 45 | ### Usage 46 | 47 | Easily filter nulls from primitive containers 48 | 49 | ```Swift 50 | struct Response: Codable { 51 | @LossyDictionary var values: [String: String] 52 | } 53 | 54 | let json = #"{ "values": {"a": "A", "b": "B", "c": null } }"#.data(using: .utf8)! 55 | let result = try JSONDecoder().decode(Response.self, from: json) 56 | 57 | print(result) // ["a": "A", "b": "B"] 58 | ``` 59 | 60 | Or silently exclude failable entities 61 | 62 | ```Swift 63 | struct Failable: Codable { 64 | let value: String 65 | } 66 | 67 | struct Response: Codable { 68 | @LossyDictionary var values: [String: Failable] 69 | } 70 | 71 | let json = #"{ "values": {"a": {"value": "A"}, "b": {"value": 2}} }"#.data(using: .utf8)! 72 | let result = try JSONDecoder().decode(Response.self, from: json) 73 | 74 | print(result) // ["a": "A"] 75 | ``` 76 | 77 | ## @DefaultCodable 78 | 79 | `@DefaultCodable` provides a generic property wrapper that allows for default values using a custom `DefaultCodableStrategy`. This allows one to implement their own default behavior for missing data and get the property wrapper behavior for free. Below are a few common default strategies, but they also serve as a template to implement a custom property wrapper to suit your specific use case. 80 | 81 | While not provided in the source code, it's a sinch to create your own default strategy for your custom data flow. 82 | 83 | ```Swift 84 | struct RefreshDaily: DefaultCodableStrategy { 85 | static var defaultValue: CacheInterval { return CacheInterval.daily } 86 | } 87 | 88 | struct Cache: Codable { 89 | @DefaultCodable var refreshInterval: CacheInterval 90 | } 91 | 92 | let json = #"{ "refreshInterval": null }"#.data(using: .utf8)! 93 | let result = try JSONDecoder().decode(Cache.self, from: json) 94 | 95 | print(result) // Cache(refreshInterval: .daily) 96 | ``` 97 | 98 | ## @DefaultFalse 99 | 100 | Optional Bools are weird. A type that once meant true or false, now has three possible states: `.some(true)`, `.some(false)`, or `.none`. And the `.none` condition _could_ indicate truthiness if BadDecisions™ were made. 101 | 102 | `@DefaultFalse` mitigates the confusion by defaulting decoded Bools to false if the Decoder is unable to decode the value, either when null is encountered or some unexpected type. 103 | 104 | ### Usage 105 | 106 | ```Swift 107 | struct UserPrivilege: Codable { 108 | @DefaultFalse var isAdmin: Bool 109 | } 110 | 111 | let json = #"{ "isAdmin": null }"#.data(using: .utf8)! 112 | let result = try JSONDecoder().decode(Response.self, from: json) 113 | 114 | print(result) // UserPrivilege(isAdmin: false) 115 | ``` 116 | 117 | ## @DefaultEmptyArray 118 | 119 | The weirdness of Optional Booleans extends to other types, such as Arrays. Soroush has a [great blog post](http://khanlou.com/2016/10/emptiness/) explaining why you may want to avoid Optional Arrays. Unfortunately, this idea doesn't come for free in Swift out of the box. Being forced to implement a custom initializer in order to nil coalesce nil arrays to empty arrays is no fun. 120 | 121 | `@DefaultEmptyArray` decodes Arrays and returns an empty array instead of nil if the Decoder is unable to decode the container. 122 | 123 | ### Usage 124 | 125 | ```Swift 126 | struct Response: Codable { 127 | @DefaultEmptyArray var favorites: [Favorite] 128 | } 129 | 130 | let json = #"{ "favorites": null }"#.data(using: .utf8)! 131 | let result = try JSONDecoder().decode(Response.self, from: json) 132 | 133 | print(result) // Response(favorites: []) 134 | ``` 135 | 136 | ## @DefaultEmptyDictionary 137 | 138 | As mentioned previously, Optional Dictionaries are yet another container where nil and emptiness collide. 139 | 140 | `@DefaultEmptyDictionary` decodes Dictionaries and returns an empty dictionary instead of nil if the Decoder is unable to decode the container. 141 | 142 | ### Usage 143 | 144 | ```Swift 145 | struct Response: Codable { 146 | @DefaultEmptyDictionary var scores: [String: Int] 147 | } 148 | 149 | let json = #"{ "scores": null }"#.data(using: .utf8)! 150 | let result = try JSONDecoder().decode(Response.self, from: json) 151 | 152 | print(result) // Response(values: [:]) 153 | ``` 154 | 155 | ## @LosslessValue 156 | 157 | All credit for this goes to [Ian Keen](https://twitter.com/iankay). 158 | 159 | Somtimes APIs can be unpredictable. They may treat some form of Identifiers or SKUs as `Int`s for one response and `String`s for another. Or you might find yourself encountering `"true"` when you expect a boolean. This is where `@LosslessValue` comes into play. 160 | 161 | `@LosslessValue` will attempt to decode a value into the type that you expect, preserving the data that would otherwise throw an exception or be lost altogether. 162 | 163 | ### Usage 164 | 165 | ```Swift 166 | struct Response: Codable { 167 | @LosslessValue var sku: String 168 | @LosslessValue var isAvailable: Bool 169 | } 170 | 171 | let json = #"{ "sku": 12345, "isAvailable": "true" }"#.data(using: .utf8)! 172 | let result = try JSONDecoder().decode(Response.self, from: json) 173 | 174 | print(result) // Response(sku: "12355", isAvailable: true) 175 | ``` 176 | 177 | ## Date Wrappers 178 | 179 | One common frustration with `Codable` is decoding entities that have mixed date formats. `JSONDecoder` comes built in with a handy `dateDecodingStrategy` property, but that uses the same date format for all dates that it will decode. And often, `JSONDecoder` lives elsewhere from the entity forcing tight coupling with the entities if you choose to use its date decoding strategy. 180 | 181 | Property wrappers are a nice solution to the aforementioned issues. It allows tight binding of the date formatting strategy directly with the property of the entity, and allows the `JSONDecoder` to remain decoupled from the entities it decodes. The `@DateValue` wrapper is generic across a custom `DateValueCodableStrategy`. This allows anyone to implement their own date decoding strategy and get the property wrapper behavior for free. Below are a few common Date strategies, but they also serve as a template to implement a custom property wrapper to suit your specific date format needs. 182 | 183 | The following property wrappers are heavily inspired by [Ian Keen](https://twitter.com/iankay). 184 | 185 | ## ISO8601Strategy 186 | 187 | `ISO8601Strategy` relies on an `ISO8601DateFormatter` in order to decode `String` values into `Date`s. Encoding the date will encode the value into the original string value. 188 | 189 | ### Usage 190 | 191 | ```Swift 192 | struct Response: Codable { 193 | @DateValue var date: Date 194 | } 195 | 196 | let json = #"{ "date": "1996-12-19T16:39:57-08:00" }"#.data(using: .utf8)! 197 | let result = try JSONDecoder().decode(Response.self, from: json) 198 | 199 | // This produces a valid `Date` representing 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC (Pacific Standard Time). 200 | ``` 201 | 202 | ## RFC3339Strategy 203 | 204 | `RFC3339Strategy` decodes RFC 3339 date strings into `Date`s. Encoding the date will encode the value back into the original string value. 205 | 206 | ### Usage 207 | 208 | ```Swift 209 | struct Response: Codable { 210 | @DateValue var date: Date 211 | } 212 | 213 | let json = #"{ "date": "1996-12-19T16:39:57-08:00" }"#.data(using: .utf8)! 214 | let result = try JSONDecoder().decode(Response.self, from: json) 215 | 216 | // This produces a valid `Date` representing 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC (Pacific Standard Time). 217 | ``` 218 | 219 | ## TimestampStrategy 220 | 221 | `TimestampStrategy` decodes `Double`s of a unix epoch into `Date`s. Encoding the date will encode the value into the original `TimeInterval` value. 222 | 223 | ### Usage 224 | 225 | ```Swift 226 | struct Response: Codable { 227 | @DateValue var date: Date 228 | } 229 | 230 | let json = #"{ "date": 978307200.0 }"#.data(using: .utf8)! 231 | let result = try JSONDecoder().decode(Response.self, from: json) 232 | 233 | // This produces a valid `Date` representing January 1st, 2001. 234 | ``` 235 | 236 | ## YearMonthDayStrategy 237 | 238 | `@DateValue` decodes string values into `Date`s using the date format `y-MM-dd`. Encoding the date will encode the value back into the original string format. 239 | 240 | ### Usage 241 | 242 | ```Swift 243 | struct Response: Codable { 244 | @DateValue var date: Date 245 | } 246 | 247 | let json = #"{ "date": "2001-01-01" }"#.data(using: .utf8)! 248 | let result = try JSONDecoder().decode(Response.self, from: json) 249 | 250 | // This produces a valid `Date` representing January 1st, 2001. 251 | ``` 252 | 253 | Or lastly, you can mix and match date wrappers as needed where the benefits truly shine 254 | 255 | ```Swift 256 | struct Response: Codable { 257 | @DateValue var updatedAt: Date 258 | @DateValue var birthday: Date 259 | } 260 | 261 | let json = #"{ "updatedAt": "2019-10-19T16:14:32-05:00", "birthday": "1984-01-22" }"#.data(using: .utf8)! 262 | let result = try JSONDecoder().decode(Response.self, from: json) 263 | 264 | // This produces two valid `Date` values, `updatedAt` representing October 19, 2019 and `birthday` January 22nd, 1984. 265 | ``` 266 | 267 | ## Installation 268 | 269 | ### CocoaPods 270 | 271 | ```ruby 272 | pod 'BetterCodable', '~> 0.1.0' 273 | ``` 274 | 275 | ### Swift Package Manager 276 | 277 | ## Attribution 278 | 279 | This project is licensed under MIT. If you find these useful, please tell your boss where you found them. 280 | -------------------------------------------------------------------------------- /Sources/BetterCodable/Base64Strategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Decodes `String` values as a Base64-encoded `Data`. 4 | /// 5 | /// Decodes strictly valid Base64. This does not handle b64url encoding, invalid padding, or unknown characters. 6 | public struct Base64Strategy: DataValueCodableStrategy { 7 | public static func decode(_ value: String) throws -> DataType { 8 | if let data = Data(base64Encoded: value) { 9 | return DataType(data) 10 | } else { 11 | throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Invalid Base64 Format!")) 12 | } 13 | } 14 | 15 | public static func encode(_ data: DataType) -> String { 16 | Data(data).base64EncodedString() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/BetterCodable/DataValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A protocol for providing a custom strategy for encoding and decoding data as strings. 4 | /// 5 | /// `DataValueCodableStrategy` provides a generic strategy type that the `DataValue` property wrapper can use to inject 6 | /// custom strategies for encoding and decoding data values. 7 | /// 8 | public protocol DataValueCodableStrategy { 9 | associatedtype DataType: MutableDataProtocol 10 | static func decode(_ value: String) throws -> DataType 11 | static func encode(_ data: DataType) -> String 12 | } 13 | 14 | /// Decodes and encodes data using a strategy type. 15 | /// 16 | /// `@DataValue` decodes data using a `DataValueCodableStrategy` which provides custom decoding and encoding functionality. 17 | @propertyWrapper 18 | public struct DataValue { 19 | public var wrappedValue: Coder.DataType 20 | 21 | public init(wrappedValue: Coder.DataType) { 22 | self.wrappedValue = wrappedValue 23 | } 24 | } 25 | 26 | extension DataValue: Decodable { 27 | public init(from decoder: Decoder) throws { 28 | self.wrappedValue = try Coder.decode(String(from: decoder)) 29 | } 30 | } 31 | 32 | extension DataValue: Encodable { 33 | public func encode(to encoder: Encoder) throws { 34 | try Coder.encode(wrappedValue).encode(to: encoder) 35 | } 36 | } 37 | 38 | extension DataValue: Equatable where Coder.DataType: Equatable {} 39 | extension DataValue: Hashable where Coder.DataType: Hashable {} 40 | -------------------------------------------------------------------------------- /Sources/BetterCodable/DateValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A protocol for providing a custom strategy for encoding and decoding dates. 4 | /// 5 | /// `DateValueCodableStrategy` provides a generic strategy type that the `DateValue` property wrapper can use to inject 6 | /// custom strategies for encoding and decoding date values. 7 | public protocol DateValueCodableStrategy { 8 | associatedtype RawValue 9 | 10 | static func decode(_ value: RawValue) throws -> Date 11 | static func encode(_ date: Date) -> RawValue 12 | } 13 | 14 | /// Decodes and encodes dates using a strategy type. 15 | /// 16 | /// `@DateValue` decodes dates using a `DateValueCodableStrategy` which provides custom decoding and encoding functionality. 17 | @propertyWrapper 18 | public struct DateValue { 19 | public var wrappedValue: Date 20 | 21 | public init(wrappedValue: Date) { 22 | self.wrappedValue = wrappedValue 23 | } 24 | } 25 | 26 | extension DateValue: Decodable where Formatter.RawValue: Decodable { 27 | public init(from decoder: Decoder) throws { 28 | let value = try Formatter.RawValue(from: decoder) 29 | self.wrappedValue = try Formatter.decode(value) 30 | } 31 | } 32 | 33 | extension DateValue: Encodable where Formatter.RawValue: Encodable { 34 | public func encode(to encoder: Encoder) throws { 35 | let value = Formatter.encode(wrappedValue) 36 | try value.encode(to: encoder) 37 | } 38 | } 39 | 40 | extension DateValue: Equatable { 41 | public static func == (lhs: DateValue, rhs: DateValue) -> Bool { 42 | return lhs.wrappedValue == rhs.wrappedValue 43 | } 44 | } 45 | 46 | extension DateValue: Hashable { 47 | public func hash(into hasher: inout Hasher) { 48 | hasher.combine(wrappedValue) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/BetterCodable/DefaultCodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Provides a default value for missing `Decodable` data. 4 | /// 5 | /// `DefaultCodableStrategy` provides a generic strategy type that the `DefaultCodable` property wrapper can use to provide 6 | /// a reasonable default value for missing Decodable data. 7 | public protocol DefaultCodableStrategy { 8 | associatedtype DefaultValue: Decodable 9 | 10 | /// The fallback value used when decoding fails 11 | static var defaultValue: DefaultValue { get } 12 | } 13 | 14 | /// Decodes values with a reasonable default value 15 | /// 16 | /// `@Defaultable` attempts to decode a value and falls back to a default type provided by the generic 17 | /// `DefaultCodableStrategy`. 18 | @propertyWrapper 19 | public struct DefaultCodable { 20 | public var wrappedValue: Default.DefaultValue 21 | 22 | public init(wrappedValue: Default.DefaultValue) { 23 | self.wrappedValue = wrappedValue 24 | } 25 | } 26 | 27 | extension DefaultCodable: Decodable { 28 | public init(from decoder: Decoder) throws { 29 | let container = try decoder.singleValueContainer() 30 | self.wrappedValue = (try? container.decode(Default.DefaultValue.self)) ?? Default.defaultValue 31 | } 32 | } 33 | 34 | extension DefaultCodable: Encodable where Default.DefaultValue: Encodable { 35 | public func encode(to encoder: Encoder) throws { 36 | var container = encoder.singleValueContainer() 37 | try container.encode(wrappedValue) 38 | } 39 | } 40 | 41 | extension DefaultCodable: Equatable where Default.DefaultValue: Equatable { } 42 | extension DefaultCodable: Hashable where Default.DefaultValue: Hashable { } 43 | 44 | // MARK: - KeyedDecodingContainer 45 | public protocol BoolCodableStrategy: DefaultCodableStrategy where DefaultValue == Bool {} 46 | 47 | public extension KeyedDecodingContainer { 48 | 49 | /// Default implementation of decoding a DefaultCodable 50 | /// 51 | /// Decodes successfully if key is available if not fallsback to the default value provided. 52 | func decode

(_: DefaultCodable

.Type, forKey key: Key) throws -> DefaultCodable

{ 53 | if let value = try decodeIfPresent(DefaultCodable

.self, forKey: key) { 54 | return value 55 | } else { 56 | return DefaultCodable(wrappedValue: P.defaultValue) 57 | } 58 | } 59 | 60 | /// Default implementation of decoding a `DefaultCodable` where its strategy is a `BoolCodableStrategy`. 61 | /// 62 | /// Tries to initially Decode a `Bool` if available, otherwise tries to decode it as an `Int` or `String` 63 | /// when there is a `typeMismatch` decoding error. This preserves the actual value of the `Bool` in which 64 | /// the data provider might be sending the value as different types. If everything fails defaults to 65 | /// the `defaultValue` provided by the strategy. 66 | func decode(_: DefaultCodable

.Type, forKey key: Key) throws -> DefaultCodable

{ 67 | do { 68 | let value = try decode(Bool.self, forKey: key) 69 | return DefaultCodable(wrappedValue: value) 70 | } catch let error { 71 | guard let decodingError = error as? DecodingError, 72 | case .typeMismatch = decodingError else { 73 | return DefaultCodable(wrappedValue: P.defaultValue) 74 | } 75 | if let intValue = try? decodeIfPresent(Int.self, forKey: key), 76 | let bool = Bool(exactly: NSNumber(value: intValue)) { 77 | return DefaultCodable(wrappedValue: bool) 78 | } else if let stringValue = try? decodeIfPresent(String.self, forKey: key), 79 | let bool = Bool(stringValue) { 80 | return DefaultCodable(wrappedValue: bool) 81 | } else { 82 | return DefaultCodable(wrappedValue: P.defaultValue) 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/BetterCodable/DefaultEmptyArray.swift: -------------------------------------------------------------------------------- 1 | public struct DefaultEmptyArrayStrategy: DefaultCodableStrategy { 2 | public static var defaultValue: [T] { return [] } 3 | } 4 | 5 | /// Decodes Arrays returning an empty array instead of nil if applicable 6 | /// 7 | /// `@DefaultEmptyArray` decodes Arrays and returns an empty array instead of nil if the Decoder is unable to decode the 8 | /// container. 9 | public typealias DefaultEmptyArray = DefaultCodable> where T: Decodable 10 | -------------------------------------------------------------------------------- /Sources/BetterCodable/DefaultEmptyDictionary.swift: -------------------------------------------------------------------------------- 1 | public struct DefaultEmptyDictionaryStrategy: DefaultCodableStrategy { 2 | public static var defaultValue: [Key: Value] { return [:] } 3 | } 4 | 5 | /// Decodes Dictionaries returning an empty dictionary instead of nil if applicable 6 | /// 7 | /// `@DefaultEmptyDictionary` decodes Dictionaries and returns an empty dictionary instead of nil if the Decoder is unable 8 | /// to decode the container. 9 | public typealias DefaultEmptyDictionary = DefaultCodable> where K: Decodable & Hashable, V: Decodable 10 | -------------------------------------------------------------------------------- /Sources/BetterCodable/DefaultFalse.swift: -------------------------------------------------------------------------------- 1 | public struct DefaultFalseStrategy: BoolCodableStrategy { 2 | public static var defaultValue: Bool { return false } 3 | } 4 | 5 | /// Decodes Bools defaulting to `false` if applicable 6 | /// 7 | /// `@DefaultFalse` decodes Bools and defaults the value to false if the Decoder is unable to decode the value. 8 | public typealias DefaultFalse = DefaultCodable 9 | -------------------------------------------------------------------------------- /Sources/BetterCodable/DefaultTrue.swift: -------------------------------------------------------------------------------- 1 | public struct DefaultTrueStrategy: BoolCodableStrategy { 2 | public static var defaultValue: Bool { return true } 3 | } 4 | 5 | /// Decodes Bools defaulting to `true` if applicable 6 | /// 7 | /// `@DefaultTrue` decodes Bools and defaults the value to true if the Decoder is unable to decode the value. 8 | public typealias DefaultTrue = DefaultCodable 9 | -------------------------------------------------------------------------------- /Sources/BetterCodable/ISO8601Strategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Decodes `String` values as an ISO8601 `Date`. 4 | /// 5 | /// `@ISO8601Date` relies on an `ISO8601DateFormatter` in order to decode `String` values into `Date`s. Encoding the `Date` 6 | /// will encode the value into the original string value. 7 | /// 8 | /// For example, decoding json data with a `String` representation of `"1996-12-19T16:39:57-08:00"` produces a valid `Date` 9 | /// representing 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC 10 | /// (Pacific Standard Time). 11 | public struct ISO8601Strategy: DateValueCodableStrategy { 12 | public static func decode(_ value: String) throws -> Date { 13 | guard let date = ISO8601DateFormatter().date(from: value) else { 14 | throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Invalid Date Format!")) 15 | } 16 | return date 17 | } 18 | 19 | public static func encode(_ date: Date) -> String { 20 | return ISO8601DateFormatter().string(from: date) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/BetterCodable/ISO8601WithFractionalSecondsStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Decodes `String` values as an ISO8601 `Date`. 4 | /// 5 | /// `@ISO8601Date` relies on an `ISO8601DateFormatter` in order to decode `String` values into `Date`s. Encoding the `Date` 6 | /// will encode the value into the original string value. 7 | /// 8 | /// For example, decoding json data with a `String` representation of `"1996-12-19T16:39:57-08:00"` produces a valid `Date` 9 | /// representing 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC 10 | /// (Pacific Standard Time). 11 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) 12 | public struct ISO8601WithFractionalSecondsStrategy: DateValueCodableStrategy { 13 | private static let formatter: ISO8601DateFormatter = { 14 | let formatter = ISO8601DateFormatter() 15 | formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] 16 | return formatter 17 | }() 18 | 19 | public static func decode(_ value: String) throws -> Date { 20 | guard let date = Self.formatter.date(from: value) else { 21 | throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Invalid Date Format!")) 22 | } 23 | return date 24 | } 25 | 26 | public static func encode(_ date: Date) -> String { 27 | return Self.formatter.string(from: date) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/BetterCodable/LosslessArray.swift: -------------------------------------------------------------------------------- 1 | /// Decodes Arrays by attempting to decode its elements into their preferred types. 2 | /// 3 | /// `@LosslessArray` attempts to decode Arrays and their elements into their preferred types while preserving the data. 4 | /// 5 | /// This is useful when data may return unpredictable values when a consumer is expecting a certain type. For instance, 6 | /// if an API sends an array of SKUs as either `Int`s or `String`s, then a `@LosslessArray` can ensure the elements are 7 | /// always decoded as `String`s. 8 | @propertyWrapper 9 | public struct LosslessArray { 10 | public var wrappedValue: [T] 11 | 12 | public init(wrappedValue: [T]) { 13 | self.wrappedValue = wrappedValue 14 | } 15 | } 16 | 17 | extension LosslessArray: Decodable where T: Decodable { 18 | private struct AnyDecodableValue: Decodable {} 19 | 20 | public init(from decoder: Decoder) throws { 21 | var container = try decoder.unkeyedContainer() 22 | 23 | var elements: [T] = [] 24 | while !container.isAtEnd { 25 | do { 26 | let value = try container.decode(LosslessValue.self).wrappedValue 27 | elements.append(value) 28 | } catch { 29 | _ = try? container.decode(AnyDecodableValue.self) 30 | } 31 | } 32 | 33 | self.wrappedValue = elements 34 | } 35 | } 36 | 37 | extension LosslessArray: Encodable where T: Encodable { 38 | public func encode(to encoder: Encoder) throws { 39 | try wrappedValue.encode(to: encoder) 40 | } 41 | } 42 | 43 | extension LosslessArray: Equatable where T: Equatable {} 44 | extension LosslessArray: Hashable where T: Hashable {} 45 | -------------------------------------------------------------------------------- /Sources/BetterCodable/LosslessValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias LosslessStringCodable = LosslessStringConvertible & Codable 4 | 5 | /// Provides an ordered list of types for decoding the lossless value, prioritizing the first type that successfully decodes as the inferred type. 6 | /// 7 | /// `LosslessDecodingStrategy` provides a generic strategy that the `LosslessValueCodable` property wrapper can use to provide 8 | /// the ordered list of decodable types in order to maximize preservation for the inferred type. 9 | public protocol LosslessDecodingStrategy { 10 | associatedtype Value: LosslessStringCodable 11 | 12 | /// An ordered list of decodable scenarios used to infer the encoded type 13 | static var losslessDecodableTypes: [(Decoder) -> LosslessStringCodable?] { get } 14 | } 15 | 16 | /// Decodes Codable values into their respective preferred types. 17 | /// 18 | /// `@LosslessValueCodable` attempts to decode Codable types into their preferred order while preserving the data in the most lossless format. 19 | /// 20 | /// The preferred type order is provided by a generic `LosslessDecodingStrategy` that provides an ordered list of `losslessDecodableTypes`. 21 | @propertyWrapper 22 | public struct LosslessValueCodable: Codable { 23 | private let type: LosslessStringCodable.Type 24 | 25 | public var wrappedValue: Strategy.Value 26 | 27 | public init(wrappedValue: Strategy.Value) { 28 | self.wrappedValue = wrappedValue 29 | self.type = Strategy.Value.self 30 | } 31 | 32 | public init(from decoder: Decoder) throws { 33 | do { 34 | self.wrappedValue = try Strategy.Value.init(from: decoder) 35 | self.type = Strategy.Value.self 36 | } catch let error { 37 | guard 38 | let rawValue = Strategy.losslessDecodableTypes.lazy.compactMap({ $0(decoder) }).first, 39 | let value = Strategy.Value.init("\(rawValue)") 40 | else { throw error } 41 | 42 | self.wrappedValue = value 43 | self.type = Swift.type(of: rawValue) 44 | } 45 | } 46 | 47 | public func encode(to encoder: Encoder) throws { 48 | let string = String(describing: wrappedValue) 49 | 50 | guard let original = type.init(string) else { 51 | let description = "Unable to encode '\(wrappedValue)' back to source type '\(type)'" 52 | throw EncodingError.invalidValue(string, .init(codingPath: [], debugDescription: description)) 53 | } 54 | 55 | try original.encode(to: encoder) 56 | } 57 | } 58 | 59 | extension LosslessValueCodable: Equatable where Strategy.Value: Equatable { 60 | public static func == (lhs: LosslessValueCodable, rhs: LosslessValueCodable) -> Bool { 61 | return lhs.wrappedValue == rhs.wrappedValue 62 | } 63 | } 64 | 65 | extension LosslessValueCodable: Hashable where Strategy.Value: Hashable { 66 | public func hash(into hasher: inout Hasher) { 67 | hasher.combine(wrappedValue) 68 | } 69 | } 70 | 71 | public struct LosslessDefaultStrategy: LosslessDecodingStrategy { 72 | public static var losslessDecodableTypes: [(Decoder) -> LosslessStringCodable?] { 73 | @inline(__always) 74 | func decode(_: T.Type) -> (Decoder) -> LosslessStringCodable? { 75 | return { try? T.init(from: $0) } 76 | } 77 | 78 | return [ 79 | decode(String.self), 80 | decode(Bool.self), 81 | decode(Int.self), 82 | decode(Int8.self), 83 | decode(Int16.self), 84 | decode(Int64.self), 85 | decode(UInt.self), 86 | decode(UInt8.self), 87 | decode(UInt16.self), 88 | decode(UInt64.self), 89 | decode(Double.self), 90 | decode(Float.self), 91 | ] 92 | } 93 | } 94 | 95 | public struct LosslessBooleanStrategy: LosslessDecodingStrategy { 96 | public static var losslessDecodableTypes: [(Decoder) -> LosslessStringCodable?] { 97 | @inline(__always) 98 | func decode(_: T.Type) -> (Decoder) -> LosslessStringCodable? { 99 | return { try? T.init(from: $0) } 100 | } 101 | 102 | @inline(__always) 103 | func decodeBoolFromNSNumber() -> (Decoder) -> LosslessStringCodable? { 104 | return { (try? Int.init(from: $0)).flatMap { Bool(exactly: NSNumber(value: $0)) } } 105 | } 106 | 107 | return [ 108 | decode(String.self), 109 | decodeBoolFromNSNumber(), 110 | decode(Bool.self), 111 | decode(Int.self), 112 | decode(Int8.self), 113 | decode(Int16.self), 114 | decode(Int64.self), 115 | decode(UInt.self), 116 | decode(UInt8.self), 117 | decode(UInt16.self), 118 | decode(UInt64.self), 119 | decode(Double.self), 120 | decode(Float.self), 121 | ] 122 | } 123 | } 124 | 125 | /// Decodes Codable values into their respective preferred types. 126 | /// 127 | /// `@LosslessValue` attempts to decode Codable types into their respective preferred types while preserving the data. 128 | /// 129 | /// This is useful when data may return unpredictable values when a consumer is expecting a certain type. For instance, 130 | /// if an API sends SKUs as either an `Int` or `String`, then a `@LosslessValue` can ensure the types are always decoded 131 | /// as `String`s. 132 | /// 133 | /// ``` 134 | /// struct Product: Codable { 135 | /// @LosslessValue var sku: String 136 | /// @LosslessValue var id: String 137 | /// } 138 | /// 139 | /// // json: { "sku": 87, "id": 123 } 140 | /// let value = try JSONDecoder().decode(Product.self, from: json) 141 | /// // value.sku == "87" 142 | /// // value.id == "123" 143 | /// ``` 144 | public typealias LosslessValue = LosslessValueCodable> where T: LosslessStringCodable 145 | 146 | /// Decodes Codable values into their respective preferred types. 147 | /// 148 | /// `@LosslessBoolValue` attempts to decode Codable types into their respective preferred types while preserving the data. 149 | /// 150 | /// - Note: 151 | /// This uses a `LosslessBooleanStrategy` in order to prioritize boolean values, and as such, some integer values will be lossy. 152 | /// 153 | /// For instance, if you decode `{ "some_type": 1 }` then `some_type` will be `true` and not `1`. If you do not want this 154 | /// behavior then use `@LosslessValue` or create a custom `LosslessDecodingStrategy`. 155 | /// 156 | /// ``` 157 | /// struct Example: Codable { 158 | /// @LosslessBoolValue var foo: Bool 159 | /// @LosslessValue var bar: Int 160 | /// } 161 | /// 162 | /// // json: { "foo": 1, "bar": 2 } 163 | /// let value = try JSONDecoder().decode(Fixture.self, from: json) 164 | /// // value.foo == true 165 | /// // value.bar == 2 166 | /// ``` 167 | public typealias LosslessBoolValue = LosslessValueCodable> where T: LosslessStringCodable 168 | -------------------------------------------------------------------------------- /Sources/BetterCodable/LossyArray.swift: -------------------------------------------------------------------------------- 1 | /// Decodes Arrays filtering invalid values if applicable 2 | /// 3 | /// `@LossyArray` decodes Arrays and filters invalid values if the Decoder is unable to decode the value. 4 | /// 5 | /// This is useful if the Array is intended to contain non-optional types. 6 | @propertyWrapper 7 | public struct LossyArray { 8 | public var wrappedValue: [T] 9 | 10 | public init(wrappedValue: [T]) { 11 | self.wrappedValue = wrappedValue 12 | } 13 | } 14 | 15 | extension LossyArray: Decodable where T: Decodable { 16 | private struct AnyDecodableValue: Decodable {} 17 | 18 | public init(from decoder: Decoder) throws { 19 | var container = try decoder.unkeyedContainer() 20 | 21 | var elements: [T] = [] 22 | while !container.isAtEnd { 23 | do { 24 | let value = try container.decode(T.self) 25 | elements.append(value) 26 | } catch { 27 | _ = try? container.decode(AnyDecodableValue.self) 28 | } 29 | } 30 | 31 | self.wrappedValue = elements 32 | } 33 | } 34 | 35 | extension LossyArray: Encodable where T: Encodable { 36 | public func encode(to encoder: Encoder) throws { 37 | try wrappedValue.encode(to: encoder) 38 | } 39 | } 40 | 41 | extension LossyArray: Equatable where T: Equatable { } 42 | extension LossyArray: Hashable where T: Hashable { } 43 | -------------------------------------------------------------------------------- /Sources/BetterCodable/LossyDictionary.swift: -------------------------------------------------------------------------------- 1 | /// Decodes Dictionaries filtering invalid key-value pairs if applicable 2 | /// 3 | /// `@LossyDictionary` decodes Dictionaries and filters invalid key-value pairs if the Decoder is unable to decode the value. 4 | /// 5 | /// This is useful if the Dictionary is intended to contain non-optional values. 6 | @propertyWrapper 7 | public struct LossyDictionary { 8 | public var wrappedValue: [Key: Value] 9 | 10 | public init(wrappedValue: [Key: Value]) { 11 | self.wrappedValue = wrappedValue 12 | } 13 | } 14 | 15 | extension LossyDictionary: Decodable where Key: Decodable, Value: Decodable { 16 | struct DictionaryCodingKey: CodingKey { 17 | let stringValue: String 18 | let intValue: Int? 19 | 20 | init?(stringValue: String) { 21 | self.stringValue = stringValue 22 | self.intValue = Int(stringValue) 23 | } 24 | 25 | init?(intValue: Int) { 26 | self.stringValue = "\(intValue)" 27 | self.intValue = intValue 28 | } 29 | } 30 | 31 | private struct AnyDecodableValue: Decodable {} 32 | private struct LossyDecodableValue: Decodable { 33 | let value: Value 34 | 35 | public init(from decoder: Decoder) throws { 36 | let container = try decoder.singleValueContainer() 37 | value = try container.decode(Value.self) 38 | } 39 | } 40 | 41 | public init(from decoder: Decoder) throws { 42 | var elements: [Key: Value] = [:] 43 | if Key.self == String.self { 44 | let container = try decoder.container(keyedBy: DictionaryCodingKey.self) 45 | let keys = try Self.extractKeys(from: decoder, container: container) 46 | 47 | for (key, stringKey) in keys { 48 | do { 49 | let value = try container.decode(LossyDecodableValue.self, forKey: key).value 50 | elements[stringKey as! Key] = value 51 | } catch { 52 | _ = try? container.decode(AnyDecodableValue.self, forKey: key) 53 | } 54 | } 55 | } else if Key.self == Int.self { 56 | let container = try decoder.container(keyedBy: DictionaryCodingKey.self) 57 | 58 | for key in container.allKeys { 59 | guard key.intValue != nil else { 60 | var codingPath = decoder.codingPath 61 | codingPath.append(key) 62 | throw DecodingError.typeMismatch( 63 | Int.self, 64 | DecodingError.Context( 65 | codingPath: codingPath, 66 | debugDescription: "Expected Int key but found String key instead.")) 67 | } 68 | 69 | do { 70 | let value = try container.decode(LossyDecodableValue.self, forKey: key).value 71 | elements[key.intValue! as! Key] = value 72 | } catch { 73 | _ = try? container.decode(AnyDecodableValue.self, forKey: key) 74 | } 75 | } 76 | } else { 77 | throw DecodingError.dataCorrupted( 78 | DecodingError.Context( 79 | codingPath: decoder.codingPath, 80 | debugDescription: "Unable to decode key type.")) 81 | } 82 | 83 | self.wrappedValue = elements 84 | } 85 | 86 | private static func extractKeys( 87 | from decoder: Decoder, 88 | container: KeyedDecodingContainer 89 | ) throws -> [(DictionaryCodingKey, String)] { 90 | // Decode a dictionary ignoring the values to decode the original keys 91 | // without using the `JSONDecoder.KeyDecodingStrategy`. 92 | let keys = try decoder.singleValueContainer().decode([String: AnyDecodableValue].self).keys 93 | 94 | return zip( 95 | container.allKeys.sorted(by: { $0.stringValue < $1.stringValue }), 96 | keys.sorted() 97 | ) 98 | .map { ($0, $1) } 99 | } 100 | } 101 | 102 | extension LossyDictionary: Encodable where Key: Encodable, Value: Encodable { 103 | public func encode(to encoder: Encoder) throws { 104 | try wrappedValue.encode(to: encoder) 105 | } 106 | } 107 | 108 | extension LossyDictionary: Equatable where Value: Equatable { } 109 | -------------------------------------------------------------------------------- /Sources/BetterCodable/LossyOptional.swift: -------------------------------------------------------------------------------- 1 | public struct DefaultNilStrategy: DefaultCodableStrategy { 2 | public static var defaultValue: T? { nil } 3 | } 4 | 5 | /// Decodes optional types, defaulting to `nil` instead of throwing an error, if applicable. 6 | /// 7 | /// `@LossyOptional` decodes optionals, and defaults to `nil` in cases where the decoder fails (e.g. decoding a non-url string to the `URL` type) 8 | public typealias LossyOptional = DefaultCodable> where T: Decodable 9 | -------------------------------------------------------------------------------- /Sources/BetterCodable/RFC2822Strategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Decodes `String` values as an RFC 2822 `Date`. 4 | /// 5 | /// `@RFC2822Date` decodes RFC 2822 date strings into `Date`s. Encoding the `Date` will encode the value back into the 6 | /// original string value. 7 | /// 8 | /// For example, decoding json data with a `String` representation of `"Tue, 24 Dec 2019 16:39:57 -0000"` produces a 9 | /// valid `Date` representing 39 minutes and 57 seconds after the 16th hour of December 24th, 2019 with an offset of 10 | /// -00:00 from UTC (Pacific Standard Time). 11 | public struct RFC2822Strategy: DateValueCodableStrategy { 12 | private static let dateFormatter: DateFormatter = { 13 | let dateFormatter = DateFormatter() 14 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 15 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 16 | dateFormatter.dateFormat = "EEE, d MMM y HH:mm:ss zzz" 17 | return dateFormatter 18 | }() 19 | 20 | public static func decode(_ value: String) throws -> Date { 21 | if let date = RFC2822Strategy.dateFormatter.date(from: value) { 22 | return date 23 | } else { 24 | throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Invalid Date Format!")) 25 | } 26 | } 27 | 28 | public static func encode(_ date: Date) -> String { 29 | return RFC2822Strategy.dateFormatter.string(from: date) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/BetterCodable/RFC3339Strategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Decodes `String` values as an RFC 3339 `Date`. 4 | /// 5 | /// `@RFC3339Date` decodes RFC 3339 date strings into `Date`s. Encoding the `Date` will encode the value back into the 6 | /// original string value. 7 | /// 8 | /// For example, decoding json data with a `String` representation of `"1996-12-19T16:39:57-08:00"` produces a valid 9 | /// `Date` representing 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 10 | /// from UTC (Pacific Standard Time). 11 | public struct RFC3339Strategy: DateValueCodableStrategy { 12 | private static let dateFormatter: DateFormatter = { 13 | let dateFormatter = DateFormatter() 14 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 15 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 16 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 17 | return dateFormatter 18 | }() 19 | 20 | public static func decode(_ value: String) throws -> Date { 21 | if let date = RFC3339Strategy.dateFormatter.date(from: value) { 22 | return date 23 | } else { 24 | throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Invalid Date Format!")) 25 | } 26 | } 27 | 28 | public static func encode(_ date: Date) -> String { 29 | return RFC3339Strategy.dateFormatter.string(from: date) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/BetterCodable/TimestampStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Decodes `TimeInterval` values as a `Date`. 4 | /// 5 | /// `@TimestampDate` decodes `Double`s of a unix epoch into `Date`s. Encoding the `Date` will encode the value into the 6 | /// original `TimeInterval` value. 7 | /// 8 | /// For example, decoding json data with a unix timestamp of `978307200.0` produces a valid `Date` representing January 9 | /// 1, 2001. 10 | public struct TimestampStrategy: DateValueCodableStrategy { 11 | public static func decode(_ value: TimeInterval) throws -> Date { 12 | return Date(timeIntervalSince1970: value) 13 | } 14 | 15 | public static func encode(_ date: Date) -> TimeInterval { 16 | return date.timeIntervalSince1970 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/BetterCodable/YearMonthDayStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Decodes `String` values of format `y-MM-dd` as a `Date`. 4 | /// 5 | /// `@YearMonthDayDate` decodes string values of format `y-MM-dd` as a `Date`. Encoding the `Date` will encode the value 6 | /// back into the original string format. 7 | /// 8 | /// For example, decoding json data with a `String` representation of `"2001-01-01"` produces a valid `Date` representing 9 | /// January 1st, 2001. 10 | public struct YearMonthDayStrategy: DateValueCodableStrategy { 11 | private static let dateFormatter: DateFormatter = { 12 | let dateFormatter = DateFormatter() 13 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 14 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 15 | dateFormatter.dateFormat = "y-MM-dd" 16 | return dateFormatter 17 | }() 18 | 19 | public static func decode(_ value: String) throws -> Date { 20 | if let date = YearMonthDayStrategy.dateFormatter.date(from: value) { 21 | return date 22 | } else { 23 | throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Invalid Date Format!")) 24 | } 25 | } 26 | 27 | public static func encode(_ date: Date) -> String { 28 | return YearMonthDayStrategy.dateFormatter.string(from: date) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/DataValueTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | class DataValueTests: XCTestCase { 5 | func testDecodingAndEncodingBase64String() throws { 6 | struct Fixture: Codable { 7 | @DataValue var data: Data 8 | } 9 | let jsonData = #"{"data":"QmV0dGVyQ29kYWJsZQ=="}"#.data(using: .utf8)! 10 | 11 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 12 | XCTAssertEqual(fixture.data, Data("BetterCodable".utf8)) 13 | 14 | let outputJSON = try JSONEncoder().encode(fixture) 15 | XCTAssertEqual(outputJSON, jsonData) 16 | } 17 | 18 | func testDecodingMalformedBase64Fails() throws { 19 | struct Fixture: Codable { 20 | @DataValue var data: Data 21 | } 22 | let jsonData = #"{"data":"invalidBase64!"}"#.data(using: .utf8)! 23 | 24 | XCTAssertThrowsError(try JSONDecoder().decode(Fixture.self, from: jsonData)) 25 | } 26 | 27 | func testDecodingAndEncodingBase64StringToArray() throws { 28 | struct Fixture: Codable { 29 | @DataValue var data: [UInt8] 30 | } 31 | let jsonData = #"{"data":"QmV0dGVyQ29kYWJsZQ=="}"#.data(using: .utf8)! 32 | 33 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 34 | XCTAssertEqual(fixture.data, Array("BetterCodable".utf8)) 35 | 36 | let outputJSON = try JSONEncoder().encode(fixture) 37 | XCTAssertEqual(outputJSON, jsonData) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/DateValueTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | class CustomDateCodableValueTests: XCTestCase { 5 | func testDecodingAndEncodingISO8601DateString() throws { 6 | struct Fixture: Codable { 7 | @DateValue var iso8601: Date 8 | } 9 | let jsonData = #"{"iso8601": "1996-12-19T16:39:57-08:00"}"#.data(using: .utf8)! 10 | 11 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 12 | XCTAssertEqual(fixture.iso8601, Date(timeIntervalSince1970: 851042397)) 13 | } 14 | 15 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) 16 | func testDecodingAndEncodingISO8601DateStringWithFractionalSeconds() throws { 17 | struct Fixture: Codable { 18 | @DateValue var iso8601: Date 19 | @DateValue var iso8601Short: Date 20 | } 21 | let jsonData = """ 22 | { 23 | "iso8601": "1996-12-19T16:39:57.123456Z", 24 | "iso8601Short": "1996-12-19T16:39:57.000Z-08:00" 25 | } 26 | """.data(using: .utf8)! 27 | 28 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 29 | XCTAssertEqual(fixture.iso8601Short, Date(timeIntervalSince1970: 851013597.0)) 30 | XCTAssertEqual(fixture.iso8601, Date(timeIntervalSince1970: 851013597.123)) 31 | } 32 | 33 | func testDecodingAndEncodingRFC3339DateString() throws { 34 | struct Fixture: Codable { 35 | @DateValue var rfc3339Date: Date 36 | } 37 | let jsonData = #"{"rfc3339Date": "1996-12-19T16:39:57-08:00"}"#.data(using: .utf8)! 38 | 39 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 40 | XCTAssertEqual(fixture.rfc3339Date, Date(timeIntervalSince1970: 851042397)) 41 | } 42 | 43 | func testDecodingAndEncodingRFC2822DateString() throws { 44 | struct Fixture: Codable { 45 | @DateValue var rfc2822Date: Date 46 | } 47 | let jsonData = #"{"rfc2822Date": "Fri, 27 Dec 2019 22:43:52 -0000"}"#.data(using: .utf8)! 48 | 49 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 50 | XCTAssertEqual(fixture.rfc2822Date, Date(timeIntervalSince1970: 1577486632)) 51 | } 52 | 53 | func testDecodingAndEncodingUTCTimestamp() throws { 54 | struct Fixture: Codable { 55 | @DateValue var timestamp: Date 56 | } 57 | let jsonData = #"{"timestamp": 851042397.0}"#.data(using: .utf8)! 58 | 59 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 60 | XCTAssertEqual(fixture.timestamp, Date(timeIntervalSince1970: 851042397)) 61 | } 62 | 63 | func testDecodingAndEncodingYearMonthDateString() throws { 64 | struct Fixture: Codable { 65 | @DateValue var ymd: Date 66 | } 67 | let jsonData = #"{"ymd": "1996-12-19"}"#.data(using: .utf8)! 68 | 69 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 70 | XCTAssertEqual(fixture.ymd, Date(timeIntervalSince1970: 850953600)) 71 | } 72 | 73 | func testDecodingAndEncodingWithCustomStrategies() throws { 74 | struct Fixture: Codable { 75 | @DateValue var timeStamp: Date 76 | } 77 | let jsonData = #"{"time_stamp": 851042397.0}"#.data(using: .utf8)! 78 | 79 | let decoder = JSONDecoder() 80 | decoder.keyDecodingStrategy = .convertFromSnakeCase 81 | decoder.dateDecodingStrategy = .iso8601 82 | let fixture = try decoder.decode(Fixture.self, from: jsonData) 83 | XCTAssertEqual(fixture.timeStamp, Date(timeIntervalSince1970: 851042397)) 84 | 85 | let encoder = JSONEncoder() 86 | encoder.keyEncodingStrategy = .convertToSnakeCase 87 | encoder.dateEncodingStrategy = .iso8601 88 | let data = try encoder.encode(fixture) 89 | let fixture2 = try decoder.decode(Fixture.self, from: data) 90 | XCTAssertEqual(fixture2.timeStamp, Date(timeIntervalSince1970: 851042397)) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/DefaulEmptyDictionaryTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | class DefaultEmptyDictionaryTests: XCTestCase { 5 | struct Fixture: Equatable, Codable { 6 | @DefaultEmptyDictionary var stringToInt: [String: Int] 7 | } 8 | 9 | func testDecodingFailableDictionaryDefaultsToEmptyDictionary() throws { 10 | let jsonData = #"{ "stringToInt": null }"#.data(using: .utf8)! 11 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 12 | XCTAssertEqual(fixture.stringToInt, [:]) 13 | } 14 | 15 | func testDecodingKeyNotPresentDefaultsToEmptyDictionary() throws { 16 | let jsonData = #"{}"#.data(using: .utf8)! 17 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 18 | XCTAssertEqual(fixture.stringToInt, [:]) 19 | } 20 | 21 | func testEncodingDecodedFailableDictionaryDefaultsToEmptyDictionary() throws { 22 | let jsonData = #"{ "stringToInt": null }"#.data(using: .utf8)! 23 | var _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 24 | 25 | _fixture.stringToInt["one"] = 1 26 | 27 | let fixtureData = try JSONEncoder().encode(_fixture) 28 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 29 | XCTAssertEqual(fixture.stringToInt, ["one": 1]) 30 | } 31 | 32 | func testEncodingDecodedFulfillableDictionaryRetainsContents() throws { 33 | let jsonData = #"{ "stringToInt": {"one": 1, "two": 2} }"#.data(using: .utf8)! 34 | let _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 35 | let fixtureData = try JSONEncoder().encode(_fixture) 36 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 37 | XCTAssertEqual(fixture.stringToInt, ["one": 1, "two": 2]) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/DefaultCodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | // MARK: - DefaultCodable 5 | 6 | // MARK: - 7 | // MARK: Date Decoding Strategy 8 | 9 | private extension Date { 10 | enum DefaultToNow: DefaultCodableStrategy { 11 | static var defaultValue: Date { Date() } 12 | } 13 | } 14 | 15 | private let iso8601: DateFormatter = { 16 | let iso8601 = DateFormatter() 17 | iso8601.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" 18 | iso8601.timeZone = TimeZone(secondsFromGMT: 0) 19 | iso8601.locale = Locale(identifier: "en_US_POSIX") 20 | return iso8601 21 | }() 22 | 23 | private extension JSONDecoder { 24 | static var iso: JSONDecoder { 25 | let encoder = JSONDecoder() 26 | encoder.dateDecodingStrategy = .formatted(iso8601) 27 | return encoder 28 | } 29 | } 30 | 31 | private extension JSONEncoder { 32 | static var iso: JSONEncoder { 33 | let encoder = JSONEncoder() 34 | encoder.dateEncodingStrategy = .formatted(iso8601) 35 | return encoder 36 | } 37 | } 38 | 39 | class DefaultCodableTest_DateStrategy: XCTestCase { 40 | struct Fixture: Equatable, Codable { 41 | @DefaultCodable 42 | fileprivate var discoverDate: Date 43 | } 44 | 45 | func testDecodingAndEncodingWithDateStrategy() throws { 46 | let expectedDate = Date(timeIntervalSinceReferenceDate: 222601260) 47 | let jsonData = #"{ "discoverDate": "2008-01-21T09:41:00.000Z" }"#.data(using: .utf8)! 48 | let fixture = try JSONDecoder.iso.decode(Fixture.self, from: jsonData) 49 | XCTAssertEqual(fixture.discoverDate, expectedDate) 50 | 51 | let data = try JSONEncoder.iso.encode(fixture) 52 | let str = String(data: data, encoding: .utf8) 53 | XCTAssertEqual(str, #"{"discoverDate":"2008-01-21T09:41:00.000Z"}"#) 54 | } 55 | } 56 | 57 | // MARK: - 58 | // MARK: Nested Property Wrapper 59 | 60 | class DefaultCodableTest_NestedPropertyWrapper: XCTestCase { 61 | enum DefaultToNowTimeStampDateValue: DefaultCodableStrategy { 62 | static var defaultValue: DateValue { 63 | .init(wrappedValue: Date(timeIntervalSince1970: 0)) 64 | } 65 | } 66 | 67 | struct Fixture: Codable { 68 | @DefaultCodable 69 | @DateValue 70 | var returnDate: Date 71 | } 72 | 73 | func testNestedPropertyWrappersCanMergeDefaultCodableWithDateStrategy() throws { 74 | let _1970 = Date(timeIntervalSince1970: 0) 75 | let _1971 = Date(timeIntervalSince1970: 31536000) 76 | 77 | let jsonData1 = #"{ "returnDate": null }"#.data(using: .utf8)! 78 | let jsonData2 = #"{ }"#.data(using: .utf8)! 79 | let jsonData3 = #"{ "returnDate": 31536000 }"#.data(using: .utf8)! 80 | 81 | let fixture1 = try JSONDecoder().decode(Fixture.self, from: jsonData1) 82 | let fixture2 = try JSONDecoder().decode(Fixture.self, from: jsonData2) 83 | let fixture3 = try JSONDecoder().decode(Fixture.self, from: jsonData3) 84 | 85 | XCTAssertEqual(fixture1.returnDate, _1970) 86 | XCTAssertEqual(fixture2.returnDate, _1970) 87 | XCTAssertEqual(fixture3.returnDate, _1971) 88 | } 89 | } 90 | 91 | 92 | // MARK: - 93 | // MARK: Types with Containers 94 | 95 | class DefaultCodableTests_TypesWithContainers: XCTestCase { 96 | struct ArrayContainer: Codable { 97 | var value: [Int] 98 | } 99 | 100 | enum DefaultArrayContainerType: DefaultCodableStrategy { 101 | static var defaultValue: ArrayContainer { .init(value: [3, 7]) } 102 | } 103 | 104 | struct DictionaryContainer: Codable { 105 | var value: [String: Int] 106 | } 107 | 108 | enum DefaultDictionaryContainerType: DefaultCodableStrategy { 109 | static var defaultValue: DictionaryContainer { .init(value: ["a": 1]) } 110 | } 111 | 112 | struct Fixture: Codable { 113 | @DefaultCodable 114 | public var type: ArrayContainer 115 | } 116 | 117 | struct Fixture2: Codable { 118 | @DefaultCodable 119 | public var type: DictionaryContainer 120 | } 121 | 122 | func testDecodingAndEncodingWithArrayContainer() throws { 123 | let jsonData = #"{ "type": { "value": [2, 4, 6] } }"#.data(using: .utf8)! 124 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 125 | XCTAssertEqual(fixture.type.value, [2, 4, 6]) 126 | 127 | let data = try JSONEncoder().encode(fixture) 128 | let str = String(data: data, encoding: .utf8) 129 | XCTAssertEqual(str, #"{"type":{"value":[2,4,6]}}"#) 130 | } 131 | 132 | func testDecodingAndEncodingWithDictionaryContainer() throws { 133 | let jsonData = #"{ "type": { "value": {"b": 17 } } }"#.data(using: .utf8)! 134 | let fixture = try JSONDecoder().decode(Fixture2.self, from: jsonData) 135 | XCTAssertEqual(fixture.type.value, ["b": 17]) 136 | 137 | let data = try JSONEncoder().encode(fixture) 138 | let str = String(data: data, encoding: .utf8) 139 | XCTAssertEqual(str, #"{"type":{"value":{"b":17}}}"#) 140 | } 141 | } 142 | 143 | // MARK: - 144 | // MARK: Enums with Associated Values 145 | 146 | class DefaultCodableTests_EnumWithAssociatedValue: XCTestCase { 147 | enum Zar: Equatable { 148 | case ziz(Int) 149 | case zaz(Int) 150 | } 151 | 152 | struct CustomType: Codable, Equatable { 153 | enum CodingKeys: String, CodingKey { 154 | case z = "fish" 155 | case i = "int" 156 | } 157 | 158 | var z: Zar 159 | 160 | init(z: Zar) { 161 | self.z = z 162 | } 163 | 164 | init(from decoder: Decoder) throws { 165 | let c = try decoder.container(keyedBy: CodingKeys.self) 166 | let k = try c.decode(String.self, forKey: .z) 167 | let i = try c.decode(Int.self, forKey: .i) 168 | 169 | if k == "ziz" { z = .ziz(i) } 170 | else { z = .zaz(i) } 171 | } 172 | 173 | func encode(to encoder: Encoder) throws { 174 | var c = encoder.container(keyedBy: CodingKeys.self) 175 | switch z { 176 | case .ziz(let i): 177 | try c.encode("ziz", forKey: .z) 178 | try c.encode(i, forKey: .i) 179 | case .zaz(let i): 180 | try c.encode("zaz", forKey: .z) 181 | try c.encode(i, forKey: .i) 182 | } 183 | } 184 | 185 | enum Default42: DefaultCodableStrategy { 186 | static var defaultValue: CustomType { .init(z: .zaz(42)) } 187 | } 188 | } 189 | 190 | struct Fixture: Equatable, Codable { 191 | @DefaultCodable 192 | public var value: CustomType 193 | } 194 | 195 | func testDecodingAndEncodingCustomEnumWithAssociatedValue() throws { 196 | let jsonData = #"{ "value": { "fish": "ziz", "int": 4 } }"#.data(using: .utf8)! 197 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 198 | XCTAssertEqual(fixture.value.z, .ziz(4)) 199 | 200 | let data = try JSONEncoder().encode(fixture) 201 | let str = String(data: data, encoding: .utf8) 202 | XCTAssertEqual(str, #"{"value":{"int":4,"fish":"ziz"}}"#) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/DefaultEmptyArrayTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | class DefaultEmptyArrayTests: XCTestCase { 5 | struct Fixture: Equatable, Codable { 6 | struct NestedFixture: Equatable, Codable { 7 | var one: String 8 | var two: [String: [String]] 9 | } 10 | 11 | @DefaultEmptyArray var values: [Int] 12 | @DefaultEmptyArray var nonPrimitiveValues: [NestedFixture] 13 | } 14 | 15 | func testDecodingFailableArrayDefaultsToEmptyArray() throws { 16 | let jsonData = #"{ "values": null, "nonPrimitiveValues": null }"#.data(using: .utf8)! 17 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 18 | XCTAssertEqual(fixture.values, []) 19 | XCTAssertEqual(fixture.nonPrimitiveValues, []) 20 | } 21 | 22 | func testDecodingKeyNotPresentDefaultsToEmptyArray() throws { 23 | let jsonData = #"{}"#.data(using: .utf8)! 24 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 25 | XCTAssertEqual(fixture.values, []) 26 | XCTAssertEqual(fixture.nonPrimitiveValues, []) 27 | } 28 | 29 | func testEncodingDecodedFailableArrayDefaultsToEmptyArray() throws { 30 | let jsonData = #"{ "values": null, "nonPrimitiveValues": null }"#.data(using: .utf8)! 31 | var _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 32 | 33 | _fixture.values += [1, 2, 3] 34 | _fixture.nonPrimitiveValues += [Fixture.NestedFixture(one: "a", two: ["b": ["c"]])] 35 | 36 | let fixtureData = try JSONEncoder().encode(_fixture) 37 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 38 | XCTAssertEqual(fixture.values, [1, 2, 3]) 39 | XCTAssertEqual(fixture.nonPrimitiveValues, [Fixture.NestedFixture(one: "a", two: ["b": ["c"]])]) 40 | } 41 | 42 | func testEncodingDecodedFulfillableArrayRetainsContents() throws { 43 | let jsonData = #"{ "values": [1, 2], "nonPrimitiveValues": [{ "one": "one", "two": {"key": ["value"]}}] }"#.data(using: .utf8)! 44 | let _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 45 | let fixtureData = try JSONEncoder().encode(_fixture) 46 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 47 | 48 | XCTAssertEqual(fixture.values, [1, 2]) 49 | XCTAssertEqual(fixture.nonPrimitiveValues, [Fixture.NestedFixture(one: "one", two: ["key": ["value"]])]) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/DefaultFalseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | class DefaultFalseTests: XCTestCase { 5 | struct Fixture: Equatable, Codable { 6 | @DefaultFalse var truthy: Bool 7 | } 8 | 9 | func testDecodingFailableArrayDefaultsToFalse() throws { 10 | let jsonData = #"{ "truthy": null }"#.data(using: .utf8)! 11 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 12 | XCTAssertEqual(fixture.truthy, false) 13 | } 14 | 15 | func testDecodingKeyNotPresentDefaultsToFalse() throws { 16 | let jsonData = #"{}"#.data(using: .utf8)! 17 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 18 | XCTAssertEqual(fixture.truthy, false) 19 | } 20 | 21 | func testEncodingDecodedFailableArrayDefaultsToFalse() throws { 22 | let jsonData = #"{ "truthy": null }"#.data(using: .utf8)! 23 | var _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 24 | 25 | _fixture.truthy = true 26 | 27 | let fixtureData = try JSONEncoder().encode(_fixture) 28 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 29 | XCTAssertEqual(fixture.truthy, true) 30 | } 31 | 32 | func testEncodingDecodedFulfillableBoolRetainsValue() throws { 33 | let jsonData = #"{ "truthy": true }"#.data(using: .utf8)! 34 | let _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 35 | let fixtureData = try JSONEncoder().encode(_fixture) 36 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 37 | 38 | XCTAssertEqual(fixture.truthy, true) 39 | } 40 | 41 | func testDecodingMisalignedBoolIntValueDecodesCorrectBoolValue() throws { 42 | let jsonData = #"{ "truthy": 1 }"#.data(using: .utf8)! 43 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 44 | XCTAssertEqual(fixture.truthy, true) 45 | 46 | let jsonData2 = #"{ "truthy": 0 }"#.data(using: .utf8)! 47 | let fixture2 = try JSONDecoder().decode(Fixture.self, from: jsonData2) 48 | XCTAssertEqual(fixture2.truthy, false) 49 | } 50 | 51 | func testDecodingMisalignedBoolStringValueDecodesCorrectBoolValue() throws { 52 | let jsonData = #"{ "truthy": "true" }"#.data(using: .utf8)! 53 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 54 | XCTAssertEqual(fixture.truthy, true) 55 | 56 | let jsonData2 = #"{ "truthy": "false" }"#.data(using: .utf8)! 57 | let fixture2 = try JSONDecoder().decode(Fixture.self, from: jsonData2) 58 | XCTAssertEqual(fixture2.truthy, false) 59 | } 60 | 61 | func testDecodingInvalidValueDecodesToDefaultValue() throws { 62 | let jsonData = #"{ "truthy": "invalidValue" }"#.data(using: .utf8)! 63 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 64 | XCTAssertEqual( 65 | fixture.truthy, 66 | false, 67 | "Should fall in to the else block and return default value" 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/DefaultTrueTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BetterCodable 3 | 4 | class DefaultTrueTests: XCTestCase { 5 | struct Fixture: Equatable, Codable { 6 | @DefaultTrue var truthy: Bool 7 | } 8 | 9 | func testDecodingFailableArrayDefaultsToFalse() throws { 10 | let jsonData = #"{ "truthy": null }"#.data(using: .utf8)! 11 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 12 | XCTAssertEqual(fixture.truthy, true) 13 | } 14 | 15 | func testDecodingKeyNotPresentDefaultsToFalse() throws { 16 | let jsonData = #"{}"#.data(using: .utf8)! 17 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 18 | XCTAssertEqual(fixture.truthy, true) 19 | } 20 | 21 | func testEncodingDecodedFailableArrayDefaultsToFalse() throws { 22 | let jsonData = #"{ "truthy": null }"#.data(using: .utf8)! 23 | var _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 24 | 25 | _fixture.truthy = false 26 | 27 | let fixtureData = try JSONEncoder().encode(_fixture) 28 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 29 | XCTAssertEqual(fixture.truthy, false) 30 | } 31 | 32 | func testEncodingDecodedFulfillableBoolRetainsValue() throws { 33 | let jsonData = #"{ "truthy": true }"#.data(using: .utf8)! 34 | let _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 35 | let fixtureData = try JSONEncoder().encode(_fixture) 36 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 37 | 38 | XCTAssertEqual(fixture.truthy, true) 39 | } 40 | 41 | func testDecodingMisalignedBoolIntValueDecodesCorrectBoolValue() throws { 42 | let jsonData = #"{ "truthy": 1 }"#.data(using: .utf8)! 43 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 44 | XCTAssertEqual(fixture.truthy, true) 45 | 46 | let jsonData2 = #"{ "truthy": 0 }"#.data(using: .utf8)! 47 | let fixture2 = try JSONDecoder().decode(Fixture.self, from: jsonData2) 48 | XCTAssertEqual(fixture2.truthy, false) 49 | } 50 | 51 | func testDecodingInvalidValueDecodesToDefaultValue() throws { 52 | let jsonData = #"{ "truthy": "invalidValue" }"#.data(using: .utf8)! 53 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 54 | XCTAssertEqual( 55 | fixture.truthy, 56 | true, 57 | "Should fall in to the else block and return default value" 58 | ) 59 | } 60 | 61 | func testDecodingMisalignedBoolStringValueDecodesCorrectBoolValue() throws { 62 | let jsonData = #"{ "truthy": "true" }"#.data(using: .utf8)! 63 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 64 | XCTAssertEqual(fixture.truthy, true) 65 | 66 | let jsonData2 = #"{ "truthy": "false" }"#.data(using: .utf8)! 67 | let fixture2 = try JSONDecoder().decode(Fixture.self, from: jsonData2) 68 | XCTAssertEqual(fixture2.truthy, false) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/LosslessArrayTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | class LosslessArrayTests: XCTestCase { 5 | struct Fixture: Equatable, Codable { 6 | @LosslessArray var values: [Int] 7 | } 8 | 9 | struct Fixture2: Equatable, Codable { 10 | @LosslessArray var values: [String] 11 | } 12 | 13 | func testDecodingLosslessArrayActsLikeLossyArray() throws { 14 | let jsonData = #"{ "values": [1, null, 3, 4] }"#.data(using: .utf8)! 15 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 16 | XCTAssertEqual(fixture.values, [1, 3, 4]) 17 | } 18 | 19 | func testDecodingIntsConvertsStringsIntoLosslessElements() throws { 20 | let jsonData = #"{ "values": ["1", 2, null, "4"] }"#.data(using: .utf8)! 21 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 22 | XCTAssertEqual(fixture.values, [1, 2, 4]) 23 | } 24 | 25 | func testDecodingStringsPreservesLosslessElements() throws { 26 | let jsonData = #"{ "values": ["1", 2, 3.14, null, false, "4"] }"#.data(using: .utf8)! 27 | let fixture = try JSONDecoder().decode(Fixture2.self, from: jsonData) 28 | XCTAssertEqual(fixture.values, ["1", "2", "3.14", "false", "4"]) 29 | } 30 | 31 | func testEncodingDecodedLosslessArrayIgnoresFailableElements() throws { 32 | let jsonData = #"{ "values": [null, "2", null, 4] }"#.data(using: .utf8)! 33 | var _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 34 | 35 | _fixture.values += [5] 36 | 37 | let fixtureData = try JSONEncoder().encode(_fixture) 38 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 39 | XCTAssertEqual(fixture.values, [2, 4, 5]) 40 | } 41 | 42 | func testEncodingDecodedLosslessArrayRetainsContents() throws { 43 | let jsonData = #"{ "values": [1, 2, "3"] }"#.data(using: .utf8)! 44 | let _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 45 | let fixtureData = try JSONEncoder().encode(_fixture) 46 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 47 | 48 | XCTAssertEqual(fixture.values, [1, 2, 3]) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/LosslessCustomValueTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | struct MyLosslessStrategy: LosslessDecodingStrategy { 5 | static var losslessDecodableTypes: [(Decoder) -> LosslessStringCodable?] { 6 | [ 7 | { try? String(from: $0) }, 8 | { try? Bool(from: $0) }, 9 | { try? Int(from: $0) }, 10 | { _ in return 42 }, 11 | ] 12 | } 13 | } 14 | 15 | typealias MyLosslessType = LosslessValueCodable> where T: LosslessStringCodable 16 | 17 | class LosslessCustomValueTests: XCTestCase { 18 | struct Fixture: Equatable, Codable { 19 | @MyLosslessType var int: Int 20 | @MyLosslessType var string: String 21 | @MyLosslessType var fortytwo: Int 22 | @MyLosslessType var bool: Bool 23 | } 24 | 25 | func testDecodingCustomLosslessStrategyDecodesCorrectly() throws { 26 | let jsonData = #"{ "string": 7, "int": "1", "fortytwo": null, "bool": true }"#.data(using: .utf8)! 27 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 28 | XCTAssertEqual(fixture.string, "7") 29 | XCTAssertEqual(fixture.int, 1) 30 | XCTAssertEqual(fixture.fortytwo, 42) 31 | XCTAssertEqual(fixture.bool, true) 32 | } 33 | 34 | func testDecodingCustomLosslessStrategyWithBrokenFieldsThrowsError() throws { 35 | let jsonData = #"{ "string": 7, "int": "1", "fortytwo": null, "bool": 9 }"#.data(using: .utf8)! 36 | XCTAssertThrowsError(try JSONDecoder().decode(Fixture.self, from: jsonData)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/LosslessValueTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | class LosslessValueTests: XCTestCase { 5 | struct Fixture: Equatable, Codable { 6 | @LosslessValue var bool: Bool 7 | @LosslessValue var string: String 8 | @LosslessValue var int: Int 9 | @LosslessValue var double: Double 10 | } 11 | 12 | func testDecodingMisalignedTypesFromJSONTraversesCorrectType() throws { 13 | let jsonData = #"{ "bool": "true", "string": 42, "int": "1", "double": "7.1" }"#.data(using: .utf8)! 14 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 15 | XCTAssertEqual(fixture.bool, true) 16 | XCTAssertEqual(fixture.string, "42") 17 | XCTAssertEqual(fixture.int, 1) 18 | XCTAssertEqual(fixture.double, 7.1) 19 | } 20 | 21 | func testDecodingEncodedMisalignedTypesFromJSONDecodesCorrectTypes() throws { 22 | let jsonData = #"{ "bool": "true", "string": 42, "int": "7", "double": "7.1" }"#.data(using: .utf8)! 23 | var _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 24 | 25 | _fixture.bool = false 26 | _fixture.double = 3.14 27 | 28 | let fixtureData = try JSONEncoder().encode(_fixture) 29 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 30 | XCTAssertEqual(fixture.bool, false) 31 | XCTAssertEqual(fixture.string, "42") 32 | XCTAssertEqual(fixture.int, 7) 33 | XCTAssertEqual(fixture.double, 3.14) 34 | } 35 | 36 | func testEncodingAndDecodedExpectedTypes() throws { 37 | let jsonData = #"{ "bool": true, "string": "42", "int": 7, "double": 7.1 }"#.data(using: .utf8)! 38 | let _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 39 | let fixtureData = try JSONEncoder().encode(_fixture) 40 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 41 | XCTAssertEqual(fixture.bool, true) 42 | XCTAssertEqual(fixture.string, "42") 43 | XCTAssertEqual(fixture.int, 7) 44 | XCTAssertEqual(fixture.double, 7.1) 45 | } 46 | 47 | func testDecodingBoolIntValueFromJSONDecodesCorrectly() throws { 48 | struct FixtureWithBooleanAsInteger: Equatable, Codable { 49 | @LosslessBoolValue var bool: Bool 50 | @LosslessValue var string: String 51 | @LosslessValue var int: Int 52 | @LosslessValue var double: Double 53 | } 54 | 55 | let jsonData = #"{ "bool": 1, "string": "42", "int": 7, "double": 7.1 }"#.data(using: .utf8)! 56 | let _fixture = try JSONDecoder().decode(FixtureWithBooleanAsInteger.self, from: jsonData) 57 | let fixtureData = try JSONEncoder().encode(_fixture) 58 | let fixture = try JSONDecoder().decode(FixtureWithBooleanAsInteger.self, from: fixtureData) 59 | XCTAssertEqual(fixture.bool, true) 60 | XCTAssertEqual(fixture.string, "42") 61 | XCTAssertEqual(fixture.int, 7) 62 | XCTAssertEqual(fixture.double, 7.1) 63 | } 64 | 65 | func testBoolAsIntegerShouldNotConflictWithDefaultStrategy() throws { 66 | struct Response: Codable { 67 | @LosslessValue var id: String 68 | @LosslessBoolValue var bool: Bool 69 | } 70 | 71 | let json = #"{ "id": 1, "bool": 1 }"#.data(using: .utf8)! 72 | let result = try JSONDecoder().decode(Response.self, from: json) 73 | 74 | XCTAssertEqual(result.id, "1") 75 | XCTAssertEqual(result.bool, true) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/LossyArrayTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | class LossyArrayTests: XCTestCase { 5 | struct Fixture: Equatable, Codable { 6 | struct NestedFixture: Equatable, Codable { 7 | var one: String 8 | var two: [String: [String]] 9 | } 10 | 11 | @LossyArray var values: [Int] 12 | @LossyArray var nonPrimitiveValues: [NestedFixture] 13 | } 14 | 15 | func testDecodingLossyArrayIgnoresFailableElements() throws { 16 | let jsonData = #"{ "values": [1, null, 3, 4], "nonPrimitiveValues": [null] }"#.data(using: .utf8)! 17 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 18 | XCTAssertEqual(fixture.values, [1, 3, 4]) 19 | XCTAssertEqual(fixture.nonPrimitiveValues, []) 20 | } 21 | 22 | func testDecodingLossyArrayIgnoresLossyElements() throws { 23 | let jsonData = #"{ "values": [1, null, "3", false, 4], "nonPrimitiveValues": [null] }"#.data(using: .utf8)! 24 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 25 | XCTAssertEqual(fixture.values, [1, 4]) 26 | XCTAssertEqual(fixture.nonPrimitiveValues, []) 27 | } 28 | 29 | func testEncodingDecodedLossyArrayIgnoresFailableElements() throws { 30 | let jsonData = #"{ "values": [null, 2, null, 4], "nonPrimitiveValues": [null] }"#.data(using: .utf8)! 31 | var _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 32 | 33 | _fixture.values += [5] 34 | _fixture.nonPrimitiveValues += [Fixture.NestedFixture(one: "1", two: ["x": ["y"]])] 35 | 36 | let fixtureData = try JSONEncoder().encode(_fixture) 37 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 38 | XCTAssertEqual(fixture.values, [2, 4, 5]) 39 | XCTAssertEqual(fixture.nonPrimitiveValues, [Fixture.NestedFixture(one: "1", two: ["x": ["y"]])]) 40 | } 41 | 42 | func testEncodingDecodedLossyArrayRetainsContents() throws { 43 | let jsonData = #"{ "values": [1, 2], "nonPrimitiveValues": [{ "one": "one", "two": {"key": ["value"]}}] }"#.data(using: .utf8)! 44 | let _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 45 | let fixtureData = try JSONEncoder().encode(_fixture) 46 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 47 | 48 | XCTAssertEqual(fixture.values, [1, 2]) 49 | XCTAssertEqual(fixture.nonPrimitiveValues, [Fixture.NestedFixture(one: "one", two: ["key": ["value"]])]) 50 | } 51 | 52 | func testEncodingDecodingLossyArrayWorksWithCustomStrategies() throws { 53 | struct Fixture: Equatable, Codable { 54 | @LossyArray var theValues: [Date] 55 | } 56 | 57 | let jsonData = #"{ "the_values": [123, null] }"#.data(using: .utf8)! 58 | let decoder = JSONDecoder() 59 | decoder.keyDecodingStrategy = .convertFromSnakeCase 60 | decoder.dateDecodingStrategy = .secondsSince1970 61 | let fixture = try decoder.decode(Fixture.self, from: jsonData) 62 | 63 | XCTAssertEqual(fixture.theValues, [Date(timeIntervalSince1970: 123)]) 64 | 65 | let encoder = JSONEncoder() 66 | encoder.keyEncodingStrategy = .convertToSnakeCase 67 | encoder.dateEncodingStrategy = .secondsSince1970 68 | let data = try encoder.encode(fixture) 69 | let fixture2 = try decoder.decode(Fixture.self, from: data) 70 | 71 | XCTAssertEqual(fixture2.theValues, [Date(timeIntervalSince1970: 123)]) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/LossyDictionaryTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | class LossyDictionaryTests: XCTestCase { 5 | struct Fixture: Equatable, Codable { 6 | @LossyDictionary var stringToInt: [String: Int] 7 | @LossyDictionary var intToString: [Int: String] 8 | } 9 | 10 | func testDecodingLossyDictionaryIgnoresFailableElements() throws { 11 | let jsonData = """ 12 | { 13 | "stringToInt": { 14 | "one": 1, 15 | "two": 2, 16 | "three": null 17 | }, 18 | "intToString": { 19 | "1": "one", 20 | "2": "two", 21 | "3": null 22 | } 23 | } 24 | """.data(using: .utf8)! 25 | 26 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 27 | XCTAssertEqual(fixture.stringToInt, ["one": 1, "two": 2]) 28 | XCTAssertEqual(fixture.intToString, [1: "one", 2: "two"]) 29 | } 30 | 31 | func testEncodingDecodedLossyDictionaryIgnoresFailableElements() throws { 32 | let jsonData = """ 33 | { 34 | "stringToInt": { 35 | "one": 1, 36 | "two": 2, 37 | "three": null 38 | }, 39 | "intToString": { 40 | "1": "one", 41 | "2": "two", 42 | "3": null 43 | } 44 | } 45 | """.data(using: .utf8)! 46 | 47 | var _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 48 | 49 | _fixture.stringToInt["three"] = 3 50 | _fixture.intToString[3] = "three" 51 | 52 | let fixtureData = try JSONEncoder().encode(_fixture) 53 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 54 | XCTAssertEqual(fixture.stringToInt, ["one": 1, "two": 2, "three": 3]) 55 | XCTAssertEqual(fixture.intToString, [1: "one", 2: "two", 3: "three"]) 56 | } 57 | 58 | func testEncodingDecodedLosslessArrayRetainsContents() throws { 59 | let jsonData = """ 60 | { 61 | "stringToInt": { 62 | "one": 1, 63 | "two": 2, 64 | "three": 3 65 | }, 66 | "intToString": { 67 | "1": "one", 68 | "2": "two", 69 | "3": "three" 70 | } 71 | } 72 | """.data(using: .utf8)! 73 | 74 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 75 | XCTAssertEqual(fixture.stringToInt, ["one": 1, "two": 2, "three": 3]) 76 | XCTAssertEqual(fixture.intToString, [1: "one", 2: "two", 3: "three"]) 77 | } 78 | 79 | func testEncodingLosslessDictionaryRetainsKeys() throws { 80 | let decoder = JSONDecoder() 81 | decoder.keyDecodingStrategy = .convertFromSnakeCase 82 | 83 | let encoder = JSONEncoder() 84 | encoder.keyEncodingStrategy = .convertToSnakeCase 85 | 86 | let fixture = Fixture( 87 | stringToInt: [ 88 | "snake_case.with.dots_99.and_numbers": 1, 89 | "dots.and_2.00.1_numbers": 2, 90 | "key.1": 3, 91 | "normal key": 4, 92 | "another_key": 5 93 | ], 94 | intToString: [:] 95 | ) 96 | 97 | let reencodedFixture = try decoder.decode(Fixture.self, from: encoder.encode(fixture)) 98 | 99 | XCTAssertEqual(reencodedFixture, fixture) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/LossyOptionalTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | class DefaultNilTests: XCTestCase { 5 | /// This test demonstrates the problem that `@LossyOptional` solves. When decoding 6 | /// optional types, it often the case that we end up with an error instead of 7 | /// defaulting back to `nil`. 8 | func testDecodingBadUrlAsOptionalWithoutDefaultNil() { 9 | struct Fixture: Codable { 10 | var a: URL? 11 | } 12 | 13 | let jsonData = #"{"a":"https://example .com"}"#.data(using: .utf8)! 14 | 15 | XCTAssertThrowsError(try JSONDecoder().decode(Fixture.self, from: jsonData)) 16 | } 17 | 18 | func testDecodingWithUrlConversions() throws { 19 | struct Fixture: Codable { 20 | @LossyOptional var a: URL? 21 | @LossyOptional var b: URL? 22 | } 23 | 24 | let badUrlString = "https://example .com" 25 | let goodUrlString = "https://example.com" 26 | 27 | let jsonData = #"{"a":"\#(badUrlString)", "b":"\#(goodUrlString)"}"#.data(using: .utf8)! 28 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 29 | 30 | XCTAssertNil(fixture.a) 31 | XCTAssertEqual(fixture.b, URL(string: goodUrlString)) 32 | } 33 | 34 | func testDecodingWithIntegerConversions() throws { 35 | struct Fixture: Codable { 36 | @LossyOptional var a: Int? 37 | @LossyOptional var b: Int? 38 | } 39 | 40 | let jsonData = #"{ "a": 3.14, "b": 3 }"#.data(using: .utf8)! 41 | let _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 42 | let fixtureData = try JSONEncoder().encode(_fixture) 43 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 44 | 45 | XCTAssertNil(fixture.a) 46 | XCTAssertEqual(fixture.b, 3) 47 | } 48 | 49 | func testDecodingWithNullValue() throws { 50 | struct Fixture: Codable { 51 | @LossyOptional var a: String? 52 | } 53 | 54 | let jsonData = #"{"a":null}"#.data(using: .utf8)! 55 | let fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 56 | 57 | XCTAssertNil(fixture.a) 58 | } 59 | 60 | func testDecodingWithMissingKey() throws { 61 | struct Fixture: Codable { 62 | @LossyOptional var a: String? 63 | } 64 | 65 | let jsonData = "{}".data(using: .utf8)! 66 | let _fixture = try JSONDecoder().decode(Fixture.self, from: jsonData) 67 | let fixtureData = try JSONEncoder().encode(_fixture) 68 | let fixture = try JSONDecoder().decode(Fixture.self, from: fixtureData) 69 | 70 | XCTAssertNil(fixture.a) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/RawRepresentableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BetterCodable 3 | 4 | class RawRepresentableTests: XCTestCase { 5 | 6 | func testEnumDecodingWithDefaultValue() throws { 7 | 8 | enum VehicleType: String, Codable, DefaultCodableStrategy { 9 | case car 10 | case motorcycle 11 | case unknown 12 | 13 | static var defaultValue: VehicleType { 14 | .unknown 15 | } 16 | } 17 | 18 | struct Vehicle: Codable { 19 | let name: String 20 | @DefaultCodable 21 | var vehicleType: VehicleType 22 | } 23 | 24 | let json = "{ \"name\": \"Tesla\", \"vehicleType\": \"electric\" }".data(using: .utf8)! 25 | 26 | let car = try JSONDecoder().decode(Vehicle.self, from: json) 27 | XCTAssertEqual(car.vehicleType, .unknown) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/BetterCodableTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(BetterCodableTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import BetterCodableTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += BetterCodableTests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------