Swift 5.9中的@Codable宏开发探索:第一章——设计目标与手动实现

721 阅读4分钟

关于CodableWrapper

  • Codable很好,但是有一些缺陷:比如严格要求数据源,定义为String给了Int就抛异常、支持自定义CodingKey但是写法十分麻烦、缺字段的情况下不使用Optional会抛异常而不是使用缺省值等等。
  • 之前发布过PropertyWrapper版,主要使用PropertyWrapper标记属性来提高了Codable的使用体验,使用了几个比较tricky的黑科技,所以API也比市面上的同类库要简单。
  • 现在Swift5.9支持宏了,决定写一个宏版本的CodableWrapper。目前已开发完毕Github - CodableWrapper/swift5.9-macro

首先确定目标

  1. 支持缺省值,JSON缺少字段容错
  2. 支持 String Bool Number 等基本类型互转
  3. 驼峰大小写自动互转
  4. 自定义解析key
  5. 自定义解析规则 (Transformer)
  6. 方便的 Codable Class 子类

设计API

  • 定义宏:

    • @Codable
    • @CodableSubclass
    • @CodableKey(..)
    • @CodableNestedKey(..)
    • @CodableTransformer(..)
  • 例子:

    @Codable
    struct BasicModel {
        var defaultVal: String = "hello world"
        var defaultVal2: String = Bool.random() ? "hello world" : ""
        let strict: String
        let noStrict: String?
        let autoConvert: Int?
    
        @CodableKey("hello")
        var hi: String = "there"
    
        @CodableNestedKey("nested.hi")
        @CodableTransformer(StringPrefixTransform("HELLO -> "))
        var codingKeySupport: String
    
        @CodableNestedKey("nested.b")
        var nestedB: String
    
        var testGetter: String {
            nestedB
        }
    }
    

手动实现这些目标特性

想要宏自动生成代码,需要先弄清楚怎么手动实现这些目标特性。先想象一下编译器怎么生成Codable的实现的:

struct BasicModel: Codable {
    var defaultVal: String = "hello world"
}
// 编译器生成的
struct BasicModel: Codable {
    var defaultVal: String = "hello world"

    enum CodingKeys: String, CodingKey {
        case defaultVal
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.defaultVal = try container.decode(String.self, forKey: .defaultVal)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.defaultVal, forKey: .defaultVal)
    }
}

手动实现目标1:支持缺省值

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.defaultVal = (try? container.decode(String.self, forKey: .defaultVal)) ?? "hello world"
}

手动实现目标2:支持 String Bool Number 等基本类型互转

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    if let value = try? container.decode(Int.self, forKey: .defaultVal) {
        self.defaultVal = String(value)
    } else if let value = try? container.decode(String.self, forKey: .defaultVal) {
        self.defaultVal = String(value)
    } else if let value = try? container.decode(Float.self, forKey: .defaultVal) {
        self.defaultVal = String(value)
    } 
    // 各种基本类型尝试转换 else if ...
    else {
        self.defaultVal = "hello world"
    }
}

这么解析肯定不是个办法,这里结合了两个开源库的实现,第一步使用AnyDecodable解析出JSON内的数据,解析出来可能是任意基本类型。第二步使用BuiltInBridgeTypeBuiltInBridgeType将解析出来的基本类型尝试转换成目标类型。完整实现如下:

extension Decodable {
    static func decode<K>(from container: KeyedDecodingContainer<K>, forKey key: KeyedDecodingContainer<K>.Key) throws -> Self {
        return try container.decode(Self.self, forKey: key)
    }
}

struct BasicModel: Codable {
    var defaultVal: String = "hello world"

    enum CodingKeys: CodingKey {
        case defaultVal
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        defaultVal = container.tryNormalKeyDecode(type: String.self, key: .defaultVal) ?? "hello world"
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(defaultVal, forKey: .defaultVal)
    }
}

extension KeyedDecodingContainer {
    func tryNormalKeyDecode<Value>(type _: Value.Type, key: K) -> Value? {
        let value = try? decodeIfPresent(AnyDecodable.self, forKey: key)?.value
        if let value = value {
            if let converted = value as? Value {
                return converted
            }
            if let _bridged = (Value.self as? _BuiltInBridgeType.Type)?._transform(from: value), let __bridged = _bridged as? Value {
                return __bridged
            }
            // 如果是非基本类型,那继续尝试Decode
            if let valueType = Value.self as? Decodable.Type {
                if let value = try? valueType.decode(from: self, forKey: key) as? Value {
                    return value
                }
            }
        }
        return nil
    }
}

手动实现目标3:支持驼峰大小写自动互转

// 为了简化CodingKey,使用AnyCodingKey实现CodingKey
public struct AnyCodingKey: CodingKey {
    public var stringValue: String
    public var intValue: Int?

    public init?(stringValue: String) {
        self.stringValue = stringValue
        intValue = nil
    }

    public init?(intValue: Int) {
        stringValue = "\(intValue)"
        self.intValue = intValue
    }

    public init(index: Int) {
        stringValue = "\(index)"
        intValue = index
    }
}

struct BasicModel: Codable {
    var defaultVal: String = "hello world"

    init(from decoder: Decoder) throws {
        // CodingKeys 改为 AnyCodingKey
        let container = try decoder.container(keyedBy: AnyCodingKey.self)
        defaultVal = container.tryNormalKeyDecode(type: String.self, key: "defaultVal") ?? "hello world"
    }
}

extension KeyedDecodingContainer where K == AnyCodingKey {
    func tryNormalKeyDecode<Value>(type: Value.Type, key: String) -> Value? {
        func _decode(key: String) -> Value? {
            let value = try? decodeIfPresent(AnyDecodable.self, forKey: key)?.value
            if let value = value {
                if let converted = value as? Value {
                    return converted
                }
                if let _bridged = (Value.self as? _BuiltInBridgeType.Type)?._transform(from: value), let __bridged = _bridged as? Value {
                    return __bridged
                }
                if let valueType = Value.self as? Decodable.Type {
                    if let value = try? valueType.decode(from: self, forKey: key) as? Value {
                        return value
                    }
                }
            }
            return nil
        }

        for newKey in [key, key.snakeCamelConvert()].compactMap({ $0 }) {
            if let value = _decode(key: newKey) {
                return value
            }
        }
        return nil
    }
}

extension String {
    func snakeCamelConvert() -> String? {
        // 驼峰大小写互转
        ...
    }
}

手动实现目标4:自定义解析Key和NestedKey

container.decode封装 container.encode封装

struct TestModel: Codable {
    // @CodableKey("u1", "u2", "u9")
    let userName: String = ""

    // @CodableNestedKey("data.u1", "data.u2", "data.u9")
    let userName2: String = ""

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: AnyCodingKey.self)
        userName = container.decode(type: String.self, keys: ["u1", "u2", "u9"], nestedKeys: []) ?? ""
        userName2 = container.decode(type: String.self, keys: [], nestedKeys: ["data.u1", "data.u2", "data.u9"]) ?? ""
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(value: self.userName, keys: ["u1", "u2", "u9"], nestedKeys: [])
        try container.encode(value: self.userName2, keys: [], nestedKeys: ["data.u1", "data.u2", "data.u9"])
    }
}

手动实现目标5:自定义解析规则Transformer

// 定义TransformType协议
public protocol TransformType {
    associatedtype Object
    associatedtype JSON: Codable

    func transformFromJSON(_ json: JSON?) -> Object
    func transformToJSON(_ object: Object) -> JSON?
}
// 定义一个不遵循Codable的结构体
struct DateWrapper {
    let timestamp: TimeInterval

    var date: Date {
        Date(timeIntervalSince1970: timestamp)
    }

    init(timestamp: TimeInterval) {
        self.timestamp = timestamp
    }

    static var transformer = TransformOf<DateWrapper, TimeInterval>(fromJSON: { DateWrapper(timestamp: $0 ?? 0) }, toJSON: { $0.timestamp })
}

struct DateModel: Codable {
    // @CodableTransformer(DateWrapper.transformer)
    var time: DateWrapper? = DateWrapper(timestamp: 0)

    // @CodableTransformer(DateWrapper.transformer)
    var time1: DateWrapper = .init(timestamp: 0)

    // @CodableTransformer(DateWrapper.transformer)
    var time2: DateWrapper?

    // @CodableTransformer(DateWrapper.transformer)
    var time3: DateWrapper

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: AnyCodingKey.self)
        let time = try? container.decode(type: TimeInterval.self, keys: ["time"], nestedKeys: [])
        self.time = DateWrapper.transformer.transformFromJSON(time) ?? DateWrapper(timestamp: 0)

        let time1 = try? container.decode(type: TimeInterval.self, keys: ["time1"], nestedKeys: [])
        self.time1 = DateWrapper.transformer.transformFromJSON(time1) ?? .init(timestamp: 0)

        let time2 = try? container.decode(type: TimeInterval.self, keys: ["time2"], nestedKeys: [])
        self.time2 = DateWrapper.transformer.transformFromJSON(time2)

        let time3 = try? container.decode(type: TimeInterval.self, keys: ["time3"], nestedKeys: [])
        self.time3 = DateWrapper.transformer.transformFromJSON(time3)
    }

    func encode(to encoder: Encoder) throws {
        let container = encoder.container(keyedBy: AnyCodingKey.self)
        if let time = self.time, let value = DateWrapper.transformer.transformToJSON(self.time) {
            try container.encode(value: value, keys: ["time"], nestedKeys: [])
        }
        if let value = DateWrapper.transformer.transformToJSON(self.time1) {
            try container.encode(value: value, keys: ["time1"], nestedKeys: [])
        }
        if let time2 = self.time2, let value = DateWrapper.transformer.transformToJSON(time2) {
            try container.encode(value: value, keys: ["time2"], nestedKeys: [])
        }
        if let value = DateWrapper.transformer.transformToJSON(time3) {
            try container.encode(value: value, keys: ["time3"], nestedKeys: [])
        }
    }
}

手动实现目标6:“不方便”的 Codable Class 子类

class ClassModel1: Codable {
    var val: String?
}

class ClassSubmodel1: ClassModel1 {
    var subVal: String = "1_1"

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: AnyCodingKey.self)
        self.subVal = (try? container.decode(type: type(of: self.subVal), keys: ["subVal"], nestedKeys: [])) ?? ("1_1")
        try super.init(from: decoder)
    }

    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        let container = encoder.container(keyedBy: AnyCodingKey.self)
        try container.encode(value: self.subVal, keys: ["subVal"], nestedKeys: [])
    }
}

下一篇文章将介绍Codable宏的开发和实现。


文章目录