Skip to content

zhmios/ExCodable

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

88 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ExCodable

Swift 5.0 Swift Package Manager Platforms Build and Test GitHub Releases (latest SemVer) Deploy to CocoaPods Cocoapods LICENSE @minglq

En | 中文

Contents

Features

  • Extends Swift Codable - Encodable & Decodable;
  • Supports Key-Mapping via KeyPath and Coding-Key:
    • ExCodable did not read/write memory via unsafe pointers;
    • No need to encode/decode properties one by one;
    • Just requires using var to declare properties and provide default values;
    • In most cases, the CodingKey type is no longer necessary, because it will only be used once, String literals may be better.
  • Supports multiple Key-Mappings for different data sources;
  • Supports multiple Alternative-Keys via Array for decoding;
  • Supports Nested-Keys via String with dot syntax;
  • Supports customized encode/decode via subscripts;
  • Supports builtin and custom Type-Conversions;
  • Supports struct, class and subclass;
  • Supports encode/decode with or without IfPresent;
  • Supports abort (throws error) or continue (returns nil) encode/decode if error encountered;
  • Uses JSON encoder/decoder by default, and supports PList;
  • Uses Type-Inference, supports JSON Data, String and Object.

Usage

0. Codable:

With Codable, it just needs to adop the Codable protocol without implementing any method of it.

struct TestAutoCodable: Codable, Equatable {
    private(set) var int: Int = 0
    private(set) var string: String?
    enum CodingKeys: String, CodingKey {
        case int = "i", string = "s"
    }
}

But, if you have to encode/decode manually for some reason, e.g. Alternative-Keys and Nested-Keys ...

struct TestManualCodable: Equatable {
    private(set) var int: Int = 0
    private(set) var string: String?
}

extension TestManualCodable: Codable {
    
    enum Keys: CodingKey {
        case int, i
        case nested, string
    }
    
    init(from decoder: Decoder) throws {
        if let container = try? decoder.container(keyedBy: Keys.self) {
            if let int = (try? container.decodeIfPresent(Int.self, forKey: Keys.int))
                ?? (try? container.decodeIfPresent(Int.self, forKey: Keys.i)) {
                self.int = int
            }
            if let nestedContainer = try? container.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested),
               let string = try? nestedContainer.decodeIfPresent(String.self, forKey: Keys.string) {
                self.string = string
            }
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: Keys.self)
        try? container.encodeIfPresent(int, forKey: Keys.int)
        var nestedContainer = container.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested)
        try? nestedContainer.encodeIfPresent(string, forKey: Keys.string)
    }
}

With ExCodable:

struct TestExCodable: Equatable {
    private(set) var int: Int = 0
    private(set) var string: String?
}

extension TestExCodable: ExCodable {
    static let keyMapping: [KeyMap<Self>] = [
        KeyMap(\.int, to: "int"),
        KeyMap(\.string, to: "string")
    ]
    init(from decoder: Decoder) throws {
        try decode(from: decoder, with: Self.keyMapping)
    }
}

1. Key-Mapping for struct:

With ExCodable, it needs to to declare properties with var and provide default values.

struct TestStruct: Equatable {
    private(set) var int: Int = 0
    private(set) var string: String?
    var bool: Bool!
}
extension TestStruct: ExCodable {
    
    static let keyMapping: [KeyMap<Self>] = [
        KeyMap(\.int, to: "int"),
        KeyMap(\.string, to: "string"),
    ]
    
    init(from decoder: Decoder) throws {
        try decode(from: decoder, with: Self.keyMapping)
    }
    // `encode` with default implementation can be omitted
    // func encode(to encoder: Encoder) throws {
    //     try encode(to: encoder, with: Self.keyMapping)
    // }
}

2. Alternative-Keys:

static let keyMapping: [KeyMap<Self>] = [
    KeyMap(\.int, to: "int", "i"),
    KeyMap(\.string, to: "string", "str", "s")
]

3. Nested-Keys:

static let keyMapping: [KeyMap<Self>] = [
    KeyMap(\.int, to: "int"),
    KeyMap(\.string, to: "nested.string")
]

4. Custom encode/decode:

struct TestCustomEncodeDecode: Equatable {
    var int: Int = 0
    var string: String?
}
extension TestCustomEncodeDecode: ExCodable {
    
    private enum Keys: CodingKey {
        case int, string
    }
    private static let dddd = "dddd"
    private func string(for int: Int) -> String {
        switch int {
            case 100: return "Continue"
            case 200: return "OK"
            case 304: return "Not Modified"
            case 403: return "Forbidden"
            case 404: return "Not Found"
            case 418: return "I'm a teapot"
            case 500: return "Internal Server Error"
            case 200..<400: return "success"
            default: return "failure"
        }
    }
    
    static let keyMapping: [KeyMap<Self>] = [
        KeyMap(\.int, to: Keys.int),
    ]
    
    init(from decoder: Decoder) throws {
        try decode(from: decoder, with: Self.keyMapping)
        string = decoder[Keys.string]
        if string == nil || string == Self.dddd {
            string = string(for: int)
        }
    }
    func encode(to encoder: Encoder) throws {
        try encode(to: encoder, with: Self.keyMapping)
        encoder[Keys.string] = Self.dddd
    }
}

5. Encode/decode constant properties with subscripts:

Using let to declare properties without default values.

struct TestSubscript: Equatable {
    let int: Int
    let string: String
}
extension TestSubscript: Encodable, Decodable {
    
    enum Keys: CodingKey {
        case int, string
    }
    
    init(from decoder: Decoder) throws {
        // - seealso:
        // string = decoder.decode(<#T##codingKeys: CodingKey...##CodingKey#>)
        // string = try decoder.decodeThrows(<#T##codingKeys: CodingKey...##CodingKey#>)
        // string = try decoder.decodeNonnullThrows(<#T##codingKeys: CodingKey...##CodingKey#>)
        int = decoder[Keys.int] ?? 0
        string = decoder[Keys.string] ?? ""
    }
    func encode(to encoder: Encoder) throws {
        // - seealso:
        // encoder.encode(<#T##value: Encodable?##Encodable?#>, for: <#T##CodingKey#>)
        // try encoder.encodeThrows(<#T##value: Encodable?##Encodable?#>, for: <#T##CodingKey#>)
        // try encoder.encodeNonnullThrows(<#T##value: Encodable##Encodable#>, for: <#T##CodingKey#>)
        encoder[Keys.int] = int
        encoder[Keys.string] = string
    }
}

6. Custom Type-Conversions:

Declare struct FloatToBoolDecodingTypeConverter with protocol ExCodableDecodingTypeConverter and implement its method, decode values in alternative types and convert to target type:

struct FloatToBoolDecodingTypeConverter: ExCodableDecodingTypeConverter {
    public func decode<T: Decodable, K: CodingKey>(_ container: KeyedDecodingContainer<K>, codingKey: K, as type: T.Type) -> T? {
        // Bool -> Double
        if type is Double.Type || type is Double?.Type {
            if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) {
                return (bool ? 1.0 : 0.0) as? T
            }
        }
        // Bool -> Float
        else if type is Float.Type || type is Float?.Type {
            if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) {
                return (bool ? 1.0 : 0.0) as? T
            }
        }
        // Double or Float NOT found
        return nil
    }
}

Register FloatToBoolDecodingTypeConverter with an instance:

register(FloatToBoolDecodingTypeConverter())

7. Key-Mapping for class:

Cannot adopt ExCodable in extension of classes.

class TestClass: ExCodable, Equatable {
    
    var int: Int = 0
    var string: String? = nil
    init(int: Int, string: String?) {
        (self.int, self.string) = (int, string)
    }
    
    static let keyMapping: [KeyMap<TestClass>] = [
        KeyMap(ref: \.int, to: "int"),
        KeyMap(ref: \.string, to: "string")
    ]
    
    required init(from decoder: Decoder) throws {
        try decodeReference(from: decoder, with: Self.keyMapping)
    }
    
    static func == (lhs: TestClass, rhs: TestClass) -> Bool {
        return lhs.int == rhs.int && lhs.string == rhs.string
    }
}

8. Key-Mapping for subclass:

Requires declaring another static Key-Mapping for subclass.

class TestSubclass: TestClass {
    var bool: Bool = false
    required init(int: Int, string: String, bool: Bool) {
        self.bool = bool
        super.init(int: int, string: string)
    }
    
    static let keyMappingForTestSubclass: [KeyMap<TestSubclass>] = [
        KeyMap(ref: \.bool, to: "bool")
    ]
    
    required init(from decoder: Decoder) throws {
        try super.init(from: decoder)
        try decodeReference(from: decoder, with: Self.keyMappingForTestSubclass)
    }
    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        try encode(to: encoder, with: Self.keyMappingForTestSubclass)
    }
    
    static func == (lhs: TestSubclass, rhs: TestSubclass) -> Bool {
        return lhs.int == rhs.int
            && lhs.string == rhs.string
            && lhs.bool == rhs.bool
    }
}

9. Encode/decode with Type-Inference:

let test = TestStruct(int: 304, string: "Not Modified")
let data = try? test.encoded() as Data?
let copy1 = try? data?.decoded() as TestStruct?
let copy2 = data.map { try? TestStruct.decoded(from: $0) }
XCTAssertEqual(copy1, test)
XCTAssertEqual(copy2, test)

Requirements

  • iOS 8.0+ | tvOS 9.0+ | macOS X 10.10+ | watchOS 2.0+
  • Xcode 12.0+
  • Swift 5.0+

Installation

.package(url: "https://github.com/iwill/ExCodable", from: "0.5.0")
pod 'ExCodable', '~> 0.5.0'
  • Code Snippets:

Title: ExCodable Summary: Adopte to ExCodable protocol Language: Swift
Platform: All
Completion: ExCodable
Availability: Top Level

<#extension/struct/class#> <#Type#>: ExCodable {
    static let <#keyMapping#>: [KeyMap<<#SelfType#>>] = [
        KeyMap(\.<#property#>, to: <#"key"#>),
        <#...#>
    ]
    init(from decoder: Decoder) throws {
        try decode<#Reference#>(from: decoder, with: Self.<#keyMapping#>)
    }
    func encode(to encoder: Encoder) throws {
        try encode(to: encoder, with: Self.<#keyMapping#>)
    }
}

Credits

License

ExCodable is released under the MIT license. See LICENSE for details.

About

Key-Mapping Extensions for Swift Codable

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Swift 94.4%
  • Ruby 5.6%