From b0563bc0d402ca7f2ff07db9a47517ae61fb988a Mon Sep 17 00:00:00 2001 From: Soumya Ranjan Mahunt Date: Wed, 9 Oct 2024 20:17:06 +0530 Subject: [PATCH] feat: added properties dependencies support to `CodedBy` macro (#107) --- .gitignore | 1 + Examples/Podfile.lock | 164 -- Package@swift-5.swift | 2 +- README.md | 2 +- .../SequenceCoder/SequenceCoder.swift | 74 +- Sources/MetaCodable/CodedBy.swift | 218 +++ .../MetaCodable.docc/MetaCodable.md | 33 +- Sources/PluginCore/Attributes/CodedBy.swift | 26 +- .../Variables/ComposedVariable.swift | 16 + .../Enum/Case/BasicEnumCaseVariable.swift | 6 +- .../Switcher/AdjacentlyTaggableSwitcher.swift | 8 +- .../AdjacentlyTaggedEnumSwitcher.swift | 1 + .../InternallyTaggedEnumSwitcher.swift | 4 +- .../Property/AnyPropertyVariable.swift | 16 + .../Property/BasicPropertyVariable.swift | 15 + .../Property/Data/DecodingFallback.swift | 43 +- .../Property/HelperCodedVariable.swift | 158 +- .../Variables/Property/PropertyVariable.swift | 34 + .../PropertyVariableTreeNode+CodingData.swift | 141 +- ...pertyVariableTreeNode+CodingLocation.swift | 243 ++- .../Tree/PropertyVariableTreeNode.swift | 108 +- .../Type/Data/CodingKeysMap/Key.swift | 13 +- .../Variables/Type/MemberGroup.swift | 28 +- .../CodedBy/CodedByActionTests.swift | 1345 +++++++++++++++++ 24 files changed, 2298 insertions(+), 401 deletions(-) delete mode 100644 Examples/Podfile.lock create mode 100644 Tests/MetaCodableTests/CodedBy/CodedByActionTests.swift diff --git a/.gitignore b/.gitignore index 398fb98c..8360953a 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ Package.resolved # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # Pods/ +Podfile.lock # # Add this line if you want to avoid checking in source code from the Xcode workspace *.xcworkspace diff --git a/Examples/Podfile.lock b/Examples/Podfile.lock deleted file mode 100644 index c1b22c47..00000000 --- a/Examples/Podfile.lock +++ /dev/null @@ -1,164 +0,0 @@ -PODS: - - _SwiftSyntaxTestSupport (509.1.0): - - SwiftBasicFormat (= 509.1.0) - - SwiftSyntaxBuilder (= 509.1.0) - - SwiftSyntaxLib (= 509.1.0) - - MetaCodable/HelperCoders (1.3.0): - - MetaCodableHelperCoders (= 1.3.0) - - MetaCodable/HelperCodersTests (1.3.0): - - MetaCodable/HelperCoders (= 1.3.0) - - MetaCodable/Macro (= 1.3.0) - - MetaCodableMacroPlugin (= 1.3.0) - - MetaCodableMacroPluginCore (= 1.3.0) - - SwiftSyntax/MacrosTestSupport (< 601.0.0, >= 509.1.0) - - MetaCodable/Macro (1.3.0): - - MetaCodableMacro (= 1.3.0) - - MetaCodableHelperCoders (1.3.0): - - MetaCodableMacro (= 1.3.0) - - MetaCodableMacro (1.3.0) - - MetaCodableMacroPlugin (1.3.0): - - MetaCodableMacroPluginCore (= 1.3.0) - - SwiftSyntax/CompilerPlugin (< 601.0.0, >= 509.1.0) - - SwiftSyntax/Lib (< 601.0.0, >= 509.1.0) - - SwiftSyntax/Macros (< 601.0.0, >= 509.1.0) - - MetaCodableMacroPluginCore (1.3.0): - - SwiftSyntax/Builder (< 601.0.0, >= 509.1.0) - - SwiftSyntax/Diagnostics (< 601.0.0, >= 509.1.0) - - SwiftSyntax/Lib (< 601.0.0, >= 509.1.0) - - SwiftSyntax/Macros (< 601.0.0, >= 509.1.0) - - SwiftyCollections/OrderedCollections (~> 1.0.4) - - OrderedCollections (1.0.4) - - SwiftBasicFormat (509.1.0): - - SwiftSyntaxLib (= 509.1.0) - - SwiftCompilerPlugin (509.1.0): - - SwiftCompilerPluginMessageHandling (= 509.1.0) - - SwiftSyntaxMacros (= 509.1.0) - - SwiftCompilerPluginMessageHandling (509.1.0): - - SwiftDiagnostics (= 509.1.0) - - SwiftOperators (= 509.1.0) - - SwiftParser (= 509.1.0) - - SwiftSyntaxLib (= 509.1.0) - - SwiftSyntaxMacroExpansion (= 509.1.0) - - SwiftSyntaxMacros (= 509.1.0) - - SwiftDiagnostics (509.1.0): - - SwiftSyntaxLib (= 509.1.0) - - SwiftOperators (509.1.0): - - SwiftDiagnostics (= 509.1.0) - - SwiftParser (= 509.1.0) - - SwiftSyntaxLib (= 509.1.0) - - SwiftParser (509.1.0): - - SwiftSyntaxLib (= 509.1.0) - - SwiftParserDiagnostics (509.1.0): - - SwiftBasicFormat (= 509.1.0) - - SwiftDiagnostics (= 509.1.0) - - SwiftParser (= 509.1.0) - - SwiftSyntaxLib (= 509.1.0) - - SwiftSyntax/Builder (509.1.0): - - SwiftSyntaxBuilder (= 509.1.0) - - SwiftSyntax/CompilerPlugin (509.1.0): - - SwiftCompilerPlugin (= 509.1.0) - - SwiftSyntax/Diagnostics (509.1.0): - - SwiftDiagnostics (= 509.1.0) - - SwiftSyntax/Lib (509.1.0): - - SwiftSyntaxLib (= 509.1.0) - - SwiftSyntax/Macros (509.1.0): - - SwiftSyntaxMacros (= 509.1.0) - - SwiftSyntax/MacrosTestSupport (509.1.0): - - SwiftSyntaxMacrosTestSupport (= 509.1.0) - - SwiftSyntax509 (509.1.0) - - SwiftSyntaxBuilder (509.1.0): - - SwiftBasicFormat (= 509.1.0) - - SwiftDiagnostics (= 509.1.0) - - SwiftParser (= 509.1.0) - - SwiftParserDiagnostics (= 509.1.0) - - SwiftSyntaxLib (= 509.1.0) - - SwiftSyntaxLib (509.1.0): - - SwiftSyntax509 (= 509.1.0) - - SwiftSyntaxMacroExpansion (509.1.0): - - SwiftDiagnostics (= 509.1.0) - - SwiftOperators (= 509.1.0) - - SwiftSyntaxBuilder (= 509.1.0) - - SwiftSyntaxLib (= 509.1.0) - - SwiftSyntaxMacros (= 509.1.0) - - SwiftSyntaxMacros (509.1.0): - - SwiftDiagnostics (= 509.1.0) - - SwiftParser (= 509.1.0) - - SwiftSyntaxBuilder (= 509.1.0) - - SwiftSyntaxLib (= 509.1.0) - - SwiftSyntaxMacrosTestSupport (509.1.0): - - _SwiftSyntaxTestSupport (= 509.1.0) - - SwiftDiagnostics (= 509.1.0) - - SwiftParser (= 509.1.0) - - SwiftSyntaxMacroExpansion (= 509.1.0) - - SwiftSyntaxMacros (= 509.1.0) - - SwiftyCollections/OrderedCollections (1.0.4): - - OrderedCollections (= 1.0.4) - -DEPENDENCIES: - - MetaCodable/HelperCoders (from `../`) - - MetaCodable/HelperCodersTests (from `../`) - - MetaCodable/Macro (from `../`) - - MetaCodableHelperCoders (from `../`) - - MetaCodableMacro (from `../`) - - MetaCodableMacroPlugin (from `../`) - - MetaCodableMacroPluginCore (from `../`) - -SPEC REPOS: - trunk: - - _SwiftSyntaxTestSupport - - OrderedCollections - - SwiftBasicFormat - - SwiftCompilerPlugin - - SwiftCompilerPluginMessageHandling - - SwiftDiagnostics - - SwiftOperators - - SwiftParser - - SwiftParserDiagnostics - - SwiftSyntax - - SwiftSyntax509 - - SwiftSyntaxBuilder - - SwiftSyntaxLib - - SwiftSyntaxMacroExpansion - - SwiftSyntaxMacros - - SwiftSyntaxMacrosTestSupport - - SwiftyCollections - -EXTERNAL SOURCES: - MetaCodable: - :path: "../" - MetaCodableHelperCoders: - :path: "../" - MetaCodableMacro: - :path: "../" - MetaCodableMacroPlugin: - :path: "../" - MetaCodableMacroPluginCore: - :path: "../" - -SPEC CHECKSUMS: - _SwiftSyntaxTestSupport: c1e92136de032fb0c6bcd631d115e32b56c79081 - MetaCodable: 428689f5eaaf8da18a8ec5fda7420a6284fa38cc - MetaCodableHelperCoders: cfc2a67f1887711885c332ec2cf05dff48494a27 - MetaCodableMacro: 9e7a045343a3c870f6a512395a7eb502ddb0feb2 - MetaCodableMacroPlugin: a7f329a42bec526dc3214c81d1c6a2e049cd73d4 - MetaCodableMacroPluginCore: c649aa5e789114dfe8c35164ab051a12c3fda4f1 - OrderedCollections: c754ce5f9e42cf22b73afd73582317347903ab6d - SwiftBasicFormat: 7a036edb269a4c473fe7121ed342f5eac88f1c77 - SwiftCompilerPlugin: 7dba33b3ff085451ac1800372e6a6f8a2ad1e6b6 - SwiftCompilerPluginMessageHandling: 34d194fd7864154fee88225b8d29baa31317df7e - SwiftDiagnostics: 73846c99808b454ba617c56c20593884f831393a - SwiftOperators: 97eb698da3cd577359cb3284f7e454f4dac40f2d - SwiftParser: 32d2df3f08bb0d6c6341399026d9c7fba4a18c0b - SwiftParserDiagnostics: e9adca533b3e1e58f062a7f9051899a87ebc5d17 - SwiftSyntax: 1d42b704fa38597ef2b972ba8e7735a86cb56383 - SwiftSyntax509: 8f1a57a1ba4bd6634697ec194474a0a109ae9fe0 - SwiftSyntaxBuilder: deb89291a76adfc175ceeceae4d1d86bdc954dd9 - SwiftSyntaxLib: 753c51892b84be6175d99c798b94382f6ed5077a - SwiftSyntaxMacroExpansion: f63df320598947ad28a6a1da56f63db93e61c052 - SwiftSyntaxMacros: d4f82904c17d69bbafa5087a469dbdcbba190a70 - SwiftSyntaxMacrosTestSupport: b08d784da733e27bd58bb86a607d2c5860ce0c20 - SwiftyCollections: b03cc2d1eae10be4b882e0be836c3f1acb5737e2 - -PODFILE CHECKSUM: 39ca2044c061f6e71a4474ed894863c2d0da41f7 - -COCOAPODS: 1.15.2 diff --git a/Package@swift-5.swift b/Package@swift-5.swift index c6e48b45..4ece837e 100644 --- a/Package@swift-5.swift +++ b/Package@swift-5.swift @@ -19,7 +19,7 @@ let package = Package( .plugin(name: "MetaProtocolCodable", targets: ["MetaProtocolCodable"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-syntax.git", "509.1.0"..<"601.0.0"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", "509.1.0"..<"601.0.0"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2"), .package(url: "https://github.com/swiftlang/swift-format", from: "600.0.0"), diff --git a/README.md b/README.md index d7a86ddd..23341c73 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Supercharge `Swift`'s `Codable` implementations with macros. - Allows to create composition of multiple `Codable` types with ``CodedAt(_:)`` passing no arguments. - Allows to read data from additional fallback `CodingKey`s provided with ``CodedAs(_:_:)``. - Allows to provide default value in case of decoding failures with ``Default(_:)``, or only in case of failures when missing value with ``Default(ifMissing:)``. Different default values can also be used for value missing and other errors respectively with ``Default(ifMissing:forErrors:)``. -- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` etc. +- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``, ``CodedBy(_:properties:)`` or others. i.e. ``LossySequenceCoder`` etc. - Allows specifying different case values with ``CodedAs(_:_:)`` and case value/protocol type identifier type different from `String` with ``CodedAs()``. - Allows specifying enum-case/protocol type identifier path with ``CodedAt(_:)`` and case content path with ``ContentAt(_:_:)``. - Allows decoding/encoding enums that lack distinct identifiers for each case data with ``UnTagged()``. diff --git a/Sources/HelperCoders/SequenceCoder/SequenceCoder.swift b/Sources/HelperCoders/SequenceCoder/SequenceCoder.swift index 53992347..ac4ccae6 100644 --- a/Sources/HelperCoders/SequenceCoder/SequenceCoder.swift +++ b/Sources/HelperCoders/SequenceCoder/SequenceCoder.swift @@ -86,7 +86,7 @@ where ) } - /// Create a array decoder and encoder based on provided data. + /// Create an array decoder and encoder based on provided data. /// /// By default, no additional customizations configuration is used. /// @@ -163,3 +163,75 @@ where } } } + +extension SequenceCoder { + /// Create a sequence decoder and encoder based on provided data. + /// + /// - Parameters: + /// - output: The resulting sequence type. + /// - elementHelperCreation: The `HelperCoder` creation function. + /// - configuration: The configuration for decoding and encoding. + /// - properties: Values that can be passed to creation function. + public init( + output: Sequence.Type, + elementHelperCreation: (repeat each Property) -> ElementHelper, + configuration: Configuration, + properties: repeat each Property + ) { + self.init( + output: output, + elementHelper: elementHelperCreation(repeat each properties), + configuration: configuration + ) + } + + /// Create an array decoder and encoder based on provided data. + /// + /// - Parameters: + /// - elementHelperCreation: The `HelperCoder` creation function. + /// - configuration: The configuration for decoding and encoding. + /// - properties: Values that can be passed to creation function. + public init( + elementHelperCreation: (repeat each Property) -> ElementHelper, + configuration: Configuration, + properties: repeat each Property + ) where Sequence == Array { + #if swift(>=5.10) + self.init( + output: Sequence.self, elementHelperCreation: elementHelperCreation, + configuration: configuration, properties: repeat each properties + ) + #else + self.init( + output: Sequence.self, + elementHelper: elementHelperCreation(repeat each properties), + configuration: configuration + ) + #endif + } + + /// Create an array decoder and encoder based on provided data. + /// + /// By default, no additional customizations configuration is used. + /// + /// - Parameters: + /// - elementHelperCreation: The `HelperCoder` creation function. + /// - properties: Values that can be passed to creation function. + public init( + elementHelperCreation: (repeat each Property) -> ElementHelper, + properties: repeat each Property + ) where Sequence == Array { + #if swift(>=5.10) + self.init( + elementHelperCreation: elementHelperCreation, + configuration: .init(), properties: repeat each properties + ) + #else + self.init( + output: Sequence.self, + elementHelper: elementHelperCreation(repeat each properties), + configuration: .init() + ) + #endif + } +} diff --git a/Sources/MetaCodable/CodedBy.swift b/Sources/MetaCodable/CodedBy.swift index b8196f17..a3e8c4ca 100644 --- a/Sources/MetaCodable/CodedBy.swift +++ b/Sources/MetaCodable/CodedBy.swift @@ -29,3 +29,221 @@ @available(swift 5.9) public macro CodedBy(_ helper: T) = #externalMacro(module: "MacroPlugin", type: "CodedBy") + +/// Indicates the field needs to be decoded and encoded by the created `helper` +/// instance from provided arguments. +/// +/// This can be used for decoding/encoding transformation based on properties +/// as an alternative to ``CodedBy(_:)`` macro. i.e. data transformations +/// based on specific version: +/// ```swift +/// @Codable +/// struct Dog { +/// let name: String +/// @Default(1) +/// let version: Int +/// @CodedBy(Info.VersionBasedTag.init, properties: \Dog.version) +/// let info: Info +/// +/// @Codable +/// struct Info { +/// private(set) var tag: Int +/// +/// struct VersionBasedTag: HelperCoder { +/// let version: Int +/// +/// func decode(from decoder: any Decoder) throws -> Info { +/// var info = try Info(from: decoder) +/// info.tag += version >= 2 ? 1 : 0 +/// return info +/// } +/// +/// func encode(_ value: Info, to encoder: any Encoder) throws { +/// var info = value +/// info.tag -= version >= 2 ? 1 : 0 +/// try info.encode(to: encoder) +/// } +/// } +/// } +/// } +/// ``` +/// +/// - Parameters: +/// - helperCreation: The function that will create the helper expression. +/// - properties: The key path to properties passed to the creation action. +/// +/// - Note: This macro on its own only validates if attached declaration +/// is a variable declaration. ``Codable()`` macro uses this macro +/// when generating final implementations. +/// +/// - Important: The `Parent` type must be the current `struct`/`class`/`actor` +/// type. The `Helper` type's ``HelperCoder/Coded`` associated type must be +/// the same as field type. Only stored `Property` types are supported. +@attached(peer) +@available(swift 5.9) +public macro CodedBy( + _ helperCreation: (repeat each Property) -> Helper, + properties: repeat KeyPath +) = #externalMacro(module: "MacroPlugin", type: "CodedBy") + +/// Indicates the field needs to be decoded and encoded by the created `helper` +/// instance from provided arguments. +/// +/// This can be used for decoding/encoding transformation based on additional +/// arguments and properties as an alternative to ``CodedBy(_:)`` and +/// ``CodedBy(_:properties:)`` macros. i.e. passing data to a sequence +/// of child container items: +/// ```swift +/// @Codable +/// struct Item: Identifiable { +/// let title: String +/// @CodedBy( +/// SequenceCoder.init, arguments: Image.IdentifierCoder.init, +/// .lossy, properties: \Item.id +/// ) +/// let images: [Image] +/// let id: String +/// +/// @Codable +/// struct Image: Identifiable { +/// var id: String { identifier } +/// +/// @IgnoreCoding +/// private(set) var identifier: String! +/// let width: Int +/// let height: Int +/// +/// struct IdentifierCoder: HelperCoder { +/// let id: String +/// +/// func decode(from decoder: any Decoder) throws -> Image { +/// var image = try Image(from: decoder) +/// image.identifier = id +/// return image +/// } +/// +/// func encode(_ value: Image, to encoder: any Encoder) throws +/// { +/// var image = value +/// image.identifier = nil +/// try image.encode(to: encoder) +/// } +/// } +/// } +/// } +/// ``` +/// +/// - Parameters: +/// - helperCreation: The function that will create the helper expression. +/// - arguments: Additional arguments first passed to the creation action. +/// - properties: The key path to properties passed to the creation action. +/// +/// - Note: This macro on its own only validates if attached declaration +/// is a variable declaration. ``Codable()`` macro uses this macro +/// when generating final implementations. +/// +/// - Important: The `Parent` type must be the current `struct`/`class`/`actor` +/// type. The `Helper` type's ``HelperCoder/Coded`` associated type must be +/// the same as field type. Only stored `Property` types are supported. +@attached(peer) +@available(swift 5.9) +public macro CodedBy( + _ helperCreation: (repeat each Argument, repeat each Property) -> Helper, + arguments: repeat each Argument, + properties: repeat KeyPath +) = #externalMacro(module: "MacroPlugin", type: "CodedBy") + +/// Indicates the field needs to be decoded and encoded by the created `helper` +/// instance from provided arguments. +/// +/// This can be used for decoding/encoding transformation based on additional +/// arguments and properties as an alternative to ``CodedBy(_:)`` and +/// ``CodedBy(_:properties:)`` macros. +/// +/// - Parameters: +/// - helperCreation: The function that will create the helper expression. +/// - argument1: Additional argument first passed to the creation action. +/// - properties: The key path to properties passed to the creation action. +/// +/// - Note: This macro on its own only validates if attached declaration +/// is a variable declaration. ``Codable()`` macro uses this macro +/// when generating final implementations. +/// +/// - Important: The `Parent` type must be the current `struct`/`class`/`actor` +/// type. The `Helper` type's ``HelperCoder/Coded`` associated type must be +/// the same as field type. Only stored `Property` types are supported. +/// +/// - SeeAlso: ``CodedBy(_:arguments:properties:)-7j53l`` +@attached(peer) +@available(swift 5.9) +public macro CodedBy( + _ helperCreation: (Argument1, repeat each Property) -> Helper, + arguments argument1: Argument1, + properties: repeat KeyPath +) = #externalMacro(module: "MacroPlugin", type: "CodedBy") + +/// Indicates the field needs to be decoded and encoded by the created `helper` +/// instance from provided arguments. +/// +/// This can be used for decoding/encoding transformation based on additional +/// arguments and properties as an alternative to ``CodedBy(_:)`` and +/// ``CodedBy(_:properties:)`` macros. +/// +/// - Parameters: +/// - helperCreation: The function that will create the helper expression. +/// - argument1: Additional argument first passed to the creation action. +/// - argument2: Additional argument passed second to the creation action. +/// - properties: The key path to properties passed to the creation action. +/// +/// - Note: This macro on its own only validates if attached declaration +/// is a variable declaration. ``Codable()`` macro uses this macro +/// when generating final implementations. +/// +/// - Important: The `Parent` type must be the current `struct`/`class`/`actor` +/// type. The `Helper` type's ``HelperCoder/Coded`` associated type must be +/// the same as field type. Only stored `Property` types are supported. +/// +/// - SeeAlso: ``CodedBy(_:arguments:properties:)-7j53l`` +@attached(peer) +@available(swift 5.9) +public macro CodedBy< + Parent, Helper: HelperCoder, Argument1, Argument2, each Property +>( + _ helperCreation: (Argument1, Argument2, repeat each Property) -> Helper, + arguments argument1: Argument1, _ argument2: Argument2, + properties: repeat KeyPath +) = #externalMacro(module: "MacroPlugin", type: "CodedBy") + +/// Indicates the field needs to be decoded and encoded by the created `helper` +/// instance from provided arguments. +/// +/// This can be used for decoding/encoding transformation based on additional +/// arguments and properties as an alternative to ``CodedBy(_:)`` and +/// ``CodedBy(_:properties:)`` macros. +/// +/// - Parameters: +/// - helperCreation: The function that will create the helper expression. +/// - argument1: Additional argument first passed to the creation action. +/// - argument2: Additional argument passed second to the creation action. +/// - argument3: Additional argument passed third to the creation action. +/// - properties: The key path to properties passed to the creation action. +/// +/// - Note: This macro on its own only validates if attached declaration +/// is a variable declaration. ``Codable()`` macro uses this macro +/// when generating final implementations. +/// +/// - Important: The `Parent` type must be the current `struct`/`class`/`actor` +/// type. The `Helper` type's ``HelperCoder/Coded`` associated type must be +/// the same as field type. Only stored `Property` types are supported. +/// +/// - SeeAlso: ``CodedBy(_:arguments:properties:)-7j53l`` +@attached(peer) +@available(swift 5.9) +public macro CodedBy< + Parent, Helper: HelperCoder, Argument1, Argument2, Argument3, each Property +>( + _ helperCreation: (Argument1, Argument2, Argument3, repeat each Property) -> + Helper, + arguments argument1: Argument1, _ argument2: Argument2, + _ argument3: Argument3, properties: repeat KeyPath +) = #externalMacro(module: "MacroPlugin", type: "CodedBy") diff --git a/Sources/MetaCodable/MetaCodable.docc/MetaCodable.md b/Sources/MetaCodable/MetaCodable.docc/MetaCodable.md index 19c90003..31a400a8 100644 --- a/Sources/MetaCodable/MetaCodable.docc/MetaCodable.md +++ b/Sources/MetaCodable/MetaCodable.docc/MetaCodable.md @@ -18,7 +18,7 @@ Supercharge `Swift`'s `Codable` implementations with macros. - Allows to create composition of multiple `Codable` types with ``CodedAt(_:)`` passing no arguments. - Allows to read data from additional fallback `CodingKey`s provided with ``CodedAs(_:_:)``. - Allows to provide default value in case of decoding failures with ``Default(_:)``, or only in case of failures when missing value with ``Default(ifMissing:)``. Different default values can also be used for value missing and other errors respectively with ``Default(ifMissing:forErrors:)``. -- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` etc. +- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``, ``CodedBy(_:properties:)`` or others. i.e. ``LossySequenceCoder`` etc. - Allows specifying different case values with ``CodedAs(_:_:)`` and case value/protocol type identifier type different from `String` with ``CodedAs()``. - Allows specifying enum-case/protocol type identifier path with ``CodedAt(_:)`` and case content path with ``ContentAt(_:_:)``. - Allows decoding/encoding enums that lack distinct identifiers for each case data with ``UnTagged()``. @@ -75,28 +75,39 @@ Supercharge `Swift`'s `Codable` implementations with macros. - ``CodedAt(_:)`` - ``CodedIn(_:)`` -- ``Default(_:)`` -- ``Default(ifMissing:)`` -- ``Default(ifMissing:forErrors:)`` -- ``CodedBy(_:)`` - ``CodedAs()`` - ``CodedAs(_:_:)`` - ``ContentAt(_:_:)`` - ``UnTagged()`` -- ``IgnoreCoding()`` -- ``IgnoreDecoding()`` -- ``IgnoreEncoding()`` -- ``IgnoreEncoding(if:)-1iuvv`` -- ``IgnoreEncoding(if:)-7toka`` - ``CodingKeys(_:)`` -- ``IgnoreCodingInitialized()`` - ``Inherits(decodable:encodable:)`` ### Helpers +- ``CodedBy(_:)`` +- ``CodedBy(_:properties:)`` +- ``CodedBy(_:arguments:properties:)-7j53l`` +- ``CodedBy(_:arguments:properties:)-47t86`` +- ``CodedBy(_:arguments:_:properties:)`` +- ``CodedBy(_:arguments:_:_:properties:)`` - ``HelperCoder`` - ``LossySequenceCoder`` +### Fallback value + +- ``Default(_:)`` +- ``Default(ifMissing:)`` +- ``Default(ifMissing:forErrors:)`` + +### Ignoring + +- ``IgnoreCoding()`` +- ``IgnoreDecoding()`` +- ``IgnoreEncoding()`` +- ``IgnoreEncoding(if:)-1iuvv`` +- ``IgnoreEncoding(if:)-7toka`` +- ``IgnoreCodingInitialized()`` + ### Dynamic Coding - ``DynamicCodable`` diff --git a/Sources/PluginCore/Attributes/CodedBy.swift b/Sources/PluginCore/Attributes/CodedBy.swift index 1fb75989..8b82ee30 100644 --- a/Sources/PluginCore/Attributes/CodedBy.swift +++ b/Sources/PluginCore/Attributes/CodedBy.swift @@ -10,11 +10,9 @@ package struct CodedBy: PropertyAttribute { /// during initialization. let node: AttributeSyntax - /// The helper coding instance - /// expression provided. - var expr: ExprSyntax { - return node.arguments! - .as(LabeledExprListSyntax.self)!.first!.expression + /// The helper coding arguments provided. + var args: LabeledExprListSyntax { + return node.arguments!.as(LabeledExprListSyntax.self)! } /// Creates a new instance with the provided node. @@ -76,29 +74,29 @@ where /// The optional variable data with helper expression /// that output registration will have. typealias CodedByOutput = AnyPropertyVariable - /// Update registration with helper expression data. + /// Update registration with helper expressions data. /// - /// New registration is updated with helper expression data that will be + /// New registration is updated with helper expressions data that will be /// used for decoding/encoding, if provided. /// - /// - Returns: Newly built registration with helper expression data. + /// - Returns: Newly built registration with helper expressions data. func useHelperCoderIfExists() -> Registration { guard let attr = CodedBy(from: self.decl) else { return self.updating(with: self.variable.any) } - let newVar = self.variable.with(helper: attr.expr) + let newVar = self.variable.with(helper: attr.args) return self.updating(with: newVar.any) } } fileprivate extension DefaultPropertyVariable { - /// Update variable data with the helper instance expression provided. + /// Update variable data with the helper instance expressions provided. /// /// `HelperCodedVariable` is created with this variable as base - /// and helper expression provided. + /// and helper expressions provided. /// - /// - Parameter expr: The helper expression to add. + /// - Parameter args: The helper coding arguments provided. /// - Returns: Created variable data with helper expression. - func with(helper expr: ExprSyntax) -> HelperCodedVariable { - return .init(base: self, options: .init(expr: expr)) + func with(helper args: LabeledExprListSyntax) -> HelperCodedVariable { + return .init(base: self, options: .init(parsing: args)) } } diff --git a/Sources/PluginCore/Variables/ComposedVariable.swift b/Sources/PluginCore/Variables/ComposedVariable.swift index 50d6466b..a63e38b1 100644 --- a/Sources/PluginCore/Variables/ComposedVariable.swift +++ b/Sources/PluginCore/Variables/ComposedVariable.swift @@ -130,6 +130,22 @@ where Self: PropertyVariable, Wrapped: PropertyVariable { /// /// Provides fallback for the underlying variable value. var decodingFallback: DecodingFallback { base.decodingFallback } + + /// The number of variables this variable depends on. + /// + /// Provides the number of variables underlying variable depends on. + var dependenciesCount: UInt { base.dependenciesCount } + + /// Checks whether this variable is dependent on the provided variable. + /// + /// Provides whether provided variable needs to be decoded first, + /// before decoding underlying variable value. + /// + /// - Parameter variable: The variable to check for. + /// - Returns: Whether this variable is dependent on the provided variable. + func depends(on variable: Variable) -> Bool { + return base.depends(on: variable) + } } extension ComposedVariable diff --git a/Sources/PluginCore/Variables/Enum/Case/BasicEnumCaseVariable.swift b/Sources/PluginCore/Variables/Enum/Case/BasicEnumCaseVariable.swift index 0f8494d3..5a65886f 100644 --- a/Sources/PluginCore/Variables/Enum/Case/BasicEnumCaseVariable.swift +++ b/Sources/PluginCore/Variables/Enum/Case/BasicEnumCaseVariable.swift @@ -75,7 +75,7 @@ struct BasicEnumCaseVariable: EnumCaseVariable { self.decode = nil self.encode = nil self.codingKeys = decl.codingKeys - let node = switcher.node(for: decl, in: context) + var node = switcher.node(for: decl, in: context) var data = PropertyVariableTreeNode.CodingData() var variables: [any AssociatedVariable] = [] for member in decl.codableMembers() { @@ -135,7 +135,7 @@ struct BasicEnumCaseVariable: EnumCaseVariable { ) let generated = node.decoding( with: data, in: context, - from: .coder(location.coder, keyType: codingKeys.type) + from: .withCoder(location.coder, keyType: codingKeys.type) ) let newSyntax = CodeBlockItemListSyntax { for variable in variables where variable.decode ?? true { @@ -166,7 +166,7 @@ struct BasicEnumCaseVariable: EnumCaseVariable { ) -> EnumCaseGenerated { let generated = node.encoding( with: data, in: context, - to: .coder(location.coder, keyType: codingKeys.type) + to: .withCoder(location.coder, keyType: codingKeys.type) ) let pattern = IdentifierPatternSyntax(identifier: "_") let item = SwitchCaseItemSyntax(pattern: pattern) diff --git a/Sources/PluginCore/Variables/Enum/Switcher/AdjacentlyTaggableSwitcher.swift b/Sources/PluginCore/Variables/Enum/Switcher/AdjacentlyTaggableSwitcher.swift index c477e373..72abc637 100644 --- a/Sources/PluginCore/Variables/Enum/Switcher/AdjacentlyTaggableSwitcher.swift +++ b/Sources/PluginCore/Variables/Enum/Switcher/AdjacentlyTaggableSwitcher.swift @@ -51,7 +51,7 @@ protocol AdjacentlyTaggableSwitcher: EnumSwitcherVariable { /// will be decode/encoded. /// /// - Returns: Newly created variable updating registration. - func registering( + mutating func registering( variable: AdjacentlyTaggedEnumSwitcher.CoderVariable, keyPath: [CodingKeysMap.Key] ) -> Self @@ -72,7 +72,7 @@ extension InternallyTaggedEnumSwitcher: AdjacentlyTaggableSwitcher { /// will be decode/encoded. /// /// - Returns: Newly created variable updating registration. - func registering( + mutating func registering( variable: AdjacentlyTaggedEnumSwitcher.CoderVariable, keyPath: [CodingKeysMap.Key] ) -> Self { @@ -100,7 +100,7 @@ extension InternallyTaggedEnumSwitcher: AdjacentlyTaggableSwitcher { return CodeBlockItemListSyntax { "let \(identifier): \(identifierType)" node.decoding( - in: context, from: .coder(coder, keyType: location.keyType) + in: context, from: .withCoder(coder, keyType: location.keyType) ).combined() self.decodeSwitchExpression( over: "\(identifier)", at: location, from: decoder, @@ -128,7 +128,7 @@ extension InternallyTaggedEnumSwitcher: AdjacentlyTaggableSwitcher { let coder = location.coder return CodeBlockItemListSyntax { node.encoding( - in: context, to: .coder(coder, keyType: location.keyType) + in: context, to: .withCoder(coder, keyType: location.keyType) ).combined() self.encodeSwitchExpression( over: location.selfValue, at: location, from: encoder, diff --git a/Sources/PluginCore/Variables/Enum/Switcher/AdjacentlyTaggedEnumSwitcher.swift b/Sources/PluginCore/Variables/Enum/Switcher/AdjacentlyTaggedEnumSwitcher.swift index eb43eb2d..3e285f69 100644 --- a/Sources/PluginCore/Variables/Enum/Switcher/AdjacentlyTaggedEnumSwitcher.swift +++ b/Sources/PluginCore/Variables/Enum/Switcher/AdjacentlyTaggedEnumSwitcher.swift @@ -35,6 +35,7 @@ where Wrapped: AdjacentlyTaggableSwitcher { keyPath: [String], codingKeys: CodingKeysMap, context: some MacroExpansionContext ) { + var base = base let keys = codingKeys.add(keys: keyPath, context: context) self.variable = .init(decoder: contentDecoder, encoder: contentEncoder) self.base = base.registering(variable: variable, keyPath: keys) diff --git a/Sources/PluginCore/Variables/Enum/Switcher/InternallyTaggedEnumSwitcher.swift b/Sources/PluginCore/Variables/Enum/Switcher/InternallyTaggedEnumSwitcher.swift index 1511e346..8b33e13a 100644 --- a/Sources/PluginCore/Variables/Enum/Switcher/InternallyTaggedEnumSwitcher.swift +++ b/Sources/PluginCore/Variables/Enum/Switcher/InternallyTaggedEnumSwitcher.swift @@ -44,7 +44,7 @@ where Variable: PropertyVariable { /// Identifier variable is registered with the path at this node /// during initialization. This node is used to generate identifier /// variable decoding/encoding implementations. - let node: PropertyVariableTreeNode + var node: PropertyVariableTreeNode /// The builder action for building identifier variable. /// /// This builder action is used to create and use identifier variable @@ -100,7 +100,7 @@ where Variable: PropertyVariable { self.identifierType = identifierType self.decl = decl self.variableBuilder = variableBuilder - let node = PropertyVariableTreeNode() + var node = PropertyVariableTreeNode() let variable = BasicPropertyVariable( name: identifier, type: self.identifierType, value: nil, decodePrefix: "", encodePrefix: "", diff --git a/Sources/PluginCore/Variables/Property/AnyPropertyVariable.swift b/Sources/PluginCore/Variables/Property/AnyPropertyVariable.swift index 4fa850e3..991bd356 100644 --- a/Sources/PluginCore/Variables/Property/AnyPropertyVariable.swift +++ b/Sources/PluginCore/Variables/Property/AnyPropertyVariable.swift @@ -147,6 +147,22 @@ where Initialization: VariableInitialization { ) -> CodeBlockItemListSyntax { return base.encoding(in: context, to: location) } + + /// The number of variables this variable depends on. + /// + /// Provides the number of variables underlying variable depends on. + var dependenciesCount: UInt { base.dependenciesCount } + + /// Checks whether this variable is dependent on the provided variable. + /// + /// Provides whether provided variable needs to be decoded first, + /// before decoding underlying variable value. + /// + /// - Parameter variable: The variable to check for. + /// - Returns: Whether this variable is dependent on the provided variable. + func depends(on variable: Variable) -> Bool { + return base.depends(on: variable) + } } extension AnyPropertyVariable: AssociatedVariable diff --git a/Sources/PluginCore/Variables/Property/BasicPropertyVariable.swift b/Sources/PluginCore/Variables/Property/BasicPropertyVariable.swift index ae41f15e..7296a729 100644 --- a/Sources/PluginCore/Variables/Property/BasicPropertyVariable.swift +++ b/Sources/PluginCore/Variables/Property/BasicPropertyVariable.swift @@ -211,6 +211,21 @@ struct BasicPropertyVariable: DefaultPropertyVariable, DeclaredVariable { } } } + + /// The number of variables this variable depends on. + /// + /// Doesn't depend on any variables. + var dependenciesCount: UInt { 0 } + + /// Checks whether this variable is dependent on the provided variable. + /// + /// Doesn't depend on any provided variable. + /// + /// - Parameter variable: The variable to check for. + /// - Returns: Whether this variable is dependent on the provided variable. + func depends(on variable: Variable) -> Bool { + return false + } } extension BasicPropertyVariable: InitializableVariable { diff --git a/Sources/PluginCore/Variables/Property/Data/DecodingFallback.swift b/Sources/PluginCore/Variables/Property/Data/DecodingFallback.swift index f702e05e..64b30bcc 100644 --- a/Sources/PluginCore/Variables/Property/Data/DecodingFallback.swift +++ b/Sources/PluginCore/Variables/Property/Data/DecodingFallback.swift @@ -28,7 +28,8 @@ package enum DecodingFallback { case ifMissing(CodeBlockItemListSyntax, ifError: CodeBlockItemListSyntax) /// Represents container for decoding/encoding properties. - typealias Container = PropertyVariableTreeNode.CodingLocation.Container + typealias Container = PropertyVariableTreeNode.CodingLocation.Context + .Container /// Represents generated syntax for decoding/encoding properties. typealias Generated = PropertyVariableTreeNode.Generated @@ -36,7 +37,7 @@ package enum DecodingFallback { /// decoding location applying current fallback options. /// /// - Parameters: - /// - location: The decoding location to decode from. + /// - context: The decoding context to decode from. /// - nestedContainer: The nested container name to use if present. /// - nestedContainerHasVariables: Whether nested container has /// some variables as direct children. @@ -45,12 +46,12 @@ package enum DecodingFallback { /// /// - Returns: The generated code block. func represented( - location: PropertyVariableTreeNode.CodingLocation, + context: PropertyVariableTreeNode.CodingLocation.Context, nestedContainer: TokenSyntax?, nestedContainerHasVariables: Bool, nestedDecoding decoding: (Container) -> Generated ) -> Generated { - return switch location { + return switch context { case .coder(let coder, let kType): represented( decoder: coder, keyType: kType, @@ -112,7 +113,14 @@ package enum DecodingFallback { generated.containerSyntax } - let cBinding = nestedContainerHasVariables ? container.name : "_" + let ifSyntax = generated.conditionalSyntax + let cBinding = + if nestedContainerHasVariables && !ifSyntax.isEmpty { + container.name + } else { + "_" as TokenSyntax + } + let conditionalSyntax = CodeBlockItemListSyntax { if isOptional { try! IfExprSyntax( @@ -120,12 +128,12 @@ package enum DecodingFallback { if let \(cBinding) = \(container.name) """ ) { - generated.conditionalSyntax + ifSyntax } else: { fallbacks } } else { - generated.conditionalSyntax + ifSyntax } } return .init( @@ -156,7 +164,6 @@ package enum DecodingFallback { nestedDecoding decoding: (Container) -> Generated ) -> Generated { let nContainer = nestedContainer ?? "\(key.raw)_\(container.name)" - let nContainerBinding = nestedContainerHasVariables ? nContainer : "_" let containerSyntax: CodeBlockItemListSyntax let codingSyntax: CodeBlockItemListSyntax let conditionalSyntax: CodeBlockItemListSyntax @@ -177,6 +184,14 @@ package enum DecodingFallback { conditionalSyntax = generated.conditionalSyntax case .onlyIfMissing(let fallbacks): let generated = decoding(.init(name: nContainer, isOptional: true)) + let ifSyntax = generated.conditionalSyntax + let nContainerBinding = + if nestedContainerHasVariables && !ifSyntax.isEmpty { + nContainer + } else { + "_" as TokenSyntax + } + codingSyntax = generated.codingSyntax containerSyntax = CodeBlockItemListSyntax { if nestedContainer == nil { @@ -192,13 +207,21 @@ package enum DecodingFallback { if let \(nContainerBinding) = \(nContainer) """ ) { - generated.conditionalSyntax + ifSyntax } else: { fallbacks } } case let .ifMissing(fallbacks, ifError: eFallbacks): let generated = decoding(.init(name: nContainer, isOptional: true)) + let ifSyntax = generated.conditionalSyntax + let nContainerBinding = + if nestedContainerHasVariables && !ifSyntax.isEmpty { + nContainer + } else { + "_" as TokenSyntax + } + let containerMissing: TokenSyntax = "\(nContainer)Missing" codingSyntax = generated.codingSyntax containerSyntax = CodeBlockItemListSyntax { @@ -225,7 +248,7 @@ package enum DecodingFallback { try! IfExprSyntax( "if let \(nContainerBinding) = \(nContainer)", bodyBuilder: { - generated.conditionalSyntax + ifSyntax }, elseIf: IfExprSyntax("if \(containerMissing)") { fallbacks diff --git a/Sources/PluginCore/Variables/Property/HelperCodedVariable.swift b/Sources/PluginCore/Variables/Property/HelperCodedVariable.swift index c7c5babf..b6e72e41 100644 --- a/Sources/PluginCore/Variables/Property/HelperCodedVariable.swift +++ b/Sources/PluginCore/Variables/Property/HelperCodedVariable.swift @@ -12,12 +12,108 @@ where Wrapped: DefaultPropertyVariable { /// /// `HelperCodedVariable` uses the instance of this type, /// provided during initialization, for customizing code generation. - struct Options { + enum Options { + /// Represents a single helper expression. + /// + /// This helper expression is passed to `CodedBy` macro. + /// This expression is used for decode/encode syntax generation. + /// + /// - Parameter expr: The helper expression. + case helper(_ expr: ExprSyntax) + /// Represents an action that creates helper expression + /// accepting arguments. + /// + /// This closure expression and arguments are passed to `CodedBy` macro. + /// The expression created by passing arguments to the action is used + /// for decode/encode syntax generation. + /// + /// - Parameters: + /// - action: The action that creates the helper expression. + /// - params: The arguments that action takes. + case helperAction(_ action: ExprSyntax, _ params: [Parameter]) + + /// Creates new instance based on the arguments expressions provided. + /// + /// - Parameter args: The `CodedBy` macro arguments. + init(parsing args: LabeledExprListSyntax) { + guard args.count > 1 else { + self = .helper(args.first!.expression) + return + } + + var parameters: [Parameter] = [] + var parsingProperties = false + for arg in args.dropFirst() { + if arg.label?.trimmed.text == "properties" { + parsingProperties = true + } + + if parsingProperties { + let kExpr = arg.expression.as(KeyPathExprSyntax.self)! + parameters.append(.property(kExpr.components)) + } else { + parameters.append(.argument(arg.expression)) + } + } + self = .helperAction(args.first!.expression, parameters) + } + /// The helper expression used for decoding/encoding. /// - /// This expression is provided during initialization and - /// used to generate assisted decoding/encoding syntax. - let expr: ExprSyntax + /// This expression is created from initialization arguments + /// and used to generate assisted decoding/encoding syntax. + var helperExpr: ExprSyntax { + switch self { + case let .helper(expr): + return expr + case let .helperAction(action, parameters): + let args = LabeledExprListSyntax { + for param in parameters { + LabeledExprSyntax(expression: param.asArg) + } + } + let argsType = TupleTypeElementListSyntax { + for _ in parameters { + TupleTypeElementSyntax(type: "_" as TypeSyntax) + } + } + return "{ () -> (\(argsType)) -> _ in \(action) }()(\(args))" + } + } + + /// The argument type for helper expression action. + /// + /// The helper expression action only accepts arguments + /// of the variation of this type. + enum Parameter { + /// Represents any kind of argument. + /// + /// The argument expressions are passed to `CodedBy` macro, + /// before the properties key path expression. + /// + /// - Parameter expr: The argument expression. + case argument(_ expr: ExprSyntax) + /// Represents key path expression as argument. + /// + /// The argument expressions are passed to `CodedBy` macro + /// and represents key path to instance properties of current type. + /// + /// - Parameter keyPath: The key path component expression. + case property(_ keyPath: KeyPathComponentListSyntax) + + /// The argument expression passed to helper action. + /// + /// Returns the actual expression that should be passed to + /// helper action to create the helper expression syntax. + var asArg: ExprSyntax { + switch self { + case let .argument(expr): + expr + case let .property(comps): + "self\(comps)" + } + } + } } /// The value wrapped by this instance. @@ -77,14 +173,14 @@ where Wrapped: DefaultPropertyVariable { let method = passedMethod ?? defMethod return CodeBlockItemListSyntax { """ - \(decodePrefix)\(name) = try \(options.expr).\(method)(from: \(decoder)) + \(decodePrefix)\(name) = try \(options.helperExpr).\(method)(from: \(decoder)) """ } case .container(let container, let key, let passedMethod): let method = passedMethod ?? defMethod return CodeBlockItemListSyntax { """ - \(decodePrefix)\(name) = try \(options.expr).\(method)(from: \(container), forKey: \(key)) + \(decodePrefix)\(name) = try \(options.helperExpr).\(method)(from: \(container), forKey: \(key)) """ } } @@ -116,18 +212,64 @@ where Wrapped: DefaultPropertyVariable { let method = passedMethod ?? defMethod return CodeBlockItemListSyntax { """ - try \(options.expr).\(method)(\(encodePrefix)\(name), to: \(encoder)) + try \(options.helperExpr).\(method)(\(encodePrefix)\(name), to: \(encoder)) """ } case .container(let container, let key, let passedMethod): let method = passedMethod ?? defMethod return CodeBlockItemListSyntax { """ - try \(options.expr).\(method)(\(encodePrefix)\(name), to: &\(container), atKey: \(key)) + try \(options.helperExpr).\(method)(\(encodePrefix)\(name), to: &\(container), atKey: \(key)) """ } } } + + /// The number of variables this variable depends on. + /// + /// The number of instance property key path expression + /// provided to `CodedBy` macro. + var dependenciesCount: UInt { + switch options { + case .helper: + return 0 + case .helperAction(_, let params): + let count = params.count { param in + switch param { + case .property: + return true + default: + return false + } + } + return UInt(count) + } + } + + /// Checks whether this variable is dependent on the provided variable. + /// + /// Whether any of the instance property key path expression provided to + /// `CodedBy` macro matches the provided variable name. + /// + /// - Parameter variable: The variable to check for. + /// - Returns: Whether this variable is dependent on the provided variable. + func depends(on variable: Variable) -> Bool { + switch options { + case .helper: + return base.depends(on: variable) + case let .helperAction(_, parameters): + return parameters.firstIndex { parameter in + switch parameter { + case .property(let components): + let component = components.first?.component + return component?.trimmedDescription.trimmingBackTicks + == variable.name.trimmed.text.trimmingBackTicks + case .argument: + return false + } + } != nil + } + } } extension HelperCodedVariable: InitializableVariable diff --git a/Sources/PluginCore/Variables/Property/PropertyVariable.swift b/Sources/PluginCore/Variables/Property/PropertyVariable.swift index 4bf71f2b..1a75c6e5 100644 --- a/Sources/PluginCore/Variables/Property/PropertyVariable.swift +++ b/Sources/PluginCore/Variables/Property/PropertyVariable.swift @@ -62,6 +62,20 @@ where /// In the event this decoding this variable is failed, /// appropriate fallback would be applied. var decodingFallback: DecodingFallback { get } + + /// The number of variables this variable depends on. + /// + /// The number of variables that this variable depends + /// on to be decoded first, before decoding this variable. + var dependenciesCount: UInt { get } + /// Checks whether this variable is dependent on the provided variable. + /// + /// Whether provided variable needs to be decoded first, + /// before decoding this variable. + /// + /// - Parameter variable: The variable to check for. + /// - Returns: Whether this variable is dependent on the provided variable. + func depends(on variable: Variable) -> Bool } /// Represents the location for decoding/encoding for `Variable`s. @@ -211,3 +225,23 @@ extension TypeSyntax { } } } + +#if swift(<6.0) +extension Collection { + /// Returns the number of elements in the sequence that satisfy + /// the given predicate. + /// + /// This method can be used to count the number of elements + /// that pass a test. + /// + /// - Parameter predicate: A closure that takes each element + /// of the sequence as its argument and returns a Boolean + /// value indicating whether the element should be included + /// in the count. + /// - Returns: The number of elements in the sequence that satisfy + /// the given predicate. + func count(where predicate: (Element) -> Bool) -> Int { + return self.filter(predicate).count + } +} +#endif diff --git a/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode+CodingData.swift b/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode+CodingData.swift index d424f8ac..344907b8 100644 --- a/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode+CodingData.swift +++ b/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode+CodingData.swift @@ -13,72 +13,101 @@ extension PropertyVariableTreeNode { /// /// See https://en.wikipedia.org/wiki/Trie /// for more information. - struct CodingData { + struct CodingData: VariableTreeNode { /// All the variables registered at this node. - private(set) var variables: [any PropertyVariable] + var variables: [any PropertyVariable] = [] /// Nested registration node associated with keys. - private(set) var children: OrderedDictionary - - /// List of all the linked variables registered. + var children = OrderedDictionary() + /// Whether the encoding container variable linked to this node + /// should be declared as immutable. /// - /// Gets all variables at current node - /// and children nodes. - var linkedVariables: [any PropertyVariable] { - return variables + children.flatMap { $1.linkedVariables } - } + /// This is used to suppress mutability warning in case of + /// internally tagged enums. + var immutableEncodeContainer: Bool = false + } +} - /// Creates a new node with provided variables and and linked nodes. - /// - /// - Parameters: - /// - variables: The list of variables. - /// - children: The list of linked nodes associated with keys. - /// - /// - Returns: The newly created node instance. - init( - variables: [any PropertyVariable] = [], - children: OrderedDictionary = [:] - ) { - self.variables = variables - self.children = children - } +protocol VariableTreeNode { + /// All the variables registered at this node. + var variables: [any PropertyVariable] { get set } + /// Nested registration node associated with keys. + var children: OrderedDictionary { get set } + /// Whether the encoding container variable linked to this node + /// should be declared as immutable. + /// + /// This is used to suppress mutability warning in case of + /// internally tagged enums. + var immutableEncodeContainer: Bool { get set } + /// Creates a new tree node instance. + /// + /// Creates new node with empty children and data. + init() +} - /// Register variable for the provided `CodingKey` path. - /// - /// Create node at the `CodingKey` path if doesn't exist - /// and register the variable at the 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. - mutating func register( - variable: any PropertyVariable, - keyPath: [CodingKeysMap.Key] - ) { - guard !keyPath.isEmpty else { variables.append(variable); return } +extension VariableTreeNode { + /// List of all the linked variables registered. + /// + /// Gets all variables at current node + /// and children nodes. + var linkedVariables: [any PropertyVariable] { + return variables + children.flatMap { $1.linkedVariables } + } - let key = keyPath.first! - if children[key] == nil { - children[key] = .init(variables: [], children: [:]) + /// Register variable for the provided `CodingKey` path. + /// + /// Create node at the `CodingKey` path if doesn't exist + /// and register the variable at the 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. + /// - immutableEncodeContainer: Whether the encoding container variable + /// direct parent of `variable` should be declared as immutable. + mutating func register( + variable: Variable, keyPath: [CodingKeysMap.Key], + immutableEncodeContainer: Bool = false + ) { + if keyPath.isEmpty { + let depIndex = variables.firstIndex { $0.depends(on: variable) } + if let index = depIndex { + variables.insert(variable, at: index) + } else { + variables.append(variable) } + return + } - children[key]!.register( - variable: variable, - keyPath: Array(keyPath.dropFirst()) - ) + let key = keyPath.first! + if children[key] == nil { + children[key] = .init() } - /// Checks whether node has variables registered at the key provided. - /// - /// Checks if there is node available at the specified key and it has - /// some variables or nested variables registered. - /// - /// - Parameter key: The key to search for. - /// - Returns: Whether this node has children variables at the key. - func hasKey(_ key: CodingKeysMap.Key) -> Bool { - guard let child = children[key] else { return false } - return !child.variables.isEmpty || !child.children.isEmpty + if keyPath.count == 1 { + precondition( + !immutableEncodeContainer + || linkedVariables.filter { $0.encode ?? true }.isEmpty + ) + self.immutableEncodeContainer = immutableEncodeContainer } + + children[key]!.register( + variable: variable, + keyPath: Array(keyPath.dropFirst()), + immutableEncodeContainer: immutableEncodeContainer + ) + } + + /// Checks whether node has variables registered at the key provided. + /// + /// Checks if there is node available at the specified key and it has + /// some variables or nested variables registered. + /// + /// - Parameter key: The key to search for. + /// - Returns: Whether this node has children variables at the key. + func hasKey(_ key: CodingKeysMap.Key) -> Bool { + guard let child = children[key] else { return false } + return !child.variables.isEmpty || !child.children.isEmpty } } diff --git a/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode+CodingLocation.swift b/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode+CodingLocation.swift index ed1f441e..90218be3 100644 --- a/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode+CodingLocation.swift +++ b/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode+CodingLocation.swift @@ -1,4 +1,5 @@ import SwiftSyntax +import SwiftSyntaxMacros extension PropertyVariableTreeNode { /// Represents the location for decoding/encoding that the node needs @@ -6,55 +7,225 @@ extension PropertyVariableTreeNode { /// /// Represents whether node needs to decode/encode directly /// from/to the decoder/encoder respectively or at path of a container - enum CodingLocation { - /// Represents the container for decoding/encoding. - struct Container { - /// The variable name of the container. - /// - /// This name is used for decoding/encoding syntax generation. - let name: TokenSyntax - /// Whether container is of optional type. - /// - /// Can be used to check whether container needs to be - /// unwrapped first to proceed with decoding/encoding. - let isOptional: Bool + struct CodingLocation: Variable { + /// The context representing the nesting level. + let context: Context + /// The decoding/encoding progress. + /// + /// All the decoded/encoded variables and + /// skipped variables are tracked in this instance. + let progress: Progress - var syntax: TokenSyntax { - let oToken: TokenSyntax = isOptional ? "?" : "" - return "\(name)\(oToken)" - } + /// Creates new instance with provided data. + /// + /// - Parameters: + /// - context: The context representing the nesting level. + /// - progress: The decoding/encoding progress. + private init(context: Context, progress: Progress) { + self.context = context + self.progress = progress } - /// Represents a top-level decoding/encoding location. + /// Add provided variable to list of already decoded/encoded variable. /// - /// The node needs to perform decoding/encoding directly - /// to the decoder/encoder provided, not nested at a `CodingKey`. + /// - Parameter variable: The variable to add. + func coded(_ variable: any PropertyVariable) { + progress.coded.append(variable) + } + + /// Added provided variable to list of pending variables. + /// + /// Stores pending variable with current context level + /// in the progress instance. + /// + /// - Parameter variable: The variable to add. + func pending(_ variable: any PropertyVariable) { + progress.pending.append((context, variable)) + } + + /// Creates a new location with provided context. + /// + /// Creates new location with the provided context as top-level context. /// /// - Parameters: /// - coder: The decoder/encoder for decoding/encoding. /// - keyType: The `CodingKey` type. - case coder(_ coder: TokenSyntax, keyType: ExprSyntax) - /// Represents decoding/encoding location at a `CodingKey` - /// for a container. /// - /// The node needs to perform decoding/encoding at the - /// `CodingKey` inside the container provided. + /// - Returns: The new location updated with provided context. + static func withCoder( + _ coder: TokenSyntax, keyType: ExprSyntax + ) -> Self { + return .init( + context: .coder(coder, keyType: keyType), progress: .init() + ) + } + + /// Creates a new location with provided context. /// /// - Parameters: /// - container: The container for decoding/encoding. /// - key: The `CodingKey` inside the container. - case container(_ container: Container, key: CodingKeysMap.Key) - - /// The decoding/encoding location for individual variables. - /// - /// Maps current decoding/encoding location to individual - /// variable decoding/encoding locations. - var forVariable: PropertyCodingLocation { - switch self { - case .coder(let coder, keyType: _): - return .coder(coder, method: nil) - case .container(let container, key: let key): - return .container(container.name, key: key.expr, method: nil) + /// + /// - Returns: The new location updated with provided context. + func withContainer( + _ container: Context.Container, key: CodingKeysMap.Key + ) -> Self { + return .init( + context: .container(container, key: key), progress: progress + ) + } + + /// Provides the syntax for decoding at the provided location. + /// + /// Provides syntax for remaining pending variables that + /// weren't decoded due to dependencies. + /// + /// - 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: () = () + ) -> CodeBlockItemListSyntax { + let pending = progress.pending.sorted { pending1, pending2 in + return pending2.variable.depends(on: pending1.variable) + } + return CodeBlockItemListSyntax { + for (lContext, variable) in pending { + let location = lContext.forVariable + let syntax = variable.decoding(in: context, from: location) + switch lContext { + case let .container(container, key: _) + where container.isOptional: + switch variable.decodingFallback { + case .onlyIfMissing(let fallbacks): + try! IfExprSyntax( + """ + if let \(container.name) = \(container.name) + """ + ) { + syntax + } else: { + fallbacks + } + case let .ifMissing(fallbacks, ifError: eFallbacks): + try! IfExprSyntax( + "if let \(container.name) = \(container.name)", + bodyBuilder: { + syntax + }, + elseIf: IfExprSyntax( + "if \(container.name)Missing" + ) { + fallbacks + } else: { + eFallbacks + } + ) + case .throw: + syntax + } + default: + syntax + } + } + } + } + + /// Provides the syntax for encoding at the provided location. + /// + /// Doesn't provide any syntax for 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: () = () + ) -> CodeBlockItemListSyntax { + return "" + } + + /// The decoding/encoding progress. + /// + /// Stores the variables that are decoded already + /// and pending to be decoded due to dependency. + final class Progress { + /// The pending variable and path pair type. + typealias Pending = (Context, variable: any PropertyVariable) + /// The variables that are already decoded. + fileprivate(set) var coded: [any PropertyVariable] = [] + /// The variables and their path that are skipped + /// decoding due to dependency. + /// + /// The decoding syntax for these variables can be created + /// from the location instance. + fileprivate(set) var pending: [Pending] = [] + } + + /// Represents the context for decoding/encoding that the node needs + /// to perform. + /// + /// Represents whether node needs to decode/encode directly + /// from/to the decoder/encoder respectively or at path of a container + enum Context { + /// Represents a top-level decoding/encoding location. + /// + /// The node needs to perform decoding/encoding directly + /// to the decoder/encoder provided, not nested at a `CodingKey`. + /// + /// - Parameters: + /// - coder: The decoder/encoder for decoding/encoding. + /// - keyType: The `CodingKey` type. + case coder(_ coder: TokenSyntax, keyType: ExprSyntax) + /// Represents decoding/encoding location at a `CodingKey` + /// for a container. + /// + /// The node needs to perform decoding/encoding at the + /// `CodingKey` inside the container provided. + /// + /// - Parameters: + /// - container: The container for decoding/encoding. + /// - key: The `CodingKey` inside the container. + case container(_ container: Container, key: CodingKeysMap.Key) + + /// The decoding/encoding location for individual variables. + /// + /// Maps current decoding/encoding location to individual + /// variable decoding/encoding locations. + var forVariable: PropertyCodingLocation { + switch self { + case .coder(let coder, keyType: _): + return .coder(coder, method: nil) + case .container(let container, let key): + return .container( + container.name, key: key.expr, method: nil + ) + } + } + + /// Represents the container for decoding/encoding. + struct Container { + /// The variable name of the container. + /// + /// This name is used for decoding/encoding syntax generation. + let name: TokenSyntax + /// Whether container is of optional type. + /// + /// Can be used to check whether container needs to be + /// unwrapped first to proceed with decoding/encoding. + let isOptional: Bool + + /// The syntax to use for this container. + /// + /// Adds `?` mark based on whether + /// container variable is optional or not. + var syntax: TokenSyntax { + let oToken: TokenSyntax = isOptional ? "?" : "" + return "\(name)\(oToken)" + } } } } diff --git a/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode.swift b/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode.swift index f4b02069..f22f2248 100644 --- a/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode.swift +++ b/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode.swift @@ -12,12 +12,12 @@ import SwiftSyntaxMacros /// /// See https://en.wikipedia.org/wiki/Trie /// for more information. -final class PropertyVariableTreeNode: Variable { +final class PropertyVariableTreeNode: Variable, VariableTreeNode { /// All the variables registered at this node. - private(set) var variables: [any PropertyVariable] + var variables: [any PropertyVariable] = [] /// Nested registration node associated with keys. - private(set) var children: - OrderedDictionary + var children: + OrderedDictionary = [:] /// The container for decoding variables linked to this node. /// @@ -29,71 +29,7 @@ final class PropertyVariableTreeNode: Variable { /// /// This is used to suppress mutability warning in case of /// internally tagged enums. - private var immutableEncodeContainer: Bool = false - - /// List of all the linked variables registered. - /// - /// Gets all variables at current node - /// and children nodes. - var linkedVariables: [any PropertyVariable] { - return variables + children.flatMap { $1.linkedVariables } - } - - /// Creates a new node with provided variables and and linked nodes. - /// - /// - Parameters: - /// - variables: The list of variables. - /// - children: The list of linked nodes associated with keys. - /// - /// - Returns: The newly created node instance. - init( - variables: [any PropertyVariable] = [], - children: OrderedDictionary< - CodingKeysMap.Key, PropertyVariableTreeNode - > = [:] - ) { - self.variables = variables - self.children = children - } - - /// Register variable for the provided `CodingKey` path. - /// - /// Create node at the `CodingKey` path if doesn't exist - /// and register the variable at the 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. - /// - immutableEncodeContainer: Whether the encoding container variable - /// direct parent of `variable` should be declared as immutable. - func register( - variable: any PropertyVariable, - keyPath: [CodingKeysMap.Key], - immutableEncodeContainer: Bool = false - ) { - guard !keyPath.isEmpty else { variables.append(variable); return } - - let key = keyPath.first! - if children[key] == nil { - children[key] = .init(variables: [], children: [:]) - } - - if keyPath.count == 1 { - precondition( - !immutableEncodeContainer - || linkedVariables.filter { $0.encode ?? true }.isEmpty - ) - self.immutableEncodeContainer = immutableEncodeContainer - } - - children[key]!.register( - variable: variable, - keyPath: Array(keyPath.dropFirst()), - immutableEncodeContainer: immutableEncodeContainer - ) - } + var immutableEncodeContainer: Bool = false } // MARK: Decoding @@ -112,12 +48,19 @@ extension PropertyVariableTreeNode { in context: some MacroExpansionContext, from location: CodingLocation ) -> Generated { + let varLocation = location.context.forVariable let decodingSyntax = CodeBlockItemListSyntax { for variable in data?.variables ?? variables where variable.decode ?? true { - variable.decoding(in: context, from: location.forVariable) + if variable.dependenciesCount == 0 { + variable.decoding(in: context, from: varLocation) + let _ = location.coded(variable) + } else { + let _ = location.pending(variable) + } } } + let childrenDecodable = data?.children.contains { _, node in node.linkedVariables.contains { $0.decode ?? true } @@ -137,10 +80,19 @@ extension PropertyVariableTreeNode { .filter { data?.hasKey($0.key) ?? true } return decodableChildren.lazy .flatMap(\.value.linkedVariables) - .map(\.decodingFallback) + .map { variable in + switch variable.decodingFallback { + case .ifMissing where variable.dependenciesCount > 0: + return .ifMissing("", ifError: "") + case .onlyIfMissing where variable.dependenciesCount > 0: + return .onlyIfMissing("") + default: + return variable.decodingFallback + } + } .reduce(.ifMissing([], ifError: []), +) .represented( - location: location, nestedContainer: self.decodingContainer, + context: location.context, nestedContainer: decodingContainer, nestedContainerHasVariables: !self.children.lazy .flatMap(\.value.variables) .filter { $0.decode ?? true }.isEmpty @@ -150,7 +102,7 @@ extension PropertyVariableTreeNode { return node.decoding( with: data?.children[cKey], in: context, - from: .container(container, key: cKey) + from: location.withContainer(container, key: cKey) ) }.reduce( .init( @@ -203,11 +155,13 @@ extension PropertyVariableTreeNode { in context: some MacroExpansionContext, to location: CodingLocation ) -> Generated { + let varLocation = location.context.forVariable let specifier: TokenSyntax = immutableEncodeContainer ? "let" : "var" let syntax = CodeBlockItemListSyntax { for variable in data?.variables ?? variables where variable.encode ?? true { - variable.encoding(in: context, to: location.forVariable) + variable.encoding(in: context, to: varLocation) + let _ = location.coded(variable) } let childrenEncodable = @@ -219,7 +173,7 @@ extension PropertyVariableTreeNode { } if !(data?.children.isEmpty ?? children.isEmpty), childrenEncodable { - switch location { + switch location.context { case .coder(let encoder, let type): let container: TokenSyntax = "container" """ @@ -230,7 +184,7 @@ extension PropertyVariableTreeNode { node.encoding( with: data?.children[cKey], in: context, - to: .container( + to: location.withContainer( .init(name: container, isOptional: false), key: cKey ) @@ -247,7 +201,7 @@ extension PropertyVariableTreeNode { node.encoding( with: data?.children[cKey], in: context, - to: .container( + to: location.withContainer( .init(name: nestedContainer, isOptional: false), key: cKey ) diff --git a/Sources/PluginCore/Variables/Type/Data/CodingKeysMap/Key.swift b/Sources/PluginCore/Variables/Type/Data/CodingKeysMap/Key.swift index 0d2f3f12..1ae6e7cd 100644 --- a/Sources/PluginCore/Variables/Type/Data/CodingKeysMap/Key.swift +++ b/Sources/PluginCore/Variables/Type/Data/CodingKeysMap/Key.swift @@ -74,9 +74,18 @@ extension CodingKeysMap { /// - Parameter token: The input token to create from. /// - Returns: The created trimmed token. static func name(for token: TokenSyntax) -> TokenSyntax { - let trimmedChars = CharacterSet(arrayLiteral: "`") - let name = token.text.trimmingCharacters(in: trimmedChars) + let name = token.trimmed.text.trimmingBackTicks return .identifier(name) } } } + +extension String { + /// Trim ` characters. + /// + /// Uses for getting actual variable name. + var trimmingBackTicks: Self { + let trimmedChars = CharacterSet(arrayLiteral: "`") + return self.trimmingCharacters(in: trimmedChars) + } +} diff --git a/Sources/PluginCore/Variables/Type/MemberGroup.swift b/Sources/PluginCore/Variables/Type/MemberGroup.swift index 2b26eac3..9fe7a3e7 100644 --- a/Sources/PluginCore/Variables/Type/MemberGroup.swift +++ b/Sources/PluginCore/Variables/Type/MemberGroup.swift @@ -41,7 +41,7 @@ where ) -> PathRegistration ) { self.constraintGenerator = .init(decl: decl) - let node = PropertyVariableTreeNode() + var node = PropertyVariableTreeNode() for member in decl.codableMembers(input: memberInput) { let `var` = member.codableVariable(in: context) let key = [CodingKeysMap.Key.name(for: `var`.name).text] @@ -75,12 +75,15 @@ where from location: TypeCodingLocation ) -> TypeGenerated? { guard let conformance = location.conformance else { return nil } + let syntax = CodeBlockItemListSyntax { + let nLocation = PropertyVariableTreeNode.CodingLocation.withCoder( + location.method.arg, keyType: codingKeys.type + ) + node.decoding(in: context, from: nLocation).combined() + nLocation.decoding(in: context) + } return .init( - code: node.decoding( - in: context, - from: .coder(location.method.arg, keyType: codingKeys.type) - ).combined(), - modifiers: [], + code: syntax, modifiers: [], whereClause: constraintGenerator.decodingClause( withVariables: node.linkedVariables, conformingTo: conformance @@ -104,12 +107,15 @@ where to location: TypeCodingLocation ) -> TypeGenerated? { guard let conformance = location.conformance else { return nil } + let syntax = CodeBlockItemListSyntax { + let nLocation = PropertyVariableTreeNode.CodingLocation.withCoder( + location.method.arg, keyType: codingKeys.type + ) + node.encoding(in: context, to: nLocation).combined() + nLocation.encoding(in: context) + } return .init( - code: node.encoding( - in: context, - to: .coder(location.method.arg, keyType: codingKeys.type) - ).combined(), - modifiers: [], + code: syntax, modifiers: [], whereClause: constraintGenerator.encodingClause( withVariables: node.linkedVariables, conformingTo: conformance diff --git a/Tests/MetaCodableTests/CodedBy/CodedByActionTests.swift b/Tests/MetaCodableTests/CodedBy/CodedByActionTests.swift new file mode 100644 index 00000000..edcb3844 --- /dev/null +++ b/Tests/MetaCodableTests/CodedBy/CodedByActionTests.swift @@ -0,0 +1,1345 @@ +import Foundation +import HelperCoders +import MetaCodable +import Testing + +@testable import PluginCore + +struct CodedByActionTests { + // https://forums.swift.org/t/codable-passing-data-to-child-decoder/12757 + struct DependencyBefore { + @Codable + struct Dog { + let name: String + @Default(1) + let version: Int + @CodedBy(Info.VersionBasedTag.init, properties: \Dog.version) + let info: Info + + @Codable + struct Info { + private(set) var tag: Int + + struct VersionBasedTag: HelperCoder { + let version: Int + + func decode(from decoder: any Decoder) throws -> Info { + var info = try Info(from: decoder) + if version >= 2 { + info.tag += 1 + } + return info + } + + func encode(_ value: Info, to encoder: any Encoder) throws { + var info = value + if version >= 2 { + info.tag -= 1 + } + try info.encode(to: encoder) + } + } + } + } + + @Test + func expansion() { + assertMacroExpansion( + """ + @Codable + struct Dog { + let name: String + @Default(1) + let version: Int + @CodedBy(Info.VersionBasedTag.init, properties: \\Dog.version) + let info: Info + + @Codable + struct Info { + private(set) var tag: Int + + struct VersionBasedTag: HelperCoder { + let version: Int + + func decode(from decoder: any Decoder) throws -> Info { + var info = try Info(from: decoder) + if version >= 2 { + info.tag += 1 + } + return info + } + + func encode(_ value: Info, to encoder: any Encoder) throws { + var info = value + if version >= 2 { + info.tag -= 1 + } + try info.encode(to: encoder) + } + } + } + } + """, + expandedSource: + """ + struct Dog { + let name: String + let version: Int + let info: Info + struct Info { + private(set) var tag: Int + + struct VersionBasedTag: HelperCoder { + let version: Int + + func decode(from decoder: any Decoder) throws -> Info { + var info = try Info(from: decoder) + if version >= 2 { + info.tag += 1 + } + return info + } + + func encode(_ value: Info, to encoder: any Encoder) throws { + var info = value + if version >= 2 { + info.tag -= 1 + } + try info.encode(to: encoder) + } + } + } + } + + extension Dog.Info: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.tag = try container.decode(Int.self, forKey: CodingKeys.tag) + } + } + + extension Dog.Info: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.tag, forKey: CodingKeys.tag) + } + } + + extension Dog.Info { + enum CodingKeys: String, CodingKey { + case tag = "tag" + } + } + + extension Dog: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: CodingKeys.name) + do { + self.version = try container.decodeIfPresent(Int.self, forKey: CodingKeys.version) ?? 1 + } catch { + self.version = 1 + } + self.info = try { () -> (_) -> _ in + Info.VersionBasedTag.init + }()(self.version).decode(from: container, forKey: CodingKeys.info) + } + } + + extension Dog: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.name, forKey: CodingKeys.name) + try container.encode(self.version, forKey: CodingKeys.version) + try { () -> (_) -> _ in + Info.VersionBasedTag.init + }()(self.version).encode(self.info, to: &container, atKey: CodingKeys.info) + } + } + + extension Dog { + enum CodingKeys: String, CodingKey { + case name = "name" + case version = "version" + case info = "info" + } + } + """ + ) + } + } + + // https://forums.swift.org/t/codable-passing-data-to-child-decoder/12757 + struct DependencyAfter { + @Codable + struct Dog { + let name: String + @CodedBy(Info.VersionBasedTag.init, properties: \Dog.version) + let info: Info + @Default(1) + let version: Int + + @Codable + struct Info { + private(set) var tag: Int + + struct VersionBasedTag: HelperCoder { + let version: Int + + func decode(from decoder: any Decoder) throws -> Info { + var info = try Info(from: decoder) + if version >= 2 { + info.tag += 1 + } + return info + } + + func encode(_ value: Info, to encoder: any Encoder) throws { + var info = value + if version >= 2 { + info.tag -= 1 + } + try info.encode(to: encoder) + } + } + } + } + + @Test + func expansion() { + assertMacroExpansion( + """ + @Codable + struct Dog { + let name: String + @CodedBy(Info.VersionBasedTag.init, properties: \\Dog.version) + let info: Info + @Default(1) + let version: Int + + @Codable + struct Info { + private(set) var tag: Int + + struct VersionBasedTag: HelperCoder { + let version: Int + + func decode(from decoder: any Decoder) throws -> Info { + var info = try Info(from: decoder) + if version >= 2 { + info.tag += 1 + } + return info + } + + func encode(_ value: Info, to encoder: any Encoder) throws { + var info = value + if version >= 2 { + info.tag -= 1 + } + try info.encode(to: encoder) + } + } + } + } + """, + expandedSource: + """ + struct Dog { + let name: String + let info: Info + let version: Int + struct Info { + private(set) var tag: Int + + struct VersionBasedTag: HelperCoder { + let version: Int + + func decode(from decoder: any Decoder) throws -> Info { + var info = try Info(from: decoder) + if version >= 2 { + info.tag += 1 + } + return info + } + + func encode(_ value: Info, to encoder: any Encoder) throws { + var info = value + if version >= 2 { + info.tag -= 1 + } + try info.encode(to: encoder) + } + } + } + } + + extension Dog.Info: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.tag = try container.decode(Int.self, forKey: CodingKeys.tag) + } + } + + extension Dog.Info: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.tag, forKey: CodingKeys.tag) + } + } + + extension Dog.Info { + enum CodingKeys: String, CodingKey { + case tag = "tag" + } + } + + extension Dog: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: CodingKeys.name) + do { + self.version = try container.decodeIfPresent(Int.self, forKey: CodingKeys.version) ?? 1 + } catch { + self.version = 1 + } + self.info = try { () -> (_) -> _ in + Info.VersionBasedTag.init + }()(self.version).decode(from: container, forKey: CodingKeys.info) + } + } + + extension Dog: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.name, forKey: CodingKeys.name) + try { () -> (_) -> _ in + Info.VersionBasedTag.init + }()(self.version).encode(self.info, to: &container, atKey: CodingKeys.info) + try container.encode(self.version, forKey: CodingKeys.version) + } + } + + extension Dog { + enum CodingKeys: String, CodingKey { + case name = "name" + case info = "info" + case version = "version" + } + } + """ + ) + } + + @Test(arguments: [nil, 1, 2, 5]) + func decoding(version: Int?) throws { + let data = try #require(dogJSON(version: version)) + let dog = try JSONDecoder().decode(Dog.self, from: data) + #expect(dog.name == "Dog") + #expect(dog.version == version ?? 1) + if dog.version >= 2 { + #expect(dog.info.tag == 13) + } else { + #expect(dog.info.tag == 12) + } + } + } + + // https://stackoverflow.com/questions/62242365/access-property-of-parent-struct-in-a-nested-codable-struct-when-decoding-the-ch + struct NestedPropertyDependencyBefore { + @Codable + struct Item: Identifiable { + let id: String + let title: String + @CodedAt("images", "original") + @CodedBy(Image.IdentifierCoder.init, properties: \Item.id) + let originalImage: Image + @CodedAt("images", "small") + @CodedBy(Image.IdentifierCoder.init, properties: \Item.id) + let smallImage: Image + + @Codable + struct Image: Identifiable { + var id: String { identifier } + + @IgnoreCoding + private(set) var identifier: String! + let width: Int + let height: Int + + struct IdentifierCoder: HelperCoder { + let id: String + + func decode(from decoder: any Decoder) throws -> Image { + var image = try Image(from: decoder) + image.identifier = id + return image + } + + func encode(_ value: Image, to encoder: any Encoder) throws + { + var image = value + image.identifier = nil + try image.encode(to: encoder) + } + } + } + } + + @Test + func expansion() { + assertMacroExpansion( + """ + @Codable + struct Item: Identifiable { + let id: String + let title: String + @CodedAt("images", "original") + @CodedBy(Image.IdentifierCoder.init, properties: \\Item.id) + let originalImage: Image + @CodedAt("images", "small") + @CodedBy(Image.IdentifierCoder.init, properties: \\Item.id) + let smallImage: Image + + @Codable + struct Image: Identifiable { + var id: String { identifier } + + @IgnoreCoding + private(set) var identifier: String! + let width: Int + let height: Int + + struct IdentifierCoder: HelperCoder { + let id: String + + func decode(from decoder: any Decoder) throws -> Image { + var image = try Image(from: decoder) + image.identifier = id + return image + } + + func encode(_ value: Image, to encoder: any Encoder) throws { + var image = value + image.identifier = nil + try image.encode(to: encoder) + } + } + } + } + """, + expandedSource: + """ + struct Item: Identifiable { + let id: String + let title: String + let originalImage: Image + let smallImage: Image + struct Image: Identifiable { + var id: String { identifier } + private(set) var identifier: String! + let width: Int + let height: Int + + struct IdentifierCoder: HelperCoder { + let id: String + + func decode(from decoder: any Decoder) throws -> Image { + var image = try Image(from: decoder) + image.identifier = id + return image + } + + func encode(_ value: Image, to encoder: any Encoder) throws { + var image = value + image.identifier = nil + try image.encode(to: encoder) + } + } + } + } + + extension Item.Image: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.width = try container.decode(Int.self, forKey: CodingKeys.width) + self.height = try container.decode(Int.self, forKey: CodingKeys.height) + } + } + + extension Item.Image: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.width, forKey: CodingKeys.width) + try container.encode(self.height, forKey: CodingKeys.height) + } + } + + extension Item.Image { + enum CodingKeys: String, CodingKey { + case width = "width" + case height = "height" + } + } + + extension Item: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let images_container = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.images) + self.id = try container.decode(String.self, forKey: CodingKeys.id) + self.title = try container.decode(String.self, forKey: CodingKeys.title) + self.originalImage = try { () -> (_) -> _ in + Image.IdentifierCoder.init + }()(self.id).decode(from: images_container, forKey: CodingKeys.originalImage) + self.smallImage = try { () -> (_) -> _ in + Image.IdentifierCoder.init + }()(self.id).decode(from: images_container, forKey: CodingKeys.smallImage) + } + } + + extension Item: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.id, forKey: CodingKeys.id) + try container.encode(self.title, forKey: CodingKeys.title) + var images_container = container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.images) + try { () -> (_) -> _ in + Image.IdentifierCoder.init + }()(self.id).encode(self.originalImage, to: &images_container, atKey: CodingKeys.originalImage) + try { () -> (_) -> _ in + Image.IdentifierCoder.init + }()(self.id).encode(self.smallImage, to: &images_container, atKey: CodingKeys.smallImage) + } + } + + extension Item { + enum CodingKeys: String, CodingKey { + case id = "id" + case title = "title" + case originalImage = "original" + case images = "images" + case smallImage = "small" + } + } + """ + ) + } + } + + // https://stackoverflow.com/questions/62242365/access-property-of-parent-struct-in-a-nested-codable-struct-when-decoding-the-ch + struct NestedPropertyDependencyAfter { + @Codable + struct Item: Identifiable { + let title: String + @CodedAt("images", "original") + @CodedBy(Image.IdentifierCoder.init, properties: \Item.id) + let originalImage: Image + @CodedAt("images", "small") + @CodedBy(Image.IdentifierCoder.init, properties: \Item.id) + let smallImage: Image + let id: String + + @Codable + struct Image: Identifiable { + var id: String { identifier } + + @IgnoreCoding + private(set) var identifier: String! + let width: Int + let height: Int + + struct IdentifierCoder: HelperCoder { + let id: String + + func decode(from decoder: any Decoder) throws -> Image { + var image = try Image(from: decoder) + image.identifier = id + return image + } + + func encode(_ value: Image, to encoder: any Encoder) throws + { + var image = value + image.identifier = nil + try image.encode(to: encoder) + } + } + } + } + + @Test + func expansion() { + assertMacroExpansion( + """ + @Codable + struct Item: Identifiable { + let title: String + @CodedAt("images", "original") + @CodedBy(Image.IdentifierCoder.init, properties: \\Item.id) + let originalImage: Image + @CodedAt("images", "small") + @CodedBy(Image.IdentifierCoder.init, properties: \\Item.id) + let smallImage: Image + let id: String + + @Codable + struct Image: Identifiable { + var id: String { identifier } + + @IgnoreCoding + private(set) var identifier: String! + let width: Int + let height: Int + + struct IdentifierCoder: HelperCoder { + let id: String + + func decode(from decoder: any Decoder) throws -> Image { + var image = try Image(from: decoder) + image.identifier = id + return image + } + + func encode(_ value: Image, to encoder: any Encoder) throws { + var image = value + image.identifier = nil + try image.encode(to: encoder) + } + } + } + } + """, + expandedSource: + """ + struct Item: Identifiable { + let title: String + let originalImage: Image + let smallImage: Image + let id: String + struct Image: Identifiable { + var id: String { identifier } + private(set) var identifier: String! + let width: Int + let height: Int + + struct IdentifierCoder: HelperCoder { + let id: String + + func decode(from decoder: any Decoder) throws -> Image { + var image = try Image(from: decoder) + image.identifier = id + return image + } + + func encode(_ value: Image, to encoder: any Encoder) throws { + var image = value + image.identifier = nil + try image.encode(to: encoder) + } + } + } + } + + extension Item.Image: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.width = try container.decode(Int.self, forKey: CodingKeys.width) + self.height = try container.decode(Int.self, forKey: CodingKeys.height) + } + } + + extension Item.Image: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.width, forKey: CodingKeys.width) + try container.encode(self.height, forKey: CodingKeys.height) + } + } + + extension Item.Image { + enum CodingKeys: String, CodingKey { + case width = "width" + case height = "height" + } + } + + extension Item: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let images_container = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.images) + self.title = try container.decode(String.self, forKey: CodingKeys.title) + self.id = try container.decode(String.self, forKey: CodingKeys.id) + self.originalImage = try { () -> (_) -> _ in + Image.IdentifierCoder.init + }()(self.id).decode(from: images_container, forKey: CodingKeys.originalImage) + self.smallImage = try { () -> (_) -> _ in + Image.IdentifierCoder.init + }()(self.id).decode(from: images_container, forKey: CodingKeys.smallImage) + } + } + + extension Item: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.title, forKey: CodingKeys.title) + var images_container = container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.images) + try { () -> (_) -> _ in + Image.IdentifierCoder.init + }()(self.id).encode(self.originalImage, to: &images_container, atKey: CodingKeys.originalImage) + try { () -> (_) -> _ in + Image.IdentifierCoder.init + }()(self.id).encode(self.smallImage, to: &images_container, atKey: CodingKeys.smallImage) + try container.encode(self.id, forKey: CodingKeys.id) + } + } + + extension Item { + enum CodingKeys: String, CodingKey { + case title = "title" + case originalImage = "original" + case images = "images" + case smallImage = "small" + case id = "id" + } + } + """ + ) + } + + @Test(arguments: ["unique", "some_id"]) + func decoding(id: String) throws { + let data = try #require(itemJSON(id: id)) + let item = try JSONDecoder().decode(Item.self, from: data) + #expect(item.id == id) + #expect(item.title == "Great") + #expect(item.originalImage.id == id) + #expect(item.originalImage.height == 1080) + #expect(item.originalImage.width == 1920) + #expect(item.smallImage.id == id) + #expect(item.smallImage.height == 108) + #expect(item.smallImage.width == 192) + } + } + + struct MultiChainedDependency { + @Codable + struct SomeCodable { + @CodedBy(Multiplier.init, properties: \SomeCodable.three) + let one: Int + @CodedIn("deeply", "nested", "value") + @CodedBy( + Multiplier.init, properties: \SomeCodable.one, \SomeCodable.four + ) + @Default(ifMissing: 2, forErrors: 4) + let two: Int + @CodedIn("deeply", "nested") + let three: Int + let four: Int + @CodedAt("deeply", "value", "six") + @CodedBy(Multiplier.init, properties: \SomeCodable.six) + @Default(ifMissing: 5) + let five: Int + @CodedAt("deeply", "value", "six") + @CodedBy(ValueCoder()) + @Default(ifMissing: 6) + let six: Int + + struct Multiplier: HelperCoder { + let multipliers: [Int] + + init(multiplier: Int) { + self.multipliers = [multiplier] + } + + init(multiplier1: Int, multiplier2: Int) { + self.multipliers = [multiplier1, multiplier2] + } + + func decode(from decoder: any Decoder) throws -> Int { + return try multipliers.reduce(Int(from: decoder), *) + } + + func encode(_ value: Int, to encoder: any Encoder) throws { + return try multipliers.reduce(value, /).encode(to: encoder) + } + } + } + + @Test + func expansion() { + assertMacroExpansion( + """ + @Codable + struct SomeCodable { + @CodedBy(Multiplier.init, properties: \\SomeCodable.three) + let one: Int + @CodedIn("deeply", "nested", "value") + @CodedBy( + Multiplier.init, properties: \\SomeCodable.one, \\SomeCodable.four + ) + @Default(ifMissing: 2, forErrors: 4) + let two: Int + @CodedIn("deeply", "nested") + let three: Int + let four: Int + @CodedAt("deeply", "value", "six") + @CodedBy(Multiplier.init, properties: \\SomeCodable.six) + @Default(ifMissing: 5) + let five: Int + @CodedAt("deeply", "value", "six") + @CodedBy(ValueCoder()) + @Default(ifMissing: 6) + let six: Int + + struct Multiplier: HelperCoder { + let multipliers: [Int] + + init(multiplier: Int) { + self.multipliers = [multiplier] + } + + init(multiplier1: Int, multiplier2: Int) { + self.multipliers = [multiplier1, multiplier2] + } + + func decode(from decoder: any Decoder) throws -> Int { + return try multipliers.reduce(Int(from: decoder), *) + } + + func encode(_ value: Int, to encoder: any Encoder) throws { + return try multipliers.reduce(value, /).encode(to: encoder) + } + } + } + """, + expandedSource: + """ + struct SomeCodable { + let one: Int + let two: Int + let three: Int + let four: Int + let five: Int + let six: Int + + struct Multiplier: HelperCoder { + let multipliers: [Int] + + init(multiplier: Int) { + self.multipliers = [multiplier] + } + + init(multiplier1: Int, multiplier2: Int) { + self.multipliers = [multiplier1, multiplier2] + } + + func decode(from decoder: any Decoder) throws -> Int { + return try multipliers.reduce(Int(from: decoder), *) + } + + func encode(_ value: Int, to encoder: any Encoder) throws { + return try multipliers.reduce(value, /).encode(to: encoder) + } + } + } + + extension SomeCodable: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let deeply_container = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.deeply) + let nested_deeply_container = try deeply_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.nested) + let value_nested_deeply_container: KeyedDecodingContainer? + let value_nested_deeply_containerMissing: Bool + if (try? nested_deeply_container.decodeNil(forKey: CodingKeys.value)) == false { + value_nested_deeply_container = try? nested_deeply_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.value) + value_nested_deeply_containerMissing = false + } else { + value_nested_deeply_container = nil + value_nested_deeply_containerMissing = true + } + let value_deeply_container = ((try? deeply_container.decodeNil(forKey: CodingKeys.value)) == false) ? try deeply_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.value) : nil + self.four = try container.decode(Int.self, forKey: CodingKeys.four) + self.three = try nested_deeply_container.decode(Int.self, forKey: CodingKeys.three) + if let _ = value_nested_deeply_container { + } else if value_nested_deeply_containerMissing { + } else { + } + if let value_deeply_container = value_deeply_container { + self.six = try ValueCoder().decodeIfPresent(from: value_deeply_container, forKey: CodingKeys.five) ?? 6 + } else { + self.six = 6 + } + self.one = try { () -> (_) -> _ in + Multiplier.init + }()(self.three).decode(from: container, forKey: CodingKeys.one) + if let value_nested_deeply_container = value_nested_deeply_container { + do { + self.two = try { () -> (_, _) -> _ in + Multiplier.init + }()(self.one, self.four).decodeIfPresent(from: value_nested_deeply_container, forKey: CodingKeys.two) ?? 2 + } catch { + self.two = 4 + } + } else if value_nested_deeply_containerMissing { + self.two = 2 + } else { + self.two = 4 + } + if let value_deeply_container = value_deeply_container { + self.five = try { () -> (_) -> _ in + Multiplier.init + }()(self.six).decodeIfPresent(from: value_deeply_container, forKey: CodingKeys.five) ?? 5 + } else { + self.five = 5 + } + } + } + + extension SomeCodable: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try { () -> (_) -> _ in + Multiplier.init + }()(self.three).encode(self.one, to: &container, atKey: CodingKeys.one) + var deeply_container = container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.deeply) + var nested_deeply_container = deeply_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.nested) + var value_nested_deeply_container = nested_deeply_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.value) + try { () -> (_, _) -> _ in + Multiplier.init + }()(self.one, self.four).encode(self.two, to: &value_nested_deeply_container, atKey: CodingKeys.two) + try nested_deeply_container.encode(self.three, forKey: CodingKeys.three) + var value_deeply_container = deeply_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.value) + try ValueCoder().encode(self.six, to: &value_deeply_container, atKey: CodingKeys.five) + try { () -> (_) -> _ in + Multiplier.init + }()(self.six).encode(self.five, to: &value_deeply_container, atKey: CodingKeys.five) + try container.encode(self.four, forKey: CodingKeys.four) + } + } + + extension SomeCodable { + enum CodingKeys: String, CodingKey { + case one = "one" + case two = "two" + case deeply = "deeply" + case nested = "nested" + case value = "value" + case three = "three" + case four = "four" + case five = "six" + } + } + """ + ) + } + } + + struct ArrayDependency { + #if swift(>=6) + @Codable + struct Item: Identifiable { + let title: String + @CodedBy( + SequenceCoder.init, arguments: Image.IdentifierCoder.init, + properties: \Item.id + ) + let images: [Image] + let id: String + + @Codable + struct Image: Identifiable { + var id: String { identifier } + + @IgnoreCoding + private(set) var identifier: String! + let width: Int + let height: Int + + struct IdentifierCoder: HelperCoder { + let id: String + + func decode(from decoder: any Decoder) throws -> Image { + var image = try Image(from: decoder) + image.identifier = id + return image + } + + func encode(_ value: Image, to encoder: any Encoder) throws + { + var image = value + image.identifier = nil + try image.encode(to: encoder) + } + } + } + } + + @Test(arguments: ["unique", "some_id"]) + func decoding(id: String) throws { + let data = try #require(itemImagesJSON(id: id, count: 5)) + let item = try JSONDecoder().decode(Item.self, from: data) + #expect(item.id == id) + #expect(item.title == "Great") + #expect(item.images.map(\.id) == .init(repeating: id, count: 5)) + #expect( + item.images.map(\.height) == .init(repeating: 1080, count: 5)) + #expect( + item.images.map(\.width) == .init(repeating: 1920, count: 5)) + } + #endif + + @Test + func expansion() { + assertMacroExpansion( + """ + @Codable + struct Item: Identifiable { + let title: String + @CodedBy( + SequenceCoder.init, arguments: Image.IdentifierCoder.init, + properties: \\Item.id + ) + let images: [Image] + let id: String + + @Codable + struct Image: Identifiable { + var id: String { identifier } + + @IgnoreCoding + private(set) var identifier: String! + let width: Int + let height: Int + + struct IdentifierCoder: HelperCoder { + let id: String + + func decode(from decoder: any Decoder) throws -> Image { + var image = try Image(from: decoder) + image.identifier = id + return image + } + + func encode(_ value: Image, to encoder: any Encoder) throws + { + var image = value + image.identifier = nil + try image.encode(to: encoder) + } + } + } + } + """, + expandedSource: + """ + struct Item: Identifiable { + let title: String + let images: [Image] + let id: String + struct Image: Identifiable { + var id: String { identifier } + private(set) var identifier: String! + let width: Int + let height: Int + + struct IdentifierCoder: HelperCoder { + let id: String + + func decode(from decoder: any Decoder) throws -> Image { + var image = try Image(from: decoder) + image.identifier = id + return image + } + + func encode(_ value: Image, to encoder: any Encoder) throws + { + var image = value + image.identifier = nil + try image.encode(to: encoder) + } + } + } + } + + extension Item.Image: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.width = try container.decode(Int.self, forKey: CodingKeys.width) + self.height = try container.decode(Int.self, forKey: CodingKeys.height) + } + } + + extension Item.Image: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.width, forKey: CodingKeys.width) + try container.encode(self.height, forKey: CodingKeys.height) + } + } + + extension Item.Image { + enum CodingKeys: String, CodingKey { + case width = "width" + case height = "height" + } + } + + extension Item: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.title = try container.decode(String.self, forKey: CodingKeys.title) + self.id = try container.decode(String.self, forKey: CodingKeys.id) + self.images = try { () -> (_, _) -> _ in + SequenceCoder.init + }()(Image.IdentifierCoder.init, self.id).decode(from: container, forKey: CodingKeys.images) + } + } + + extension Item: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.title, forKey: CodingKeys.title) + try { () -> (_, _) -> _ in + SequenceCoder.init + }()(Image.IdentifierCoder.init, self.id).encode(self.images, to: &container, atKey: CodingKeys.images) + try container.encode(self.id, forKey: CodingKeys.id) + } + } + + extension Item { + enum CodingKeys: String, CodingKey { + case title = "title" + case images = "images" + case id = "id" + } + } + """ + ) + } + } + + struct LossySetDependency { + #if swift(>=6) + @Codable + struct Item: Identifiable { + let title: String + @CodedBy( + SequenceCoder.init, arguments: Set.self, + Image.IdentifierCoder.init, .lossy, properties: \Item.id + ) + let images: Set + let id: String + + @Codable + struct Image: Identifiable, Hashable { + var id: String { identifier } + + @IgnoreCoding + private(set) var identifier: String! + let width: Int + let height: Int + + struct IdentifierCoder: HelperCoder { + let id: String + + func decode(from decoder: any Decoder) throws -> Image { + var image = try Image(from: decoder) + image.identifier = id + return image + } + + func encode(_ value: Image, to encoder: any Encoder) throws + { + var image = value + image.identifier = nil + try image.encode(to: encoder) + } + } + } + } + #endif + + @Test + func expansion() { + assertMacroExpansion( + """ + @Codable + struct Item: Identifiable { + let title: String + @CodedBy( + SequenceCoder.init, arguments: Set.self, + Image.IdentifierCoder.init, .lossy, properties: \\Item.id + ) + let images: Set + let id: String + + @Codable + struct Image: Identifiable, Hashable { + var id: String { identifier } + + @IgnoreCoding + private(set) var identifier: String! + let width: Int + let height: Int + + struct IdentifierCoder: HelperCoder { + let id: String + + func decode(from decoder: any Decoder) throws -> Image { + var image = try Image(from: decoder) + image.identifier = id + return image + } + + func encode(_ value: Image, to encoder: any Encoder) throws + { + var image = value + image.identifier = nil + try image.encode(to: encoder) + } + } + } + } + """, + expandedSource: + """ + struct Item: Identifiable { + let title: String + let images: Set + let id: String + struct Image: Identifiable, Hashable { + var id: String { identifier } + private(set) var identifier: String! + let width: Int + let height: Int + + struct IdentifierCoder: HelperCoder { + let id: String + + func decode(from decoder: any Decoder) throws -> Image { + var image = try Image(from: decoder) + image.identifier = id + return image + } + + func encode(_ value: Image, to encoder: any Encoder) throws + { + var image = value + image.identifier = nil + try image.encode(to: encoder) + } + } + } + } + + extension Item.Image: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.width = try container.decode(Int.self, forKey: CodingKeys.width) + self.height = try container.decode(Int.self, forKey: CodingKeys.height) + } + } + + extension Item.Image: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.width, forKey: CodingKeys.width) + try container.encode(self.height, forKey: CodingKeys.height) + } + } + + extension Item.Image { + enum CodingKeys: String, CodingKey { + case width = "width" + case height = "height" + } + } + + extension Item: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.title = try container.decode(String.self, forKey: CodingKeys.title) + self.id = try container.decode(String.self, forKey: CodingKeys.id) + self.images = try { () -> (_, _, _, _) -> _ in + SequenceCoder.init + }()(Set.self, + Image.IdentifierCoder.init, .lossy, self.id).decode(from: container, forKey: CodingKeys.images) + } + } + + extension Item: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.title, forKey: CodingKeys.title) + try { () -> (_, _, _, _) -> _ in + SequenceCoder.init + }()(Set.self, + Image.IdentifierCoder.init, .lossy, self.id).encode(self.images, to: &container, atKey: CodingKeys.images) + try container.encode(self.id, forKey: CodingKeys.id) + } + } + + extension Item { + enum CodingKeys: String, CodingKey { + case title = "title" + case images = "images" + case id = "id" + } + } + """ + ) + } + } +} + +fileprivate func dogJSON(version: Int?) -> Data? { + let versionStr = + if let version { + "\"version\": \(version)," + } else { + "" + } + return """ + { + "name": "Dog", + \(versionStr) + "info": { + "tag": 12 + } + } + """.data(using: .utf8) +} + +fileprivate func itemJSON(id: String) -> Data? { + return """ + { + "id": "\(id)", + "title": "Great", + "images": { + "original": { + "height": 1080, + "width": 1920 + }, + "small": { + "height": 108, + "width": 192 + } + } + } + """.data(using: .utf8) +} + +fileprivate func itemImagesJSON(id: String, count: UInt) -> Data? { + let imagesJSON = (0..