Skip to content

Commit

Permalink
feat: added CodingKey alias support
Browse files Browse the repository at this point in the history
  • Loading branch information
soumyamahunt committed Jan 9, 2024
1 parent 2670fde commit 665306f
Show file tree
Hide file tree
Showing 50 changed files with 970 additions and 205 deletions.
83 changes: 71 additions & 12 deletions Sources/CodableMacroPlugin/Attributes/CodedAs.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@_implementationOnly import SwiftSyntax
@_implementationOnly import SwiftSyntaxMacros

/// Attribute type for `CodedAs` macro-attribute.
///
Expand All @@ -11,9 +12,9 @@ struct CodedAs: PropertyAttribute {
let node: AttributeSyntax

/// The alternate value expression provided.
var expr: ExprSyntax? {
var exprs: [ExprSyntax] {
return node.arguments?
.as(LabeledExprListSyntax.self)?.first?.expression
.as(LabeledExprListSyntax.self)?.map(\.expression) ?? []
}

/// The type to which to be decoded/encoded.
Expand Down Expand Up @@ -53,32 +54,62 @@ struct CodedAs: PropertyAttribute {
/// * This attribute mustn't be combined with `CodedBy`
/// attribute.
/// * If macro has one argument provided:
/// * Attached declaration is an enum-case declaration.
/// * Attached declaration is an enum-case or variable declaration.
/// * This attribute isn't used combined with `IgnoreCoding`
/// attribute.
/// * If macro attached declaration is variable declaration:
/// * Attached declaration is not a grouped variable declaration.
/// * Attached declaration is not a static variable declaration.
///
/// - Returns: The built diagnoser instance.
func diagnoser() -> DiagnosticProducer {
return AggregatedDiagnosticProducer {
cantDuplicate()
`if`(
has(arguments: 1),
has(arguments: 0),
AggregatedDiagnosticProducer {
expect(syntaxes: EnumCaseDeclSyntax.self)
cantBeCombined(with: IgnoreCoding.self)
},
else: AggregatedDiagnosticProducer {
expect(syntaxes: EnumDeclSyntax.self)
mustBeCombined(with: Codable.self)
mustBeCombined(with: CodedAt.self)
cantBeCombined(with: CodedBy.self)
}
},
else: `if`(
isVariable,
AggregatedDiagnosticProducer {
attachedToUngroupedVariable()
attachedToNonStaticVariable()
cantBeCombined(with: IgnoreCoding.self)
},
else: AggregatedDiagnosticProducer {
expect(
syntaxes: EnumCaseDeclSyntax.self,
VariableDeclSyntax.self
)
cantBeCombined(with: IgnoreCoding.self)
}
)
)
}
}
}

extension Registration where Key == ExprSyntax?, Decl: AttributableDeclSyntax {
extension CodedAs: KeyPathProvider {
/// Indicates whether `CodingKey` path
/// data is provided to this instance.
///
/// Always `true` for this type.
var provided: Bool { true }

/// Updates `CodingKey` path using the provided path.
///
/// The `CodingKey` path overrides current `CodingKey` path data.
///
/// - Parameter path: Current `CodingKey` path.
/// - Returns: Updated `CodingKey` path.
func keyPath(withExisting path: [String]) -> [String] { providedPath }
}

extension Registration where Key == [ExprSyntax], Decl: AttributableDeclSyntax {
/// Update registration with alternate value expression data.
///
/// New registration is updated with value expression data that will be
Expand All @@ -88,9 +119,37 @@ extension Registration where Key == ExprSyntax?, Decl: AttributableDeclSyntax {
/// - Returns: Newly built registration with value expression data.
func checkForAlternateValue() -> Self {
guard
self.key == nil,
self.key.isEmpty,
let attr = CodedAs(from: self.decl)
else { return self }
return self.updating(with: attr.expr)
return self.updating(with: attr.exprs)
}
}

extension Registration
where Key == [String], Decl: AttributableDeclSyntax, Var: PropertyVariable {
/// Update registration with alternate `CodingKey`s data.
///
/// New registration is updated with `CodingKey`s data that will be
/// used for decoding/encoding, if provided.
///
/// - Parameters:
/// - codingKeys: The `CodingKeys` map new data will be added.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: Newly built registration with additional `CodingKey`s data.
func checkForAlternateKeyValues(
addTo codingKeys: CodingKeysMap,
context: some MacroExpansionContext
) -> Registration<Decl, Key, AnyPropertyVariable<Var.Initialization>> {
guard
let attr = CodedAs(from: self.decl),
case let path = attr.providedPath,
!path.isEmpty
else { return self.updating(with: self.variable.any) }
let keys = codingKeys.add(keys: path, context: context)
let oldVar = self.variable
let newVar = AliasedPropertyVariable(base: oldVar, additionalKeys: keys)
return self.updating(with: newVar.any)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ struct IgnoreCoding: PropertyAttribute {
/// * Attached declaration is a variable or enum case declaration.
/// * Attached variable declaration has default initialization or
/// variable is a computed property.
/// * This attribute isn't used combined with `CodedIn` and
/// `CodedAt` attribute.
/// * This attribute isn't used combined with `CodedIn`,
/// `CodedAt` and `CodedAs` attribute.
/// * Additionally, warning generated if macro usage is duplicated
/// for the same declaration.
///
Expand All @@ -44,6 +44,7 @@ struct IgnoreCoding: PropertyAttribute {
return AggregatedDiagnosticProducer {
cantBeCombined(with: CodedIn.self)
cantBeCombined(with: CodedAt.self)
cantBeCombined(with: CodedAs.self)
shouldNotDuplicate()
`if`(
isVariable, attachedToInitializedVariable(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ struct CodedAt: PropertyAttribute {
/// * Attached declaration is not a grouped variable
/// declaration.
/// * Attached declaration is not a static variable
/// declaration
/// declaration.
/// * This attribute isn't used combined with `CodedIn`
/// and `IgnoreCoding` attribute.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,8 @@ struct ArgumentCountCondition<Attr>: DiagnosticCondition where Attr: Attribute {
/// - Parameter syntax: The syntax to validate.
/// - Returns: Whether syntax passes validation.
func satisfied(by syntax: some SyntaxProtocol) -> Bool {
guard
let args = attr.node.arguments?.as(LabeledExprListSyntax.self),
args.count == expected
else { return false }
return true
return expected == attr.node.arguments?
.as(LabeledExprListSyntax.self)?.count ?? 0
}
}

Expand Down
21 changes: 21 additions & 0 deletions Sources/CodableMacroPlugin/Variables/ComposedVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,34 @@ extension ComposedVariable where Self: ValuedVariable, Wrapped: ValuedVariable {
var value: ExprSyntax? { base.value }
}

extension ComposedVariable
where Self: ConditionalVariable, Wrapped: ConditionalVariable {
/// Whether the variable is to be decoded.
///
/// Whether underlying wrapped variable is to be decoded.
var decode: Bool? { base.decode }
/// Whether the variable is to be encoded.
///
/// Whether underlying wrapped variable is to be encoded.
var encode: Bool? { base.encode }
}

extension ComposedVariable
where Self: PropertyVariable, Wrapped: PropertyVariable {
/// The type of the variable.
///
/// Provides type of the underlying variable value.
var type: TypeSyntax { base.type }

/// Whether the variable type requires `Decodable` conformance.
///
/// Whether underlying wrapped variable requires `Decodable` conformance.
var requireDecodable: Bool? { base.requireDecodable }
/// Whether the variable type requires `Encodable` conformance.
///
/// Whether underlying wrapped variable requires `Encodable` conformance.
var requireEncodable: Bool? { base.requireEncodable }

/// The prefix token to use along with `name` when decoding.
///
/// Provides decode prefix of the underlying variable value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,23 @@ where Wrapped: AdjacentlyTaggableSwitcher {
self.base = base.registering(variable: variable, keyPath: keys)
}

/// Creates value expression for provided enum-case variable.
/// Creates value expressions 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.
/// - values: The values 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?,
for variable: Var, values: [ExprSyntax],
codingKeys: CodingKeysMap, context: some MacroExpansionContext
) -> EnumVariable.CaseValue {
return base.keyExpression(
for: variable, value: value,
for: variable, values: values,
codingKeys: codingKeys, context: context
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ struct AnyEnumSwitcher: EnumSwitcherVariable {
/// operators (`as?`, `as!`, or `as`).
let base: any EnumSwitcherVariable

/// Creates value expression for provided enum-case variable.
/// Creates value expressions 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.
/// - values: The values 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: SwiftSyntax.ExprSyntax?,
for variable: Var, values: [ExprSyntax],
codingKeys: CodingKeysMap, context: some MacroExpansionContext
) -> EnumVariable.CaseValue {
return base.keyExpression(
for: variable, value: value,
for: variable, values: values,
codingKeys: codingKeys, context: context
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,6 @@ struct BasicAssociatedVariable: AssociatedVariable, ComposedVariable,
/// initialization of this variable.
let label: TokenSyntax?

/// Whether the variable is to be decoded.
///
/// Whether underlying wrapped variable is to be decoded.
var decode: Bool? { base.decode }
/// Whether the variable is to be encoded.
///
/// Whether underlying wrapped variable is to be encoded.
var encode: Bool? { base.encode }

/// Whether the variable type requires `Decodable` conformance.
///
/// Whether underlying wrapped variable requires `Decodable` conformance.
var requireDecodable: Bool? { base.requireDecodable }
/// Whether the variable type requires `Encodable` conformance.
///
/// Whether underlying wrapped variable requires `Encodable` conformance.
var requireEncodable: Bool? { base.requireEncodable }

/// Creates a new variable from declaration and expansion context.
///
/// Uses the declaration to read variable specific data,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,15 @@ struct BasicEnumCaseVariable: EnumCaseVariable {
in context: some MacroExpansionContext,
from location: EnumCaseCodingLocation
) -> SwitchCaseSyntax {
let value = location.value
let pattern = ExpressionPatternSyntax(expression: value)
let label = SwitchCaseLabelSyntax { .init(pattern: pattern) }
let firstKey = location.values.first!
let label = SwitchCaseLabelSyntax(
caseItems: .init {
for value in location.values {
let pattern = ExpressionPatternSyntax(expression: value)
SwitchCaseItemSyntax.init(pattern: pattern)
}
}
)
let caseArgs = variables.map { variable in
let decode = (variable.decode ?? true)
let expr: ExprSyntax = decode ? "\(variable.name)" : variable.value!
Expand All @@ -118,12 +124,25 @@ struct BasicEnumCaseVariable: EnumCaseVariable {
switch location.data {
case .container(let container):
contentCoder = "contentDecoder"
prefix = """
let \(contentCoder) = try \(container).superDecoder(forKey: \(value))
"""
prefix = CodeBlockItemListSyntax {
if location.values.count > 1 {
let keyName: TokenSyntax = "identifierKey"
let keyList = ArrayExprSyntax(expressions: location.values)
"""
let \(keyName) = \(keyList).first { \(container).allKeys.contains($0) } ?? \(firstKey)
"""
"""
let \(contentCoder) = try \(container).superDecoder(forKey: \(keyName))
"""
} else {
"""
let \(contentCoder) = try \(container).superDecoder(forKey: \(firstKey))
"""
}
}
case .coder(let coder, let callback):
contentCoder = coder
prefix = callback("\(value)")
prefix = callback("\(firstKey)")
}
return SwitchCaseSyntax(label: .case(label)) {
prefix
Expand Down Expand Up @@ -160,7 +179,7 @@ struct BasicEnumCaseVariable: EnumCaseVariable {
let expr: ExprSyntax = ".\(name)\(argExpr)"
let pattern = ExpressionPatternSyntax(expression: expr)
let label = SwitchCaseLabelSyntax { .init(pattern: pattern) }
let value = location.value
let value = location.values.first!
let contentCoder: TokenSyntax
let prefix: CodeBlockItemListSyntax
switch location.data {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ struct EnumCaseCodingLocation {
/// Represents the data related to switch statement
/// of enum passed to each case.
let data: EnumSwitcherGenerated.CaseData
/// The value of the variable.
/// The values of the variable.
///
/// Represents the actual value that will be decoded/encoded.
let value: ExprSyntax
/// Represents the actual values that will be decoded.
/// Only the first value will be encoded.
let values: [ExprSyntax]
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@
protocol EnumSwitcherVariable: Variable
where CodingLocation == EnumSwitcherLocation, Generated == EnumSwitcherGenerated
{
/// Creates value expression for provided enum-case variable.
/// Creates value expressions for provided enum-case variable.
///
/// Determines the value of enum-case variable to have a `CodingKey`
/// based value or any raw value.
/// Determines the value of enum-case variable to have `CodingKey`
/// based values or any raw values.
///
/// - Parameters:
/// - variable: The variable for which generated.
/// - value: The optional value present in syntax.
/// - values: The values 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?,
for variable: Var, values: [ExprSyntax],
codingKeys: CodingKeysMap, context: some MacroExpansionContext
) -> EnumVariable.CaseValue

Expand Down

0 comments on commit 665306f

Please sign in to comment.