Skip to content

Commit 2ebadd5

Browse files
authored
feat: added common strategies applicable to all properties
1 parent cc973b0 commit 2ebadd5

13 files changed

Lines changed: 689 additions & 7 deletions

File tree

Examples/App/Sources/ContentView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import SwiftUI
2-
import MetaCodable
31
import HelperCoders
2+
import MetaCodable
3+
import SwiftUI
44

55
public struct ContentView: View {
66
public init() {}

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// swift-tools-version: 6.0
2+
// swift-format-ignore-file
23

4+
import CompilerPluginSupport
35
import Foundation
46
import PackageDescription
5-
import CompilerPluginSupport
67

78
let package = Package(
89
name: "MetaCodable",

Package@swift-5.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// swift-tools-version: 5.9
2+
// swift-format-ignore-file
23

4+
import CompilerPluginSupport
35
import Foundation
46
import PackageDescription
5-
import CompilerPluginSupport
67

78
let package = Package(
89
name: "MetaCodable",

Sources/HelperCoders/HelperCoders.docc/HelperCoders.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,7 @@ Level up `MetaCodable`'s generated implementations with helpers assisting common
7575
### Sequence
7676

7777
- ``SequenceCoder``
78+
79+
### Strategies
80+
81+
- ``HelperCoderStrategy``
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import MetaCodable
2+
3+
/// An enumeration of supported helper coder strategies for use with the `@Codable` macro's `commonStrategies` parameter.
4+
///
5+
/// Use cases such as `.valueCoder()` allow you to specify that all properties should use a particular value coding strategy
6+
/// (e.g., `ValueCoder`) for encoding and decoding, without annotating each property individually.
7+
public enum HelperCoderStrategy {
8+
/// Applies the `ValueCoder` strategy to all properties, optionally specifying additional types.
9+
case valueCoder(_ additionalTypes: [any ValueCodingStrategy.Type] = [])
10+
// Future cases can be added here
11+
}
12+
13+
public extension CodableCommonStrategy {
14+
/// Returns a `CodableCommonStrategy` representing the use of a helper coder strategy for all properties.
15+
///
16+
/// - Parameter helperCoderStrategy: The helper coder strategy to apply (e.g., `.valueCoder()`).
17+
/// - Returns: A `CodableCommonStrategy` value for use in the `commonStrategies` parameter of `@Codable`.
18+
static func codedBy(_ helperCoderStrategy: HelperCoderStrategy) -> Self {
19+
return .init()
20+
}
21+
}

Sources/MetaCodable/Codable/Codable.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
/// * If attached declaration already conforms to `Codable` this macro expansion
4747
/// is skipped.
4848
///
49+
/// - Parameters:
50+
/// - commonStrategies: An array of CodableCommonStrategy values specifying
51+
/// type conversion strategies to be automatically applied to all properties of the type.
52+
///
4953
/// - Important: The attached declaration must be of a `struct`, `class`, `enum`
5054
/// or `actor` type. [See the limitations for this macro](<doc:Limitations>).
5155
@attached(
@@ -58,7 +62,7 @@
5862
names: named(CodingKeys), named(init(from:)), named(encode(to:))
5963
)
6064
@available(swift 5.9)
61-
public macro Codable() =
65+
public macro Codable(commonStrategies: [CodableCommonStrategy] = []) =
6266
#externalMacro(module: "MacroPlugin", type: "Codable")
6367

6468
/// Indicates whether super class conforms to `Codable` or not.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// CodableCommonStrategy.swift
2+
// Defines the CodableCommonStrategy struct for commonStrategies parameter in @Codable macro.
3+
4+
/// A marker type used to represent a common type conversion strategy for the `@Codable` macro.
5+
///
6+
/// `CodableCommonStrategy` is used as the element type for the `commonStrategies` parameter in the
7+
/// `@Codable` macro. It allows users to specify strategies (such as value coding) that should be
8+
/// automatically applied to all properties of a type, so that users do not have to annotate each property
9+
/// individually. The macro system interprets these strategies and injects the appropriate coding logic
10+
/// during macro expansion.
11+
///
12+
/// Example usage:
13+
/// ```swift
14+
/// @Codable(commonStrategies: [.codedBy(.valueCoder())])
15+
/// struct MyModel {
16+
/// let int: Int
17+
/// let string: String
18+
/// }
19+
/// ```
20+
public struct CodableCommonStrategy {
21+
// Only allow MetaCodable to construct
22+
package init() {}
23+
}

Sources/MetaCodable/MetaCodable.docc/MetaCodable.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Supercharge `Swift`'s `Codable` implementations with macros.
6868

6969
### Macros
7070

71-
- ``Codable()``
71+
- ``Codable(commonStrategies:)``
7272
- ``MemberInit()``
7373

7474
### Strategies
@@ -81,6 +81,7 @@ Supercharge `Swift`'s `Codable` implementations with macros.
8181
- ``UnTagged()``
8282
- ``CodingKeys(_:)``
8383
- ``Inherits(decodable:encodable:)``
84+
- ``CodableCommonStrategy``
8485

8586
### Helpers
8687

Sources/PluginCore/Attributes/Codable/Codable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import SwiftSyntax
1616
/// methods.
1717
/// * If attached declaration already conforms to `Codable` this macro expansion
1818
/// is skipped.
19-
package struct Codable: Attribute {
19+
package struct Codable: PeerAttribute {
2020
/// The node syntax provided
2121
/// during initialization.
2222
let node: AttributeSyntax
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import SwiftSyntax
2+
import SwiftSyntaxMacros
3+
4+
/// Finds and stores common strategies from a type declaration's macro attributes.
5+
///
6+
/// This struct parses the macro attributes on a type declaration to extract any
7+
/// common type conversion strategies (such as value coding strategies) that should
8+
/// be applied to all properties. The extracted strategies can then be used to
9+
/// automatically transform property registrations during macro expansion.
10+
struct StrategyFinder {
11+
/// The list of value coding strategies (as type names) to apply to properties.
12+
let valueCodingStrategies: [TokenSyntax]
13+
// Extend with more strategies as needed
14+
15+
/// Initializes a new `StrategyFinder` by parsing the macro attributes of the given declaration.
16+
///
17+
/// - Parameter decl: The declaration to extract strategies from.
18+
init(decl: some AttributableDeclSyntax) {
19+
guard
20+
let attr = Codable(from: decl),
21+
let arguments = attr.node.arguments?.as(LabeledExprListSyntax.self),
22+
let arg = arguments.first(where: {
23+
$0.label?.text == "commonStrategies"
24+
}),
25+
let expr = arg.expression.as(ArrayExprSyntax.self)
26+
else {
27+
self.valueCodingStrategies = []
28+
return
29+
}
30+
31+
let codedBys = expr.elements.lazy
32+
.compactMap({ $0.expression.as(FunctionCallExprSyntax.self) })
33+
.filter({
34+
$0.calledExpression.as(MemberAccessExprSyntax.self)?.declName
35+
.baseName.text == "codedBy"
36+
})
37+
let valueCoders = codedBys.flatMap({
38+
$0.arguments
39+
.compactMap({ $0.expression.as(FunctionCallExprSyntax.self) })
40+
.filter({
41+
$0.calledExpression.as(MemberAccessExprSyntax.self)?
42+
.declName.baseName.text == "valueCoder"
43+
})
44+
})
45+
46+
var valueCodingStrategies: [TokenSyntax] = []
47+
if !valueCoders.isEmpty {
48+
// Default strategies for primitive types
49+
valueCodingStrategies = [
50+
"Bool", "Double", "Float", "String",
51+
"Int", "Int8", "Int16", "Int32", "Int64",
52+
"UInt", "UInt8", "UInt16", "UInt32", "UInt64",
53+
]
54+
// Add any additional types specified in valueCoder(...)
55+
valueCodingStrategies.append(
56+
contentsOf: valueCoders.flatMap {
57+
$0.arguments.first?.expression.as(ArrayExprSyntax.self)?
58+
.elements.compactMap {
59+
$0.expression.as(MemberAccessExprSyntax.self)?.base?
60+
.as(DeclReferenceExprSyntax.self)?.baseName
61+
} ?? []
62+
}
63+
)
64+
}
65+
self.valueCodingStrategies = valueCodingStrategies
66+
}
67+
}
68+
69+
extension Registration where Var: DefaultPropertyVariable {
70+
/// Applies common strategies (such as value coding) to the property registration if the type declaration specifies them.
71+
///
72+
/// This method uses `StrategyFinder` to extract any common strategies (e.g., value coding strategies)
73+
/// from the macro attributes of the provided type declaration. If the property's type matches one of the
74+
/// strategies, it wraps the property in a `HelperCodedVariable` using the appropriate helper (such as `ValueCoder`).
75+
///
76+
/// - Parameter decl: The type declaration to extract strategies from.
77+
/// - Returns: A new registration, possibly wrapping the property variable with a strategy-based helper.
78+
func detectCommonStrategies(
79+
from decl: some AttributableDeclSyntax
80+
) -> Registration<Decl, Key, StrategyVariable<Var.Initialization>> {
81+
let finder = StrategyFinder(decl: decl)
82+
let inStrategy =
83+
finder.valueCodingStrategies.first { strategy in
84+
strategy.trimmedDescription
85+
== self.variable.type.trimmedDescription
86+
} != nil
87+
88+
let newVariable: AnyPropertyVariable<Var.Initialization>
89+
if inStrategy {
90+
newVariable =
91+
HelperCodedVariable(
92+
base: self.variable,
93+
options: .helper("ValueCoder<\(variable.type)>()")
94+
).any
95+
} else {
96+
newVariable = self.variable.any
97+
}
98+
return self.updating(with: StrategyVariable(base: newVariable))
99+
}
100+
}

0 commit comments

Comments
 (0)