Skip to content

Commit

Permalink
feat: made initialized mutable variables initialization optional in m…
Browse files Browse the repository at this point in the history
…ember-wise initializers (#15)

* feat: made initialized mutable variables initialization optional in member-wise initializers
- added multiple member-wise initializers based on mulatable initialized variables present

* wip: use code-factor suggestions
  • Loading branch information
soumyamahunt committed Sep 20, 2023
1 parent 8d61676 commit 12f3177
Show file tree
Hide file tree
Showing 23 changed files with 407 additions and 140 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Supercharge `Swift`'s `Codable` implementations with macros.
- Allows to create flattened model for nested `CodingKey` values with ``CodedAt(_:)`` and ``CodedIn(_:)``.
- Allows to create composition of multiple `Codable` types with ``CodedAt(_:)`` passing no arguments.
- Allows to provide default value in case of decoding failures with ``Default(_:)``.
- Generates member-wise initializer considering the above default value syntax as well.
- Generates member-wise initializer(s) considering the above default value syntax as well.
- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` etc.

## Requirements
Expand Down Expand Up @@ -181,7 +181,7 @@ struct Coordinate {
</details>

<details>
<summary>Provide default value in case of decoding failures and member-wise initializer generated considers these default values.</summary>
<summary>Provide default value in case of decoding failures and member-wise initializer(s) generated considers these default values.</summary>

Instead of throwing error in case of missing data or type mismatch, you can provide a default value that will be assigned in this case. The memberwise initializer generated also uses this default value for the field. The following definition with `MetaCodable`:

Expand Down
11 changes: 3 additions & 8 deletions Sources/CodableMacroPlugin/Attributes/Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import SwiftSyntaxMacros
/// for the attached struct declaration named `CodingKeys` and use
/// this type for `Codable` implementation of both `init(from:)`
/// and `encode(to:)` methods by using `CodedPropertyMacro`
/// declarations. Additionally member-wise initializer is also generated.
/// declarations. Additionally member-wise initializer(s) also generated.
struct Codable: Attribute {
/// The node syntax provided
/// during initialization.
Expand Down Expand Up @@ -110,7 +110,7 @@ extension Codable: ConformanceMacro, MemberMacro {
///
/// - Returns: `CodingKeys` type and `init(from:)`, `encode(to:)`,
/// method declarations for `Codable` implementation along with
/// member-wise initializer declaration.
/// member-wise initializer declaration(s).
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
Expand Down Expand Up @@ -147,12 +147,7 @@ extension Codable: ConformanceMacro, MemberMacro {
}

// generate
return [
DeclSyntax(registrar.memberInit(in: context)),
DeclSyntax(registrar.decoding(in: context)),
DeclSyntax(registrar.encoding(in: context)),
DeclSyntax(registrar.codingKeys(in: context)),
]
return registrar.memberDeclarations(in: context)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import SwiftSyntax

extension Default: RegistrationBuilder {
/// The any variable data with required initialization that input registration can have.
typealias Input = AnyVariable<RequiredInitialization>
/// The variable data with default expression that output registration will have.
typealias Output = DefaultValueVariable<AnyVariable>
typealias Output = DefaultValueVariable<Input>

/// Build new registration with provided input registration.
///
/// New registration is updated with default expression data that will be used
/// for decoding failure and member-wise initializer, if provided.
/// for decoding failure and member-wise initializer(s), if provided.
///
/// - Parameter input: The registration built so far.
/// - Returns: Newly built registration with default expression data.
func build(with input: Registration<AnyVariable>) -> Registration<Output> {
func build(with input: Registration<Input>) -> Registration<Output> {
let expr = node.argument!
.as(TupleExprElementListSyntax.self)!.first!.expression
return input.updating(with: input.variable.with(default: expr))
}
}

fileprivate extension Variable {
fileprivate extension Variable where Initialization == RequiredInitialization {
/// Update variable data with the default value expression provided.
///
/// `DefaultValueVariable` is created with this variable as base
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import SwiftSyntax
///
/// Checks whether variable can be initialized and whether variable has been already initialized
/// from the current syntax and updates the registrations variable data accordingly.
struct InitializationRegistrationBuilder<Input: Variable>: RegistrationBuilder {
struct InitializationRegistrationBuilder<Input: Variable>: RegistrationBuilder
where Input.Initialization == RequiredInitialization {
/// The output registration variable type that handles initialization data.
typealias Output = InitializationVariable<Input>

Expand Down Expand Up @@ -33,7 +34,7 @@ struct InitializationRegistrationBuilder<Input: Variable>: RegistrationBuilder {
|| input.context.binding.initializer == nil
}

let initialized = input.context.binding.initializer == nil
let initialized = input.context.binding.initializer != nil
let options = Output.Options(init: canInit, initialized: initialized)
let newVariable = Output(base: input.variable, options: options)
return input.updating(with: newVariable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
/// The optional registration is used to update registration data with current syntax.
/// If optional registration not provided, input registration is passed with variable type erased.
struct OptionalRegistrationBuilder<Builder>: RegistrationBuilder
where Builder: RegistrationBuilder {
where Builder: RegistrationBuilder,
Builder.Input.Initialization == Builder.Output.Initialization {
/// The variable data of underlying builder's input registration.
typealias Input = Builder.Input
/// The variable data generated by passed builder or the input
/// variable data if no builder passed.
typealias Output = AnyVariable<Input.Initialization>

/// The optional builder to use.
///
/// This will be used as underlying
Expand All @@ -31,9 +37,9 @@ where Builder: RegistrationBuilder {
///
/// - Parameter input: The registration built so far.
/// - Returns: Newly built registration with additional data.
func build(with input: Registration<Input>) -> Registration<AnyVariable> {
func build(with input: Registration<Input>) -> Registration<Output> {
let keyPath: [String]
let variable: Variable
let variable: any Variable<Input.Initialization>
if let reg = base?.build(with: input) {
keyPath = reg.keyPath
variable = reg.variable
Expand Down
8 changes: 4 additions & 4 deletions Sources/CodableMacroPlugin/Registration/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ extension Registrar {
}
}
/// All the variables registered at this node.
private(set) var variables: [Variable]
private(set) var variables: [any Variable]
/// Nested registration node associated with keys.
private(set) var children: OrderedDictionary<Key, Self>

/// List of all the linked variables registered.
///
/// Gets all variables at current node
/// and children nodes.
var linkedVariables: [Variable] {
var linkedVariables: [any Variable] {
return variables + children.flatMap { $1.linkedVariables }
}

Expand All @@ -73,7 +73,7 @@ extension Registrar {
///
/// - Returns: The newly created node instance.
init(
variables: [Variable] = [],
variables: [any Variable] = [],
children: OrderedDictionary<Key, Self> = [:]
) {
self.variables = variables
Expand All @@ -90,7 +90,7 @@ extension Registrar {
/// additional macro metadata.
/// - keyPath: The `CodingKey` path where the value
/// will be decode/encoded.
mutating func register(variable: Variable, keyPath: [Key]) {
mutating func register(variable: any Variable, keyPath: [Key]) {
guard !keyPath.isEmpty else { variables.append(variable); return }

let key = keyPath.first!
Expand Down
73 changes: 39 additions & 34 deletions Sources/CodableMacroPlugin/Registration/Registrar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ struct Registrar {
/// conformance implementation declarations.
fileprivate let modifiers: ModifierListSyntax?

/// Member-wise initialization generator with provided options.
///
/// Creates member-wise initialization generator by passing
/// the provided access modifiers.
var initGenerator: MemberwiseInitGenerator {
return .init(options: .init(modifiers: modifiers))
}

/// Creates a new options instance with provided parameters.
///
/// - Parameters:
Expand Down Expand Up @@ -82,6 +90,26 @@ struct Registrar {
)
}

/// Generates member declarations for `Codable` macro.
///
/// From the variables registered by `Codable` macro,
/// member-wise initialization, `Codable` protocol conformance
/// and `CodingKey` declarations are generated.
///
/// - Parameter context: The context in which to perform
/// the macro expansion.
///
/// - Returns: The generated member declarations.
func memberDeclarations(
in context: some MacroExpansionContext
) -> [DeclSyntax] {
var decls = memberInit(in: context).map { DeclSyntax($0) }
decls.append(DeclSyntax(decoding(in: context)))
decls.append(DeclSyntax(encoding(in: context)))
decls.append(DeclSyntax(codingKeys(in: context)))
return decls
}

/// Provides the declaration of `CodingKey` type that is used
/// for `Codable` implementation generation.
///
Expand All @@ -92,7 +120,7 @@ struct Registrar {
/// the macro expansion.
///
/// - Returns: The generated enum declaration.
func codingKeys(
private func codingKeys(
in context: some MacroExpansionContext
) -> EnumDeclSyntax {
return caseMap.decl(in: context)
Expand All @@ -105,7 +133,7 @@ struct Registrar {
/// the macro expansion.
///
/// - Returns: The generated initializer declaration.
func decoding(
private func decoding(
in context: some MacroExpansionContext
) -> InitializerDeclSyntax {
return InitializerDeclSyntax.decode(
Expand All @@ -125,7 +153,7 @@ struct Registrar {
/// the macro expansion.
///
/// - Returns: The generated function declaration.
func encoding(
private func encoding(
in context: some MacroExpansionContext
) -> FunctionDeclSyntax {
return FunctionDeclSyntax.encode(
Expand All @@ -138,43 +166,20 @@ struct Registrar {
}
}

/// Provides the member-wise initializer declaration.
/// Provides the member-wise initializer declaration(s).
///
/// - Parameter context: The context in which to perform
/// the macro expansion.
///
/// - Returns: The generated initializer declaration.
func memberInit(
/// - Returns: The generated initializer declarations.
private func memberInit(
in context: some MacroExpansionContext
) -> InitializerDeclSyntax {
let allVariables = root.linkedVariables
var params: [FunctionParameterSyntax] = []
var exprs: [CodeBlockItemSyntax] = []
params.reserveCapacity(allVariables.count)
exprs.reserveCapacity(allVariables.count)

allVariables.forEach { variable in
switch variable.initializing(in: context) {
case .ignored:
break
case .optional(let param, let expr),
.required(let param, let expr):
params.append(param)
exprs.append(expr)
}
}
return InitializerDeclSyntax(
modifiers: options.modifiers,
signature: .init(
input: .init(
parameterList: .init {
for param in params { param }
}
)
)
) {
for expr in exprs { expr }
) -> [InitializerDeclSyntax] {
var generator = options.initGenerator
for variable in root.linkedVariables {
generator = variable.initializing(in: context).add(to: generator)
}
return generator.declarations(in: context)
}
}

Expand Down
18 changes: 11 additions & 7 deletions Sources/CodableMacroPlugin/Variables/AnyVariable.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import SwiftSyntax
import SwiftSyntaxMacros

/// A type-erased variable value.
/// A type-erased variable value only containing initialization type data.
///
/// The `AnyVariable` type forwards `Variable`
/// implementations to an underlying variable value,
/// hiding the type of the wrapped value.
struct AnyVariable: Variable {
/// The `AnyVariable` type forwards `Variable` implementations to an underlying
/// variable value, hiding the type of the wrapped value.
struct AnyVariable<Initialization: VariableInitialization>: Variable {
/// The value wrapped by this instance.
///
/// The base property can be cast back
/// to its original type using type casting
/// operators (`as?`, `as!`, or `as`).
let base: Variable
let base: any Variable<Initialization>

/// The name of the variable.
///
Expand All @@ -22,6 +21,11 @@ struct AnyVariable: Variable {
///
/// Provides type of the underlying variable value.
var type: TypeSyntax { base.type }
/// Whether the variable is needed for final code generation.
///
/// Provides whether underlying variable value is needed
/// for final code generation.
var canBeRegistered: Bool { base.canBeRegistered }

/// Indicates the initialization type for this variable.
///
Expand All @@ -32,7 +36,7 @@ struct AnyVariable: Variable {
/// - Returns: The type of initialization for variable.
func initializing(
in context: some MacroExpansionContext
) -> VariableInitialization {
) -> Initialization {
return base.initializing(in: context)
}

Expand Down

This file was deleted.

0 comments on commit 12f3177

Please sign in to comment.