├── .swift-version ├── .gitignore ├── Logo.png ├── .travis.yml ├── Tests ├── LinuxMain.swift └── CodextendedTests │ ├── XCTestManifests.swift │ ├── LinuxTestable.swift │ └── CodextendedTests.swift ├── Package.swift ├── Codextended.podspec ├── LICENSE ├── Sources └── Codextended │ └── Codextended.swift └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/Codextended/HEAD/Logo.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | language: generic 3 | sudo: required 4 | dist: trusty 5 | env: 6 | - SWIFT_VERSION=5.0 7 | install: 8 | - eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" 9 | script: 10 | - swift test 11 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Codextended 3 | * Copyright (c) John Sundell 2019 4 | * Licensed under the MIT license (see LICENSE file) 5 | */ 6 | 7 | import XCTest 8 | import CodextendedTests 9 | 10 | var tests = [XCTestCaseEntry]() 11 | tests += CodextendedTests.allTests() 12 | XCTMain(tests) 13 | -------------------------------------------------------------------------------- /Tests/CodextendedTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Codextended 3 | * Copyright (c) John Sundell 2019 4 | * Licensed under the MIT license (see LICENSE file) 5 | */ 6 | 7 | import XCTest 8 | 9 | #if !canImport(ObjectiveC) 10 | public func allTests() -> [XCTestCaseEntry] { 11 | return [ 12 | testCase(CodextendedTests.allTests), 13 | ] 14 | } 15 | #endif 16 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | /** 4 | * Codextended 5 | * Copyright (c) John Sundell 2019 6 | * Licensed under the MIT license (see LICENSE file) 7 | */ 8 | 9 | import PackageDescription 10 | 11 | let package = Package( 12 | name: "Codextended", 13 | products: [ 14 | .library( 15 | name: "Codextended", 16 | targets: ["Codextended"] 17 | ) 18 | ], 19 | targets: [ 20 | .target(name: "Codextended"), 21 | .testTarget( 22 | name: "CodextendedTests", 23 | dependencies: ["Codextended"] 24 | ) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Codextended.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "Codextended" 3 | spec.version = "0.2.0" 4 | spec.summary = "Extensions giving Swift's Codable API type inference super powers." 5 | spec.description = "Codextended adds a set of extensions on top of Swift's Codable API to give it type inference super powers." 6 | spec.homepage = "https://github.com/JohnSundell/Codextended" 7 | spec.license = { :type => "MIT", :file => "LICENSE" } 8 | spec.author = { "John Sundell" => "john@sundell.co" } 9 | spec.source = { :git => "https://github.com/JohnSundell/Codextended.git", :tag => "#{spec.version}" } 10 | spec.source_files = "Sources/Codextended/*.swift" 11 | spec.ios.deployment_target = "9.0" 12 | spec.osx.deployment_target = "10.9" 13 | spec.watchos.deployment_target = "3.0" 14 | spec.tvos.deployment_target = "9.0" 15 | end 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 John Sundell 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 | -------------------------------------------------------------------------------- /Tests/CodextendedTests/LinuxTestable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Codextended 3 | * Copyright (c) John Sundell 2019 4 | * Licensed under the MIT license (see LICENSE.md) 5 | */ 6 | 7 | import XCTest 8 | 9 | protocol LinuxTestable: XCTestCase { 10 | static var allTests: [(String, (Self) -> () throws -> Void)] { get } 11 | } 12 | 13 | extension LinuxTestable { 14 | func verifyAllTestsRunOnLinux(excluding excludedTestNames: Set) { 15 | #if os(macOS) 16 | let testNames = Set(Self.allTests.map { $0.0 }) 17 | 18 | for name in Self.testNames { 19 | guard name != "testAllTestsRunOnLinux" else { 20 | continue 21 | } 22 | 23 | guard !excludedTestNames.contains(name) else { 24 | continue 25 | } 26 | 27 | if !testNames.contains(name) { 28 | XCTFail(""" 29 | Test case \(Self.self) does not include test \(name) on Linux. 30 | Please add it to the test case's 'allTests' array. 31 | """) 32 | } 33 | } 34 | #endif 35 | } 36 | } 37 | 38 | #if os(macOS) 39 | private extension LinuxTestable { 40 | static var testNames: [String] { 41 | return defaultTestSuite.tests.map { test in 42 | let components = test.name.components(separatedBy: .whitespaces) 43 | return components[1].replacingOccurrences(of: "]", with: "") 44 | } 45 | } 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /Sources/Codextended/Codextended.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Codextended 3 | * Copyright (c) John Sundell 2019 4 | * Licensed under the MIT license (see LICENSE file) 5 | */ 6 | 7 | import Foundation 8 | 9 | // MARK: - Encoding 10 | 11 | /// Protocol acting as a common API for all types of encoders, 12 | /// such as `JSONEncoder` and `PropertyListEncoder`. 13 | public protocol AnyEncoder { 14 | /// Encode a given value into binary data. 15 | func encode(_ value: T) throws -> Data 16 | } 17 | 18 | extension JSONEncoder: AnyEncoder {} 19 | 20 | #if canImport(ObjectiveC) || swift(>=5.1) 21 | extension PropertyListEncoder: AnyEncoder {} 22 | #endif 23 | 24 | public extension Encodable { 25 | /// Encode this value, optionally using a specific encoder. 26 | /// If no explicit encoder is passed, then the value is encoded into JSON. 27 | func encoded(using encoder: AnyEncoder = JSONEncoder()) throws -> Data { 28 | return try encoder.encode(self) 29 | } 30 | } 31 | 32 | public extension Encoder { 33 | /// Encode a singular value into this encoder. 34 | func encodeSingleValue(_ value: T) throws { 35 | var container = singleValueContainer() 36 | try container.encode(value) 37 | } 38 | 39 | /// Encode a value for a given key, specified as a string. 40 | func encode(_ value: T, for key: String) throws { 41 | try encode(value, for: AnyCodingKey(key)) 42 | } 43 | 44 | /// Encode a value for a given key, specified as a `CodingKey`. 45 | func encode(_ value: T, for key: K) throws { 46 | var container = self.container(keyedBy: K.self) 47 | try container.encode(value, forKey: key) 48 | } 49 | 50 | /// Encode a date for a given key (specified as a string), using a specific formatter. 51 | /// To encode a date without using a specific formatter, simply encode it like any other value. 52 | func encode(_ date: Date, for key: String, using formatter: F) throws { 53 | try encode(date, for: AnyCodingKey(key), using: formatter) 54 | } 55 | 56 | /// Encode a date for a given key (specified using a `CodingKey`), using a specific formatter. 57 | /// To encode a date without using a specific formatter, simply encode it like any other value. 58 | func encode(_ date: Date, for key: K, using formatter: F) throws { 59 | let string = formatter.string(from: date) 60 | try encode(string, for: key) 61 | } 62 | } 63 | 64 | // MARK: - Decoding 65 | 66 | /// Protocol acting as a common API for all types of decoders, 67 | /// such as `JSONDecoder` and `PropertyListDecoder`. 68 | public protocol AnyDecoder { 69 | /// Decode a value of a given type from binary data. 70 | func decode(_ type: T.Type, from data: Data) throws -> T 71 | } 72 | 73 | extension JSONDecoder: AnyDecoder {} 74 | 75 | #if canImport(ObjectiveC) || swift(>=5.1) 76 | extension PropertyListDecoder: AnyDecoder {} 77 | #endif 78 | 79 | public extension Data { 80 | /// Decode this data into a value, optionally using a specific decoder. 81 | /// If no explicit encoder is passed, then the data is decoded as JSON. 82 | func decoded(as type: T.Type = T.self, 83 | using decoder: AnyDecoder = JSONDecoder()) throws -> T { 84 | return try decoder.decode(T.self, from: self) 85 | } 86 | } 87 | 88 | public extension Decoder { 89 | /// Decode a singular value from the underlying data. 90 | func decodeSingleValue(as type: T.Type = T.self) throws -> T { 91 | let container = try singleValueContainer() 92 | return try container.decode(type) 93 | } 94 | 95 | /// Decode a value for a given key, specified as a string. 96 | func decode(_ key: String, as type: T.Type = T.self) throws -> T { 97 | return try decode(AnyCodingKey(key), as: type) 98 | } 99 | 100 | /// Decode a value for a given key, specified as a `CodingKey`. 101 | func decode(_ key: K, as type: T.Type = T.self) throws -> T { 102 | let container = try self.container(keyedBy: K.self) 103 | return try container.decode(type, forKey: key) 104 | } 105 | 106 | /// Decode an optional value for a given key, specified as a string. Throws an error if the 107 | /// specified key exists but is not able to be decoded as the inferred type. 108 | func decodeIfPresent(_ key: String, as type: T.Type = T.self) throws -> T? { 109 | return try decodeIfPresent(AnyCodingKey(key), as: type) 110 | } 111 | 112 | /// Decode an optional value for a given key, specified as a `CodingKey`. Throws an error if the 113 | /// specified key exists but is not able to be decoded as the inferred type. 114 | func decodeIfPresent(_ key: K, as type: T.Type = T.self) throws -> T? { 115 | let container = try self.container(keyedBy: K.self) 116 | return try container.decodeIfPresent(type, forKey: key) 117 | } 118 | 119 | /// Decode a date from a string for a given key (specified as a string), using a 120 | /// specific formatter. To decode a date using the decoder's default settings, 121 | /// simply decode it like any other value instead of using this method. 122 | func decode(_ key: String, using formatter: F) throws -> Date { 123 | return try decode(AnyCodingKey(key), using: formatter) 124 | } 125 | 126 | /// Decode a date from a string for a given key (specified as a `CodingKey`), using 127 | /// a specific formatter. To decode a date using the decoder's default settings, 128 | /// simply decode it like any other value instead of using this method. 129 | func decode(_ key: K, using formatter: F) throws -> Date { 130 | let container = try self.container(keyedBy: K.self) 131 | let rawString = try container.decode(String.self, forKey: key) 132 | 133 | guard let date = formatter.date(from: rawString) else { 134 | throw DecodingError.dataCorruptedError( 135 | forKey: key, 136 | in: container, 137 | debugDescription: "Unable to format date string" 138 | ) 139 | } 140 | 141 | return date 142 | } 143 | } 144 | 145 | // MARK: - Date formatters 146 | 147 | /// Protocol acting as a common API for all types of date formatters, 148 | /// such as `DateFormatter` and `ISO8601DateFormatter`. 149 | public protocol AnyDateFormatter { 150 | /// Format a string into a date 151 | func date(from string: String) -> Date? 152 | /// Format a date into a string 153 | func string(from date: Date) -> String 154 | } 155 | 156 | extension DateFormatter: AnyDateFormatter {} 157 | 158 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) 159 | extension ISO8601DateFormatter: AnyDateFormatter {} 160 | 161 | // MARK: - Private supporting types 162 | 163 | private struct AnyCodingKey: CodingKey { 164 | var stringValue: String 165 | var intValue: Int? 166 | 167 | init(_ string: String) { 168 | stringValue = string 169 | } 170 | 171 | init?(stringValue: String) { 172 | self.stringValue = stringValue 173 | } 174 | 175 | init?(intValue: Int) { 176 | self.intValue = intValue 177 | self.stringValue = String(intValue) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Tests/CodextendedTests/CodextendedTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Codextended 3 | * Copyright (c) John Sundell 2019 4 | * Licensed under the MIT license (see LICENSE file) 5 | */ 6 | 7 | import XCTest 8 | import Codextended 9 | 10 | final class CodextendedTests: XCTestCase { 11 | func testEncodingAndDecoding() throws { 12 | struct Value: Codable, Equatable { 13 | let string: String 14 | } 15 | 16 | let valueA = Value(string: "Hello, world!") 17 | let data = try valueA.encoded() 18 | let valueB = try data.decoded() as Value 19 | XCTAssertEqual(valueA, valueB) 20 | } 21 | 22 | func testDecodeIfPresent() throws { 23 | struct Value: Decodable, Equatable { 24 | let stringA: String? 25 | let stringB: String? 26 | 27 | init(from decoder: Decoder) throws { 28 | stringA = try decoder.decodeIfPresent("stringA") 29 | stringB = try decoder.decodeIfPresent("stringB") 30 | } 31 | } 32 | 33 | let value = try Data(#"{"stringA": "stringA"}"#.utf8).decoded() as Value 34 | XCTAssertEqual(value.stringA, "stringA") 35 | XCTAssertNil(value.stringB) 36 | } 37 | 38 | func testDecodeIfPresentTypeMismatch() throws { 39 | struct Value: Decodable, Equatable { 40 | let string: String? 41 | 42 | init(from decoder: Decoder) throws { 43 | string = try decoder.decodeIfPresent("string") 44 | } 45 | } 46 | 47 | do { 48 | let _ = try Data(#"{"string": 123}"#.utf8).decoded() as Value 49 | XCTFail("Decoding expected to fail due to type mismatch.") 50 | } catch DecodingError.typeMismatch { 51 | return 52 | } catch { 53 | XCTFail("Expected `typeMismatch` error.") 54 | } 55 | } 56 | 57 | func testSingleValue() throws { 58 | struct Value: Codable, Equatable { 59 | let string: String 60 | 61 | init(string: String) { 62 | self.string = string 63 | } 64 | 65 | init(from decoder: Decoder) throws { 66 | string = try decoder.decodeSingleValue() 67 | } 68 | 69 | func encode(to encoder: Encoder) throws { 70 | try encoder.encodeSingleValue(string) 71 | } 72 | } 73 | 74 | let valuesA = [Value(string: "Hello, world!")] 75 | let data = try valuesA.encoded() 76 | let valuesB = try data.decoded() as [Value] 77 | XCTAssertEqual(valuesA, valuesB) 78 | } 79 | 80 | func testUsingStringAsKey() throws { 81 | struct Value: Codable, Equatable { 82 | let string: String 83 | 84 | init(string: String) { 85 | self.string = string 86 | } 87 | 88 | init(from decoder: Decoder) throws { 89 | string = try decoder.decode("key") 90 | } 91 | 92 | func encode(to encoder: Encoder) throws { 93 | try encoder.encode(string, for: "key") 94 | } 95 | } 96 | 97 | let valueA = Value(string: "Hello, world!") 98 | let data = try valueA.encoded() 99 | let valueB = try data.decoded() as Value 100 | XCTAssertEqual(valueA, valueB) 101 | } 102 | 103 | func testUsingCodingKey() throws { 104 | struct Value: Codable, Equatable { 105 | enum CodingKeys: CodingKey { 106 | case key 107 | } 108 | 109 | let string: String 110 | 111 | init(string: String) { 112 | self.string = string 113 | } 114 | 115 | init(from decoder: Decoder) throws { 116 | string = try decoder.decode(CodingKeys.key) 117 | } 118 | 119 | func encode(to encoder: Encoder) throws { 120 | try encoder.encode(string, for: CodingKeys.key) 121 | } 122 | } 123 | 124 | let valueA = Value(string: "Hello, world!") 125 | let data = try valueA.encoded() 126 | let valueB = try data.decoded() as Value 127 | XCTAssertEqual(valueA, valueB) 128 | } 129 | 130 | func testDateWithCustomFormatter() throws { 131 | struct Value: Codable, Equatable { 132 | static func makeDateFormatter() -> DateFormatter { 133 | let formatter = DateFormatter() 134 | formatter.dateFormat = "yyyy-MM-dd" 135 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 136 | return formatter 137 | } 138 | 139 | let date: Date 140 | 141 | init(date: Date) { 142 | self.date = date 143 | } 144 | 145 | init(from decoder: Decoder) throws { 146 | let formatter = Value.makeDateFormatter() 147 | date = try decoder.decode("key", using: formatter) 148 | } 149 | 150 | func encode(to encoder: Encoder) throws { 151 | let formatter = Value.makeDateFormatter() 152 | try encoder.encode(date, for: "key", using: formatter) 153 | } 154 | } 155 | 156 | let valueA = Value(date: Date()) 157 | let data = try valueA.encoded() 158 | let valueB = try data.decoded() as Value 159 | let formatter = Value.makeDateFormatter() 160 | 161 | XCTAssertEqual(formatter.string(from: valueA.date), 162 | formatter.string(from: valueB.date)) 163 | } 164 | 165 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) 166 | func testDateWithISO8601Formatter() throws { 167 | struct Value: Codable, Equatable { 168 | let date: Date 169 | 170 | init(date: Date) { 171 | self.date = date 172 | } 173 | 174 | init(from decoder: Decoder) throws { 175 | let formatter = ISO8601DateFormatter() 176 | date = try decoder.decode("key", using: formatter) 177 | } 178 | 179 | func encode(to encoder: Encoder) throws { 180 | let formatter = ISO8601DateFormatter() 181 | try encoder.encode(date, for: "key", using: formatter) 182 | } 183 | } 184 | 185 | let valueA = Value(date: Date()) 186 | let data = try valueA.encoded() 187 | let valueB = try data.decoded() as Value 188 | let formatter = ISO8601DateFormatter() 189 | 190 | XCTAssertEqual(formatter.string(from: valueA.date), 191 | formatter.string(from: valueB.date)) 192 | } 193 | 194 | func testDecodingErrorThrownForInvalidDateString() { 195 | struct Value: Decodable { 196 | let date: Date 197 | 198 | init(date: Date) { 199 | self.date = date 200 | } 201 | 202 | init(from decoder: Decoder) throws { 203 | date = try decoder.decode("key", using: DateFormatter()) 204 | } 205 | } 206 | 207 | let data = Data(#"{"key": "notADate"}"#.utf8) 208 | 209 | XCTAssertThrowsError(try data.decoded() as Value) { error in 210 | XCTAssertTrue(error is DecodingError, 211 | "Expected DecodingError but got \(type(of: error))") 212 | } 213 | } 214 | 215 | func testAllTestsRunOnLinux() { 216 | verifyAllTestsRunOnLinux(excluding: ["testDateWithISO8601Formatter"]) 217 | } 218 | } 219 | 220 | extension CodextendedTests: LinuxTestable { 221 | static var allTests = [ 222 | ("testEncodingAndDecoding", testEncodingAndDecoding), 223 | ("testDecodeIfPresent", testDecodeIfPresent), 224 | ("testDecodeIfPresentTypeMismatch", testDecodeIfPresentTypeMismatch), 225 | ("testSingleValue", testSingleValue), 226 | ("testUsingStringAsKey", testUsingStringAsKey), 227 | ("testUsingCodingKey", testUsingCodingKey), 228 | ("testDateWithCustomFormatter", testDateWithCustomFormatter), 229 | ("testDecodingErrorThrownForInvalidDateString", testDecodingErrorThrownForInvalidDateString) 230 | ] 231 | } 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Codextended 3 |

4 | 5 |

6 | 7 | 8 | Swift Package Manager 9 | 10 | Mac + Linux 11 | 12 | Twitter: @johnsundell 13 | 14 |

15 | 16 | Welcome to **Codextended** — a suite of extensions that aims to make Swift’s `Codable` API easier to use by giving it type inference-powered capabilities and conveniences. It’s not a wrapper, nor is it a brand new framework, instead it augments `Codable` directly in a very lightweight way. 17 | 18 | ## Codable is awesome! 19 | 20 | No third-party serialization framework can beat the convenience of `Codable`. Since it’s built in, it can both leverage the compiler to automatically synthesize all serialization code needed in many situations, and it can also be used as a common bridge between multiple different modules — without having to introduce any shared dependencies. 21 | 22 | However, once some form of customization is needed — for example to transform parts of the decoded data, or to provide default values for certain keys — the standard `Codable` API starts to become *really* verbose. It also doesn’t take advantage of Swift’s robust type inference capabilities, which produces a lot of unnecessary boilerplate. 23 | 24 | That’s what **Codextended** aims to fix. 25 | 26 | ## Examples 27 | 28 | Here are a few examples that demonstrate the difference between using “vanilla” `Codable` and the APIs that **Codextended** adds to it. The goal is to turn all common serialization operations into one-liners, rather than having to set up a ton of boilerplate. 29 | 30 | ### 🏢 Top-level API 31 | 32 | **Codextended** makes a few slight tweaks to the top-level API used to encode and decode values, making it possible to leverage type inference and use methods on the actual values that are being encoded or decoded. 33 | 34 | 🍨 With vanilla `Codable`: 35 | 36 | ```swift 37 | // Encoding 38 | let encoder = JSONEncoder() 39 | let data = try encoder.encode(value) 40 | 41 | // Decoding 42 | let decoder = JSONDecoder() 43 | let article = try decoder.decode(Article.self, from: data) 44 | ``` 45 | 46 | 🦸‍♀️ With **Codextended**: 47 | 48 | ```swift 49 | // Encoding 50 | let data = try value.encoded() 51 | 52 | // Decoding 53 | let article = try data.decoded() as Article 54 | 55 | // Decoding when the type can be inferred 56 | try saveArticle(data.decoded()) 57 | ``` 58 | 59 | ### 🔑 Overriding the behavior for a single key 60 | 61 | While `Codable` is amazing as long as the serialized data’s format exactly matches the format of the Swift types that’ll use it — as soon as we need to make just a small tweak, things quickly go from really convenient to very verbose. 62 | 63 | As an example, let’s just say that we want to provide a default value for one single property (without having to make it an optional, which would make it harder to handle in the rest of our code base). To do that, we need to completely manually implement our type’s decoding — like below for the `tags` property of an `Article` type. 64 | 65 | 🍨 With vanilla `Codable`: 66 | 67 | ```swift 68 | struct Article: Codable { 69 | enum CodingKeys: CodingKey { 70 | case title 71 | case body 72 | case footnotes 73 | case tags 74 | } 75 | 76 | var title: String 77 | var body: String 78 | var footnotes: String? 79 | var tags: [String] 80 | 81 | init(from decoder: Decoder) throws { 82 | let container = try decoder.container(keyedBy: CodingKeys.self) 83 | title = try container.decode(String.self, forKey: .title) 84 | body = try container.decode(String.self, forKey: .body) 85 | footnotes = try container.decodeIfPresent(String.self, forKey: .footnotes) 86 | tags = (try? container.decode([String].self, forKey: .tags)) ?? [] 87 | } 88 | } 89 | ``` 90 | 91 | 🦸‍♂️ With **Codextended**: 92 | 93 | ```swift 94 | struct Article: Codable { 95 | var title: String 96 | var body: String 97 | var footnotes: String? 98 | var tags: [String] 99 | 100 | init(from decoder: Decoder) throws { 101 | title = try decoder.decode("title") 102 | body = try decoder.decode("body") 103 | footnotes = try decoder.decodeIfPresent("footnotes") 104 | tags = (try? decoder.decode("tags")) ?? [] 105 | } 106 | } 107 | ``` 108 | 109 | **Codextended** includes decoding overloads both for `CodingKey`-based values and for string literals, so that we can pick the approach that’s the most appropriate/convenient for each given situation. 110 | 111 | ### 📆 Using date formatters 112 | 113 | `Codable` already comes with support for custom date formats through assigning a `DateFormatter` to either a `JSONEncoder` or `JSONDecoder`. However, requiring each call site to be aware of the specific date formats used for each type isn’t always great — so with **Codextended**, it’s easy for a type itself to pick what date format it needs to use. 114 | 115 | That’s really convenient when working with third-party data, and we only want to customize the date format for some of our types, or when we want to produce more readable date strings when encoding a value. 116 | 117 | 🍨 With vanilla `Codable`: 118 | 119 | ```swift 120 | struct Bookmark: Codable { 121 | enum CodingKeys: CodingKey { 122 | case url 123 | case date 124 | } 125 | 126 | struct DateCodingError: Error {} 127 | 128 | static let dateFormatter = makeDateFormatter() 129 | 130 | var url: URL 131 | var date: Date 132 | 133 | init(from decoder: Decoder) throws { 134 | let container = try decoder.container(keyedBy: CodingKeys.self) 135 | url = try container.decode(URL.self, forKey: .url) 136 | 137 | let dateString = try container.decode(String.self, forKey: .date) 138 | 139 | guard let date = Bookmark.dateFormatter.date(from: dateString) else { 140 | throw DateCodingError() 141 | } 142 | 143 | self.date = date 144 | } 145 | 146 | func encode(to encoder: Encoder) throws { 147 | var container = encoder.container(keyedBy: CodingKeys.self) 148 | try container.encode(url, forKey: .url) 149 | 150 | let dateString = Bookmark.dateFormatter.string(from: date) 151 | try container.encode(dateString, forKey: .date) 152 | } 153 | } 154 | ``` 155 | 156 | 🦹‍♀️ With **Codextended**: 157 | 158 | ```swift 159 | struct Bookmark: Codable { 160 | static let dateFormatter = makeDateFormatter() 161 | 162 | var url: URL 163 | var date: Date 164 | 165 | init(from decoder: Decoder) throws { 166 | url = try decoder.decode("url") 167 | date = try decoder.decode("date", using: Bookmark.dateFormatter) 168 | } 169 | 170 | func encode(to encoder: Encoder) throws { 171 | try encoder.encode(url, for: "url") 172 | try encoder.encode(date, for: "date", using: Bookmark.dateFormatter) 173 | } 174 | } 175 | ``` 176 | 177 | Again, we could’ve chosen to use a `CodingKeys` enum above to represent our keys, rather than using inline strings. 178 | 179 | ## Mix and match 180 | 181 | Since **Codextended** is 100% implemented through extensions, you can easily mix and match it with “vanilla” `Codable` code within the same project. It also doesn’t change what makes `Codable` so great — the fact that it often doesn’t require any manual code at all, and that it can be used as a bridge between frameworks. 182 | 183 | All it does is give `Codable` a *helping hand* when some form of customization is needed. 184 | 185 | ## Installation 186 | 187 | Since **Codextended** is implemented within a single file, the easiest way to use it is to simply drag and drop it into your Xcode project. 188 | 189 | But if you wish to use a dependency manager, you can either use the [Swift Package Manager](https://github.com/apple/swift-package-manager) by declaring **Codextended** as a dependency in your `Package.swift` file: 190 | 191 | ```swift 192 | .package(url: "https://github.com/JohnSundell/Codextended", from: "0.1.0") 193 | ``` 194 | 195 | *For more information, see [the Swift Package Manager documentation](https://github.com/apple/swift-package-manager/tree/master/Documentation).* 196 | 197 | You can also use [CocoaPods](https://cocoapods.org) by adding the following line to your `Podfile`: 198 | 199 | ```ruby 200 | pod "Codextended" 201 | ``` 202 | 203 | ## Contributions & support 204 | 205 | **Codextended** is developed completely in the open, and your contributions are more than welcome. 206 | 207 | Before you start using **Codextended** in any of your projects, it’s highly recommended that you spend a few minutes familiarizing yourself with its documentation and internal implementation (it all fits [in a single file](https://github.com/JohnSundell/Codextended/blob/master/Sources/Codextended/Codextended.swift)!), so that you’ll be ready to tackle any issues or edge cases that you might encounter. 208 | 209 | To learn more about the principles used to implement **Codextended**, check out *[“Type inference-powered serialization in Swift”](https://www.swiftbysundell.com/posts/type-inference-powered-serialization-in-swift)* on Swift by Sundell. 210 | 211 | This project does not come with GitHub Issues-based support, and users are instead encouraged to become active participants in its continued development — by fixing any bugs that they encounter, or improving the documentation wherever it’s found to be lacking. 212 | 213 | If you wish to make a change, [open a Pull Request](https://github.com/JohnSundell/Codextended/pull/new) — even if it just contains a draft of the changes you’re planning, or a test that reproduces an issue — and we can discuss it further from there. 214 | 215 | Hope you’ll enjoy using **Codextended**! 😀 216 | 217 | --------------------------------------------------------------------------------