diff --git a/README.md b/README.md new file mode 100644 index 0000000..b90b0de --- /dev/null +++ b/README.md @@ -0,0 +1,561 @@ +# AutoCodable + +`AutoCodable` exposes Swift macros that generate code to fulfill `Encodable` and `Decodable` requirements when adding the protocol conformance to an extension of a type in a different file. + +## Motivation + +The Swift's built-in `Codable` API has a major advantage - it automatically synthesizes many different encoding and decoding implementations when using it. This behavior allows to easy create custom types and makes them conform to `Encodable` or `Decodable` without a need to implement `func encode(to encoder: Encoder) throws` or `init(from decoder: Decoder) throws` explicitly. + +However, one of its limitations is the necessity to keep everything within the same file. It means that the following scenario may happen: + +```swift +// User.swift +struct User { + let firstName: String + let lastName: String +} + +// User+Decodable.swift +extension User: Decodable { + // πŸ›‘ Extension outside of file declaring struct 'User' prevents + // automatic synthesis of 'init(from:)' for protocol 'Decodable' +} + +// User+Encodable.swift +extension User: Encodable { + // πŸ›‘ Extension outside of file declaring struct 'User' prevents + // automatic synthesis of 'encode(to:)' for protocol 'Encodable' +} +``` + +There may be many reasons why you would like to keep the conformance to `Codable` or `Decodable` outside of the file with the type declaration. Unfortunately, in such cases, it's required to implement it explicitly. The `AutoDecodable` and `AutoEncodable` macro fills this gap. It allows to generation of necessary code and still keeps the declaration separate from the conformance to the protocols. + +## Usage + +### Encodable + +
+@AutoEncodable + +```swift +// User.swift +struct User { + let firstName: String + let lastName: String +} + +// User+Encodable.swift +@AutoEncodable +extension User: Encodable { + enum CodingKeys: String, CodingKey { + case firstName + case lastName + } +} + +πŸ”½ + +// User+Encodable.swift +extension User: Encodable { + enum CodingKeys: String, CodingKey { + case firstName + case lastName + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(firstName, forKey: .firstName) + try container.encode(lastName, forKey: .lastName) + } +} +``` +
+ +
+@AutoEncodable + public access control + +```swift +// User.swift +public struct User { + public let firstName: String + public let lastName: String +} + +// User+Encodable.swift +@AutoEncodable(accessControl: .public) +extension User: Encodable { + enum CodingKeys: String, CodingKey { + case firstName + case lastName + } +} + +πŸ”½ + +// User+Encodable.swift +extension User: Encodable { + enum CodingKeys: String, CodingKey { + case firstName + case lastName + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(firstName, forKey: .firstName) + try container.encode(lastName, forKey: .lastName) + } +} +``` +
+ +
+@AutoEncodable + singleValueContainer + +```swift +// Identifier.swift +struct Identifier { + let value: Int +} + +// Identifier+Encodable.swift +//❗️The name associated with `singleValue` must match the property name inside the type. +@AutoEncodable(container: .singleValue("value")) +extension Identifier: Encodable {} + +πŸ”½ + +// Identifier+Encodable.swift +extension Identifier: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(value) + } +} +``` +
+ +
+@AutoEncodable + nestedContainer + +```swift +// User.swift +struct User { + let firstName: String + let lastName: String +} + +// User+Encodable.swift +@AutoEncodable +extension User: Encodable { + enum CodingKeys: String, CodingKey { + case names + + //❗️The nested coding keys enum must follow the name convention: `CaseName` + `CodingKeys` + enum NamesCodingKeys: String, CodingKey { + case firstName + case lastName + } + } +} + +πŸ”½ + +// User+Encodable.swift +extension User: Encodable { + enum CodingKeys: String, CodingKey { + case names + + enum NamesCodingKeys: String, CodingKey { + case firstName + case lastName + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + var namesContainer = container.nestedContainer( + keyedBy: CodingKeys.NamesCodingKeys.self, + forKey: .names + ) + try namesContainer.encode(firstName, forKey: .firstName) + try namesContainer.encode(lastName, forKey: .lastName) + } +} +``` +
+ +
+@AutoEncodable + enum + +```swift +// Membership.swift +enum Membership { + case regular + case premium +} + +// Membership+Encodable.swift +@AutoEncodable(container: .singleValueForEnum) +extension Membership: Encodable { + enum CodingKeys: String, CodingKey { + case regular + case premium + } +} + +πŸ”½ + +// Membership+Encodable.swift +extension Membership: Encodable { + enum CodingKeys: String, CodingKey { + case regular + case premium + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .regular: + try container.encode(CodingKeys.regular.rawValue) + case .premium: + try container.encode(CodingKeys.premium.rawValue) + } + } +} +``` +
+ +
+@AutoEncodable + property custom encoding + +```swift +// User.swift +struct User { + let firstName: String + let lastName: String + let avatarUrl: URL +} + +// User+Encodable.swift +@AutoEncodable +extension User: Encodable { + enum CodingKeys: String, CodingKey { + case firstName + case lastName + @EncodedValue(Avatar.self) + case avatarUrl + } + + private struct Avatar: EncodableValue { + let path: String + let `extension`: String + + init(from value: URL) { + self.path = value.deletingPathExtension().absoluteString + self.extension = value.pathExtension + } + } +} + +πŸ”½ + +// User+Encodable.swift +extension User: Encodable { + enum CodingKeys: String, CodingKey { + case firstName + case lastName + @EncodedValue(Avatar.self) + case avatarUrl + } + + private struct Avatar: EncodableValue { + let path: String + let `extension`: String + + init(from value: URL) { + self.path = value.deletingPathExtension().absoluteString + self.extension = value.pathExtension + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(firstName, forKey: .firstName) + try container.encode(lastName, forKey: .lastName) + try container.encode(Avatar(from: avatarUrl), forKey: .avatarUrl) + } +} +``` +
+ +### Decodable + +
+@AutoDecodable + +```swift +// User.swift +struct User { + let firstName: String + let lastName: String +} + +// User+Decodable.swift +@AutoDecodable +extension User: Decodable { + enum CodingKeys: String, CodingKey { + case firstName + case lastName + } +} + +πŸ”½ + +// User+Decodable.swift +extension User: Decodable { + enum CodingKeys: String, CodingKey { + case firstName + case lastName + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + try self.init( + firstName: container.decode(for: .firstName), + lastName: container.decode(for: .lastName) + ) + } +} +``` +
+ +
+@AutoDecodable + public access control + +```swift +// User.swift +public struct User { + public let firstName: String + public let lastName: String +} + +// User+Decodable.swift +@AutoDecodable(accessControl: .public) +extension User: Decodable { + enum CodingKeys: String, CodingKey { + case firstName + case lastName + } +} + +πŸ”½ + +// User+Decodable.swift +extension User: Decodable { + enum CodingKeys: String, CodingKey { + case firstName + case lastName + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + try self.init( + firstName: container.decode(for: .firstName), + lastName: container.decode(for: .lastName) + ) + } +} +``` +
+ +
+@AutoDecodable + singleValueContainer + +```swift +// Identifier.swift +struct Identifier { + let value: Int +} + +// Identifier+Decodable.swift +//❗️The name associated with `singleValue` must match the property name inside the type. +@AutoDecodable(container: .singleValue("value")) +extension Identifier: Encodable {} + +πŸ”½ + +// Identifier+Decodable.swift +extension Identifier: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + try self.init(value: container.decode()) + } +} +``` +
+ +
+@AutoDecodable + nestedContainer + +```swift +// User.swift +struct User { + let firstName: String + let lastName: String +} + +// User+Decodable.swift +@AutoDecodable +extension User: Decodable { + enum CodingKeys: String, CodingKey { + case names + + //❗️The nested coding keys enum must follow the name convention: `CaseName` + `CodingKeys` + enum NamesCodingKeys: String, CodingKey { + case firstName + case lastName + } + } +} + +πŸ”½ + +// User+Decodable.swift +extension User: Decodable { + enum CodingKeys: String, CodingKey { + case names + + enum NamesCodingKeys: String, CodingKey { + case firstName + case lastName + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let namesContainer = try container.nestedContainer( + keyedBy: CodingKeys.NamesCodingKeys.self, + forKey: .names + ) + try self.init( + firstName: namesContainer.decode(for: .firstName), + lastName: namesContainer.decode(for: .lastName) + ) + } +} +``` +
+ +
+@AutoDecodable + enum + +```swift +// Membership.swift +enum Membership { + case regular + case premium +} + +// Membership+Decodable.swift +@AutoDecodable(container: .singleValueForEnum) +extension Membership: Decodable { + enum CodingKeys: String, CodingKey { + case regular + case premium + } +} + +πŸ”½ + +// Membership+Decodable.swift +extension Membership: Decodable { + enum CodingKeys: String, CodingKey { + case regular + case premium + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let stringValue = try container.decode(String.self) + switch stringValue { + case CodingKeys.regular.rawValue: + self = .regular + case CodingKeys.premium.rawValue: + self = .premium + default: + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Invalid value: \(stringValue)" + ) + } + } +} +``` +
+ +
+@AutoDecodable + property custom decoding + +```swift +// User.swift +struct User { + let firstName: String + let lastName: String + let avatarUrl: URL? +} + +// User+Decodable.swift +@AutoDecodable +extension User: Decodable { + enum CodingKeys: String, CodingKey { + case firstName + case lastName + @DecodedValue(Avatar.self) + case avatarUrl + } + + private struct Avatar: DecodableValue { + let path: String + let `extension`: String + + func value() -> URL? { + .init(string: path + "." + `extension`) + } + } +} + +πŸ”½ + +// User+Decodable.swift +extension User: Decodable { + enum CodingKeys: String, CodingKey { + case firstName + case lastName + @DecodedValue(Avatar.self) + case avatarUrl + } + + private struct Avatar: DecodableValue { + let path: String + let `extension`: String + + func value() -> URL? { + .init(string: path + "." + `extension`) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + try self.init( + firstName: container.decode(for: .firstName), + lastName: container.decode(for: .lastName), + avatarUrl: container.decode(Avatar.self, forKey: .avatarUrl).value() + ) + } +} +``` +
+ +## License + +`AutoCodable` is released under the MIT license. See the [LICENSE](LICENSE) file for more info. \ No newline at end of file