Skip to content

Commit 89d9cc0

Browse files
authored
feat: added support for Encodable/Decodable partial conformance (#137)
1 parent 2da1ee9 commit 89d9cc0

File tree

18 files changed

+1355
-3
lines changed

18 files changed

+1355
-3
lines changed

.swift-format

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,62 @@
11
{
22
"version": 1,
33
"lineLength": 80,
4+
"tabWidth": 4,
45
"indentation": {
56
"spaces": 4
67
},
8+
"fileScopedDeclarationPrivacy": {
9+
"accessLevel": "private"
10+
},
711
"maximumBlankLines": 1,
812
"respectsExistingLineBreaks": true,
913
"indentConditionalCompilationBlocks": false,
14+
"lineBreakBetweenDeclarationAttributes": true,
15+
"multiElementCollectionTrailingCommas": true,
16+
"prioritizeKeepingFunctionOutputTogether": true,
17+
"spacesAroundRangeFormationOperators": false,
1018
"rules": {
1119
"AllPublicDeclarationsHaveDocumentation": true,
20+
"AlwaysUseLiteralForEmptyCollectionInit": true,
21+
"AlwaysUseLowerCamelCase": true,
22+
"AmbiguousTrailingClosureOverload": true,
23+
"BeginDocumentationCommentWithOneLineSummary": false,
24+
"DoNotUseSemicolons": true,
25+
"DontRepeatTypeInStaticProperties": true,
26+
"FileScopedDeclarationPrivacy": true,
27+
"FullyIndirectEnum": true,
28+
"GroupNumericLiterals": true,
29+
"IdentifiersMustBeASCII": true,
1230
"NeverForceUnwrap": true,
1331
"NeverUseForceTry": true,
32+
"NeverUseImplicitlyUnwrappedOptionals": false,
1433
"NoAccessLevelOnExtensionDeclaration": false,
34+
"NoAssignmentInExpressions": true,
35+
"NoBlockComments": true,
36+
"NoCasesWithOnlyFallthrough": true,
37+
"NoEmptyLinesOpeningClosingBraces": true,
38+
"NoEmptyTrailingClosureParentheses": true,
39+
"NoLabelsInCasePatterns": true,
40+
"NoLeadingUnderscores": false,
41+
"NoParensAroundConditions": true,
42+
"NoPlaygroundLiterals": true,
43+
"NoVoidReturnOnFunctionSignature": true,
44+
"OmitExplicitReturns": true,
45+
"OneCasePerLine": true,
46+
"OneVariableDeclarationPerLine": true,
47+
"OnlyOneTrailingClosureArgument": true,
1548
"OrderedImports": true,
49+
"ReplaceForEachWithForLoop": true,
50+
"ReturnVoidInsteadOfEmptyTuple": true,
51+
"TypeNamesShouldBeCapitalized": true,
52+
"UseEarlyExits": true,
53+
"UseExplicitNilCheckInConditions": true,
54+
"UseLetInEveryBoundCaseVariable": true,
55+
"UseShorthandTypeNames": true,
56+
"UseSingleLinePropertyGetter": true,
57+
"UseSynthesizedInitializer": true,
58+
"UseTripleSlashForDocumentationComments": true,
59+
"UseWhereClausesInForLoops": true,
1660
"ValidateDocumentationComments": true
1761
}
1862
}

Sources/MacroPlugin/Definitions.swift

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,3 +608,121 @@ struct UnTagged: PeerMacro {
608608
)
609609
}
610610
}
611+
612+
/// A declaration macro that generates `Decodable`
613+
/// protocol conformance.
614+
///
615+
/// This implementation will delegate to the plugin core
616+
/// implementation depending on the type of attached declaration:
617+
/// * `struct`/`class`/`enum`/`actor` types: Expansion of `Decodable`
618+
/// protocol conformance members.
619+
public struct ConformDecodable: MemberMacro, ExtensionMacro {
620+
/// Expand to produce members for `Decodable`.
621+
///
622+
/// Membership macro expansion for `ConformDecodable` macro
623+
/// will delegate to `PluginCore.ConformDecodable`.
624+
///
625+
/// - Parameters:
626+
/// - node: The custom attribute describing this attached macro.
627+
/// - declaration: The declaration this macro attribute is attached to.
628+
/// - context: The context in which to perform the macro expansion.
629+
///
630+
/// - Returns: Delegated member expansion from `PluginCore.ConformDecodable`.
631+
public static func expansion(
632+
of node: AttributeSyntax,
633+
providingMembersOf declaration: some DeclGroupSyntax,
634+
in context: some MacroExpansionContext
635+
) throws -> [DeclSyntax] {
636+
return try PluginCore.ConformDecodable.expansion(
637+
of: node, providingMembersOf: declaration, in: context
638+
)
639+
}
640+
641+
/// Expand to produce extensions for `Decodable`.
642+
///
643+
/// Extension macro expansion for `ConformDecodable` macro
644+
/// will delegate to `PluginCore.ConformDecodable`.
645+
///
646+
/// - Parameters:
647+
/// - node: The custom attribute describing this attached macro.
648+
/// - declaration: The declaration this macro attribute is attached to.
649+
/// - type: The type to provide extensions of.
650+
/// - protocols: The list of protocols to add conformances to. These will
651+
/// always be protocols that `type` does not already state a conformance
652+
/// to.
653+
/// - context: The context in which to perform the macro expansion.
654+
///
655+
/// - Returns: Delegated extension expansion from `PluginCore.ConformDecodable`.
656+
public static func expansion(
657+
of node: AttributeSyntax,
658+
attachedTo declaration: some DeclGroupSyntax,
659+
providingExtensionsOf type: some TypeSyntaxProtocol,
660+
conformingTo protocols: [TypeSyntax],
661+
in context: some MacroExpansionContext
662+
) throws -> [ExtensionDeclSyntax] {
663+
return try PluginCore.ConformDecodable.expansion(
664+
of: node, attachedTo: declaration,
665+
providingExtensionsOf: type, conformingTo: protocols,
666+
in: context
667+
)
668+
}
669+
}
670+
671+
/// A declaration macro that generates `Encodable`
672+
/// protocol conformance.
673+
///
674+
/// This implementation will delegate to the plugin core
675+
/// implementation depending on the type of attached declaration:
676+
/// * `struct`/`class`/`enum`/`actor` types: Expansion of `Encodable`
677+
/// protocol conformance members.
678+
public struct ConformEncodable: MemberMacro, ExtensionMacro {
679+
/// Expand to produce members for `Encodable`.
680+
///
681+
/// Membership macro expansion for `ConformEncodable` macro
682+
/// will delegate to `PluginCore.ConformEncodable`.
683+
///
684+
/// - Parameters:
685+
/// - node: The custom attribute describing this attached macro.
686+
/// - declaration: The declaration this macro attribute is attached to.
687+
/// - context: The context in which to perform the macro expansion.
688+
///
689+
/// - Returns: Delegated member expansion from `PluginCore.ConformEncodable`.
690+
public static func expansion(
691+
of node: AttributeSyntax,
692+
providingMembersOf declaration: some DeclGroupSyntax,
693+
in context: some MacroExpansionContext
694+
) throws -> [DeclSyntax] {
695+
return try PluginCore.ConformEncodable.expansion(
696+
of: node, providingMembersOf: declaration, in: context
697+
)
698+
}
699+
700+
/// Expand to produce extensions for `Encodable`.
701+
///
702+
/// Extension macro expansion for `ConformEncodable` macro
703+
/// will delegate to `PluginCore.ConformEncodable`.
704+
///
705+
/// - Parameters:
706+
/// - node: The custom attribute describing this attached macro.
707+
/// - declaration: The declaration this macro attribute is attached to.
708+
/// - type: The type to provide extensions of.
709+
/// - protocols: The list of protocols to add conformances to. These will
710+
/// always be protocols that `type` does not already state a conformance
711+
/// to.
712+
/// - context: The context in which to perform the macro expansion.
713+
///
714+
/// - Returns: Delegated extension expansion from `PluginCore.ConformEncodable`.
715+
public static func expansion(
716+
of node: AttributeSyntax,
717+
attachedTo declaration: some DeclGroupSyntax,
718+
providingExtensionsOf type: some TypeSyntaxProtocol,
719+
conformingTo protocols: [TypeSyntax],
720+
in context: some MacroExpansionContext
721+
) throws -> [ExtensionDeclSyntax] {
722+
return try PluginCore.ConformEncodable.expansion(
723+
of: node, attachedTo: declaration,
724+
providingExtensionsOf: type, conformingTo: protocols,
725+
in: context
726+
)
727+
}
728+
}

Sources/MacroPlugin/Plugin.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ struct MetaCodablePlugin: CompilerPlugin {
2121
IgnoreDecoding.self,
2222
IgnoreEncoding.self,
2323
Codable.self,
24+
ConformDecodable.self,
25+
ConformEncodable.self,
2426
MemberInit.self,
2527
CodingKeys.self,
2628
IgnoreCodingInitialized.self,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/// Generate `Decodable` implementation of `struct`, `class`, `enum`, `actor`
2+
/// and `protocol` types by leveraging custom attributes provided on variable
3+
/// declarations. This macro is named `ConformDecodable` to avoid conflicts
4+
/// with the standard library `Decodable` protocol.
5+
///
6+
/// # Usage
7+
/// By default the field name is used as `CodingKey` for the field value during
8+
/// decoding. Following customization can be done on fields to
9+
/// provide custom decode behavior:
10+
/// * Use ``CodedAt(_:)`` providing single string value as custom coding key.
11+
/// * Use ``CodedAt(_:)`` providing multiple string value as nested coding
12+
/// key path.
13+
/// * Use ``CodedIn(_:)`` with one or more string value as nested container
14+
/// coding key path, with variable name as coding key.
15+
/// * Use ``CodedAt(_:)`` with no path arguments, when type is composition
16+
/// of multiple `Decodable` types.
17+
/// * Use ``CodedBy(_:)`` to provide custom decoding behavior for
18+
/// `Decodable` types or implement decoding for non-`Decodable` types.
19+
/// * Use ``Default(_:)`` to provide default value when decoding fails.
20+
/// * Use ``CodedAs(_:_:)`` to provide custom values for enum cases.
21+
/// * Use ``CodedAt(_:)`` to provide enum-case/protocol identifier tag path.
22+
/// * Use ``CodedAs()`` to provide enum-case/protocol identifier tag type.
23+
/// * Use ``ContentAt(_:_:)`` to provided enum-case/protocol content path.
24+
/// * Use ``IgnoreCoding()``, ``IgnoreDecoding()`` to ignore specific
25+
/// properties/cases/types from decoding.
26+
/// * Use ``CodingKeys(_:)`` to work with different case style `CodingKey`s.
27+
/// * Use ``IgnoreCodingInitialized()`` to ignore decoding
28+
/// all initialized properties/case associated variables.
29+
///
30+
/// # Effect
31+
/// This macro composes extension macro expansion for `Decodable`
32+
/// conformance of type:
33+
/// * Extension macro expansion, to confirm to `Decodable` protocol
34+
/// if the type doesn't already conform to `Decodable`.
35+
/// * Extension macro expansion, to generate custom `CodingKey` type for
36+
/// the attached declaration named `CodingKeys` and use this type for
37+
/// `Decodable` implementation of `init(from:)` method.
38+
/// * If attached declaration already conforms to `Decodable` this macro expansion
39+
/// is skipped.
40+
///
41+
/// - Parameters:
42+
/// - commonStrategies: An array of CodableCommonStrategy values specifying
43+
/// type conversion strategies to be automatically applied to all properties of the type.
44+
///
45+
/// - Important: The attached declaration must be of a `struct`, `class`, `enum`
46+
/// or `actor` type. [See the limitations for this macro](<doc:Limitations>).
47+
@attached(
48+
extension, conformances: Decodable,
49+
names: named(CodingKeys), named(DecodingKeys), named(init(from:))
50+
)
51+
@attached(
52+
member, conformances: Decodable,
53+
names: named(CodingKeys), named(init(from:))
54+
)
55+
@available(swift 5.9)
56+
public macro ConformDecodable(commonStrategies: [CodableCommonStrategy] = []) =
57+
#externalMacro(module: "MacroPlugin", type: "ConformDecodable")
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/// Generate `Encodable` implementation of `struct`, `class`, `enum`, `actor`
2+
/// and `protocol` types by leveraging custom attributes provided on variable
3+
/// declarations. This macro is named `ConformEncodable` to avoid conflicts
4+
/// with the standard library `Encodable` protocol.
5+
///
6+
/// # Usage
7+
/// By default the field name is used as `CodingKey` for the field value during
8+
/// encoding. Following customization can be done on fields to
9+
/// provide custom encode behavior:
10+
/// * Use ``CodedAt(_:)`` providing single string value as custom coding key.
11+
/// * Use ``CodedAt(_:)`` providing multiple string value as nested coding
12+
/// key path.
13+
/// * Use ``CodedIn(_:)`` with one or more string value as nested container
14+
/// coding key path, with variable name as coding key.
15+
/// * Use ``CodedAt(_:)`` with no path arguments, when type is composition
16+
/// of multiple `Encodable` types.
17+
/// * Use ``CodedAs(_:_:)`` to provide additional coding key values where
18+
/// field value can appear.
19+
/// * Use ``CodedBy(_:)`` to provide custom encoding behavior for
20+
/// `Encodable` types or implement encoding for non-`Encodable` types.
21+
/// * Use ``CodedAs(_:_:)`` to provide custom values for enum cases.
22+
/// * Use ``CodedAt(_:)`` to provide enum-case/protocol identifier tag path.
23+
/// * Use ``CodedAs()`` to provide enum-case/protocol identifier tag type.
24+
/// * Use ``ContentAt(_:_:)`` to provided enum-case/protocol content path.
25+
/// * Use ``IgnoreCoding()``, ``IgnoreEncoding()`` to ignore specific
26+
/// properties/cases/types from encoding.
27+
/// * Use ``IgnoreEncoding(if:)-1iuvv`` and ``IgnoreEncoding(if:)-7toka``
28+
/// to ignore encoding based on custom conditions.
29+
/// * Use ``CodingKeys(_:)`` to work with different case style `CodingKey`s.
30+
/// * Use ``IgnoreCodingInitialized()`` to ignore encoding
31+
/// all initialized properties/case associated variables.
32+
///
33+
/// # Effect
34+
/// This macro composes extension macro expansion for `Encodable`
35+
/// conformance of type:
36+
/// * Extension macro expansion, to confirm to `Encodable` protocol
37+
/// if the type doesn't already conform to `Encodable`.
38+
/// * Extension macro expansion, to generate custom `CodingKey` type for
39+
/// the attached declaration named `CodingKeys` and use this type for
40+
/// `Encodable` implementation of `encode(to:)` method.
41+
/// * If attached declaration already conforms to `Encodable` this macro expansion
42+
/// is skipped.
43+
///
44+
/// - Parameters:
45+
/// - commonStrategies: An array of CodableCommonStrategy values specifying
46+
/// type conversion strategies to be automatically applied to all properties of the type.
47+
///
48+
/// - Important: The attached declaration must be of a `struct`, `class`, `enum`
49+
/// or `actor` type. [See the limitations for this macro](<doc:Limitations>).
50+
@attached(
51+
extension, conformances: Encodable,
52+
names: named(CodingKeys), named(encode(to:))
53+
)
54+
@attached(
55+
member, conformances: Encodable,
56+
names: named(CodingKeys), named(encode(to:))
57+
)
58+
@available(swift 5.9)
59+
public macro ConformEncodable(commonStrategies: [CodableCommonStrategy] = []) =
60+
#externalMacro(module: "MacroPlugin", type: "ConformEncodable")

Sources/MetaCodable/MetaCodable.docc/MetaCodable.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ Supercharge `Swift`'s `Codable` implementations with macros.
7070
### Macros
7171

7272
- ``Codable(commonStrategies:)``
73+
- ``ConformDecodable(commonStrategies:)``
74+
- ``ConformEncodable(commonStrategies:)``
7375
- ``MemberInit()``
7476

7577
### Strategies

Sources/PluginCore/Attributes/Codable/Codable.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ package struct Codable: PeerAttribute {
5151
EnumDeclSyntax.self, ActorDeclSyntax.self,
5252
ProtocolDeclSyntax.self
5353
)
54+
cantBeCombined(with: ConformDecodable.self)
55+
cantBeCombined(with: ConformEncodable.self)
5456
cantDuplicate()
5557
}
5658
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import SwiftSyntax
2+
import SwiftSyntaxBuilder
3+
import SwiftSyntaxMacros
4+
5+
extension AttributeExpander {
6+
/// Generates extension declarations for `Decodable` macro.
7+
///
8+
/// From the variables registered by `Decodable` macro,
9+
/// `Decodable` protocol conformance and `CodingKey` type
10+
/// declarations are generated in separate extensions.
11+
///
12+
/// - Parameters:
13+
/// - type: The type for which extensions provided.
14+
/// - protocols: The list of `Decodable` protocols to add
15+
/// conformances to. These will always be `Decodable`.
16+
/// - context: The context in which to perform the macro expansion.
17+
///
18+
/// - Returns: The generated extension declarations.
19+
func decodableExpansion(
20+
for type: some TypeSyntaxProtocol,
21+
to protocols: [TypeSyntax],
22+
in context: some MacroExpansionContext
23+
) -> [ExtensionDeclSyntax] {
24+
let dProtocol = TypeCodingLocation.Method.decode().protocol
25+
let decodable = variable.protocol(named: dProtocol, in: protocols)
26+
27+
var extensions = [
28+
decoding(type: type, conformingTo: decodable, in: context),
29+
codingKeys(for: type, confirmingTo: protocols, in: context),
30+
].compactMap { $0 }
31+
for index in extensions.indices {
32+
// attach available attributes from original declaration
33+
// to generated expanded declaration
34+
extensions[index].attributes = AttributeListSyntax {
35+
for attr in options.availableAttributes {
36+
.attribute(attr)
37+
}
38+
extensions[index].attributes
39+
}
40+
}
41+
return extensions
42+
}
43+
}

0 commit comments

Comments
 (0)