Search code examples
swift

What's the best way to use Codable structs with PATCH calls?


What's the best way to use Codable structs with PATCH calls?

I guess, I need to know which properties of my struct have been updated, and then pass only those values as partial updates for the patch call.

Do I need to use didSet observers on all properies, so that I can flag which one has changed or is there a better way?

thanks

UPDATE: please check comments to the question


Solution

  • One way I thought of is to use a property wrapper like this to associate a didSet flag with the properties you are interested in:

    @propertyWrapper
    public struct DidSetFlag<T: Codable>: Codable {
        public var wrappedValue: T {
            didSet {
                didSet = true
            }
        }
        
        public var didSet = false
        
        public init(wrappedValue: T) {
            self.wrappedValue = wrappedValue
        }
        
        public init(from decoder: any Decoder) throws {
            wrappedValue = try T(from: decoder)
        }
        
        public func encode(to encoder: any Encoder) throws {
            try wrappedValue.encode(to: encoder)
        }
    }
    

    Then, "hijack" KeyedEncodingContainer.encode so that if didSet is false, nothing is encoded:

    extension KeyedEncodingContainer {
        mutating func encode<T: Codable>(
            _ value: DidSetFlag<T>,
            forKey key: KeyedEncodingContainer<K>.Key
        ) throws {
            if value.didSet {
                try encode(value.wrappedValue, forKey: key)
            }
        }
    }
    

    The synthesised Codable implementation would resolve to this implementation in the extension, unless the Codable type is in a different module.

    Example usage:

    struct Foo: Codable {
        @DidSetFlag var foo = 0
        @DidSetFlag var bar = 0
        
        mutating func resetFlags() {
            _foo.didSet = false
            _bar.didSet = false
        }
    }
    
    var foo = Foo()
    let encoder = JSONEncoder()
    print(String(data: try encoder.encode(foo), encoding:. utf8)!)
    foo.foo = 1
    print(String(data: try encoder.encode(foo), encoding:. utf8)!)
    foo.bar = 2
    print(String(data: try encoder.encode(foo), encoding:. utf8)!)
    
    /*
    output:
    
    {}
    {"foo":1}
    {"bar":2,"foo":1}
    */
    

    As a bonus, here is a macro implementation to automatically add the @DidSetFlag to each property, and also generate the resetFlags method.

    // declaration
    @attached(memberAttribute)
    @attached(member, names: named(resetFlags))
    public macro EncodePropertiesWhenSet() = #externalMacro(module: "Your Module Here...", type: "EncodePropertiesWhenSet")
    
    // implementation
    enum EncodePropertiesWhenSet: MemberAttributeMacro, MemberMacro {
        static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
            let hasMutating = !declaration.is(ClassDeclSyntax.self)
            let function = try FunctionDeclSyntax("\(raw: hasMutating ? "mutating " : "")func resetFlags()") {
                for member in declaration.memberBlock.members {
                    if let name = name(for: member.decl) {
                        "_\(raw: name).didSet = false"
                    }
                }
            }
            return [DeclSyntax(function)]
        }
        
        static func expansion(of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingAttributesFor member: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [AttributeSyntax] {
            if name(for: member) != nil {
                ["@DidSetFlag"]
            } else {
                []
            }
        }
        
        static func name(for decl: some DeclSyntaxProtocol) -> String? {
            // filter out computed properties and 'let's
            guard let varDecl = decl.as(VariableDeclSyntax.self),
                  varDecl.bindingSpecifier.text == "var",
                  varDecl.bindings.count == 1,
                  let binding = varDecl.bindings.first,
                  binding.accessorBlock == nil,
                  let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text
            else { return nil }
            return identifier
        }
    }
    
    
    @main
    struct MyMacroPlugin: CompilerPlugin {
        let providingMacros: [Macro.Type] = [
            EncodePropertiesWhenSet.self
        ]
    }
    
    // usage:
    @EncodePropertiesWhenSet
    struct Foo: Codable {
        var foo = 0
        var bar = 0