Skip to content

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.

License

Notifications You must be signed in to change notification settings

pawel-sp/AutoCodable

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

8 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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:

// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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 file for more info.

About

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.

Resources

License

Stars

Watchers

Forks

Languages