Skip to content

Commit

Permalink
feat: added adjacently tagged enum support
Browse files Browse the repository at this point in the history
  • Loading branch information
soumyamahunt committed Jan 9, 2024
1 parent fcdafa8 commit a22e9d1
Show file tree
Hide file tree
Showing 7 changed files with 687 additions and 14 deletions.
38 changes: 25 additions & 13 deletions Sources/CodableMacroPlugin/Attributes/KeyPath/CodedAt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,36 @@ struct CodedAt: PropertyAttribute {
///
/// The following conditions are checked by the
/// built diagnoser:
/// * Attached declaration is a variable declaration.
/// * Macro usage is not duplicated for the same
/// declaration.
/// * Attached declaration is not a grouped variable
/// declaration.
/// * Attached declaration is not a static variable
/// declaration
/// * This attribute isn't used combined with `CodedIn`
/// and `IgnoreCoding` attribute.
/// * Macro usage is not duplicated for the same declaration.
/// * If macro is attached to enum declaration:
/// * This attribute must be combined with `Codable`
/// and `TaggedAt` attribute.
/// * else:
/// * Attached declaration is a variable declaration.
/// * Attached declaration is not a grouped variable
/// declaration.
/// * Attached declaration is not a static variable
/// declaration
/// * This attribute isn't used combined with `CodedIn`
/// and `IgnoreCoding` attribute.
///
/// - Returns: The built diagnoser instance.
func diagnoser() -> DiagnosticProducer {
return AggregatedDiagnosticProducer {
attachedToUngroupedVariable()
attachedToNonStaticVariable()
cantDuplicate()
cantBeCombined(with: CodedIn.self)
cantBeCombined(with: IgnoreCoding.self)
`if`(
isEnum,
AggregatedDiagnosticProducer {
mustBeCombined(with: Codable.self)
mustBeCombined(with: TaggedAt.self)
},
else: AggregatedDiagnosticProducer {
attachedToUngroupedVariable()
attachedToNonStaticVariable()
cantBeCombined(with: CodedIn.self)
cantBeCombined(with: IgnoreCoding.self)
}
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
@_implementationOnly import SwiftSyntax
@_implementationOnly import SwiftSyntaxMacros

/// A type of `EnumSwitcherVariable` that can have adjacent tagging.
///
/// Only internally tagged enums can have adjacent tagging, while externally
/// tagged enums can't have adjacent tagging.
protocol AdjacentlyTaggableSwitcher: EnumSwitcherVariable {
/// Register variable for the provided `CodingKey` path.
///
/// Creates new switcher variable of this type updating with provided
/// variable registration.
///
/// - Parameters:
/// - variable: The variable data, i.e. name, type and
/// additional macro metadata.
/// - keyPath: The `CodingKey` path where the value
/// will be decode/encoded.
///
/// - Returns: Newly created variable updating registration.
func registering(
variable: AdjacentlyTaggedEnumSwitcher<Self>.CoderVariable,
keyPath: [CodingKeysMap.Key]
) -> Self
}

extension InternallyTaggedEnumSwitcher: AdjacentlyTaggableSwitcher {
/// Register variable for the provided `CodingKey` path.
///
/// Creates new switcher variable of this type updating with provided
/// variable registration.
///
/// Registers variable at the provided `CodingKey` path on the current node.
///
/// - Parameters:
/// - variable: The variable data, i.e. name, type and
/// additional macro metadata.
/// - keyPath: The `CodingKey` path where the value
/// will be decode/encoded.
///
/// - Returns: Newly created variable updating registration.
func registering(
variable: AdjacentlyTaggedEnumSwitcher<Self>.CoderVariable,
keyPath: [CodingKeysMap.Key]
) -> Self {
var node = node
node.register(variable: variable, keyPath: keyPath)
return .init(
encodeContainer: encodeContainer,
identifier: identifier, identifierType: identifierType,
node: node, keys: keys,
decl: decl, variableBuilder: variableBuilder
)
}
}

extension Registration
where Var: AdjacentlyTaggableSwitcher, Decl: AttributableDeclSyntax {
/// Checks if enum declares adjacent tagging.
///
/// Checks if enum-case content path provided with `CodedAt` macro.
///
/// - Parameters:
/// - contentDecoder: The mapped name for decoder.
/// - contentEncoder: The mapped name for encoder.
/// - codingKeys: The map where `CodingKeys` maintained.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: Variable registration with adjacent tagging data if exists.
func checkForAdjacentTagging(
contentDecoder: TokenSyntax, contentEncoder: TokenSyntax,
codingKeys: CodingKeysMap, context: MacroExpansionContext
) -> Registration<Decl, Key, AnyEnumSwitcher> {
guard
let attr = CodedAt(from: decl),
case let keyPath = attr.keyPath(withExisting: []),
!keyPath.isEmpty
else { return self.updating(with: variable.any) }
let variable = AdjacentlyTaggedEnumSwitcher(
base: variable,
contentDecoder: contentDecoder, contentEncoder: contentEncoder,
keyPath: keyPath, codingKeys: codingKeys, context: context
)
return self.updating(with: variable.any)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
@_implementationOnly import SwiftSyntax
@_implementationOnly import SwiftSyntaxMacros

/// An `EnumSwitcherVariable` generating switch expression for adjacently
/// tagged enums.
///
/// Registers path for enum-case associated variables container root and
/// generated syntax for this common container.
struct AdjacentlyTaggedEnumSwitcher<Wrapped>: EnumSwitcherVariable,
ComposedVariable
where Wrapped: AdjacentlyTaggableSwitcher {
/// The switcher value wrapped by this instance.
///
/// The wrapped variable's type data is preserved and this variable is used
/// to chain code generation implementations while changing the root
/// container for enum-case associated variables.
let base: Wrapped
/// The container mapping variable.
///
/// This variable is used to map the nested container
/// for associated variables to a pre-defined name.
let variable: CoderVariable

/// Creates switcher variable with provided data.
///
/// - Parameters:
/// - base: The base variable that handles implementation.
/// - contentDecoder: The mapped name for content root decoder.
/// - contentEncoder: The mapped name for content root encoder.
/// - keyPath: The key path to enum-case content root.
/// - codingKeys: The map where `CodingKeys` maintained.
/// - context: The context in which to perform the macro expansion.
init(
base: Wrapped, contentDecoder: TokenSyntax, contentEncoder: TokenSyntax,
keyPath: [String], codingKeys: CodingKeysMap,
context: some MacroExpansionContext
) {
let keys = codingKeys.add(keys: keyPath, context: context)
self.variable = .init(decoder: contentDecoder, encoder: contentEncoder)
self.base = base.registering(variable: variable, keyPath: keys)
}

/// Creates value expression for provided enum-case variable.
///
/// Provides value generated by the underlying variable value.
///
/// - Parameters:
/// - variable: The variable for which generated.
/// - value: The optional value present in syntax.
/// - codingKeys: The map where `CodingKeys` maintained.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: The generated value.
func keyExpression<Var: EnumCaseVariable>(
for variable: Var, value: ExprSyntax?,
codingKeys: CodingKeysMap, context: some MacroExpansionContext
) -> EnumVariable.CaseValue {
return base.keyExpression(
for: variable, value: value,
codingKeys: codingKeys, context: context
)
}

/// Provides the syntax for decoding at the provided location.
///
/// Provides implementation generated by the underlying variable value
/// while changing the root container name for enum-case associated
/// values decoding.
///
/// - Parameters:
/// - context: The context in which to perform the macro expansion.
/// - location: The decoding location.
///
/// - Returns: The generated decoding syntax.
func decoding(
in context: some MacroExpansionContext,
from location: EnumSwitcherLocation
) -> EnumSwitcherGenerated {
let generated = base.decoding(in: context, from: location)
let newData: EnumSwitcherGenerated.CaseData
switch generated.data {
case .container:
newData = generated.data
case .coder(_, let postfix):
newData = .coder(variable.decoder, postfix)
}
return .init(
data: newData, expr: generated.expr, code: generated.code,
defaultCase: generated.defaultCase
)
}

/// Provides the syntax for encoding at the provided location.
///
/// Provides implementation generated by the underlying variable value
/// while changing the root container name for enum-case associated
/// values encoding.
///
/// - Parameters:
/// - context: The context in which to perform the macro expansion.
/// - location: The encoding location.
///
/// - Returns: The generated encoding syntax.
func encoding(
in context: some MacroExpansionContext,
to location: EnumSwitcherLocation
) -> EnumSwitcherGenerated {
let generated = base.encoding(in: context, to: location)
let newData: EnumSwitcherGenerated.CaseData
switch generated.data {
case .container:
newData = generated.data
case .coder(_, let postfix):
newData = .coder(variable.encoder, postfix)
}
return .init(
data: newData, expr: generated.expr, code: generated.code,
defaultCase: generated.defaultCase
)
}

/// Creates additional enum declarations for enum variable.
///
/// Provides enum declarations for of the underlying variable value.
///
/// - Parameter context: The context in which to perform the macro
/// expansion.
/// - Returns: The generated enum declaration syntax.
func codingKeys(
in context: some MacroExpansionContext
) -> MemberBlockItemListSyntax {
return base.codingKeys(in: context)
}
}

extension AdjacentlyTaggedEnumSwitcher {
/// A variable value exposing decoder and encoder.
///
/// The `CoderVariable` exposes decoder and encoder via variable
/// provided with `decoder` and `encoder` name respectively.
struct CoderVariable: PropertyVariable, ComposedVariable {
/// The initialization type of this variable.
///
/// Initialization type is the same as underlying wrapped variable.
typealias Initialization = BasicPropertyVariable.Initialization
/// The mapped name for decoder.
///
/// The decoder at location passed will be exposed
/// with this variable name.
let decoder: TokenSyntax
/// The mapped name for encoder.
///
/// The encoder at location passed will be exposed
/// with this variable name.
let encoder: TokenSyntax

/// The value wrapped by this instance.
///
/// The wrapped variable's type data is
/// preserved and this variable is used
/// to chain code generation implementations.
let base = BasicPropertyVariable(
name: "", type: "", value: nil,
decodePrefix: "", encodePrefix: "",
decode: true, encode: true
)

/// Whether the variable is to be decoded.
///
/// This variable is always set as to be decoded.
var decode: Bool? { true }
/// Whether the variable is to be encoded.
///
/// This variable is always set as to be encoded.
var encode: Bool? { true }

/// Whether the variable type requires `Decodable` conformance.
///
/// This variable never requires `Decodable` conformance
var requireDecodable: Bool? { false }
/// Whether the variable type requires `Encodable` conformance.
///
/// This variable never requires `Encodable` conformance
var requireEncodable: Bool? { false }

/// Provides the code syntax for decoding this variable
/// at the provided location.
///
/// Creates/assigns the decoder passed in location to the variable
/// created with the `decoder` name provided.
///
/// - Parameters:
/// - context: The context in which to perform the macro expansion.
/// - location: The decoding location for the variable.
///
/// - Returns: The generated variable decoding code.
func decoding(
in context: some MacroExpansionContext,
from location: PropertyCodingLocation
) -> CodeBlockItemListSyntax {
return switch location {
case .coder(let decoder, _):
"let \(self.decoder) = \(decoder)"
case .container(let container, let key, _):
"let \(self.decoder) = try \(container).superDecoder(forKey: \(key))"
}
}

/// Provides the code syntax for encoding this variable
/// at the provided location.
///
/// Creates/assigns the encoder passed in location to the variable
/// created with the `encoder` name provided.
///
/// - Parameters:
/// - context: The context in which to perform the macro expansion.
/// - location: The encoding location for the variable.
///
/// - Returns: The generated variable encoding code.
func encoding(
in context: some MacroExpansionContext,
to location: PropertyCodingLocation
) -> CodeBlockItemListSyntax {
return switch location {
case .coder(let encoder, _):
"let \(self.encoder) = \(encoder)"
case .container(let container, let key, _):
"let \(self.encoder) = \(container).superEncoder(forKey: \(key))"
}
}
}
}
6 changes: 5 additions & 1 deletion Sources/CodableMacroPlugin/Variables/Type/EnumVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ struct EnumVariable: TypeVariable, DeclaredVariable {
) { registration in
return registration.useHelperCoderIfExists()
} switcherBuilder: { registration in
return registration
return registration.checkForAdjacentTagging(
contentDecoder: "contentDecoder",
contentEncoder: "contentEncoder",
codingKeys: codingKeys, context: context
)
}
} caseBuilder: { input in
return input.checkForAlternateValue().checkCodingIgnored()
Expand Down
1 change: 1 addition & 0 deletions Sources/MetaCodable/Codable/Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
/// * Use ``Default(_:)`` to provide default value when decoding fails.
/// * Use ``CodedAs(_:)`` to provided custom value for enum cases.
/// * Use ``TaggedAt(_:_:)`` to provide enum-case identifier tag path.
/// * Use ``CodedAt(_:)`` to provided enum-case content path.
/// * Use ``IgnoreCoding()``, ``IgnoreDecoding()`` and
/// ``IgnoreEncoding()`` to ignore specific properties/cases from
/// decoding/encoding or both.
Expand Down

0 comments on commit a22e9d1

Please sign in to comment.