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
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