@@ -53,12 +53,14 @@ struct PathValueMatch {
5353struct EnumCase {
5454 var caseName : String
5555 var rawValue : String
56+ var hasExplicitRawValue : Bool = false
5657 // [Type: Value]
5758 var matches : [ String : [ String ] ] = [ : ]
5859 var matchOrder : [ String ] = [ ]
5960 var keyPathMatches : [ PathValueMatch ] = [ ]
6061 var associatedMatch : [ AssociatedMatch ] = [ ]
6162 var associated : [ AssociatedValue ] = [ ]
63+ var caseStyles : [ CaseStyle ] = [ ]
6264
6365 var initText : String {
6466 let associated = " \( associated. compactMap { " \( $0. label == nil ? $0. variableName : " \( $0. variableName) : \( $0. variableName) " ) " } . joined ( separator: " , " ) ) "
@@ -110,12 +112,15 @@ struct TypeInfo {
110112 var index = 0
111113 var lastIntRawValue : Int = 0
112114 try enumDecl. memberBlock. members. forEach {
115+ let casesStartCount = enumCases. count
113116 try $0. decl. as ( EnumCaseDeclSyntax . self) ? . elements. forEach { caseElement in
114117 let name = caseElement. name. trimmedDescription
115118 var raw : String
119+ var hasExplicitRaw = false
116120 if let rawValueExpr = caseElement. rawValue? . value {
117121 let rawValue = rawValueExpr. trimmedDescription
118122 raw = rawValue
123+ hasExplicitRaw = true
119124 if let intRaw = rawValueExpr. as ( IntegerLiteralExprSyntax . self) {
120125 lastIntRawValue = Int ( intRaw. trimmedDescription) ?? 0
121126 }
@@ -144,7 +149,7 @@ struct TypeInfo {
144149 associated. append ( . init( label: label, type: type, index: paramIndex, defaultValue: defaultValue) )
145150 paramIndex += 1
146151 }
147- enumCases. append ( . init( caseName: name, rawValue: raw, associated: associated) )
152+ enumCases. append ( . init( caseName: name, rawValue: raw, hasExplicitRawValue : hasExplicitRaw , associated: associated) )
148153 index += 1
149154 }
150155
@@ -218,6 +223,25 @@ struct TypeInfo {
218223 }
219224 }
220225 }
226+
227+ if let caseDecl = $0. decl. as ( EnumCaseDeclSyntax . self) {
228+ let caseLevelStyles : [ CaseStyle ] = caseDecl. attributes. compactMap { attr in
229+ guard let attrId = attr. as ( AttributeSyntax . self) ?
230+ . attributeName. as ( IdentifierTypeSyntax . self) ?
231+ . trimmedDescription else { return nil }
232+ for style in CaseStyle . allCases {
233+ if style. macroName == attrId {
234+ return style
235+ }
236+ }
237+ return nil
238+ }
239+ if !caseLevelStyles. isEmpty {
240+ for i in casesStartCount..< enumCases. count {
241+ enumCases [ i] . caseStyles = caseLevelStyles
242+ }
243+ }
244+ }
221245 }
222246 }
223247 try validateEnumCases ( enumCases)
@@ -233,6 +257,10 @@ struct TypeInfo {
233257 }
234258 return nil
235259 }
260+ if isEnum {
261+ try validateEnumCaseStyles ( )
262+ enrichEnumCasesWithStyles ( )
263+ }
236264 if let attribute = decl. attributes. firstAttribute ( named: " CodingContainer " ) ,
237265 let arguments = attribute. as ( AttributeSyntax . self) ? . arguments? . as ( LabeledExprListSyntax . self) {
238266 codingContainer = arguments
@@ -556,6 +584,87 @@ extension TypeInfo {
556584 throw MacroError ( text: " Only CaseMatcher with key path and .string() patterns are allowed for enum cases with associated values " )
557585 }
558586 }
587+
588+ /// Validates that case style macros are used correctly on enum cases.
589+ ///
590+ /// Rules enforced:
591+ /// 1. Numeric raw type enums (Int, Double, etc.) cannot use case style macros.
592+ /// 2. Case style macros cannot coexist with `@CodingCase` using the `at:` parameter in the same enum.
593+ /// 3. `@CodingCase` with `values:` parameter on a specific case cannot coexist with any effective case style.
594+ func validateEnumCaseStyles( ) throws {
595+ let anyStyles = !caseStyles. isEmpty || enumCases. contains { !$0. caseStyles. isEmpty }
596+ guard anyStyles else { return }
597+
598+ if let enumRawType, enumRawType != " String " {
599+ throw MacroError ( text: " Case style macros cannot be used with \( enumRawType) raw type enum. Only String or untyped enums are supported. " )
600+ }
601+
602+ if enumCases. contains ( where: { !$0. keyPathMatches. isEmpty } ) {
603+ throw MacroError ( text: " Case style macros cannot be used when @CodingCase with 'at:' parameter is present in the same enum. " )
604+ }
605+
606+ for enumCase in enumCases where !enumCase. associatedMatch. isEmpty {
607+ let effectiveStyles = enumCase. caseStyles. uniqueMerged ( with: caseStyles)
608+ if !effectiveStyles. isEmpty {
609+ let styleNames = effectiveStyles. map { " @ \( $0. macroName) " } . joined ( separator: " , " )
610+ throw MacroError ( text: " @CodingCase with 'values:' parameter on case ' \( enumCase. caseName) ' cannot coexist with case style \( styleNames) . " )
611+ }
612+ }
613+ }
614+
615+ /// Populates enum case `matches` with style-converted string values for decoding (union / many-to-one).
616+ ///
617+ /// Values are appended in priority order:
618+ /// - P1: Existing `@CodingCase` explicit values (already present from parsing)
619+ /// - P2: Explicit rawValue that differs from the case name
620+ /// - P3/P4: Case-level then enum-level style-converted names
621+ /// - P5: Original case name (fallback, only used when P1–P4 produce nothing)
622+ ///
623+ /// Encoding uses the first (highest priority) non-range value via `firstMatchValue(for:)`.
624+ /// Cases with `at:` or `values:` parameters are skipped (handled separately and validated above).
625+ mutating func enrichEnumCasesWithStyles( ) {
626+ for i in enumCases. indices {
627+ var ec = enumCases [ i]
628+
629+ if !ec. keyPathMatches. isEmpty || !ec. associatedMatch. isEmpty {
630+ continue
631+ }
632+
633+ let effectiveStyles = ec. caseStyles. uniqueMerged ( with: caseStyles)
634+ if effectiveStyles. isEmpty { continue }
635+
636+ // P2: Add explicit rawValue when it differs from the case name
637+ if ec. hasExplicitRawValue {
638+ let quotedCaseName = " \" \( ec. caseName) \" "
639+ if ec. rawValue != quotedCaseName {
640+ if !ec. matchOrder. contains ( " String " ) {
641+ ec. matchOrder. append ( " String " )
642+ }
643+ var values = ec. matches [ " String " ] ?? [ ]
644+ if !values. contains ( ec. rawValue) {
645+ values. append ( ec. rawValue)
646+ }
647+ ec. matches [ " String " ] = values
648+ }
649+ }
650+
651+ // P3/P4: Add style-converted names (case-level styles first, then enum-level)
652+ let converted = KeyConverter . convert ( value: ec. caseName, caseStyles: effectiveStyles)
653+ for name in converted {
654+ let quotedName = " \" \( name) \" "
655+ if !ec. matchOrder. contains ( " String " ) {
656+ ec. matchOrder. append ( " String " )
657+ }
658+ var values = ec. matches [ " String " ] ?? [ ]
659+ if !values. contains ( quotedName) {
660+ values. append ( quotedName)
661+ }
662+ ec. matches [ " String " ] = values
663+ }
664+
665+ enumCases [ i] = ec
666+ }
667+ }
559668}
560669
561670// MARK: - Generate
@@ -929,6 +1038,11 @@ extension TypeInfo {
9291038 } else {
9301039 keys = theCase. associatedMatch. first { $0. index == " \( value. index) " } ? . keys ?? [ ]
9311040 }
1041+ let assocEffectiveStyles = theCase. caseStyles. uniqueMerged ( with: self . caseStyles)
1042+ if !assocEffectiveStyles. isEmpty && theCase. associatedMatch. isEmpty {
1043+ let convertedKeys = KeyConverter . convert ( value: value. variableName, caseStyles: assocEffectiveStyles)
1044+ keys. append ( contentsOf: convertedKeys. map { " \" \( $0) \" " } )
1045+ }
9321046 keys. append ( " \" \( value. variableName) \" " )
9331047 keys. removeDuplicates ( )
9341048 let hasDefault = value. defaultValue != nil
@@ -1008,6 +1122,8 @@ extension TypeInfo {
10081122 let hasPathValue = enumCases. contains { !$0. keyPathMatches. isEmpty }
10091123 let encodeCase = """
10101124 \( enumCases. compactMap {
1125+ let effectiveAssocStyles = $0. caseStyles. uniqueMerged ( with: self . caseStyles)
1126+ let convertAssocKeys = !effectiveAssocStyles. isEmpty && $0. associatedMatch. isEmpty
10111127 let associated = " \( $0. associated. compactMap { value in value. variableName } . joined ( separator: " , " ) ) "
10121128 let postfix = $0. associated. isEmpty ? " \( associated) " : " ( \( associated) ) "
10131129 let hasAssociated = !$0. associated. isEmpty
@@ -1030,8 +1146,11 @@ extension TypeInfo {
10301146 case \( hasAssociated ? " let " : " " ) . \( $0. caseName) \( postfix) :
10311147 \( encodeCase)
10321148 \( $0. associated. compactMap { value in
1033- """
1034- try \( hasPathValue ? " container " : " nestedContainer " ) .encode( \( value. variableName) , forKey: AnyCodingKey( " \( value. variableName) " ))
1149+ let encodingKey = convertAssocKeys
1150+ ? KeyConverter . convert ( value: value. variableName, caseStyle: effectiveAssocStyles. first!)
1151+ : value. variableName
1152+ return """
1153+ try \( hasPathValue ? " container " : " nestedContainer " ) .encode( \( value. variableName) , forKey: AnyCodingKey( " \( encodingKey) " ))
10351154 """
10361155 } . joined ( separator: " \n " ) )
10371156 """
0 commit comments