Skip to content

Commit 9937890

Browse files
committed
initial commit
0 parents  commit 9937890

26 files changed

+1134
-0
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.

Package.swift

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// swift-tools-version:5.1
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "BetterCodable",
8+
platforms: [
9+
.iOS(.v13),
10+
.macOS(.v10_15),
11+
.tvOS(.v13),
12+
.watchOS(.v6)
13+
],
14+
products: [
15+
.library(
16+
name: "BetterCodable",
17+
targets: ["BetterCodable"]),
18+
],
19+
dependencies: [],
20+
targets: [
21+
.target(
22+
name: "BetterCodable",
23+
dependencies: []),
24+
.testTarget(
25+
name: "BetterCodableTests",
26+
dependencies: ["BetterCodable"]),
27+
],
28+
swiftLanguageVersions: [
29+
.version("5.1")
30+
]
31+
)

README.md

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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+
## @DefaultFalse
78+
79+
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.
80+
81+
`@DefaultFalse` mitigates the confusion by defaulting decoded Bools to false if the Decoder is unable to decode the value, eitehr when null is encountered or some unexpected type.
82+
83+
### Usage
84+
85+
```Swift
86+
struct UserPrivilege: Codable {
87+
@DefaultFalse var isAdmin: Bool
88+
}
89+
90+
let json = #"{ "isAdmin": null }"#.data(using: .utf8)!
91+
let result = try JSONDecoder().decode(Response.self, from: json)
92+
93+
print(result) // UserPrivilege(isAdmin: false)
94+
```
95+
96+
## @DefaultEmptyArray
97+
98+
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.
99+
100+
`@DefaultEmptyArray` decodes Arrays and returns an empty array instead of nil if the Decoder is unable to decode the container.
101+
102+
### Usage
103+
104+
```Swift
105+
struct Response: Codable {
106+
@DefaultEmptyArray var favorites: [Favorite]
107+
}
108+
109+
let json = #"{ "favorites": null }"#.data(using: .utf8)!
110+
let result = try JSONDecoder().decode(Response.self, from: json)
111+
112+
print(result) // Response(favorites: [])
113+
```
114+
115+
## @DefaultEmptyDictionary
116+
117+
As mentioned previously, Optional Dictionaries are yet another container where nil and emptiness collide.
118+
119+
`@DefaultEmptyDictionary` decodes Dictionaries and returns an empty dictionary instead of nil if the Decoder is unable to decode the container.
120+
121+
### Usage
122+
123+
```Swift
124+
struct Response: Codable {
125+
@DefaultEmptyDictionary var scores: [String: Int]
126+
}
127+
128+
let json = #"{ "scores": null }"#.data(using: .utf8)!
129+
let result = try JSONDecoder().decode(Response.self, from: json)
130+
131+
print(result) // Response(values: [:])
132+
```
133+
134+
## @LosslessValue
135+
136+
All credit for this goes to [Ian Keen](https://twitter.com/iankay).
137+
138+
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.
139+
140+
`@LesslessValue` 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.
141+
142+
### Usage
143+
144+
```Swift
145+
struct Response: Codable {
146+
@LosslessValue var sku: String
147+
@LosslessValue var isAvailable: Bool
148+
}
149+
150+
let json = #"{ "sku": 12345, "isAvailable": "true" }"#.data(using: .utf8)!
151+
let result = try JSONDecoder().decode(Response.self, from: json)
152+
153+
print(result) // Response(sku: "12355", isAvailable: true)
154+
```
155+
156+
## Date Wrappers
157+
158+
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.
159+
160+
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 enties it decodes. Below are a few common Date strategies, but they also serve as a template to implement a custom property wrapper to suit your spcific date format needs.
161+
162+
The following property wrappers are heavily inspired by [Ian Keen](https://twitter.com/iankay).
163+
164+
## @ISO8601Date
165+
166+
`@ISO8601Date` 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.
167+
168+
### Usage
169+
170+
```Swift
171+
struct Response: Codable {
172+
@ISO8601Date var date: Date
173+
}
174+
175+
let json = #"{ "date": "1996-12-19T16:39:57-08:00" }"#.data(using: .utf8)!
176+
let result = try JSONDecoder().decode(Response.self, from: json)
177+
178+
// 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).
179+
```
180+
181+
## @RFC3339Date
182+
183+
`@RFC3339Date` decodes RFC 3339 date strings into `Date`s. Encoding the date will encode the value back into the original string value.
184+
185+
### Usage
186+
187+
```Swift
188+
struct Response: Codable {
189+
@RFC3339Date var date: Date
190+
}
191+
192+
let json = #"{ "date": "1996-12-19T16:39:57-08:00" }"#.data(using: .utf8)!
193+
let result = try JSONDecoder().decode(Response.self, from: json)
194+
195+
// 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).
196+
```
197+
198+
## @TimestampDate
199+
200+
`@TimestampDate` decodes `Double`s of a unix epoch into `Date`s. Encoding the date will encode the value into the original `TimeInterval` value.
201+
202+
### Usage
203+
204+
```Swift
205+
struct Response: Codable {
206+
@TimestampDate var date: Date
207+
}
208+
209+
let json = #"{ "date": 978307200.0 }"#.data(using: .utf8)!
210+
let result = try JSONDecoder().decode(Response.self, from: json)
211+
212+
// This produces a valid `Date` representing January 1st, 2001.
213+
```
214+
215+
## @YearMonthDayDate
216+
217+
`@YearMonthDayDate` 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.
218+
219+
### Usage
220+
221+
```Swift
222+
struct Response: Codable {
223+
@YearMonthDayDate var date: Date
224+
}
225+
226+
let json = #"{ "date": "2001-01-01" }"#.data(using: .utf8)!
227+
let result = try JSONDecoder().decode(Response.self, from: json)
228+
229+
// This produces a valid `Date` representing January 1st, 2001.
230+
```
231+
232+
Or lastly, you can mix and match date wrappers as needed where the benefits truly shine
233+
234+
```Swift
235+
struct Response: Codable {
236+
@ISO8601Date var updatedAt: Date
237+
@YearMonthDayDate var birthday: Date
238+
}
239+
240+
let json = #"{ "updatedAt": "2019-10-19T16:14:32-05:00", "birthday": "1984-01-22" }"#.data(using: .utf8)!
241+
let result = try JSONDecoder().decode(Response.self, from: json)
242+
243+
// This produces two valid `Date` values, `updatedAt` representing October 19, 2019 and `birthday` January 22nd, 1984.
244+
```
245+
246+
## Installation
247+
248+
Swift Package Manager
249+
250+
### Attribution
251+
252+
This project is licensed under MIT. If you find these useful, please tell your boss where you found them.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Foundation
2+
3+
protocol DateFormattingCodableStrategy {
4+
associatedtype RawValue: Codable
5+
6+
static func decode(_ value: RawValue) throws -> Date
7+
static func encode(_ date: Date) -> RawValue
8+
}
9+
10+
struct DateCodableValue<Formatter: DateFormattingCodableStrategy>: Codable {
11+
let value: Formatter.RawValue
12+
let date: Date
13+
14+
init(date: Date) {
15+
self.date = date
16+
self.value = Formatter.encode(date)
17+
}
18+
19+
init(from decoder: Decoder) throws {
20+
self.value = try Formatter.RawValue(from: decoder)
21+
self.date = try Formatter.decode(value)
22+
}
23+
24+
func encode(to encoder: Encoder) throws {
25+
try value.encode(to: encoder)
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// Decodes Arrays returning an empty array instead of nil if appicable
2+
///
3+
/// `@DefaultEmptyArray` decodes Arrays and returns an empty array instead of nil if the Decoder is unable to decode the container.
4+
@propertyWrapper
5+
public struct DefaultEmptyArray<T: Codable>: Codable {
6+
public var wrappedValue: [T]
7+
8+
public init(wrappedValue: [T]) {
9+
self.wrappedValue = wrappedValue
10+
}
11+
12+
public init(from decoder: Decoder) throws {
13+
let container = try decoder.singleValueContainer()
14+
self.wrappedValue = (try? container.decode([T].self)) ?? []
15+
}
16+
17+
public func encode(to encoder: Encoder) throws {
18+
try wrappedValue.encode(to: encoder)
19+
}
20+
}
21+
22+
extension DefaultEmptyArray: Equatable where T: Equatable { }
23+
extension DefaultEmptyArray: Hashable where T: Hashable { }

0 commit comments

Comments
 (0)