Skip to content

Commit a22e9d1

Browse files
committed
feat: added adjacently tagged enum support
1 parent fcdafa8 commit a22e9d1

File tree

7 files changed

+687
-14
lines changed

7 files changed

+687
-14
lines changed

Sources/CodableMacroPlugin/Attributes/KeyPath/CodedAt.swift

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,36 @@ struct CodedAt: PropertyAttribute {
3232
///
3333
/// The following conditions are checked by the
3434
/// built diagnoser:
35-
/// * Attached declaration is a variable declaration.
36-
/// * Macro usage is not duplicated for the same
37-
/// declaration.
38-
/// * Attached declaration is not a grouped variable
39-
/// declaration.
40-
/// * Attached declaration is not a static variable
41-
/// declaration
42-
/// * This attribute isn't used combined with `CodedIn`
43-
/// and `IgnoreCoding` attribute.
35+
/// * Macro usage is not duplicated for the same declaration.
36+
/// * If macro is attached to enum declaration:
37+
/// * This attribute must be combined with `Codable`
38+
/// and `TaggedAt` attribute.
39+
/// * else:
40+
/// * Attached declaration is a variable declaration.
41+
/// * Attached declaration is not a grouped variable
42+
/// declaration.
43+
/// * Attached declaration is not a static variable
44+
/// declaration
45+
/// * This attribute isn't used combined with `CodedIn`
46+
/// and `IgnoreCoding` attribute.
4447
///
4548
/// - Returns: The built diagnoser instance.
4649
func diagnoser() -> DiagnosticProducer {
4750
return AggregatedDiagnosticProducer {
48-
attachedToUngroupedVariable()
49-
attachedToNonStaticVariable()
5051
cantDuplicate()
51-
cantBeCombined(with: CodedIn.self)
52-
cantBeCombined(with: IgnoreCoding.self)
52+
`if`(
53+
isEnum,
54+
AggregatedDiagnosticProducer {
55+
mustBeCombined(with: Codable.self)
56+
mustBeCombined(with: TaggedAt.self)
57+
},
58+
else: AggregatedDiagnosticProducer {
59+
attachedToUngroupedVariable()
60+
attachedToNonStaticVariable()
61+
cantBeCombined(with: CodedIn.self)
62+
cantBeCombined(with: IgnoreCoding.self)
63+
}
64+
)
5365
}
5466
}
5567
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
@_implementationOnly import SwiftSyntax
2+
@_implementationOnly import SwiftSyntaxMacros
3+
4+
/// A type of `EnumSwitcherVariable` that can have adjacent tagging.
5+
///
6+
/// Only internally tagged enums can have adjacent tagging, while externally
7+
/// tagged enums can't have adjacent tagging.
8+
protocol AdjacentlyTaggableSwitcher: EnumSwitcherVariable {
9+
/// Register variable for the provided `CodingKey` path.
10+
///
11+
/// Creates new switcher variable of this type updating with provided
12+
/// variable registration.
13+
///
14+
/// - Parameters:
15+
/// - variable: The variable data, i.e. name, type and
16+
/// additional macro metadata.
17+
/// - keyPath: The `CodingKey` path where the value
18+
/// will be decode/encoded.
19+
///
20+
/// - Returns: Newly created variable updating registration.
21+
func registering(
22+
variable: AdjacentlyTaggedEnumSwitcher<Self>.CoderVariable,
23+
keyPath: [CodingKeysMap.Key]
24+
) -> Self
25+
}
26+
27+
extension InternallyTaggedEnumSwitcher: AdjacentlyTaggableSwitcher {
28+
/// Register variable for the provided `CodingKey` path.
29+
///
30+
/// Creates new switcher variable of this type updating with provided
31+
/// variable registration.
32+
///
33+
/// Registers variable at the provided `CodingKey` path on the current node.
34+
///
35+
/// - Parameters:
36+
/// - variable: The variable data, i.e. name, type and
37+
/// additional macro metadata.
38+
/// - keyPath: The `CodingKey` path where the value
39+
/// will be decode/encoded.
40+
///
41+
/// - Returns: Newly created variable updating registration.
42+
func registering(
43+
variable: AdjacentlyTaggedEnumSwitcher<Self>.CoderVariable,
44+
keyPath: [CodingKeysMap.Key]
45+
) -> Self {
46+
var node = node
47+
node.register(variable: variable, keyPath: keyPath)
48+
return .init(
49+
encodeContainer: encodeContainer,
50+
identifier: identifier, identifierType: identifierType,
51+
node: node, keys: keys,
52+
decl: decl, variableBuilder: variableBuilder
53+
)
54+
}
55+
}
56+
57+
extension Registration
58+
where Var: AdjacentlyTaggableSwitcher, Decl: AttributableDeclSyntax {
59+
/// Checks if enum declares adjacent tagging.
60+
///
61+
/// Checks if enum-case content path provided with `CodedAt` macro.
62+
///
63+
/// - Parameters:
64+
/// - contentDecoder: The mapped name for decoder.
65+
/// - contentEncoder: The mapped name for encoder.
66+
/// - codingKeys: The map where `CodingKeys` maintained.
67+
/// - context: The context in which to perform the macro expansion.
68+
///
69+
/// - Returns: Variable registration with adjacent tagging data if exists.
70+
func checkForAdjacentTagging(
71+
contentDecoder: TokenSyntax, contentEncoder: TokenSyntax,
72+
codingKeys: CodingKeysMap, context: MacroExpansionContext
73+
) -> Registration<Decl, Key, AnyEnumSwitcher> {
74+
guard
75+
let attr = CodedAt(from: decl),
76+
case let keyPath = attr.keyPath(withExisting: []),
77+
!keyPath.isEmpty
78+
else { return self.updating(with: variable.any) }
79+
let variable = AdjacentlyTaggedEnumSwitcher(
80+
base: variable,
81+
contentDecoder: contentDecoder, contentEncoder: contentEncoder,
82+
keyPath: keyPath, codingKeys: codingKeys, context: context
83+
)
84+
return self.updating(with: variable.any)
85+
}
86+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
@_implementationOnly import SwiftSyntax
2+
@_implementationOnly import SwiftSyntaxMacros
3+
4+
/// An `EnumSwitcherVariable` generating switch expression for adjacently
5+
/// tagged enums.
6+
///
7+
/// Registers path for enum-case associated variables container root and
8+
/// generated syntax for this common container.
9+
struct AdjacentlyTaggedEnumSwitcher<Wrapped>: EnumSwitcherVariable,
10+
ComposedVariable
11+
where Wrapped: AdjacentlyTaggableSwitcher {
12+
/// The switcher value wrapped by this instance.
13+
///
14+
/// The wrapped variable's type data is preserved and this variable is used
15+
/// to chain code generation implementations while changing the root
16+
/// container for enum-case associated variables.
17+
let base: Wrapped
18+
/// The container mapping variable.
19+
///
20+
/// This variable is used to map the nested container
21+
/// for associated variables to a pre-defined name.
22+
let variable: CoderVariable
23+
24+
/// Creates switcher variable with provided data.
25+
///
26+
/// - Parameters:
27+
/// - base: The base variable that handles implementation.
28+
/// - contentDecoder: The mapped name for content root decoder.
29+
/// - contentEncoder: The mapped name for content root encoder.
30+
/// - keyPath: The key path to enum-case content root.
31+
/// - codingKeys: The map where `CodingKeys` maintained.
32+
/// - context: The context in which to perform the macro expansion.
33+
init(
34+
base: Wrapped, contentDecoder: TokenSyntax, contentEncoder: TokenSyntax,
35+
keyPath: [String], codingKeys: CodingKeysMap,
36+
context: some MacroExpansionContext
37+
) {
38+
let keys = codingKeys.add(keys: keyPath, context: context)
39+
self.variable = .init(decoder: contentDecoder, encoder: contentEncoder)
40+
self.base = base.registering(variable: variable, keyPath: keys)
41+
}
42+
43+
/// Creates value expression for provided enum-case variable.
44+
///
45+
/// Provides value generated by the underlying variable value.
46+
///
47+
/// - Parameters:
48+
/// - variable: The variable for which generated.
49+
/// - value: The optional value present in syntax.
50+
/// - codingKeys: The map where `CodingKeys` maintained.
51+
/// - context: The context in which to perform the macro expansion.
52+
///
53+
/// - Returns: The generated value.
54+
func keyExpression<Var: EnumCaseVariable>(
55+
for variable: Var, value: ExprSyntax?,
56+
codingKeys: CodingKeysMap, context: some MacroExpansionContext
57+
) -> EnumVariable.CaseValue {
58+
return base.keyExpression(
59+
for: variable, value: value,
60+
codingKeys: codingKeys, context: context
61+
)
62+
}
63+
64+
/// Provides the syntax for decoding at the provided location.
65+
///
66+
/// Provides implementation generated by the underlying variable value
67+
/// while changing the root container name for enum-case associated
68+
/// values decoding.
69+
///
70+
/// - Parameters:
71+
/// - context: The context in which to perform the macro expansion.
72+
/// - location: The decoding location.
73+
///
74+
/// - Returns: The generated decoding syntax.
75+
func decoding(
76+
in context: some MacroExpansionContext,
77+
from location: EnumSwitcherLocation
78+
) -> EnumSwitcherGenerated {
79+
let generated = base.decoding(in: context, from: location)
80+
let newData: EnumSwitcherGenerated.CaseData
81+
switch generated.data {
82+
case .container:
83+
newData = generated.data
84+
case .coder(_, let postfix):
85+
newData = .coder(variable.decoder, postfix)
86+
}
87+
return .init(
88+
data: newData, expr: generated.expr, code: generated.code,
89+
defaultCase: generated.defaultCase
90+
)
91+
}
92+
93+
/// Provides the syntax for encoding at the provided location.
94+
///
95+
/// Provides implementation generated by the underlying variable value
96+
/// while changing the root container name for enum-case associated
97+
/// values encoding.
98+
///
99+
/// - Parameters:
100+
/// - context: The context in which to perform the macro expansion.
101+
/// - location: The encoding location.
102+
///
103+
/// - Returns: The generated encoding syntax.
104+
func encoding(
105+
in context: some MacroExpansionContext,
106+
to location: EnumSwitcherLocation
107+
) -> EnumSwitcherGenerated {
108+
let generated = base.encoding(in: context, to: location)
109+
let newData: EnumSwitcherGenerated.CaseData
110+
switch generated.data {
111+
case .container:
112+
newData = generated.data
113+
case .coder(_, let postfix):
114+
newData = .coder(variable.encoder, postfix)
115+
}
116+
return .init(
117+
data: newData, expr: generated.expr, code: generated.code,
118+
defaultCase: generated.defaultCase
119+
)
120+
}
121+
122+
/// Creates additional enum declarations for enum variable.
123+
///
124+
/// Provides enum declarations for of the underlying variable value.
125+
///
126+
/// - Parameter context: The context in which to perform the macro
127+
/// expansion.
128+
/// - Returns: The generated enum declaration syntax.
129+
func codingKeys(
130+
in context: some MacroExpansionContext
131+
) -> MemberBlockItemListSyntax {
132+
return base.codingKeys(in: context)
133+
}
134+
}
135+
136+
extension AdjacentlyTaggedEnumSwitcher {
137+
/// A variable value exposing decoder and encoder.
138+
///
139+
/// The `CoderVariable` exposes decoder and encoder via variable
140+
/// provided with `decoder` and `encoder` name respectively.
141+
struct CoderVariable: PropertyVariable, ComposedVariable {
142+
/// The initialization type of this variable.
143+
///
144+
/// Initialization type is the same as underlying wrapped variable.
145+
typealias Initialization = BasicPropertyVariable.Initialization
146+
/// The mapped name for decoder.
147+
///
148+
/// The decoder at location passed will be exposed
149+
/// with this variable name.
150+
let decoder: TokenSyntax
151+
/// The mapped name for encoder.
152+
///
153+
/// The encoder at location passed will be exposed
154+
/// with this variable name.
155+
let encoder: TokenSyntax
156+
157+
/// The value wrapped by this instance.
158+
///
159+
/// The wrapped variable's type data is
160+
/// preserved and this variable is used
161+
/// to chain code generation implementations.
162+
let base = BasicPropertyVariable(
163+
name: "", type: "", value: nil,
164+
decodePrefix: "", encodePrefix: "",
165+
decode: true, encode: true
166+
)
167+
168+
/// Whether the variable is to be decoded.
169+
///
170+
/// This variable is always set as to be decoded.
171+
var decode: Bool? { true }
172+
/// Whether the variable is to be encoded.
173+
///
174+
/// This variable is always set as to be encoded.
175+
var encode: Bool? { true }
176+
177+
/// Whether the variable type requires `Decodable` conformance.
178+
///
179+
/// This variable never requires `Decodable` conformance
180+
var requireDecodable: Bool? { false }
181+
/// Whether the variable type requires `Encodable` conformance.
182+
///
183+
/// This variable never requires `Encodable` conformance
184+
var requireEncodable: Bool? { false }
185+
186+
/// Provides the code syntax for decoding this variable
187+
/// at the provided location.
188+
///
189+
/// Creates/assigns the decoder passed in location to the variable
190+
/// created with the `decoder` name provided.
191+
///
192+
/// - Parameters:
193+
/// - context: The context in which to perform the macro expansion.
194+
/// - location: The decoding location for the variable.
195+
///
196+
/// - Returns: The generated variable decoding code.
197+
func decoding(
198+
in context: some MacroExpansionContext,
199+
from location: PropertyCodingLocation
200+
) -> CodeBlockItemListSyntax {
201+
return switch location {
202+
case .coder(let decoder, _):
203+
"let \(self.decoder) = \(decoder)"
204+
case .container(let container, let key, _):
205+
"let \(self.decoder) = try \(container).superDecoder(forKey: \(key))"
206+
}
207+
}
208+
209+
/// Provides the code syntax for encoding this variable
210+
/// at the provided location.
211+
///
212+
/// Creates/assigns the encoder passed in location to the variable
213+
/// created with the `encoder` name provided.
214+
///
215+
/// - Parameters:
216+
/// - context: The context in which to perform the macro expansion.
217+
/// - location: The encoding location for the variable.
218+
///
219+
/// - Returns: The generated variable encoding code.
220+
func encoding(
221+
in context: some MacroExpansionContext,
222+
to location: PropertyCodingLocation
223+
) -> CodeBlockItemListSyntax {
224+
return switch location {
225+
case .coder(let encoder, _):
226+
"let \(self.encoder) = \(encoder)"
227+
case .container(let container, let key, _):
228+
"let \(self.encoder) = \(container).superEncoder(forKey: \(key))"
229+
}
230+
}
231+
}
232+
}

Sources/CodableMacroPlugin/Variables/Type/EnumVariable.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ struct EnumVariable: TypeVariable, DeclaredVariable {
5353
) { registration in
5454
return registration.useHelperCoderIfExists()
5555
} switcherBuilder: { registration in
56-
return registration
56+
return registration.checkForAdjacentTagging(
57+
contentDecoder: "contentDecoder",
58+
contentEncoder: "contentEncoder",
59+
codingKeys: codingKeys, context: context
60+
)
5761
}
5862
} caseBuilder: { input in
5963
return input.checkForAlternateValue().checkCodingIgnored()

Sources/MetaCodable/Codable/Codable.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
/// * Use ``Default(_:)`` to provide default value when decoding fails.
1818
/// * Use ``CodedAs(_:)`` to provided custom value for enum cases.
1919
/// * Use ``TaggedAt(_:_:)`` to provide enum-case identifier tag path.
20+
/// * Use ``CodedAt(_:)`` to provided enum-case content path.
2021
/// * Use ``IgnoreCoding()``, ``IgnoreDecoding()`` and
2122
/// ``IgnoreEncoding()`` to ignore specific properties/cases from
2223
/// decoding/encoding or both.

0 commit comments

Comments
 (0)