@@ -35,9 +35,10 @@ import SwiftSyntaxMacroExpansion
3535 """ )
3636 let context = BasicMacroExpansionContext ( )
3737 let members = try expandMembers ( of: node, attachedTo: declaration, in: context)
38- #expect( members. count == 5 )
38+ #expect( members. count == 6 )
3939 let source = members. map { $0. description } . joined ( separator: " \n " )
4040 #expect( source. contains ( #"public static var entityName: EntityName { "Person" }"# ) )
41+ #expect( source. contains ( " public enum CodingKeys: String, CodingKey, Sendable, CaseIterable " ) )
4142 #expect( source. contains ( " .name: .string " ) )
4243 #expect( source. contains ( " .age: .int64 " ) )
4344 #expect( source. contains ( " .created: .date " ) )
@@ -299,7 +300,56 @@ import SwiftSyntaxMacroExpansion
299300 }
300301
301302 @Test func expansionNames( ) {
302- #expect( EntityMacro . expansionNames. count == 5 )
303+ #expect( EntityMacro . expansionNames. count == 6 )
304+ }
305+
306+ // MARK: - Coding Keys
307+
308+ @Test func codingKeysExpansion( ) throws {
309+ let ( node, declaration) = try parse ( """
310+ @Entity
311+ struct Person {
312+ var id: UUID
313+ @Attribute
314+ var name: String
315+ @Relationship(destination: Pet.self, inverse: .owner)
316+ var pets: [Pet.ID]
317+ }
318+ """ )
319+ let context = BasicMacroExpansionContext ( )
320+ let expansion = try EntityMacro . codingKeysDeclarationSyntax ( of: node, providingMembersOf: declaration, in: context)
321+ let codingKeysDecl = try #require( expansion)
322+ let source = codingKeysDecl. description
323+ #expect( source. contains ( " public enum CodingKeys: String, CodingKey, Sendable, CaseIterable " ) )
324+ // `id` first, then attributes and relationships in declaration order
325+ let idIndex = try #require( source. range ( of: " case id " ) ? . lowerBound)
326+ let nameIndex = try #require( source. range ( of: " case name " ) ? . lowerBound)
327+ let petsIndex = try #require( source. range ( of: " case pets " ) ? . lowerBound)
328+ #expect( idIndex < nameIndex)
329+ #expect( nameIndex < petsIndex)
330+ }
331+
332+ /// A manually declared `CodingKeys` suppresses the generated one.
333+ @Test func manualCodingKeysSkipsGeneration( ) throws {
334+ let ( node, declaration) = try parse ( """
335+ @Entity
336+ struct Person {
337+ var id: UUID
338+ @Attribute
339+ var name: String
340+ enum CodingKeys: String, CodingKey {
341+ case id
342+ case name = " personName "
343+ }
344+ }
345+ """ )
346+ let context = BasicMacroExpansionContext ( )
347+ let expansion = try EntityMacro . codingKeysDeclarationSyntax ( of: node, providingMembersOf: declaration, in: context)
348+ #expect( expansion == nil )
349+ let members = try expandMembers ( of: node, attachedTo: declaration, in: context)
350+ #expect( members. count == 5 )
351+ let source = members. map { $0. description } . joined ( separator: " \n " )
352+ #expect( source. contains ( " enum CodingKeys " ) == false )
303353 }
304354
305355 // MARK: - Composite Attributes
@@ -418,7 +468,7 @@ import SwiftSyntaxMacroExpansion
418468 }
419469
420470 /// An entity mixing scalar attributes, required and optional composites, and a
421- /// relationship generates all five members consistently.
471+ /// relationship generates all six members consistently.
422472 @Test func mixedEntityExpansion( ) throws {
423473 let ( node, declaration) = try parse ( """
424474 @Entity( " Campground " )
@@ -436,7 +486,7 @@ import SwiftSyntaxMacroExpansion
436486 """ )
437487 let context = BasicMacroExpansionContext ( )
438488 let members = try expandMembers ( of: node, attachedTo: declaration, in: context)
439- #expect( members. count == 5 )
489+ #expect( members. count == 6 )
440490 let source = members. map { $0. description } . joined ( separator: " \n " )
441491 #expect( source. contains ( #"public static var entityName: EntityName { "Campground" }"# ) )
442492 // scalar and composite attributes live side by side
0 commit comments