├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .periphery.yml ├── .spi.yml ├── LICENSE ├── Mintfile ├── Package.swift ├── README.md ├── Sources ├── Macro │ ├── Helpers │ │ ├── CodeBuilder+Helpers.swift │ │ ├── Conformance+Helpers.swift │ │ ├── DiagnosticMessage+Helpers.swift │ │ ├── MacroToolkit+Helpers.swift │ │ ├── MessageID+Helpers.swift │ │ ├── String+Helpers.swift │ │ └── Type+Helpers.swift │ ├── Macro │ │ ├── AllOfCodable │ │ │ ├── AllOfCodable.swift │ │ │ ├── AllOfDecodableMacro.swift │ │ │ ├── AllOfEncodableMacro.swift │ │ │ └── AllOfMacroBase.swift │ │ ├── Codable │ │ │ ├── CodableMacro.swift │ │ │ ├── CodableMacroBase.swift │ │ │ ├── DecodableMacro.swift │ │ │ └── EncodableMacro.swift │ │ ├── CodingKey │ │ │ ├── CodingKey.swift │ │ │ └── CodingKeyMacro.swift │ │ ├── CustomCoding │ │ │ ├── CustomCoding.swift │ │ │ └── CustomCodingMacro.swift │ │ ├── DefaultValue │ │ │ ├── DefaultValue.swift │ │ │ └── DefaultValueMacro.swift │ │ ├── OmitCoding │ │ │ ├── OmitCoding.swift │ │ │ └── OmitCodingMacro.swift │ │ ├── OneOfCodable │ │ │ ├── OneOfCodable.swift │ │ │ ├── OneOfDecodable.swift │ │ │ ├── OneOfEncodable.swift │ │ │ ├── OneOfMacroBase.Diagnostic.swift │ │ │ ├── OneOfMacroBase.Expander.swift │ │ │ └── OneOfMacroBase.swift │ │ └── ValueStrategy │ │ │ ├── ValueStrategy.swift │ │ │ └── ValueStrategyMacro.swift │ ├── Misc │ │ ├── ClassDecl.swift │ │ ├── CodableBuilderFactory.swift │ │ ├── CodableBuilders │ │ │ ├── CodableBuildersMisc.swift │ │ │ ├── CodableBuildingData.swift │ │ │ ├── CodingKeysBuilder+Enum.swift │ │ │ ├── CodingKeysBuilder+Instance.swift │ │ │ ├── CodingKeysBuilder.swift │ │ │ ├── DecodableBuilder+Instance.swift │ │ │ ├── DecodableBuilder.swift │ │ │ ├── EncodableBuilder+Instance.swift │ │ │ ├── EncodableBuilder.swift │ │ │ ├── Variable+Diagnostic.swift │ │ │ └── Variable+KnownAttributes.swift │ │ ├── CodeBuilder │ │ │ ├── CodeBuilder.swift │ │ │ └── ExtensionBuilder.swift │ │ ├── CommonDiagnostic.swift │ │ ├── Conformance.swift │ │ ├── ConformanceDiagnosticChecker.swift │ │ ├── Diagnostic.swift │ │ ├── Instance.swift │ │ ├── InstanceExpander.swift │ │ ├── MacroCurrent.swift │ │ └── SwiftFormatter.swift │ └── Plugin.swift └── MacroCodableKit │ ├── MacroCodableKit.docc │ ├── Extensions │ │ ├── AllOfCodable.md │ │ ├── Codable.md │ │ ├── CustomCoding.md │ │ ├── CustomCodingDecoding.md │ │ ├── CustomCodingEncoding.md │ │ ├── DefaultValue.md │ │ ├── OneOfCodable.md │ │ └── ValueStrategy.md │ ├── MacroCodableKit.md │ └── Tutorials │ │ ├── CustomCoding │ │ ├── Code │ │ │ ├── create-your-own-customcoding-01-01.swift │ │ │ ├── create-your-own-customcoding-01-02.swift │ │ │ ├── create-your-own-customcoding-02-01.swift │ │ │ ├── create-your-own-customcoding-02-02.swift │ │ │ ├── create-your-own-customcoding-02-03.swift │ │ │ ├── create-your-own-customcoding-02-04.swift │ │ │ ├── create-your-own-customcoding-03-01.swift │ │ │ ├── create-your-own-customcoding-03-02.swift │ │ │ ├── create-your-own-customcoding-03-03.swift │ │ │ ├── create-your-own-customcoding-03-04.swift │ │ │ ├── create-your-own-customcoding-04-01.swift │ │ │ ├── create-your-own-customcoding-04-02.swift │ │ │ ├── create-your-own-customcoding-05-01.swift │ │ │ ├── create-your-own-customcoding-05-02.swift │ │ │ ├── create-your-own-customcoding-05-03.swift │ │ │ └── create-your-own-customcoding-05-04.swift │ │ └── Create your own CustomCoding.tutorial │ │ ├── Resources │ │ └── custom-coding.jpg │ │ └── Tutorial Table of Contents.tutorial │ ├── Macros │ ├── AllOf.swift │ ├── Annotations.swift │ ├── Codable.swift │ └── OneOf.swift │ └── Misc │ ├── CustomCoding │ ├── CustomCodingDecoding.swift │ ├── CustomCodingEncoding.swift │ ├── Predefined │ │ └── SafeDecoding │ │ │ ├── SafeDecoding+Array.swift │ │ │ ├── SafeDecoding+Dictionary.swift │ │ │ └── SafeDecoding.swift │ └── Types │ │ ├── CustomCodingDecoding+Types.swift │ │ └── CustomCodingEncoding+Types.swift │ ├── CustomDecodingName.swift │ ├── DefaultProviders │ ├── BoolFalse.swift │ ├── BoolTrue.swift │ ├── DoubleZero.swift │ ├── EmptyString.swift │ └── IntZero.swift │ ├── DefaultValueStrategy.swift │ ├── OptionalProtocol.swift │ ├── ValueCodableStrategies │ ├── Base64Strategy.swift │ └── Date │ │ ├── DateFormatterStrategy.swift │ │ ├── ISO8601DateFormatterProvider.swift │ │ ├── RFC2822DateFormatterProvider.swift │ │ ├── RFC3339DateFormatterProvider.swift │ │ ├── TimestampedDate.swift │ │ └── YearMonthDayDateFormatterProvider.swift │ └── ValueCodableStrategy.swift └── Tests └── MacroCodableKitTests ├── AllOfCodable ├── AllOfCodableMacroTests.swift ├── AllOfDecodableMacroTests.swift ├── AllOfEncodableMacroTests.swift └── AllOfMacroDecodingTests.swift ├── Annotations ├── AnnotationTests.swift ├── Base64StrategyTests.swift ├── DateValueStrategyTests.swift ├── DefaultValueTests.swift ├── Macro │ ├── AnnotationsMixMacroTests.swift │ ├── CustomCodingMacroTests.swift │ ├── DefaultValueMacroTests.swift │ ├── DiagnosticTests.swift │ └── ValueStrategyMacroTests.swift ├── SafeCodingArrayTests.swift └── SafeCodingDictionaryTests.swift ├── Codable ├── CodableMacroTests.swift ├── CodableTests.swift ├── DecodableMacroTests.swift └── EncodableMacroTests.swift ├── Helpers └── Misc.swift └── OneOfCodable ├── OneOfCodableMacroTests.swift ├── OneOfDecodableMacroTests.swift ├── OneOfEncodableMacroTests.swift └── OneOfMacroDecodingTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | container: 15 | image: swift:5.9 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Build 22 | run: swift build -v 23 | 24 | - name: Test 25 | run: swift test -v 26 | 27 | lint-unused-code: 28 | runs-on: ubuntu-latest 29 | container: 30 | image: swift:5.9 31 | 32 | env: 33 | MINT_PATH: "/github/home/.mint" 34 | MINT_LINK_PATH: "/github/home/.mint/bin" 35 | 36 | steps: 37 | - name: Checkout code 38 | uses: actions/checkout@v2 39 | 40 | - uses: irgaly/setup-mint@v1 41 | 42 | - name: Run periphery 43 | run: | 44 | mint run periphery scan --strict 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/ 9 | .swiftpm/xcode/xcuserdata/ 10 | .netrc 11 | .vs 12 | .docc-build/ 13 | Package.resolved 14 | *.profraw -------------------------------------------------------------------------------- /.periphery.yml: -------------------------------------------------------------------------------- 1 | retain_public: true 2 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [MacroCodableKit] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mikhail Maslo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Mintfile: -------------------------------------------------------------------------------- 1 | peripheryapp/periphery@2.16.0 -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import CompilerPluginSupport 5 | import PackageDescription 6 | 7 | let package = Package( 8 | name: "macro-codable-kit", 9 | platforms: [ 10 | .macOS(.v10_15), 11 | .iOS(.v13), 12 | ], 13 | products: [ 14 | .library( 15 | name: "MacroCodableKit", 16 | targets: ["MacroCodableKit"] 17 | ), 18 | ], 19 | dependencies: [ 20 | // Read Swift code 21 | .package( 22 | url: "https://github.com/apple/swift-syntax.git", 23 | from: "509.0.0" 24 | ), 25 | 26 | // Format Swift code 27 | .package( 28 | url: "https://github.com/apple/swift-format.git", 29 | from: "509.0.0" 30 | ), 31 | 32 | // Tools for macro development 33 | .package( 34 | url: "https://github.com/stackotter/swift-macro-toolkit", 35 | from: "0.3.0" 36 | ), 37 | 38 | // Tools for macro development 39 | .package( 40 | url: "https://github.com/pointfreeco/swift-macro-testing", 41 | from: "0.2.1" 42 | ), 43 | ], 44 | targets: [ 45 | // Macro declaration 46 | .target( 47 | name: "MacroCodableKit", 48 | dependencies: ["Macro"] 49 | ), 50 | 51 | // Macros 52 | .macro( 53 | name: "Macro", 54 | dependencies: [ 55 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 56 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 57 | .product(name: "MacroToolkit", package: "swift-macro-toolkit"), 58 | .product(name: "SwiftFormat", package: "swift-format"), 59 | ] 60 | ), 61 | 62 | // Tests 63 | .testTarget( 64 | name: "MacroCodableKitTests", 65 | dependencies: [ 66 | "Macro", 67 | "MacroCodableKit", 68 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 69 | .product(name: "MacroTesting", package: "swift-macro-testing"), 70 | ] 71 | ), 72 | ] 73 | ) 74 | -------------------------------------------------------------------------------- /Sources/Macro/Helpers/CodeBuilder+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeBuildable+Helpers.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 24.09.23. 6 | // 7 | 8 | import MacroToolkit 9 | 10 | enum CodeBuilders { 11 | static func encoder(accessModifier: AccessModifier? = nil, @CodeBuilder content: @escaping () -> CodeBuildable) -> some CodeBuildable { 12 | DeclarationBuilder(accessModifier: accessModifier, signature: "func encode(to encoder: Encoder) throws", content: content) 13 | } 14 | 15 | static func decoder(accessModifier: AccessModifier? = nil, @CodeBuilder content: @escaping () -> CodeBuildable) -> some CodeBuildable { 16 | DeclarationBuilder(accessModifier: accessModifier, signature: "init(from decoder: Decoder) throws", content: content) 17 | } 18 | 19 | static func extensionBuilder( 20 | accessModifier: AccessModifier? = nil, 21 | name: String, 22 | conformances: Set = [], 23 | @CodeBuilder content: @escaping () -> CodeBuildable 24 | ) -> some CodeBuildable { 25 | DeclarationBuilder( 26 | accessModifier: accessModifier, 27 | signature: "extension \(name)\(!conformances.isEmpty ? ": \(conformances.map(\.rawValue).sorted().joined(separator: ","))" : "")", 28 | content: content 29 | ) 30 | } 31 | 32 | static func content(@CodeBuilder content: @escaping () -> CodeBuildable) -> CodeBuildable { 33 | content() 34 | } 35 | } 36 | 37 | extension DeclarationBuilder { 38 | init(accessModifier: AccessModifier?, signature: String, @CodeBuilder content: @escaping () -> CodeBuildable) { 39 | self.init( 40 | signature: "\(accessModifier.map { "\($0.rawValue) " } ?? "")\(signature)", 41 | content: content 42 | ) 43 | } 44 | } 45 | 46 | struct SwitchBuilder: CodeBuildable { 47 | let expression: String 48 | let content: () -> CodeBuildable 49 | 50 | init(expression: String, @CodeBuilder content: @escaping () -> CodeBuildable) { 51 | self.expression = expression 52 | self.content = content 53 | } 54 | 55 | func build() -> String { 56 | DeclarationBuilder(signature: "switch \(expression)", content: content).build() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Macro/Helpers/Conformance+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conformance+Helpers.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 23.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | extension Conformance { 11 | private static func makeConformances(declaration: some DeclGroupSyntax) -> Set { 12 | let inheritedTypeDescriptions = declaration.inheritanceClause?.inheritedTypes 13 | .map { inheritedType in 14 | inheritedType.type.trimmedDescription 15 | } ?? [] 16 | return Conformance.makeConformances(inheritedTypeDescriptions) 17 | } 18 | 19 | private static func makeConformances(protocols: [TypeSyntax]) -> Set { 20 | var result = Set() 21 | // Macro can return ['EncodableDecodable'] instead of ['Encodable', 'Decodable'] 22 | if protocols.contains(where: { $0.trimmedDescription.contains(Conformance.Decodable.rawValue) }) { 23 | result.insert(.Decodable) 24 | } 25 | if protocols.contains(where: { $0.trimmedDescription.contains(Conformance.Encodable.rawValue) }) { 26 | result.insert(.Encodable) 27 | } 28 | return result 29 | } 30 | 31 | /// Macros in testing won't provide protocols property, whereas in real project they will be provided. As a result, it's impossible to test macros behaviour 32 | /// As a workaround in tests consider all conformances are declared at type 33 | static func makeConformances( 34 | protocols: [TypeSyntax], 35 | declaration: some DeclGroupSyntax, 36 | type: some TypeSyntaxProtocol, 37 | expectedConformances: Set 38 | ) -> Set { 39 | if type.trimmedDescription.hasSuffix("__testing__") { 40 | return expectedConformances.subtracting(makeConformances(declaration: declaration)) 41 | } else { 42 | return makeConformances(protocols: protocols) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Macro/Helpers/DiagnosticMessage+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiagnosticMessage+Helpers.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 21.09.23. 6 | // 7 | 8 | import SwiftDiagnostics 9 | import SwiftSyntax 10 | 11 | extension DiagnosticMessage { 12 | func diagnose(at node: some SyntaxProtocol) -> Diagnostic { 13 | Diagnostic(node: Syntax(node), message: self) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Macro/Helpers/MacroToolkit+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacroToolkit+Helpers.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 23.09.23. 6 | // 7 | 8 | import MacroToolkit 9 | 10 | extension Variable { 11 | var isStatic: Bool { 12 | _syntax.modifiers.contains(where: { modifier in 13 | if case .keyword(.static) = modifier.name.tokenKind { 14 | return true 15 | } else { 16 | return false 17 | } 18 | }) 19 | } 20 | } 21 | 22 | extension Type { 23 | var isOptional: Bool { 24 | switch self { 25 | case .optional: 26 | return true 27 | default: 28 | return false 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Macro/Helpers/MessageID+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import SwiftDiagnostics 9 | 10 | extension MessageID { 11 | init(id: String) { 12 | self.init( 13 | domain: MacroConfiguration.current.name, 14 | id: id 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Macro/Helpers/String+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Helpers.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | extension String { 9 | func lowercasingFirstLetter() -> String { 10 | guard !isEmpty else { return self } 11 | let firstLetterLowercased = prefix(1).lowercased() 12 | let remainingString = dropFirst() 13 | return firstLetterLowercased + remainingString 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Macro/Helpers/Type+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Type+Helpers.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | 10 | extension MacroToolkit.`Type` { 11 | func typeDescription(preservingOptional: Bool) -> String { 12 | var result: String 13 | switch self { 14 | case let .optional(optionalType): 15 | result = preservingOptional ? optionalType._baseSyntax.trimmedDescription : optionalType._baseSyntax.wrappedType.trimmedDescription 16 | default: 17 | result = _baseSyntax.trimmedDescription 18 | } 19 | return "\(result).self" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/AllOfCodable/AllOfCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllOfCodableMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 25.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct AllOfCodableMacro {} 12 | 13 | extension AllOfCodableMacro: ExtensionMacro { 14 | private static let expectedConformances: Set = [.Decodable, .Encodable] 15 | 16 | public static func expansion( 17 | of node: AttributeSyntax, 18 | attachedTo declaration: some DeclGroupSyntax, 19 | providingExtensionsOf type: some TypeSyntaxProtocol, 20 | conformingTo protocols: [TypeSyntax], 21 | in context: some MacroExpansionContext 22 | ) throws -> [ExtensionDeclSyntax] { 23 | try withMacro(Self.self, in: context) { 24 | let conformancesToGenerate = Conformance.makeConformances(protocols: protocols, declaration: declaration, type: type, expectedConformances: expectedConformances) 25 | return try AllOfMacroBase.expansion( 26 | of: node, 27 | attachedTo: declaration, 28 | providingExtensionsOf: type, 29 | conformancesToGenerate: conformancesToGenerate, 30 | expectedConformances: expectedConformances, 31 | in: context 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/AllOfCodable/AllOfDecodableMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllOfDecodableMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 25.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct AllOfDecodableMacro {} 12 | 13 | extension AllOfDecodableMacro: ExtensionMacro { 14 | private static let expectedConformances: Set = [.Decodable] 15 | 16 | public static func expansion( 17 | of node: AttributeSyntax, 18 | attachedTo declaration: some DeclGroupSyntax, 19 | providingExtensionsOf type: some TypeSyntaxProtocol, 20 | conformingTo protocols: [TypeSyntax], 21 | in context: some MacroExpansionContext 22 | ) throws -> [ExtensionDeclSyntax] { 23 | try withMacro(Self.self, in: context) { 24 | let conformancesToGenerate = Conformance.makeConformances(protocols: protocols, declaration: declaration, type: type, expectedConformances: expectedConformances) 25 | return try AllOfMacroBase.expansion( 26 | of: node, 27 | attachedTo: declaration, 28 | providingExtensionsOf: type, 29 | conformancesToGenerate: conformancesToGenerate, 30 | expectedConformances: expectedConformances, 31 | in: context 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/AllOfCodable/AllOfEncodableMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllOfEncodableMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 25.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct AllOfEncodableMacro {} 12 | 13 | extension AllOfEncodableMacro: ExtensionMacro { 14 | private static let expectedConformances: Set = [.Encodable] 15 | 16 | public static func expansion( 17 | of node: AttributeSyntax, 18 | attachedTo declaration: some DeclGroupSyntax, 19 | providingExtensionsOf type: some TypeSyntaxProtocol, 20 | conformingTo protocols: [TypeSyntax], 21 | in context: some MacroExpansionContext 22 | ) throws -> [ExtensionDeclSyntax] { 23 | try withMacro(Self.self, in: context) { 24 | let conformancesToGenerate = Conformance.makeConformances(protocols: protocols, declaration: declaration, type: type, expectedConformances: expectedConformances) 25 | return try AllOfMacroBase.expansion( 26 | of: node, 27 | attachedTo: declaration, 28 | providingExtensionsOf: type, 29 | conformancesToGenerate: conformancesToGenerate, 30 | expectedConformances: expectedConformances, 31 | in: context 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/AllOfCodable/AllOfMacroBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllOfMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 23.09.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftDiagnostics 10 | import SwiftSyntax 11 | import SwiftSyntaxBuilder 12 | import SwiftSyntaxMacros 13 | 14 | struct AllOfMacroBase { 15 | static func expansion( 16 | of _: AttributeSyntax, 17 | attachedTo declaration: some DeclGroupSyntax, 18 | providingExtensionsOf type: some TypeSyntaxProtocol, 19 | conformancesToGenerate: Set, 20 | expectedConformances: Set, 21 | in context: some MacroExpansionContext 22 | ) throws -> [ExtensionDeclSyntax] { 23 | let checker = ConformanceDiagnosticChecker( 24 | config: ConformanceDiagnosticChecker.Config( 25 | replacementMacroName: [ 26 | .Decodable: "@\(MacroConfiguration.makeName(macro: AllOfEncodableMacro.self))", 27 | .Encodable: "@\(MacroConfiguration.makeName(macro: AllOfDecodableMacro.self))", 28 | ] 29 | ) 30 | ) 31 | try checker.verify( 32 | type: type, 33 | declaration: declaration, 34 | expectedConformances: expectedConformances, 35 | conformancesToGenerate: conformancesToGenerate 36 | ) 37 | 38 | let expander = InstanceExpander(codableFactory: DefaultCodableBuilderFactoryImpl()) 39 | 40 | let buildingData: CodableBuildingData 41 | do { 42 | buildingData = try expander.verify(declaration: declaration, strategy: .singleValue, conformances: conformancesToGenerate) 43 | } catch { 44 | return [] 45 | } 46 | 47 | let formattedCode: String 48 | do { 49 | let codeBuilder = expander.extensionCodeBuilder(type: type, buildingData: buildingData, conformances: conformancesToGenerate) 50 | formattedCode = try expander.generateAndFormat(codeBuilder: codeBuilder) 51 | } catch { 52 | context.diagnose( 53 | CommonDiagnostic 54 | .internalError(message: "Internal Error = \(error). Couldn't format code") 55 | .diagnose(at: declaration) 56 | ) 57 | return [] 58 | } 59 | 60 | if formattedCode.isEmpty { 61 | return [] 62 | } 63 | 64 | guard let extensionDecl = expander.mapToExtensionDeclSyntax(code: formattedCode) else { 65 | context.diagnose( 66 | CommonDiagnostic 67 | .internalError(message: "Internal Error. Couldn't create extension from code = \(formattedCode)") 68 | .diagnose(at: declaration) 69 | ) 70 | return [] 71 | } 72 | 73 | return [extensionDecl] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/Codable/CodableMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct CodableMacro {} 12 | 13 | extension CodableMacro: ExtensionMacro { 14 | private static let expectedConformances: Set = [.Decodable, .Encodable] 15 | 16 | public static func expansion( 17 | of node: AttributeSyntax, 18 | attachedTo declaration: some DeclGroupSyntax, 19 | providingExtensionsOf type: some TypeSyntaxProtocol, 20 | conformingTo protocols: [TypeSyntax], 21 | in context: some MacroExpansionContext 22 | ) throws -> [ExtensionDeclSyntax] { 23 | try withMacro(Self.self, in: context) { 24 | let conformancesToGenerate = Conformance.makeConformances(protocols: protocols, declaration: declaration, type: type, expectedConformances: expectedConformances) 25 | return try CodableMacroBase.expansion( 26 | of: node, 27 | attachedTo: declaration, 28 | providingExtensionsOf: type, 29 | conformancesToGenerate: conformancesToGenerate, 30 | expectedConformances: expectedConformances, 31 | in: context 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/Codable/CodableMacroBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableMacroBase.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 23.09.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftDiagnostics 10 | import SwiftSyntax 11 | import SwiftSyntaxBuilder 12 | import SwiftSyntaxMacros 13 | 14 | struct CodableMacroBase { 15 | static func expansion( 16 | of _: AttributeSyntax, 17 | attachedTo declaration: some DeclGroupSyntax, 18 | providingExtensionsOf type: some TypeSyntaxProtocol, 19 | conformancesToGenerate: Set, 20 | expectedConformances: Set, 21 | in context: some MacroExpansionContext 22 | ) throws -> [ExtensionDeclSyntax] { 23 | let checker = ConformanceDiagnosticChecker( 24 | config: ConformanceDiagnosticChecker.Config( 25 | replacementMacroName: [ 26 | .Decodable: "@\(MacroConfiguration.makeName(macro: EncodableMacro.self))", 27 | .Encodable: "@\(MacroConfiguration.makeName(macro: DecodableMacro.self))", 28 | ] 29 | ) 30 | ) 31 | try checker.verify( 32 | type: type, 33 | declaration: declaration, 34 | expectedConformances: expectedConformances, 35 | conformancesToGenerate: conformancesToGenerate 36 | ) 37 | 38 | let expander = InstanceExpander(codableFactory: DefaultCodableBuilderFactoryImpl()) 39 | 40 | let buildingData: CodableBuildingData 41 | do { 42 | buildingData = try expander.verify(declaration: declaration, strategy: .codingKeys, conformances: conformancesToGenerate) 43 | } catch { 44 | return [] 45 | } 46 | 47 | let formattedCode: String 48 | do { 49 | let codeBuilder = expander.extensionCodeBuilder(type: type, buildingData: buildingData, conformances: conformancesToGenerate) 50 | formattedCode = try expander.generateAndFormat(codeBuilder: codeBuilder) 51 | } catch { 52 | context.diagnose( 53 | CommonDiagnostic 54 | .internalError(message: "Internal Error = \(error). Couldn't format code") 55 | .diagnose(at: declaration) 56 | ) 57 | return [] 58 | } 59 | 60 | if formattedCode.isEmpty { 61 | return [] 62 | } 63 | 64 | guard let extensionDecl = expander.mapToExtensionDeclSyntax(code: formattedCode) else { 65 | context.diagnose( 66 | CommonDiagnostic 67 | .internalError(message: "Internal Error. Couldn't create extension from code = \(formattedCode)") 68 | .diagnose(at: declaration) 69 | ) 70 | return [] 71 | } 72 | 73 | return [extensionDecl] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/Codable/DecodableMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DecodableMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct DecodableMacro {} 12 | 13 | extension DecodableMacro: ExtensionMacro { 14 | private static let expectedConformances: Set = [.Decodable] 15 | 16 | public static func expansion( 17 | of node: AttributeSyntax, 18 | attachedTo declaration: some DeclGroupSyntax, 19 | providingExtensionsOf type: some TypeSyntaxProtocol, 20 | conformingTo protocols: [TypeSyntax], 21 | in context: some MacroExpansionContext 22 | ) throws -> [ExtensionDeclSyntax] { 23 | try withMacro(Self.self, in: context) { 24 | let conformancesToGenerate = Conformance.makeConformances(protocols: protocols, declaration: declaration, type: type, expectedConformances: expectedConformances) 25 | return try CodableMacroBase.expansion( 26 | of: node, 27 | attachedTo: declaration, 28 | providingExtensionsOf: type, 29 | conformancesToGenerate: conformancesToGenerate, 30 | expectedConformances: expectedConformances, 31 | in: context 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/Codable/EncodableMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EncodableMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct EncodableMacro {} 12 | 13 | extension EncodableMacro: ExtensionMacro { 14 | private static let expectedConformances: Set = [.Encodable] 15 | 16 | public static func expansion( 17 | of node: AttributeSyntax, 18 | attachedTo declaration: some DeclGroupSyntax, 19 | providingExtensionsOf type: some TypeSyntaxProtocol, 20 | conformingTo protocols: [TypeSyntax], 21 | in context: some MacroExpansionContext 22 | ) throws -> [ExtensionDeclSyntax] { 23 | try withMacro(Self.self, in: context) { 24 | let conformancesToGenerate = Conformance.makeConformances(protocols: protocols, declaration: declaration, type: type, expectedConformances: expectedConformances) 25 | return try CodableMacroBase.expansion( 26 | of: node, 27 | attachedTo: declaration, 28 | providingExtensionsOf: type, 29 | conformancesToGenerate: conformancesToGenerate, 30 | expectedConformances: expectedConformances, 31 | in: context 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/CodingKey/CodingKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodingKey.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 24.09.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftSyntax 10 | 11 | struct CodingKey { 12 | let key: String 13 | 14 | init?(_ attribute: Attribute) { 15 | guard attribute.name._baseSyntax.name.trimmedDescription == "\(CodingKey.self)" else { 16 | return nil 17 | } 18 | guard let key = attribute.asMacroAttribute?.arguments.first?.1.asStringLiteral?.value else { 19 | return nil 20 | } 21 | 22 | self.key = key 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/CodingKey/CodingKeyMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodingKeyMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 23.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct CodingKeyMacro {} 12 | 13 | extension CodingKeyMacro: PeerMacro { 14 | public static func expansion( 15 | of _: AttributeSyntax, 16 | providingPeersOf _: some DeclSyntaxProtocol, 17 | in _: some MacroExpansionContext 18 | ) throws -> [DeclSyntax] { 19 | [] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/CustomCoding/CustomCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomCoding.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 01.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftSyntax 10 | 11 | struct CustomCoding { 12 | let codingNameType: String 13 | 14 | init?(_ attribute: Attribute) { 15 | guard attribute.name._baseSyntax.name.trimmedDescription == "\(Self.self)" else { 16 | return nil 17 | } 18 | guard let codingNameType = attribute.asMacroAttribute?.arguments.first?.1._syntax._syntaxNode.trimmedDescription else { 19 | return nil 20 | } 21 | 22 | self.codingNameType = codingNameType 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/CustomCoding/CustomCodingMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomCodingMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 01.10.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct CustomCodingMacro {} 12 | 13 | extension CustomCodingMacro: PeerMacro { 14 | public static func expansion( 15 | of _: AttributeSyntax, 16 | providingPeersOf _: some DeclSyntaxProtocol, 17 | in _: some MacroExpansionContext 18 | ) throws -> [DeclSyntax] { 19 | [] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/DefaultValue/DefaultValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultValue.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 01.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftSyntax 10 | 11 | struct DefaultValue { 12 | let providerType: String 13 | 14 | init?(_ attribute: Attribute) { 15 | guard attribute.name._baseSyntax.name.trimmedDescription == "\(Self.self)" else { 16 | return nil 17 | } 18 | guard let providerType = attribute.asMacroAttribute?.arguments.first?.1._syntax._syntaxNode.trimmedDescription else { 19 | return nil 20 | } 21 | 22 | self.providerType = providerType 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/DefaultValue/DefaultValueMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultValue.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 01.10.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct DefaultValueMacro {} 12 | 13 | extension DefaultValueMacro: PeerMacro { 14 | public static func expansion( 15 | of _: AttributeSyntax, 16 | providingPeersOf _: some DeclSyntaxProtocol, 17 | in _: some MacroExpansionContext 18 | ) throws -> [DeclSyntax] { 19 | [] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/OmitCoding/OmitCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OmitCoding.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 24.09.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftSyntax 10 | 11 | struct OmitCoding { 12 | init?(_ attribute: Attribute) { 13 | guard attribute.name._baseSyntax.name.trimmedDescription == "\(Self.self)" else { 14 | return nil 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/OmitCoding/OmitCodingMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OmitCodingMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 24.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct OmitCodingMacro {} 12 | 13 | extension OmitCodingMacro: PeerMacro { 14 | public static func expansion( 15 | of _: AttributeSyntax, 16 | providingPeersOf _: some DeclSyntaxProtocol, 17 | in _: some MacroExpansionContext 18 | ) throws -> [DeclSyntax] { 19 | [] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/OneOfCodable/OneOfCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OneOfCodableMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct OneOfCodableMacro {} 12 | 13 | extension OneOfCodableMacro: ExtensionMacro { 14 | private static let expectedConformances: Set = [.Decodable, .Encodable] 15 | 16 | public static func expansion( 17 | of node: AttributeSyntax, 18 | attachedTo declaration: some DeclGroupSyntax, 19 | providingExtensionsOf type: some TypeSyntaxProtocol, 20 | conformingTo protocols: [TypeSyntax], 21 | in context: some MacroExpansionContext 22 | ) throws -> [ExtensionDeclSyntax] { 23 | try withMacro(Self.self, in: context) { 24 | let conformancesToGenerate = Conformance.makeConformances(protocols: protocols, declaration: declaration, type: type, expectedConformances: expectedConformances) 25 | return try OneOfMacroBase.expansion( 26 | of: node, 27 | attachedTo: declaration, 28 | providingExtensionsOf: type, 29 | conformancesToGenerate: conformancesToGenerate, 30 | expectedConformances: expectedConformances, 31 | in: context 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/OneOfCodable/OneOfDecodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OneOfDecodableMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct OneOfDecodableMacro {} 12 | 13 | extension OneOfDecodableMacro: ExtensionMacro { 14 | private static let expectedConformances: Set = [.Decodable] 15 | 16 | public static func expansion( 17 | of node: AttributeSyntax, 18 | attachedTo declaration: some DeclGroupSyntax, 19 | providingExtensionsOf type: some TypeSyntaxProtocol, 20 | conformingTo protocols: [TypeSyntax], 21 | in context: some MacroExpansionContext 22 | ) throws -> [ExtensionDeclSyntax] { 23 | try withMacro(Self.self, in: context) { 24 | let conformancesToGenerate = Conformance.makeConformances(protocols: protocols, declaration: declaration, type: type, expectedConformances: expectedConformances) 25 | return try OneOfMacroBase.expansion( 26 | of: node, 27 | attachedTo: declaration, 28 | providingExtensionsOf: type, 29 | conformancesToGenerate: conformancesToGenerate, 30 | expectedConformances: expectedConformances, 31 | in: context 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/OneOfCodable/OneOfEncodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OneOfEncodableMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct OneOfEncodableMacro {} 12 | 13 | extension OneOfEncodableMacro: ExtensionMacro { 14 | private static let expectedConformances: Set = [.Encodable] 15 | 16 | public static func expansion( 17 | of node: AttributeSyntax, 18 | attachedTo declaration: some DeclGroupSyntax, 19 | providingExtensionsOf type: some TypeSyntaxProtocol, 20 | conformingTo protocols: [TypeSyntax], 21 | in context: some MacroExpansionContext 22 | ) throws -> [ExtensionDeclSyntax] { 23 | try withMacro(Self.self, in: context) { 24 | let conformancesToGenerate = Conformance.makeConformances(protocols: protocols, declaration: declaration, type: type, expectedConformances: expectedConformances) 25 | return try OneOfMacroBase.expansion( 26 | of: node, 27 | attachedTo: declaration, 28 | providingExtensionsOf: type, 29 | conformancesToGenerate: conformancesToGenerate, 30 | expectedConformances: expectedConformances, 31 | in: context 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/OneOfCodable/OneOfMacroBase.Diagnostic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OneOfMacroBase.Diagnostic.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 21.09.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftDiagnostics 10 | 11 | extension OneOfMacroBase { 12 | enum Diagnostic { 13 | static var requiresEnum: SimpleDiagnosticMessage { 14 | .error( 15 | message: "'@\(MacroConfiguration.current.name)' macro can only be applied to a enum", 16 | diagnosticID: MessageID(domain: MacroConfiguration.current.name, id: #function) 17 | ) 18 | } 19 | 20 | static var requiresAssociatedValue: SimpleDiagnosticMessage { 21 | .error( 22 | message: "'@\(MacroConfiguration.current.name)' macro requires each case to have one associated value", 23 | diagnosticID: MessageID(domain: MacroConfiguration.current.name, id: #function) 24 | ) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/OneOfCodable/OneOfMacroBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OneOfMacroBase.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 21.09.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftDiagnostics 10 | import SwiftSyntax 11 | import SwiftSyntaxBuilder 12 | import SwiftSyntaxMacros 13 | 14 | public enum OneOfMacroBase { 15 | static func expansion( 16 | of _: AttributeSyntax, 17 | attachedTo declaration: some DeclGroupSyntax, 18 | providingExtensionsOf type: some TypeSyntaxProtocol, 19 | conformancesToGenerate: Set, 20 | expectedConformances: Set, 21 | in context: some MacroExpansionContext 22 | ) throws -> [ExtensionDeclSyntax] { 23 | let checker = ConformanceDiagnosticChecker( 24 | config: ConformanceDiagnosticChecker.Config( 25 | replacementMacroName: [ 26 | .Decodable: "@\(MacroConfiguration.makeName(macro: OneOfEncodableMacro.self))", 27 | .Encodable: "@\(MacroConfiguration.makeName(macro: OneOfDecodableMacro.self))", 28 | ] 29 | ) 30 | ) 31 | try checker.verify( 32 | type: type, 33 | declaration: declaration, 34 | expectedConformances: expectedConformances, 35 | conformancesToGenerate: conformancesToGenerate 36 | ) 37 | 38 | let expander = Expander() 39 | 40 | let buildingData: CodableBuildingData 41 | do { 42 | buildingData = try expander.verify(declaration: declaration, conformances: conformancesToGenerate) 43 | } catch { 44 | return [] 45 | } 46 | 47 | let formattedCode: String 48 | do { 49 | let codeBuilder = expander.extensionCodeBuilder(type: type, buildingData: buildingData, conformances: conformancesToGenerate) 50 | formattedCode = try expander.generateAndFormat(codeBuilder: codeBuilder) 51 | } catch { 52 | context.diagnose( 53 | CommonDiagnostic 54 | .internalError(message: "Internal Error = \(error). Couldn't format code") 55 | .diagnose(at: declaration) 56 | ) 57 | return [] 58 | } 59 | 60 | if formattedCode.isEmpty { 61 | return [] 62 | } 63 | 64 | guard let extensionDecl = expander.mapToExtensionDeclSyntax(code: formattedCode) else { 65 | context.diagnose( 66 | CommonDiagnostic 67 | .internalError(message: "Internal Error. Couldn't create extension from code = \(formattedCode)") 68 | .diagnose(at: declaration) 69 | ) 70 | return [] 71 | } 72 | 73 | return [extensionDecl] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/ValueStrategy/ValueStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValueStrategy.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 01.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftSyntax 10 | 11 | struct ValueStrategy { 12 | let strategyType: String 13 | 14 | init?(_ attribute: Attribute) { 15 | guard attribute.name._baseSyntax.name.trimmedDescription == "\(Self.self)" else { 16 | return nil 17 | } 18 | guard let strategyType = attribute.asMacroAttribute?.arguments.first?.1._syntax._syntaxNode.trimmedDescription else { 19 | return nil 20 | } 21 | 22 | self.strategyType = strategyType 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Macro/Macro/ValueStrategy/ValueStrategyMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValueStrategyMacro.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 01.10.23. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | public struct ValueStrategyMacro {} 12 | 13 | extension ValueStrategyMacro: PeerMacro { 14 | public static func expansion( 15 | of _: AttributeSyntax, 16 | providingPeersOf _: some DeclSyntaxProtocol, 17 | in _: some MacroExpansionContext 18 | ) throws -> [DeclSyntax] { 19 | [] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/ClassDecl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClassDecl.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 25.09.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftSyntax 10 | 11 | struct ClassDecl { 12 | /// The underlying syntax node. 13 | public var _syntax: ClassDeclSyntax 14 | 15 | /// Wraps a syntax node. 16 | public init(_ syntax: ClassDeclSyntax) { 17 | _syntax = syntax 18 | } 19 | 20 | /// Attempts to get a declaration group as a struct declaration. 21 | public init?(_ syntax: any DeclGroupSyntax) { 22 | guard let syntax = syntax.as(ClassDeclSyntax.self) else { 23 | return nil 24 | } 25 | _syntax = syntax 26 | } 27 | 28 | /// The struct's members. 29 | public var members: [Decl] { 30 | _syntax.memberBlock.members.map(\.decl).map(Decl.init) 31 | } 32 | 33 | /// Types that the struct conforms to. 34 | public var inheritedTypes: [Type] { 35 | _syntax.inheritanceClause?.inheritedTypes.map(\.type).map(Type.init) ?? [] 36 | } 37 | 38 | /// Whether the `struct` was declared with the `public` access level modifier. 39 | public var isPublic: Bool { 40 | _syntax.isPublic 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodableBuilderFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableBuilderFactory.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 25.09.23. 6 | // 7 | 8 | protocol CodableBuilderFactory { 9 | func makeCodingKeysBuilder(buildingData: CodingKeysBuilder.BuildingData) -> CodeBuildable 10 | func makeDecoderBuilder(buildingData: DecodableBuilder.BuildingData) -> CodeBuildable 11 | func makeEncoderBuilder(buildingData: EncodableBuilder.BuildingData) -> CodeBuildable 12 | } 13 | 14 | final class DefaultCodableBuilderFactoryImpl: CodableBuilderFactory { 15 | func makeCodingKeysBuilder(buildingData: CodingKeysBuilder.BuildingData) -> CodeBuildable { 16 | CodingKeysBuilder(buildingData: buildingData) 17 | } 18 | 19 | func makeDecoderBuilder(buildingData: DecodableBuilder.BuildingData) -> CodeBuildable { 20 | DecodableBuilder(buildingData: buildingData) 21 | } 22 | 23 | func makeEncoderBuilder(buildingData: EncodableBuilder.BuildingData) -> CodeBuildable { 24 | EncodableBuilder(buildingData: buildingData) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodableBuilders/CodableBuildersMisc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultCodableBuilders.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 25.09.23. 6 | // 7 | 8 | enum CodableStrategy { 9 | case singleValue 10 | case codingKeys 11 | } 12 | 13 | struct FunctionBuilder: CodeBuildable { 14 | typealias Params = [(String?, String)] 15 | 16 | let name: String 17 | let parameters: Params 18 | let isOptionalTry: Bool 19 | 20 | init(name: String, parameters: Params, isOptionalTry: Bool = false) { 21 | self.name = name 22 | self.parameters = parameters 23 | self.isOptionalTry = isOptionalTry 24 | } 25 | 26 | func build() -> String { 27 | let tryKeyword = isOptionalTry ? "try?" : "try" 28 | return "\(tryKeyword) \(name)(\(parameters.map { ($0.0.map { "\($0): " } ?? "") + $0.1 }.joined(separator: ",")))" 29 | } 30 | } 31 | 32 | enum KnownAttribute: String { 33 | case omitCoding 34 | case codingKey 35 | case defaultValue 36 | case valueStrategy 37 | case customCoding 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodableBuilders/CodableBuildingData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CodableBuildingData { 11 | let codingKeysBuildingData: CodingKeysBuilder.BuildingData? 12 | let encodingBuildingData: EncodableBuilder.BuildingData? 13 | let decodingBuildingData: DecodableBuilder.BuildingData? 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodableBuilders/CodingKeysBuilder+Enum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodingKeysBuilder+Enum.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | 10 | extension CodingKeysBuilder { 11 | static func verify( 12 | accessModifier: AccessModifier?, 13 | enumDecl: Enum 14 | ) throws -> CodingKeysBuilder.BuildingData { 15 | CodingKeysBuilder.BuildingData( 16 | accessModifier: accessModifier, 17 | items: enumDecl.cases.map { CodingKeysBuilder.BuildingData.Item(identifier: $0.identifier, customCodingKey: nil) } 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodableBuilders/CodingKeysBuilder+Instance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodingKeysBuilder+Instance.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | 10 | extension CodingKeysBuilder { 11 | static func verify( 12 | accessModifier: AccessModifier?, 13 | instance: Instance 14 | ) throws -> CodingKeysBuilder.BuildingData { 15 | CodingKeysBuilder.BuildingData( 16 | accessModifier: accessModifier, 17 | items: try instance.members 18 | .compactMap { member -> CodingKeysBuilder.BuildingData.Item? in 19 | guard 20 | let variable = member.asVariable, 21 | !variable.isStatic 22 | else { 23 | return nil 24 | } 25 | 26 | let knownAttributes = variable.knownAttributes() 27 | try variable.verify(knownAttributes) 28 | 29 | guard variable.isStoredProperty else { 30 | return nil 31 | } 32 | 33 | // Skip coding if omitCoding is specified 34 | guard knownAttributes[.omitCoding] == nil else { 35 | return nil 36 | } 37 | 38 | guard 39 | let binding = variable.bindings.first, 40 | let identifier = binding.identifier 41 | else { 42 | assertionFailure("`Variable.verify(...)` should've caught it") 43 | return nil 44 | } 45 | 46 | return CodingKeysBuilder.BuildingData.Item( 47 | identifier: identifier, 48 | customCodingKey: (knownAttributes[.codingKey]?.first as? CodingKey)?.key 49 | ) 50 | } 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodableBuilders/CodingKeysBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultCodableBuilders.CodingKeysBuilder.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | 10 | struct CodingKeysBuilder: CodeBuildable { 11 | struct BuildingData { 12 | struct Item { 13 | let identifier: String 14 | let customCodingKey: String? 15 | } 16 | 17 | let accessModifier: AccessModifier? 18 | let items: [Item] 19 | } 20 | 21 | private let buildingData: BuildingData 22 | 23 | init(buildingData: BuildingData) { 24 | self.buildingData = buildingData 25 | } 26 | 27 | func build() -> String { 28 | CodeBuilders.content { 29 | if buildingData.items.isEmpty { 30 | "" 31 | } else { 32 | DeclarationBuilder( 33 | accessModifier: buildingData.accessModifier, 34 | signature: "enum CodingKeys: String, CodingKey" 35 | ) { 36 | buildingData.items.map { item in 37 | "case \(item.identifier)" + (item.customCodingKey.map { " = \"\($0)\"" } ?? "") 38 | } 39 | } 40 | } 41 | }.build() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodableBuilders/DecodableBuilder+Instance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DecodableBuilder+Instance.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | 10 | extension DecodableBuilder { 11 | static func verify( 12 | accessModifier: AccessModifier?, 13 | strategy: CodableStrategy, 14 | instance: Instance 15 | ) throws -> DecodableBuilder.BuildingData { 16 | try DecodableBuilder.BuildingData( 17 | accessModifier: accessModifier, 18 | strategy: strategy, 19 | items: instance.members 20 | .compactMap { member -> DecodableBuilder.BuildingData.Item? in 21 | guard 22 | let variable = member.asVariable, 23 | !variable.isStatic 24 | else { 25 | return nil 26 | } 27 | 28 | let knownAttributes = variable.knownAttributes() 29 | try variable.verify(knownAttributes) 30 | 31 | guard variable.isStoredProperty else { 32 | return nil 33 | } 34 | 35 | // Skip coding if omitCoding is specified 36 | guard knownAttributes[.omitCoding] == nil else { 37 | return nil 38 | } 39 | 40 | return makeItem(variable: variable, strategy: strategy, knownAttributes: knownAttributes) 41 | } 42 | ) 43 | } 44 | 45 | private static func makeItem( 46 | variable: Variable, 47 | strategy: CodableStrategy, 48 | knownAttributes: Variable.AllKnownAttributes 49 | ) -> DecodableBuilder.BuildingData.Item? { 50 | guard 51 | let binding = variable.bindings.first, 52 | let identifier = binding.identifier, 53 | let type = binding.type 54 | else { 55 | assertionFailure("`Variable.verify(...)` should've caught it") 56 | return nil 57 | } 58 | 59 | let function: FunctionBuilder 60 | if let customCoding = knownAttributes[.customCoding]?.first as? CustomCoding { 61 | /* 62 | static func \(customCoding.codingNameType)( 63 | _: [Element].Type, 64 | forKey key: KeyedDecodingContainer.Key, 65 | container: KeyedDecodingContainer 66 | ) throws -> [Element] 67 | */ 68 | function = FunctionBuilder( 69 | name: "CustomCodingDecoding.\(customCoding.codingNameType.lowercasingFirstLetter())", 70 | parameters: [ 71 | (nil, type.typeDescription(preservingOptional: true)), 72 | ("forKey", ".\(identifier)"), 73 | ("container", "container"), 74 | ] 75 | ) 76 | } else if knownAttributes[.defaultValue] != nil || knownAttributes[.valueStrategy] != nil { 77 | /* 78 | static func decode( 79 | _ type: T.Type, 80 | forKey key: KeyedDecodingContainer.Key, 81 | container: KeyedDecodingContainer, 82 | strategy: Strategy.Type, 83 | provider _: Provider.Type 84 | ) throws -> T 85 | where Strategy: ValueCodableStrategy, 86 | Strategy.Value == T, 87 | Provider: DefaultValueProvider, 88 | Provider.DefaultValue == T, 89 | T: Decodable 90 | */ 91 | var parameters: FunctionBuilder.Params = [ 92 | (nil, type.typeDescription(preservingOptional: true)), 93 | ("forKey", ".\(identifier)"), 94 | ("container", "container"), 95 | ] 96 | 97 | if let strategy = knownAttributes[.valueStrategy]?.first as? ValueStrategy { 98 | parameters.append(("strategy", "\(strategy.strategyType).self")) 99 | } 100 | 101 | if let defaultValue = knownAttributes[.defaultValue]?.first as? DefaultValue { 102 | parameters.append(("provider", "\(defaultValue.providerType).self")) 103 | } 104 | 105 | function = FunctionBuilder( 106 | name: "CustomCodingDecoding.decode", 107 | parameters: parameters 108 | ) 109 | } else { 110 | /* 111 | // keyed 112 | func decode(_ type: String.Type, forKey key: KeyedDecodingContainer.Key) throws -> String 113 | 114 | // single 115 | func decode(_ type: String.Type) throws -> String 116 | */ 117 | var parameters: FunctionBuilder.Params = [ 118 | (nil, type.typeDescription(preservingOptional: false)), 119 | ] 120 | 121 | let name: String 122 | switch strategy { 123 | case .singleValue: 124 | function = FunctionBuilder( 125 | name: "container.decode", 126 | parameters: parameters, 127 | isOptionalTry: type.isOptional 128 | ) 129 | case .codingKeys: 130 | name = type.isOptional ? "container.decodeIfPresent" : "container.decode" 131 | parameters.append(("forKey", ".\(identifier)")) 132 | 133 | function = FunctionBuilder( 134 | name: name, 135 | parameters: parameters 136 | ) 137 | } 138 | } 139 | 140 | return DecodableBuilder.BuildingData.Item(identifier: identifier, function: function.build()) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodableBuilders/DecodableBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DecodableBuilder.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | 10 | struct DecodableBuilder: CodeBuildable { 11 | struct BuildingData { 12 | struct Item { 13 | let identifier: String 14 | let function: String 15 | } 16 | 17 | let accessModifier: AccessModifier? 18 | let strategy: CodableStrategy 19 | let items: [Item] 20 | } 21 | 22 | private let buildingData: BuildingData 23 | 24 | init(buildingData: BuildingData) { 25 | self.buildingData = buildingData 26 | } 27 | 28 | func build() -> String { 29 | DeclarationBuilder(accessModifier: buildingData.accessModifier, signature: "init(from decoder: Decoder) throws") { 30 | if buildingData.items.isEmpty { 31 | "" 32 | } else { 33 | switch buildingData.strategy { 34 | case .singleValue: 35 | "let container = try decoder.singleValueContainer()" 36 | case .codingKeys: 37 | "let container = try decoder.container(keyedBy: CodingKeys.self)" 38 | } 39 | 40 | buildingData.items 41 | .map { item in 42 | "self.\(item.identifier) = \(item.function.build())" 43 | } 44 | } 45 | }.build() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodableBuilders/EncodableBuilder+Instance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EncodableBuilder+Instance.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | 10 | extension EncodableBuilder { 11 | static func verify( 12 | accessModifier: AccessModifier?, 13 | strategy: CodableStrategy, 14 | instance: Instance 15 | ) throws -> EncodableBuilder.BuildingData { 16 | try EncodableBuilder.BuildingData( 17 | accessModifier: accessModifier, 18 | strategy: strategy, 19 | items: instance.members 20 | .compactMap { member -> EncodableBuilder.BuildingData.Item? in 21 | guard 22 | let variable = member.asVariable, 23 | !variable.isStatic 24 | else { 25 | return nil 26 | } 27 | 28 | let knownAttributes = variable.knownAttributes() 29 | try variable.verify(knownAttributes) 30 | 31 | guard variable.isStoredProperty else { 32 | return nil 33 | } 34 | 35 | // Skip coding if omitCoding is specified 36 | guard knownAttributes[.omitCoding] == nil else { 37 | return nil 38 | } 39 | 40 | return makeItem(variable: variable, strategy: strategy, knownAttributes: knownAttributes) 41 | } 42 | ) 43 | } 44 | 45 | private static func makeItem( 46 | variable: Variable, 47 | strategy: CodableStrategy, 48 | knownAttributes: Variable.AllKnownAttributes 49 | ) -> EncodableBuilder.BuildingData.Item? { 50 | guard 51 | let binding = variable.bindings.first, 52 | let identifier = binding.identifier, 53 | let type = binding.type 54 | else { 55 | assertionFailure("`Variable.verify(...)` should've caught it") 56 | return nil 57 | } 58 | 59 | let function: FunctionBuilder 60 | if let customCoding = knownAttributes[.customCoding]?.first as? CustomCoding { 61 | /* 62 | static func \(customCoding.codingNameType)( 63 | _: [Element].Type, 64 | forKey key: KeyedDecodingContainer.Key, 65 | container: KeyedDecodingContainer 66 | ) throws 67 | */ 68 | function = FunctionBuilder( 69 | name: "CustomCodingEncoding.\(customCoding.codingNameType.lowercasingFirstLetter())", 70 | parameters: [ 71 | (nil, "self.\(identifier)"), 72 | ("forKey", ".\(identifier)"), 73 | ("container", "&container"), 74 | ] 75 | ) 76 | } else if knownAttributes[.valueStrategy] != nil { 77 | /* 78 | static func encode( 79 | _ value: Bool, 80 | forKey key: KeyedEncodingContainer.Key, 81 | container: inout KeyedEncodingContainer, 82 | strategy: Strategy.Type 83 | ) throws 84 | where Strategy: ValueCodableStrategy, 85 | Strategy.Value == Bool 86 | */ 87 | var parameters: FunctionBuilder.Params = [ 88 | (nil, "self.\(identifier)"), 89 | ("forKey", ".\(identifier)"), 90 | ("container", "&container"), 91 | ] 92 | 93 | if let strategy = knownAttributes[.valueStrategy]?.first as? ValueStrategy { 94 | parameters.append(("strategy", "\(strategy.strategyType).self")) 95 | } 96 | 97 | function = FunctionBuilder( 98 | name: "CustomCodingEncoding.encode", 99 | parameters: parameters 100 | ) 101 | } else { 102 | /* 103 | // keyed 104 | func decode(_ type: String.Type, forKey key: KeyedDecodingContainer.Key) throws -> String 105 | 106 | // single 107 | func decode(_ type: String.Type) throws -> String 108 | */ 109 | switch strategy { 110 | case .singleValue: 111 | function = FunctionBuilder( 112 | name: type.isOptional ? "self.\(identifier).encodeIfPresent" : "self.\(identifier).encode", 113 | parameters: [ 114 | ("to", "encoder"), 115 | ] 116 | ) 117 | case .codingKeys: 118 | function = FunctionBuilder( 119 | name: type.isOptional ? "container.encodeIfPresent" : "container.encode", 120 | parameters: [ 121 | (nil, "self.\(identifier)"), 122 | ("forKey", ".\(identifier)"), 123 | ] 124 | ) 125 | } 126 | } 127 | 128 | return EncodableBuilder.BuildingData.Item(identifier: identifier, function: function.build()) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodableBuilders/EncodableBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultCodableBuilders.EncodableBuilder.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | 10 | struct EncodableBuilder: CodeBuildable { 11 | struct BuildingData { 12 | struct Item { 13 | let identifier: String 14 | let function: String 15 | } 16 | 17 | let accessModifier: AccessModifier? 18 | let strategy: CodableStrategy 19 | let items: [Item] 20 | } 21 | 22 | private let buildingData: BuildingData 23 | 24 | init(buildingData: BuildingData) { 25 | self.buildingData = buildingData 26 | } 27 | 28 | func build() -> String { 29 | DeclarationBuilder(accessModifier: buildingData.accessModifier, signature: "func encode(to encoder: Encoder) throws") { 30 | if buildingData.items.isEmpty { 31 | "" 32 | } else { 33 | switch buildingData.strategy { 34 | case .singleValue: 35 | "" 36 | case .codingKeys: 37 | "var container = encoder.container(keyedBy: CodingKeys.self)" 38 | } 39 | 40 | buildingData.items 41 | .map { item in 42 | item.function 43 | } 44 | } 45 | }.build() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodableBuilders/Variable+Diagnostic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Variable+Diagnostic.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | 10 | extension Variable { 11 | func verify(_ allKnownAttributes: AllKnownAttributes) throws { 12 | try verifyHasTypeAndIdentifier() 13 | try verifySingleIdentifierAndType() 14 | try verifyNoDuplicatedAttributes(allKnownAttributes) 15 | try notStoredPropertiesWithoutKnownAnnotations(allKnownAttributes) 16 | } 17 | 18 | private func notStoredPropertiesWithoutKnownAnnotations(_ allKnownAttributes: AllKnownAttributes) throws { 19 | if !isStoredProperty, !allKnownAttributes.isEmpty { 20 | MacroConfiguration.current.context.diagnose( 21 | CommonDiagnostic 22 | .applicableOnlyToStoredProperties() 23 | .diagnose(at: _syntax) 24 | ) 25 | 26 | throw CommonError.diagnosticError 27 | } 28 | } 29 | 30 | private func verifyHasTypeAndIdentifier() throws { 31 | guard let binding = bindings.first else { 32 | return 33 | } 34 | guard binding.identifier != nil, binding.type != nil else { 35 | MacroConfiguration.current.context.diagnose( 36 | CommonDiagnostic 37 | .missingTypeOfIdentifierError() 38 | .diagnose(at: _syntax) 39 | ) 40 | 41 | throw CommonError.diagnosticError 42 | } 43 | } 44 | 45 | private func verifySingleIdentifierAndType() throws { 46 | guard bindings.count == 1 else { 47 | MacroConfiguration.current.context.diagnose( 48 | CommonDiagnostic 49 | .unsupportedCompoundDeclarationError() 50 | .diagnose(at: _syntax) 51 | ) 52 | 53 | throw CommonError.diagnosticError 54 | } 55 | } 56 | 57 | private func verifyNoDuplicatedAttributes(_ allKnownAttributes: AllKnownAttributes) throws { 58 | for (key, attributes) in allKnownAttributes { 59 | guard attributes.count <= 1 else { 60 | MacroConfiguration.current.context.diagnose( 61 | CommonDiagnostic 62 | .redundantAttributeError( 63 | annotation: key.rawValue.capitalized, 64 | variable: _syntax.trimmedDescription 65 | ) 66 | .diagnose(at: _syntax) 67 | ) 68 | 69 | throw CommonError.diagnosticError 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodableBuilders/Variable+KnownAttributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Variable+KnownAttribute.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | 10 | extension Variable { 11 | typealias AllKnownAttributes = [KnownAttribute: [Any]] 12 | 13 | func knownAttributes() -> AllKnownAttributes { 14 | var result: [KnownAttribute: [Any]] = [:] 15 | result.reserveCapacity(attributes.count) 16 | for attribute in attributes { 17 | if let omitCoding = attribute.attribute.flatMap({ OmitCoding($0) }) { 18 | result[.omitCoding, default: []].append(omitCoding) 19 | } else if let codingKey = attribute.attribute.flatMap({ CodingKey($0) }) { 20 | result[.codingKey, default: []].append(codingKey) 21 | } else if let defaultValue = attribute.attribute.flatMap({ DefaultValue($0) }) { 22 | result[.defaultValue, default: []].append(defaultValue) 23 | } else if let valueStrategy = attribute.attribute.flatMap({ ValueStrategy($0) }) { 24 | result[.valueStrategy, default: []].append(valueStrategy) 25 | } else if let customCoding = attribute.attribute.flatMap({ CustomCoding($0) }) { 26 | result[.customCoding, default: []].append(customCoding) 27 | } else { 28 | // ignore unknown attributes 29 | } 30 | } 31 | return result 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodeBuilder/CodeBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeBuilder.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 24.09.23. 6 | // 7 | 8 | protocol CodeBuildable { 9 | func build() -> String 10 | } 11 | 12 | @resultBuilder 13 | enum CodeBuilder { 14 | static func buildBlock(_ components: CodeBuildable...) -> CodeBuildable { 15 | CompositeCode(components: components) 16 | } 17 | 18 | // periphery:ignore 19 | static func buildIf(_ component: CodeBuildable?) -> CodeBuildable { 20 | component ?? EmptyCodeBuilder() 21 | } 22 | 23 | static func buildEither(first component: CodeBuildable) -> CodeBuildable { 24 | component 25 | } 26 | 27 | static func buildEither(second component: CodeBuildable) -> CodeBuildable { 28 | component 29 | } 30 | } 31 | 32 | // periphery:ignore 33 | struct EmptyCodeBuilder: CodeBuildable { 34 | func build() -> String { 35 | "" 36 | } 37 | } 38 | 39 | struct CompositeCode: CodeBuildable { 40 | let components: [CodeBuildable] 41 | 42 | func build() -> String { 43 | components.map { $0.build() }.joined(separator: "\n") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CodeBuilder/ExtensionBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeclarationBuilder.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 24.09.23. 6 | // 7 | 8 | enum AccessModifier: String { 9 | case `private`, `public`, `internal` 10 | } 11 | 12 | struct DeclarationBuilder: CodeBuildable { 13 | private let signature: String 14 | private let content: () -> CodeBuildable 15 | 16 | init(signature: String, @CodeBuilder content: @escaping () -> CodeBuildable) { 17 | self.signature = signature 18 | self.content = content 19 | } 20 | 21 | func build() -> String { 22 | """ 23 | \(signature) { 24 | \(content().build()) 25 | } 26 | """ 27 | } 28 | } 29 | 30 | extension String: CodeBuildable { 31 | func build() -> String { 32 | self 33 | } 34 | } 35 | 36 | extension [String]: CodeBuildable { 37 | func build() -> String { 38 | joined(separator: "\n") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/CommonDiagnostic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonDiagnostic.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftDiagnostics 10 | 11 | enum CommonDiagnostic { 12 | static func requiresStruct() -> SimpleDiagnosticMessage { 13 | .error( 14 | message: "'@\(MacroConfiguration.current.name)' macro can only be applied to a struct", 15 | diagnosticID: MessageID(id: #function) 16 | ) 17 | } 18 | 19 | static func redundantAttributeError(annotation: String, variable _: String) -> SimpleDiagnosticMessage { 20 | .error( 21 | message: "'@\(annotation)' attribute has been applied more than once. Redundant attribute applications have no effect on the generated code and may cause confusion.", 22 | diagnosticID: MessageID(id: #function) 23 | ) 24 | } 25 | 26 | static func unsupportedCompoundDeclarationError() -> SimpleDiagnosticMessage { 27 | .error( 28 | message: "'@\(MacroConfiguration.current.name)' macro is not applicable to compound declarations, declare each variable on a new line", 29 | diagnosticID: MessageID(id: #function) 30 | ) 31 | } 32 | 33 | static func missingTypeOfIdentifierError() -> SimpleDiagnosticMessage { 34 | .error( 35 | message: "'@\(MacroConfiguration.current.name)' macro is only applicable to declarations with an identifier followed by a type", 36 | diagnosticID: MessageID(id: #function) 37 | ) 38 | } 39 | 40 | static func applicableOnlyToStoredProperties() -> SimpleDiagnosticMessage { 41 | .error( 42 | message: "'@\(MacroConfiguration.current.name)' macro is only applicable to stored properties declared with an identifier followed by a type, example: `let variable: Int`", 43 | diagnosticID: MessageID(id: #function) 44 | ) 45 | } 46 | 47 | static func internalError( 48 | function: StaticString = #function, 49 | line: Int = #line, 50 | file: StaticString = #file, 51 | message: String 52 | ) -> SimpleDiagnosticMessage { 53 | .errorWithContext( 54 | function: function, 55 | line: line, 56 | file: file, 57 | message: message, 58 | diagnosticID: MessageID(domain: MacroConfiguration.current.name, id: #function) 59 | ) 60 | } 61 | } 62 | 63 | enum CommonError: Error { 64 | case diagnosticError 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/Conformance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conformance.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 23.09.23. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | enum Conformance: String, CaseIterable { 11 | case Codable, Encodable, Decodable 12 | 13 | public static let aliases: [Conformance: Set] = [ 14 | .Codable: [.Decodable, .Encodable], 15 | ] 16 | 17 | static func makeConformances(_ rawValues: [String]?) -> Set { 18 | var result = Set() 19 | for rawValue in rawValues ?? [] { 20 | guard let conformance = Conformance(rawValue: rawValue) else { 21 | continue 22 | } 23 | if let conformances = Self.aliases[conformance] { 24 | result.formUnion(conformances) 25 | } 26 | result.insert(conformance) 27 | } 28 | return result 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/ConformanceDiagnosticChecker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConformanceDiagnosticChecker.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 08.10.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftSyntax 10 | 11 | final class ConformanceDiagnosticChecker { 12 | struct Config { 13 | let replacementMacroName: [Conformance: String] 14 | 15 | init(replacementMacroName: [Conformance: String]) { 16 | self.replacementMacroName = replacementMacroName 17 | } 18 | } 19 | 20 | private let config: Config 21 | 22 | init(config: Config) { 23 | self.config = config 24 | } 25 | 26 | func verify( 27 | type: some TypeSyntaxProtocol, 28 | declaration: some DeclGroupSyntax, 29 | expectedConformances: Set, 30 | conformancesToGenerate: Set 31 | ) throws { 32 | if expectedConformances == conformancesToGenerate { 33 | return 34 | } 35 | 36 | guard !conformancesToGenerate.isEmpty else { 37 | notifyEmptyConformances(type: type, declaration: declaration, expectedConformances: expectedConformances) 38 | 39 | return 40 | } 41 | 42 | for conformance in Conformance.allCases 43 | where expectedConformances.contains(conformance) 44 | && !conformancesToGenerate.contains(conformance) 45 | { 46 | notifyConformanceMismatch(type: type, declaration: declaration, existingConformance: conformance) 47 | } 48 | } 49 | 50 | private func notifyConformanceMismatch( 51 | type: some TypeSyntaxProtocol, 52 | declaration: some DeclGroupSyntax, 53 | existingConformance: Conformance 54 | ) { 55 | let message: SimpleDiagnosticMessage = .warning( 56 | message: "'@\(MacroConfiguration.current.name)' macro won't generate '\(existingConformance)' conformance since '\(type.trimmedDescription)' already conformes to it. \(config.replacementMacroName[existingConformance].map { "Consider using '\($0)' instead" } ?? "")", 57 | diagnosticID: .init(id: #function) 58 | ) 59 | MacroConfiguration.current.context.diagnose( 60 | message.diagnose(at: declaration) 61 | ) 62 | } 63 | 64 | private func notifyEmptyConformances( 65 | type: some TypeSyntaxProtocol, 66 | declaration: some DeclGroupSyntax, 67 | expectedConformances: Set 68 | ) { 69 | let message: SimpleDiagnosticMessage = .warning( 70 | message: "'@\(MacroConfiguration.current.name)' macro has not effect since '\(type.trimmedDescription)' already conformes to \(expectedConformances.map(\.rawValue).sorted()). Consider removing '@\(MacroConfiguration.current.name)'", 71 | diagnosticID: .init(id: #function) 72 | ) 73 | MacroConfiguration.current.context.diagnose( 74 | message.diagnose(at: declaration) 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/Diagnostic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Diagnostic.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 21.09.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftDiagnostics 10 | 11 | extension SimpleDiagnosticMessage { 12 | static func error(message: String, diagnosticID: MessageID) -> SimpleDiagnosticMessage { 13 | SimpleDiagnosticMessage(message: message, diagnosticID: diagnosticID, severity: .error) 14 | } 15 | 16 | static func errorWithContext( 17 | function: StaticString = #function, 18 | line: Int = #line, 19 | file: StaticString = #file, 20 | message: String, 21 | diagnosticID: MessageID 22 | ) -> SimpleDiagnosticMessage { 23 | .error(message: "\(file):\(line) \(function) \(message)", diagnosticID: diagnosticID) 24 | } 25 | 26 | static func warning(message: String, diagnosticID: MessageID) -> SimpleDiagnosticMessage { 27 | SimpleDiagnosticMessage(message: message, diagnosticID: diagnosticID, severity: .warning) 28 | } 29 | 30 | // periphery:ignore 31 | static func note(message: String, diagnosticID: MessageID) -> SimpleDiagnosticMessage { 32 | SimpleDiagnosticMessage(message: message, diagnosticID: diagnosticID, severity: .note) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/Instance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Instance.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 25.09.23. 6 | // 7 | 8 | import MacroToolkit 9 | import SwiftSyntax 10 | 11 | protocol Instance { 12 | var declaration: any DeclGroupSyntax { get } 13 | 14 | var members: [Decl] { get } 15 | } 16 | 17 | extension Instance { 18 | var isPublic: Bool { declaration.isPublic } 19 | } 20 | 21 | final class InstanceImpl: Instance { 22 | let declaration: any DeclGroupSyntax 23 | let isStruct: Bool 24 | let members: [Decl] 25 | 26 | init?(declaration: any DeclGroupSyntax) { 27 | if let `struct` = Struct(declaration) { 28 | members = `struct`.members 29 | isStruct = true 30 | } else if let `class` = ClassDecl(declaration) { 31 | members = `class`.members 32 | isStruct = false 33 | } else { 34 | return nil 35 | } 36 | self.declaration = declaration 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/InstanceExpander.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InstanceExpander.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | final class InstanceExpander { 11 | private let codableFactory: CodableBuilderFactory 12 | 13 | init(codableFactory: CodableBuilderFactory) { 14 | self.codableFactory = codableFactory 15 | } 16 | 17 | func verify( 18 | declaration: some DeclGroupSyntax, 19 | strategy: CodableStrategy, 20 | conformances: Set 21 | ) throws -> CodableBuildingData { 22 | guard let instance = InstanceImpl(declaration: declaration), instance.isStruct else { 23 | MacroConfiguration.current.context.diagnose( 24 | CommonDiagnostic 25 | .requiresStruct() 26 | .diagnose(at: declaration) 27 | ) 28 | throw CommonError.diagnosticError 29 | } 30 | 31 | let accessModifier: AccessModifier? = instance.isPublic ? .public : nil 32 | 33 | let codingKeysBuildingData: CodingKeysBuilder.BuildingData? 34 | if strategy == .codingKeys { 35 | codingKeysBuildingData = try CodingKeysBuilder.verify( 36 | accessModifier: accessModifier, 37 | instance: instance 38 | ) 39 | } else { 40 | codingKeysBuildingData = nil 41 | } 42 | 43 | let encodingBuildingData: EncodableBuilder.BuildingData? 44 | if conformances.contains(.Encodable) { 45 | encodingBuildingData = try EncodableBuilder.verify( 46 | accessModifier: accessModifier, 47 | strategy: strategy, 48 | instance: instance 49 | ) 50 | } else { 51 | encodingBuildingData = nil 52 | } 53 | 54 | let decodingBuildingData: DecodableBuilder.BuildingData? 55 | if conformances.contains(.Decodable) { 56 | decodingBuildingData = try DecodableBuilder.verify( 57 | accessModifier: accessModifier, 58 | strategy: strategy, 59 | instance: instance 60 | ) 61 | } else { 62 | decodingBuildingData = nil 63 | } 64 | 65 | return CodableBuildingData( 66 | codingKeysBuildingData: codingKeysBuildingData, 67 | encodingBuildingData: encodingBuildingData, 68 | decodingBuildingData: decodingBuildingData 69 | ) 70 | } 71 | 72 | func extensionCodeBuilder( 73 | type: some TypeSyntaxProtocol, 74 | buildingData: CodableBuildingData, 75 | conformances: Set 76 | ) -> CodeBuildable { 77 | CodeBuilders.content { 78 | if !conformances.isEmpty { 79 | CodeBuilders.extensionBuilder( 80 | accessModifier: nil, 81 | name: type.trimmedDescription, 82 | conformances: conformances 83 | ) { [self] in 84 | membersCodeBuilder(buildingData: buildingData) 85 | } 86 | } 87 | } 88 | } 89 | 90 | func membersCodeBuilder( 91 | buildingData: CodableBuildingData 92 | ) -> CodeBuildable { 93 | CodeBuilders.content { [self] in 94 | if let codingKeysBuildingData = buildingData.codingKeysBuildingData { 95 | codableFactory.makeCodingKeysBuilder(buildingData: codingKeysBuildingData) 96 | } 97 | 98 | if let decodingBuildingData = buildingData.decodingBuildingData { 99 | codableFactory.makeDecoderBuilder(buildingData: decodingBuildingData) 100 | } 101 | 102 | if let encodingBuildingData = buildingData.encodingBuildingData { 103 | codableFactory.makeEncoderBuilder(buildingData: encodingBuildingData) 104 | } 105 | } 106 | } 107 | 108 | func generateAndFormat(codeBuilder: CodeBuildable) throws -> String { 109 | try codeBuilder.build().swiftFormatted 110 | } 111 | 112 | func mapToExtensionDeclSyntax(code: String) -> ExtensionDeclSyntax? { 113 | DeclSyntax(stringLiteral: code).as(ExtensionDeclSyntax.self) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/MacroCurrent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacroConfiguration.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import SwiftDiagnostics 9 | import SwiftSyntaxMacros 10 | 11 | struct MacroConfiguration { 12 | fileprivate static var _current: Self! 13 | 14 | static var current: Self { 15 | guard let _current else { 16 | preconditionFailure("Use withMacro(_:in:operation)") 17 | } 18 | return _current 19 | } 20 | 21 | /// Convention: Macro types should end with "Macro". If the name of the macro is "StringifyAndSquareMacro", 22 | /// then user macro name should be "macro StringifyAndSquare(...) = #externalMacro(...)" 23 | let macro: Macro.Type 24 | 25 | let context: MacroExpansionContext 26 | 27 | /// name is Macro type with dropped "Macro" suffix 28 | var name: String { 29 | Self.makeName(macro: macro) 30 | } 31 | 32 | static func makeName(macro: Macro.Type) -> String { 33 | String("\(macro)".dropLast("Macro".count)) 34 | } 35 | } 36 | 37 | func withMacro( 38 | _ macro: Macro.Type, 39 | in context: some MacroExpansionContext, 40 | operation: () throws -> R 41 | ) rethrows -> R { 42 | MacroConfiguration._current = MacroConfiguration(macro: macro, context: context) 43 | 44 | return try operation() 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Macro/Misc/SwiftFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftFormat.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 23.09.23. 6 | // 7 | 8 | import SwiftFormat 9 | 10 | extension String { 11 | /// A copy of the string formatted using swift-format. 12 | var swiftFormatted: Self { 13 | get throws { 14 | var formattedString = "" 15 | // TODO: Should be loaded from a swift-format file that we also use to format our own code. 16 | var configuration = Configuration() 17 | configuration.rules["OrderedImports"] = false 18 | configuration.rules["NoAccessLevelOnExtensionDeclaration"] = false 19 | configuration.rules["UseLetInEveryBoundCaseVariable"] = false 20 | configuration.indentation = SwiftFormat.Indent.spaces(4) 21 | configuration.respectsExistingLineBreaks = false 22 | configuration.lineBreakBeforeEachArgument = true 23 | configuration.lineBreakBeforeControlFlowKeywords = false 24 | configuration.lineBreakBeforeEachGenericRequirement = true 25 | configuration.lineBreakAroundMultilineExpressionChainComponents = true 26 | configuration.indentConditionalCompilationBlocks = false 27 | configuration.maximumBlankLines = 0 28 | configuration.lineLength = 120 29 | let formatter = SwiftFormatter(configuration: configuration) 30 | try formatter.format( 31 | source: self, 32 | assumingFileURL: nil, 33 | to: &formattedString 34 | ) { diagnostic, sourceLocation in 35 | print( 36 | """ 37 | === 38 | Formatting the following code produced diagnostic at location \(sourceLocation) (see end): 39 | --- 40 | \(diagnostic.debugDescription) 41 | === 42 | """ 43 | ) 44 | print(diagnostic) 45 | } 46 | return formattedString 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Macro/Plugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Plugin.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 21.09.23. 6 | // 7 | 8 | #if canImport(SwiftCompilerPlugin) 9 | import SwiftCompilerPlugin 10 | import SwiftSyntaxMacros 11 | 12 | @main 13 | struct Plugin: CompilerPlugin { 14 | let providingMacros: [Macro.Type] = [ 15 | // One of 16 | OneOfCodableMacro.self, 17 | OneOfEncodableMacro.self, 18 | OneOfDecodableMacro.self, 19 | 20 | // All of 21 | AllOfCodableMacro.self, 22 | AllOfDecodableMacro.self, 23 | AllOfEncodableMacro.self, 24 | 25 | // Codable 26 | CodableMacro.self, 27 | EncodableMacro.self, 28 | DecodableMacro.self, 29 | 30 | // Coding customization 31 | CodingKeyMacro.self, 32 | OmitCodingMacro.self, 33 | DefaultValueMacro.self, 34 | ValueStrategyMacro.self, 35 | CustomCodingMacro.self, 36 | ] 37 | } 38 | #endif 39 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Extensions/AllOfCodable.md: -------------------------------------------------------------------------------- 1 | # ``MacroCodableKit/AllOfCodable()`` 2 | 3 | ## Topics 4 | 5 | ### Complementary 6 | 7 | - ``AllOfEncodable()`` 8 | - ``AllOfDecodable()`` 9 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Extensions/Codable.md: -------------------------------------------------------------------------------- 1 | # ``MacroCodableKit/Codable()`` 2 | 3 | ## Topics 4 | 5 | ### Complementary 6 | 7 | - ``Encodable()`` 8 | - ``Decodable()`` 9 | 10 | ### Coding adjustment 11 | 12 | - ``ValueStrategy(_:)`` 13 | - ``DefaultValue(_:)`` 14 | - ``CustomCoding(_:)`` 15 | - ``CodingKey(_:)`` 16 | - ``OmitCoding()`` 17 | 18 | ### Decoding Error Handling 19 | 20 | - ``CustomCodingDecoding/errorHandler`` 21 | 22 | ### Encoding Error Handling 23 | 24 | - ``CustomCodingEncoding/errorHandler`` 25 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Extensions/CustomCoding.md: -------------------------------------------------------------------------------- 1 | # ``MacroCodableKit/CustomCoding(_:)`` 2 | 3 | ## Topics 4 | 5 | ### Arguments 6 | 7 | - ``CustomDecodingName`` 8 | 9 | ### Build-in 10 | 11 | - ``SafeDecoding`` 12 | 13 | ### Build your own CustomCoding 14 | 15 | - 16 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Extensions/CustomCodingDecoding.md: -------------------------------------------------------------------------------- 1 | # ``MacroCodableKit/CustomCodingDecoding`` 2 | 3 | ## Topics 4 | 5 | ### Error Handling 6 | 7 | - ``errorHandler`` 8 | - ``logError(_:)`` 9 | 10 | ### SafeDecoding 11 | 12 | - ``CustomCodingDecoding/safeDecoding(_:forKey:container:)-1qnwn`` 13 | - ``CustomCodingDecoding/safeDecoding(_:forKey:container:)-3ytig`` 14 | 15 | - ``CustomCodingDecoding/safeDecoding(_:forKey:container:)-5nus0`` 16 | - ``CustomCodingDecoding/safeDecoding(_:forKey:container:)-9nz0q`` 17 | 18 | ### Bool 19 | 20 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-7kg3e`` 21 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-22a7t`` 22 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-9f4zo`` 23 | 24 | ### String 25 | 26 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-7gzh2`` 27 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-2rz2s`` 28 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-9c1xz`` 29 | 30 | ### Double 31 | 32 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-4znpw`` 33 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-49gb0`` 34 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-1m4qz`` 35 | 36 | ### Float 37 | 38 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-80av7`` 39 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-2b2tv`` 40 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-673l9`` 41 | 42 | ### Int 43 | 44 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-4mttn`` 45 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-24fsq`` 46 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-48tqa`` 47 | 48 | ### Int8 49 | 50 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-33ctb`` 51 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-bzr2`` 52 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-55iyw`` 53 | 54 | ### Int16 55 | 56 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-5xt2z`` 57 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-926o5`` 58 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-31x9d`` 59 | 60 | ### Int32 61 | 62 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-6rs13`` 63 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-l2uf`` 64 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-5jnq7`` 65 | 66 | ### Int64 67 | 68 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-8ck37`` 69 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-euev`` 70 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-6blfc`` 71 | 72 | ### UInt 73 | 74 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-6xkp8`` 75 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-29auz`` 76 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-4z7p0`` 77 | 78 | ### UInt8 79 | 80 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-6geun`` 81 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-4ld25`` 82 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-44bwe`` 83 | 84 | ### UInt16 85 | 86 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-2pg7i`` 87 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-8s9tb`` 88 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-2d1kj`` 89 | 90 | ### UInt32 91 | 92 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-21c84`` 93 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-79sak`` 94 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-6ocol`` 95 | 96 | ### UInt64 97 | 98 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-658sr`` 99 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-45h7x`` 100 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-7thk9`` 101 | 102 | ### Generic 103 | 104 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-9cwj7`` 105 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:)-9y9km`` 106 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-6ycot`` 107 | - ``CustomCodingDecoding/decode(_:forKey:container:provider:)-678rr`` 108 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-5crni`` 109 | - ``CustomCodingDecoding/decode(_:forKey:container:strategy:provider:)-6m0gn`` 110 | 111 | ### Helpers 112 | 113 | - ``OptionalProtocol`` 114 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Extensions/CustomCodingEncoding.md: -------------------------------------------------------------------------------- 1 | # ``MacroCodableKit/CustomCodingEncoding`` 2 | 3 | ## Topics 4 | 5 | ### Error Handling 6 | 7 | - ``errorHandler`` 8 | - ``logError(_:)`` 9 | 10 | ### SafeDecoding 11 | 12 | - ``CustomCodingEncoding/safeDecoding(_:forKey:container:)-7cwmw`` 13 | - ``CustomCodingEncoding/safeDecoding(_:forKey:container:)-9b19z`` 14 | 15 | - ``CustomCodingEncoding/safeDecoding(_:forKey:container:)-5sgwu`` 16 | - ``CustomCodingEncoding/safeDecoding(_:forKey:container:)-54c5h`` 17 | 18 | ### Bool 19 | 20 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-3hi4w`` 21 | 22 | ### String 23 | 24 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-1hfex`` 25 | 26 | ### Double 27 | 28 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-k2fp`` 29 | 30 | ### Float 31 | 32 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-3xsao`` 33 | 34 | ### Int 35 | 36 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-298h8`` 37 | 38 | ### Int8 39 | 40 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-gqpe`` 41 | 42 | ### Int16 43 | 44 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-51rts`` 45 | 46 | ### Int32 47 | 48 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-3ht6m`` 49 | 50 | ### Int64 51 | 52 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-3jofe`` 53 | 54 | ### UInt 55 | 56 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-5hqzv`` 57 | 58 | ### UInt8 59 | 60 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-8eudj`` 61 | 62 | ### UInt16 63 | 64 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-1gfxu`` 65 | 66 | ### UInt32 67 | 68 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-7n69y`` 69 | 70 | ### UInt64 71 | 72 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-29ps8`` 73 | 74 | ### Generic 75 | 76 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-4s32f`` 77 | - ``CustomCodingEncoding/encode(_:forKey:container:strategy:)-1n297`` 78 | 79 | ### Helpers 80 | 81 | - ``OptionalProtocol`` 82 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Extensions/DefaultValue.md: -------------------------------------------------------------------------------- 1 | # ``MacroCodableKit/DefaultValue(_:)`` 2 | 3 | ## Topics 4 | 5 | ### Arguments 6 | 7 | - ``DefaultValueProvider`` 8 | 9 | ### Build-in 10 | 11 | - ``BoolTrue`` 12 | - ``BoolFalse`` 13 | - ``IntZero`` 14 | - ``DoubleZero`` 15 | - ``EmptyString`` 16 | 17 | ### Underlying types 18 | 19 | - ``DefaultTrueValueProvider`` 20 | - ``DefaultFalseValueProvider`` 21 | - ``DefaultIntZeroValueProvider`` 22 | - ``DefaultDoubleZeroValueProvider`` 23 | - ``DefaultEmptyStringValueProvider`` 24 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Extensions/OneOfCodable.md: -------------------------------------------------------------------------------- 1 | # ``MacroCodableKit/OneOfCodable()`` 2 | 3 | ## Topics 4 | 5 | ### Complementary 6 | 7 | - ``OneOfDecodable()`` 8 | - ``OneOfEncodable()`` 9 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Extensions/ValueStrategy.md: -------------------------------------------------------------------------------- 1 | # ``MacroCodableKit/ValueStrategy(_:)`` 2 | 3 | ## Topics 4 | 5 | ### Arguments 6 | 7 | - ``ValueCodableStrategy`` 8 | 9 | ### Build-in Date 10 | 11 | - ``TimestampedDate`` 12 | - ``ISO8601Default`` 13 | - ``ISO8601WithFullDate`` 14 | - ``ISO8601WithFractionalSeconds`` 15 | - ``RFC2822Date`` 16 | - ``RFC3339Date`` 17 | - ``YearMonthDayDate`` 18 | 19 | ### Build-in base64 20 | 21 | - ``Base64Data`` 22 | 23 | ### Underlying types 24 | 25 | - ``DateFormatterStrategy`` 26 | - ``DateFormatterStrategy`` 27 | - ``DateFormatterProvider`` 28 | - ``ISO8601DefaultDateFormatterProvider`` 29 | - ``ISO8601FullDateDateFormatterProvider`` 30 | - ``ISO8601WithFractionalSecondsDateFormatterProvider`` 31 | - ``RFC2822DateFormatterProvider`` 32 | - ``RFC3339DateFormatterProvider`` 33 | - ``YearMonthDayDateFormatterProvider`` 34 | - ``DateFormatterProtocol`` 35 | - ``TimestampStrategy`` 36 | - ``Base64Strategy`` 37 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/MacroCodableKit.md: -------------------------------------------------------------------------------- 1 | # ``MacroCodableKit`` 2 | 3 | Conform to `Codable`, adjust coding strategy, and implement OpenAPI spec with ease 4 | 5 | ## Overview 6 | 7 | **MacroCodableKit** is a comprehensive solution to conform to [Codable](https://developer.apple.com/documentation/swift/codable), which leverages [Swift Macros](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/) under the hood, thus providing flexible coding customizations. 8 | 9 | To start, add a simple ``Codable()`` annotation next to a struct. Or use ``Decodable()`` or ``Encodable()``, if you need only `Decodable` or `Encodable` accordingly. 10 | 11 | Adjust default [CodingKeys](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types#2904057) with ``CodingKey(_:)`` or skip a property entiraly from coding using ``OmitCoding()``. 12 | 13 | Perform custom coding with ``ValueStrategy(_:)`` to use your or a build-in coding strategy. For example, you might need to map a timestamps to a Date and you can do it with build-in ``TimestampedDate`` strategy. Or you might want to use a default value if decoding fails and ``DefaultValue(_:)`` is here to the rescue. 14 | 15 | ## Topics 16 | 17 | ### Coding 18 | 19 | - ``Codable()`` 20 | 21 | ### Annotations 22 | 23 | - ``DefaultValue(_:)`` 24 | - ``ValueStrategy(_:)`` 25 | - ``CustomCoding(_:)`` 26 | 27 | ### OpenAPI coding 28 | 29 | - ``OneOfCodable()`` 30 | - ``AllOfCodable()`` 31 | 32 | ### Under the hood 33 | 34 | - ``CustomCodingDecoding`` 35 | - ``CustomCodingEncoding`` 36 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-01-01.swift: -------------------------------------------------------------------------------- 1 | import MacroCodableKit 2 | 3 | public struct OmitEmpty: CustomDecodingName {} 4 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-01-02.swift: -------------------------------------------------------------------------------- 1 | import MacroCodableKit 2 | 3 | public struct OmitEmpty: CustomDecodingName {} 4 | 5 | public protocol OmitEmptyCheckable { 6 | var omitEmpty_isEmpty: Bool { get } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-02-01.swift: -------------------------------------------------------------------------------- 1 | import MacroCodableKit 2 | 3 | public extension CustomCodingEncoding { 4 | } 5 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-02-02.swift: -------------------------------------------------------------------------------- 1 | import MacroCodableKit 2 | 3 | public extension CustomCodingEncoding { 4 | static func omitEmpty( 5 | _ value: Element, 6 | forKey key: KeyedEncodingContainer.Key, 7 | container: inout KeyedEncodingContainer 8 | ) throws { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-02-03.swift: -------------------------------------------------------------------------------- 1 | import MacroCodableKit 2 | 3 | public extension CustomCodingEncoding { 4 | static func omitEmpty( 5 | _ value: Element, 6 | forKey key: KeyedEncodingContainer.Key, 7 | container: inout KeyedEncodingContainer 8 | ) throws { 9 | if (value as? OmitEmptyCheckable)?.omitEmpty_isEmpty != true { 10 | try container.encode(value, forKey: key) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-02-04.swift: -------------------------------------------------------------------------------- 1 | import MacroCodableKit 2 | 3 | public extension CustomCodingEncoding { 4 | static func omitEmpty( 5 | _ value: Element, 6 | forKey key: KeyedEncodingContainer.Key, 7 | container: inout KeyedEncodingContainer 8 | ) throws { 9 | if (value as? OmitEmptyCheckable)?.omitEmpty_isEmpty != true { 10 | try container.encode(value, forKey: key) 11 | } 12 | } 13 | 14 | static func omitEmpty( 15 | _ value: Element?, 16 | forKey key: KeyedEncodingContainer.Key, 17 | container: inout KeyedEncodingContainer 18 | ) throws { 19 | try container.encodeIfPresent(value, forKey: key) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-03-01.swift: -------------------------------------------------------------------------------- 1 | import MacroCodableKit 2 | 3 | public extension CustomCodingDecoding { 4 | } 5 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-03-02.swift: -------------------------------------------------------------------------------- 1 | import MacroCodableKit 2 | 3 | public extension CustomCodingDecoding { 4 | static func safeDecoding( 5 | _ type: Element.Type, 6 | forKey key: KeyedDecodingContainer.Key, 7 | container: KeyedDecodingContainer 8 | ) throws -> Element { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-03-03.swift: -------------------------------------------------------------------------------- 1 | import MacroCodableKit 2 | 3 | public extension CustomCodingDecoding { 4 | static func safeDecoding( 5 | _ type: Element.Type, 6 | forKey key: KeyedDecodingContainer.Key, 7 | container: KeyedDecodingContainer 8 | ) throws -> Element { 9 | try container.decode(type, forKey: key) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-03-04.swift: -------------------------------------------------------------------------------- 1 | import MacroCodableKit 2 | 3 | public extension CustomCodingDecoding { 4 | static func safeDecoding( 5 | _ type: Element.Type, 6 | forKey key: KeyedDecodingContainer.Key, 7 | container: KeyedDecodingContainer 8 | ) throws -> Element { 9 | try container.decode(type, forKey: key) 10 | } 11 | 12 | static func safeDecoding( 13 | _ type: Element?.Type, 14 | forKey key: KeyedDecodingContainer.Key, 15 | container: KeyedDecodingContainer 16 | ) throws -> Element { 17 | try container.decodeIfPresent(type, forKey: key) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-04-01.swift: -------------------------------------------------------------------------------- 1 | import MacroCodableKit 2 | 3 | public struct OmitEmpty: CustomDecodingName {} 4 | 5 | public protocol OmitEmptyCheckable { 6 | var omitEmpty_isEmpty: Bool { get } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-04-02.swift: -------------------------------------------------------------------------------- 1 | import MacroCodableKit 2 | 3 | public struct OmitEmpty: CustomDecodingName {} 4 | 5 | public protocol OmitEmptyCheckable { 6 | var omitEmpty_isEmpty: Bool { get } 7 | } 8 | 9 | extension Bool: OmitEmptyCheckable { 10 | public var omitEmpty_isEmpty: Bool { self == false } 11 | } 12 | 13 | extension String: OmitEmptyCheckable { 14 | public var omitEmpty_isEmpty: Bool { isEmpty } 15 | } 16 | 17 | extension Int: OmitEmptyCheckable { 18 | public var omitEmpty_isEmpty: Bool { self == 0 } 19 | } 20 | 21 | // etc ... 22 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-05-01.swift: -------------------------------------------------------------------------------- 1 | struct User { 2 | let name: String 3 | let followersCount: Int 4 | let isVerified: Bool 5 | } 6 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-05-02.swift: -------------------------------------------------------------------------------- 1 | @Encodable 2 | struct User { 3 | let name: String 4 | let followersCount: Int 5 | let isVerified: Bool 6 | } 7 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-05-03.swift: -------------------------------------------------------------------------------- 1 | @Encodable 2 | struct User { 3 | let name: String 4 | @CustomCoding(OmitEmpty) 5 | let followersCount: Int 6 | @CustomCoding(OmitEmpty) 7 | let isVerified: Bool 8 | } 9 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Code/create-your-own-customcoding-05-04.swift: -------------------------------------------------------------------------------- 1 | @Encodable 2 | struct User { 3 | let name: String 4 | @CustomCoding(OmitEmpty) 5 | let followersCount: Int 6 | @CustomCoding(OmitEmpty) 7 | let isVerified: Bool 8 | } 9 | 10 | let user = User(name: "Mikhail", followersCount: 0, isVerified: false) 11 | // followersCount, isVerified won't be included 12 | // {"name": "Mikhail"} 13 | let data = try JSONEncoder().encode(user) 14 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/CustomCoding/Create your own CustomCoding.tutorial: -------------------------------------------------------------------------------- 1 | @Metadata { 2 | @PageImage(purpose: card, source: custom-coding) 3 | } 4 | 5 | @Tutorial(time: 15) { 6 | @Intro(title: "Building Custom Codable Strategies with Swift") { 7 | This tutorial guides you through creation of your own ``CustomCoding(_:)`` strategy. You will implement `OmitEmpty` which works pretty much the same as [omitempty](https://www.sohamkamani.com/golang/omitempty/) in Go. Since `omitempty` affects only encoding, we will implement only this part 8 | 9 | @Image(source: custom-coding) 10 | } 11 | 12 | @Section(title: "Define Core Types") { 13 | @ContentAndMedia { 14 | Defining core types for `OmitEmpty` strategy: a custom coding name and a an "emptyness" check for a given type. 15 | } 16 | 17 | @Steps { 18 | @Step { 19 | Create a new file named `OmitEmpty.swift` 20 | } 21 | 22 | @Step { 23 | Define a struct `OmitEmpty` inside this file, it should conform to ``CustomDecodingName``. 24 | 25 | @Code(name: "OmitEmpty.swift", file: create-your-own-customcoding-01-01.swift) 26 | } 27 | 28 | @Step { 29 | Create a protocol `OmitEmpty_IsEmptyCheck` which provides a type emptyness check. 30 | 31 | @Code(name: "OmitEmpty.swift", file: create-your-own-customcoding-01-02.swift) 32 | } 33 | 34 | 35 | } 36 | } 37 | 38 | @Section(title: "Implementing Encoding") { 39 | @ContentAndMedia { 40 | Implement encoding function to describe how encoding of a property annotated with `@CustomCoding(OmitEmpty)` should be handled. 41 | } 42 | 43 | @Steps { 44 | @Step { 45 | Add a file `CustomCodingEncoding+OmitEmpty.swift` 46 | } 47 | 48 | @Step { 49 | Create an extension of ``CustomCodingEncoding`` 50 | 51 | @Code(name: "CustomCodingEncoding+OmitEmpty.swift", file: create-your-own-customcoding-02-01.swift) 52 | } 53 | 54 | @Step { 55 | Define `omitEmpty` function, which derives from `OmitEmpty`, to handle encoding of an arbiterary type, 56 | 57 | @Code(name: "CustomCodingEncoding+OmitEmpty.swift", file: create-your-own-customcoding-02-02.swift) 58 | } 59 | 60 | @Step { 61 | Implement `omitEmpty` logic 62 | 63 | @Code(name: "CustomCodingEncoding+OmitEmpty.swift", file: create-your-own-customcoding-02-03.swift) 64 | } 65 | 66 | @Step { 67 | Support optional handling. `nil` is considered to be "empty" 68 | 69 | @Code(name: "CustomCodingEncoding+OmitEmpty.swift", file: create-your-own-customcoding-02-04.swift) 70 | } 71 | } 72 | } 73 | 74 | @Section(title: "Handle Decoding") { 75 | @ContentAndMedia { 76 | A type using `OmitEmpty` can use ``Codable()`` thus requiring us to provide decoding imlementation as well, so let's do it here 77 | } 78 | 79 | Note something 80 | 81 | @Steps { 82 | @Step { 83 | Add a file `CustomCodingDecoding+OmitEmpty.swift` 84 | } 85 | 86 | @Step { 87 | In the same way create as with encoding an extension of ``CustomCodingDecoding`` 88 | 89 | @Code(name: "CustomCodingDecoding+OmitEmpty.swift", file: create-your-own-customcoding-03-01.swift) 90 | } 91 | 92 | @Step { 93 | Add `omitEmpty` function to handle decoding 94 | 95 | @Code(name: "CustomCodingDecoding+OmitEmpty.swift", file: create-your-own-customcoding-03-02.swift) 96 | } 97 | 98 | @Step { 99 | Use default imlementation of `Decodable` 100 | 101 | @Code(name: "CustomCodingDecoding+OmitEmpty.swift", file: create-your-own-customcoding-03-03.swift) 102 | } 103 | 104 | @Step { 105 | Support optionals with default `Decodable` implementation 106 | 107 | @Code(name: "CustomCodingDecoding+OmitEmpty.swift", file: create-your-own-customcoding-03-04.swift) 108 | } 109 | } 110 | } 111 | 112 | @Section(title: "Conform to `OmitEmptyCheckable`") { 113 | @ContentAndMedia { 114 | Now, we need to conform `OmitEmptyCheckable` on concrete types: `Bool`, `String`, `Int`, etc 115 | } 116 | 117 | @Steps { 118 | @Step { 119 | To make things simplier, we will define conformances in existing file `OmitEmpty.swift` 120 | 121 | @Code(name: "CustomCodingDecoding+OmitEmpty.swift", file: create-your-own-customcoding-04-01.swift) 122 | } 123 | 124 | @Step { 125 | Make conformances for `Bool`, `String`, `Int` and others 126 | 127 | @Code(name: "CustomCodingDecoding+OmitEmpty.swift", file: create-your-own-customcoding-04-02.swift) 128 | } 129 | } 130 | } 131 | 132 | @Section(title: "Usage") { 133 | @ContentAndMedia { 134 | Let's test our `OmitEmpty` implementation 135 | } 136 | 137 | @Steps { 138 | @Step { 139 | Let's assume you have a `User` struct you need to send 140 | 141 | @Code(name: "User.swift", file: create-your-own-customcoding-05-01.swift) 142 | } 143 | 144 | @Step { 145 | Conform to Encodable 146 | 147 | @Code(name: "User.swift", file: create-your-own-customcoding-05-02.swift) 148 | } 149 | 150 | @Step { 151 | Add `OmitEmpty` where it's needed 152 | 153 | @Code(name: "User.swift", file: create-your-own-customcoding-05-03.swift) 154 | } 155 | 156 | @Step { 157 | Finally you can use `JSONEncoder` to encode! 158 | 159 | @Code(name: "User.swift", file: create-your-own-customcoding-05-04.swift) 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/Resources/custom-coding.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikhailmaslo/macro-codable-kit/4491fcda995fc0036d033cd1fd791bc1d35acf9e/Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/Resources/custom-coding.jpg -------------------------------------------------------------------------------- /Sources/MacroCodableKit/MacroCodableKit.docc/Tutorials/Tutorial Table of Contents.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorials(name: "MacroCodableKit") { 2 | @Intro(title: "How to") { 3 | } 4 | 5 | @Chapter(name: "How to create your own CustomCoding") { 6 | @TutorialReference(tutorial: "doc:Create-your-own-CustomCoding") 7 | 8 | @Image(source: custom-coding) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Macros/AllOf.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllOf.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | /// Generates `Decodable` and `Encodable` conformances following the OpenAPI `allOf` specification for composition. 9 | /// 10 | /// Use a struct to describe openAPI `allOf` relationship: 11 | /// ```swift 12 | /// @AllOfCodable 13 | /// private struct AllOfExample: Equatable { 14 | /// struct B: Codable, Equatable { 15 | /// let int: Int 16 | /// let boolean: Bool 17 | /// let string: String 18 | /// } 19 | /// struct C: Codable, Equatable { 20 | /// let anotherInt: Int 21 | /// } 22 | /// 23 | /// let b: B 24 | /// let c: C 25 | /// } 26 | /// ``` 27 | /// 28 | /// Corresponding json: 29 | /// ``` 30 | /// { 31 | /// "int": 1, 32 | /// "boolean": true, 33 | /// "string": "some string", 34 | /// "anotherInt": 1 35 | /// } 36 | /// ``` 37 | @attached(extension, conformances: Decodable, Encodable, names: named(CodingKeys), named(init(from:)), named(encode(to:))) 38 | public macro AllOfCodable() = #externalMacro(module: "Macro", type: "AllOfCodableMacro") 39 | 40 | /// Generates `Decodable` conformance following the OpenAPI `allOf` specification for composition. 41 | /// 42 | /// Use a struct to describe openAPI `allOf` relationship: 43 | /// ```swift 44 | /// @AllOfDecodable 45 | /// private struct AllOfExample: Equatable { 46 | /// struct B: Codable, Equatable { 47 | /// let int: Int 48 | /// let boolean: Bool 49 | /// let string: String 50 | /// } 51 | /// struct C: Codable, Equatable { 52 | /// let anotherInt: Int 53 | /// } 54 | /// 55 | /// let b: B 56 | /// let c: C 57 | /// } 58 | /// ``` 59 | /// 60 | /// Corresponding json: 61 | /// ``` 62 | /// { 63 | /// "int": 1, 64 | /// "boolean": true, 65 | /// "string": "some string", 66 | /// "anotherInt": 1 67 | /// } 68 | /// ``` 69 | @attached(extension, conformances: Decodable, names: named(CodingKeys), named(init(from:))) 70 | public macro AllOfDecodable() = #externalMacro(module: "Macro", type: "AllOfDecodableMacro") 71 | 72 | /// Generates `Encodable` conformance following the OpenAPI `allOf` specification for composition. 73 | /// 74 | /// Use a struct to describe openAPI `allOf` relationship: 75 | /// ```swift 76 | /// @AllOfEncodable 77 | /// private struct AllOfExample: Equatable { 78 | /// struct B: Codable, Equatable { 79 | /// let int: Int 80 | /// let boolean: Bool 81 | /// let string: String 82 | /// } 83 | /// struct C: Codable, Equatable { 84 | /// let anotherInt: Int 85 | /// } 86 | /// 87 | /// let b: B 88 | /// let c: C 89 | /// } 90 | /// ``` 91 | /// 92 | /// Corresponding json: 93 | /// ``` 94 | /// { 95 | /// "int": 1, 96 | /// "boolean": true, 97 | /// "string": "some string", 98 | /// "anotherInt": 1 99 | /// } 100 | /// ``` 101 | @attached(extension, conformances: Encodable, names: named(CodingKeys), named(encode(to:))) 102 | public macro AllOfEncodable() = #externalMacro(module: "Macro", type: "AllOfEncodableMacro") 103 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Macros/Annotations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Annotations.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | /// Changes default codingKey of a property to the specified `key`. 9 | /// 10 | /// Adjust coding key annotating 11 | /// ```swift 12 | /// @Codable 13 | /// struct User { 14 | /// let birthday: Date 15 | /// let name: String 16 | /// @CodingKey("is_verified") 17 | /// let isVerified: Bool 18 | /// } 19 | /// ``` 20 | @attached(peer) 21 | public macro CodingKey(_ key: String) = #externalMacro(module: "Macro", type: "CodingKeyMacro") 22 | 23 | /// Ignores a property in encoding and decoding 24 | /// 25 | /// Skip coding entirely 26 | /// ```swift 27 | /// @Codable 28 | /// struct User { 29 | /// let birthday: Date 30 | /// let name: String 31 | /// @OmitCoding 32 | /// let isVerified: Bool 33 | /// } 34 | /// ``` 35 | @attached(peer) 36 | public macro OmitCoding() = #externalMacro(module: "Macro", type: "OmitCodingMacro") 37 | 38 | /// Provides default value if coding fails or there's no such key 39 | /// 40 | /// Each provider should be of the same type as a property it attached to 41 | /// ```swift 42 | /// @Codable 43 | /// struct User { 44 | /// let birthday: Date 45 | /// @DefaultValue(EmptyString) 46 | /// let name: String 47 | /// @DefaultValue(BoolFalse) 48 | /// let isVerified: Bool 49 | /// } 50 | /// ``` 51 | /// 52 | /// - If a value is decoded with an error, the default is returned by the given ``DefaultValueProvider``. 53 | /// - Doesn't have effect on encoding. 54 | /// - During decoding, any non-thrown errors encountered is sent to ``CustomCodingDecoding/errorHandler`` 55 | @attached(peer) 56 | public macro DefaultValue( 57 | _ type: Provider.Type 58 | ) = #externalMacro(module: "Macro", type: "DefaultValueMacro") 59 | 60 | /// Customizes decoding and encoding of a specific type 61 | /// 62 | /// If your type is generic or it can't be described a static one, for example, `Array` with an unknown generic `Element` type, then you should use ``CustomCoding(_:)`` 63 | /// 64 | /// Annotate needed property and provided a strategy. In this example, we use build-in ``TimestampedDate``, which converts timestamp to `Date` 65 | /// ```swift 66 | /// @Codable 67 | /// struct User { 68 | /// @ValueStrategy(TimestampedDate) 69 | /// let birthday: Date 70 | /// let name: String 71 | /// let isVerified: Bool 72 | /// } 73 | /// ``` 74 | /// corresponding json 75 | /// ``` 76 | /// { 77 | /// "birthday":"1696291200.0", 78 | /// "isVerified":true, 79 | /// "name":"Mikhail" 80 | /// } 81 | /// ``` 82 | /// 83 | /// - The custom strategy is defined by implementing the `ValueCodableStrategy` protocol. 84 | /// - Can be used in conjunction with the `DefaultValue` annotation. If decoding with the provided strategy fails, the default value is used. 85 | /// - During decoding and encoding, any non-thrown errors encountered is sent to ``CustomCodingDecoding/errorHandler`` or ``CustomCodingEncoding/errorHandler`` respectively. 86 | @attached(peer) 87 | public macro ValueStrategy( 88 | _ strategy: Strategy.Type 89 | ) = #externalMacro(module: "Macro", type: "ValueStrategyMacro") 90 | 91 | /// Customizes decoding and encoding of any type following conventions 92 | /// 93 | /// This macro modifies the generated `Codable` conformance, redirecting the encoding and decoding of the annotated property to custom functions. 94 | /// 95 | /// The name of the custom functions is derived from the type name passed to the ``CustomCoding(_:)`` annotation. For instance, `@CustomCoding(SafeDecoding)` directs the ``Codable()`` macro to use the `SafeDecoding` function in ``CustomCodingDecoding/safeDecoding(_:forKey:container:)-1qnwn`` and ``CustomCodingEncoding/safeDecoding(_:forKey:container:)-7cwmw`` for decoding and encoding respectively. 96 | /// 97 | /// Annotate needed property and provided a strategy. In this example, we use build-in ``SafeDecoding``, which ignores elements decoded with an error 98 | /// ```swift 99 | /// @Codable 100 | /// struct ArrayExample: Equatable { 101 | /// @CustomCoding(SafeDecoding) 102 | /// var elements: [Element] 103 | /// } 104 | /// ``` 105 | /// 106 | /// **Build-in**: 107 | /// - `SafeDecoding`: A custom decoding strategy that safely handles decoding errors for arrays and dictionaries. 108 | /// 109 | /// **Note**: 110 | /// - Implementations in ``CustomCodingDecoding`` and ``CustomCodingEncoding`` are required. 111 | @attached(peer) 112 | public macro CustomCoding( 113 | _ name: Name.Type 114 | ) = #externalMacro(module: "Macro", type: "CustomCodingMacro") 115 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Macros/Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Codable.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | /// Generates `Decodable` and `Encodable` conformances respecting property annotations. 9 | /// 10 | /// Conform to default `Decodable` and `Encodable` annotating needed type with `@Codable` 11 | /// ```swift 12 | /// @Codable 13 | /// struct User { 14 | /// let birthday: Date 15 | /// let name: String 16 | /// let isVerified: Bool 17 | /// } 18 | /// ``` 19 | /// 20 | /// If you need only `Decodable` or `Encodable` use ``Decodable()`` or ``Encodable()`` respectively 21 | /// 22 | /// **Adjust coding behaviour**: 23 | /// - Adjust coding key with ``CodingKey(_:)`` 24 | /// ```swift 25 | /// @Codable 26 | /// struct User { 27 | /// let birthday: Date 28 | /// let name: String 29 | /// @CodingKey("is_verified") 30 | /// let isVerified: Bool 31 | /// } 32 | /// ``` 33 | /// 34 | /// - Skip coding entirely by using ``OmitCoding()`` 35 | /// ```swift 36 | /// @Codable 37 | /// struct User { 38 | /// let birthday: Date 39 | /// let name: String 40 | /// @OmitCoding 41 | /// let isVerified: Bool 42 | /// } 43 | /// ``` 44 | /// 45 | /// - Provide default value if coding fails or there's no such key with ``DefaultValue(_:)`` 46 | /// ```swift 47 | /// @Codable 48 | /// struct User { 49 | /// let birthday: Date 50 | /// let name: String 51 | /// @DefaultValue(BoolFalse) 52 | /// let isVerified: Bool 53 | /// } 54 | /// ``` 55 | /// 56 | /// - Use custom decoding and encoding strategy with ``ValueStrategy(_:)``. For example, for Date conversion: 57 | /// ```swift 58 | /// @Codable 59 | /// struct User { 60 | /// @ValueStrategy(TimestampedDate) 61 | /// let birthday: Date 62 | /// let name: String 63 | /// let isVerified: Bool 64 | /// } 65 | /// ``` 66 | /// corresponding json 67 | /// ``` 68 | /// { 69 | /// "birthday":"1696291200.0", 70 | /// "isVerified":true, 71 | /// "name":"Mikhail" 72 | /// } 73 | /// ``` 74 | /// 75 | /// - Customize encoding and decoding strategies for complex or non-standard data handling with ``CustomCoding(_:)``: 76 | /// 77 | /// ```swift 78 | /// @Codable 79 | /// struct User { 80 | /// let birthday: Date 81 | /// let name: String 82 | /// let isVerified: Bool 83 | /// // Decoding won't fail if some elements are invalid 84 | /// @CustomCoding(SafeDecoding) 85 | /// let followers: [User] 86 | /// // Decoding won't fail if some dictionary values are invalid 87 | /// @CustomCoding(SafeDecoding) 88 | /// let followerById: [String: User] 89 | /// } 90 | /// ``` 91 | /// **Error Handling**: 92 | /// - Handle decoding errors with ``CustomCodingDecoding/errorHandler`` in ``CustomCodingDecoding`` 93 | /// - Handle encoding errors with ``CustomCodingEncoding/errorHandler`` in ``CustomCodingEncoding`` 94 | @attached(extension, conformances: Decodable, Encodable, names: named(CodingKeys), named(init(from:)), named(encode(to:))) 95 | public macro Codable() = #externalMacro(module: "Macro", type: "CodableMacro") 96 | 97 | /// Generates `Decodable` conformance respecting property annotations. 98 | /// 99 | /// The usage and behavior of annotations are analogous to the ``Codable()`` macro but exclusively geared towards decoding. 100 | /// 101 | /// Conform to default `Decodable` annotating needed type with `@Decodable` 102 | /// ```swift 103 | /// @Decodable 104 | /// struct Example { 105 | /// let property: String 106 | /// } 107 | /// ``` 108 | @attached(extension, conformances: Decodable, names: named(CodingKeys), named(init(from:))) 109 | public macro Decodable() = #externalMacro(module: "Macro", type: "DecodableMacro") 110 | 111 | /// Generates `Encodable` conformance respecting property annotations. 112 | /// 113 | /// The usage and behavior of annotations are analogous to the ``Codable()`` macro but exclusively geared towards encoding. 114 | /// 115 | /// Conform to default `Encodable` annotating needed type with `@Encodable` 116 | /// ```swift 117 | /// @Decodable 118 | /// struct Example { 119 | /// let property: String 120 | /// } 121 | /// ``` 122 | @attached(extension, conformances: Encodable, names: named(CodingKeys), named(encode(to:))) 123 | public macro Encodable() = #externalMacro(module: "Macro", type: "EncodableMacro") 124 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Macros/OneOf.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OneOf.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | /// Generates `Decodable` and `Encodable` conformances following the OpenAPI `oneOf` specification for composition. 9 | /// 10 | /// Describe oneOf relationship with enum consisting of cases with a single associated value 11 | /// ```swift 12 | /// @OneOfCodable 13 | /// enum Example { 14 | /// case int(Int) 15 | /// case boolean(Bool) 16 | /// case string(String) 17 | /// } 18 | /// ``` 19 | /// Corresponding jsons 20 | /// ``` 21 | /// {"int": 100} 22 | /// {"boolean": true} 23 | /// {"string": "Hello world"} 24 | /// ``` 25 | @attached(extension, conformances: Decodable, Encodable, names: named(CodingKeys), named(init(from:)), named(encode(to:))) 26 | public macro OneOfCodable() = #externalMacro(module: "Macro", type: "OneOfCodableMacro") 27 | 28 | /// Generates `Decodable` conformance following the OpenAPI `oneOf` specification for composition. 29 | /// 30 | /// Describe oneOf relationship with enum consisting of cases with a single associated value 31 | /// ```swift 32 | /// @OneOfDecodable 33 | /// enum Example { 34 | /// case int(Int) 35 | /// case boolean(Bool) 36 | /// case string(String) 37 | /// } 38 | /// ``` 39 | /// Corresponding jsons 40 | /// ``` 41 | /// {"int": 100} 42 | /// {"boolean": true} 43 | /// {"string": "Hello world"} 44 | /// ``` 45 | @attached(extension, conformances: Decodable, names: named(CodingKeys), named(init(from:))) 46 | public macro OneOfDecodable() = #externalMacro(module: "Macro", type: "OneOfDecodableMacro") 47 | 48 | /// Generates `Encodable` conformance following the OpenAPI `oneOf` specification for composition. 49 | /// 50 | /// Describe oneOf relationship with enum consisting of cases with a single associated value 51 | /// ```swift 52 | /// @OneOfEncodable 53 | /// enum Example { 54 | /// case int(Int) 55 | /// case boolean(Bool) 56 | /// case string(String) 57 | /// } 58 | /// ``` 59 | /// Corresponding jsons 60 | /// ``` 61 | /// {"int": 100} 62 | /// {"boolean": true} 63 | /// {"string": "Hello world"} 64 | /// ``` 65 | @attached(extension, conformances: Encodable, names: named(CodingKeys), named(encode(to:))) 66 | public macro OneOfEncodable() = #externalMacro(module: "Macro", type: "OneOfEncodableMacro") 67 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/CustomCoding/CustomCodingDecoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableKitUtils.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 01.10.23. 6 | // 7 | 8 | /// Namespace to handle custom decoding strategies. 9 | /// 10 | /// Serves as a centralized extension for handling custom decoding strategies across various types. Decoding errros that was ignored are passed to ``errorHandler``. 11 | /// 12 | /// 13 | /// - Example: 14 | /// ```swift 15 | /// @Decodable 16 | /// struct Exmaple { 17 | /// @ValueStrategy(SomeVariableStrategy) 18 | /// let variable: SomeVariable 19 | /// } 20 | /// ``` 21 | /// 22 | /// Will generate `Decodable` conformance using ``CustomCodingDecoding`` for variables with custom coding 23 | /// ```swift 24 | /// extension Exmaple: Decodable { 25 | /// enum CodingKeys: String, CodingKey { 26 | /// case variable 27 | /// } 28 | /// init(from decoder: Decoder) throws { 29 | /// let container = try decoder.container(keyedBy: CodingKeys.self) 30 | /// self.variable = try CustomCodingDecoding.decode( 31 | /// SomeVariable.self, 32 | /// forKey: .variable, 33 | /// container: container, 34 | /// strategy: SomeVariableStrategy.self 35 | /// ) 36 | /// } 37 | /// } 38 | /// ``` 39 | /// 40 | /// Custom strategies utilize this namespace to implement decoding implementations which then is used by ``Codable()`` and ``Decodable()``. 41 | /// 42 | /// You can provide decoding logic yourself by using ``CustomCoding(_:)`` and implementing needed extension in ``CustomCodingDecoding``. See build-in ``SafeDecoding`` along with ``safeDecoding(_:forKey:container:)-1qnwn`` and ``safeDecoding(_:forKey:container:)-3ytig``. 43 | public enum CustomCodingDecoding { 44 | /// A closure that handles errors during coding and decoding operations. 45 | /// 46 | /// A closure that captures and handles any errors encountered during the decoding process. The handling might include logging the error for debugging purposes or taking other actions as needed. 47 | public static var errorHandler: ((Error) -> Void)? 48 | 49 | /// Logs an unhandled error during an decoding operation through ``errorHandler`` 50 | public static func logError(_ error: Error) { 51 | errorHandler?(error) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/CustomCoding/CustomCodingEncoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomCodingEncoding.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | /// Namespace to handle custom encoding strategies. 9 | /// 10 | /// Serves as a centralized extension for handling custom encoding strategies across various types. Encoding errros that was ignored are passed to ``errorHandler``. 11 | /// 12 | /// 13 | /// For example the following code: 14 | /// ```swift 15 | /// @Encodable 16 | /// struct Exmaple { 17 | /// @ValueStrategy(SomeVariableStrategy) 18 | /// let variable: SomeVariable 19 | /// } 20 | /// ``` 21 | /// 22 | /// Will generate `Encodable` conformance using ``CustomCodingEncoding`` for variables with custom coding 23 | /// ```swift 24 | /// extension Exmaple: Encodable { 25 | /// enum CodingKeys: String, CodingKey { 26 | /// case variable 27 | /// } 28 | /// func encode(to encoder: Encoder) throws { 29 | /// var container = encoder.container(keyedBy: CodingKeys.self) 30 | /// try CustomCodingEncoding.encode( 31 | /// self.variable, 32 | /// forKey: .variable, 33 | /// container: &container, 34 | /// strategy: SomeVariableStrategy.self 35 | /// ) 36 | /// } 37 | /// } 38 | /// ``` 39 | /// 40 | /// Custom strategies utilize this namespace to implement encoding implementations which then is used by ``Codable()`` and ``Encodable()``. 41 | /// 42 | /// You can provide encoding logic yourself by using ``CustomCoding(_:)`` and implementing needed extension in ``CustomCodingEncoding``. See build-in ``SafeDecoding`` along with ``safeDecoding(_:forKey:container:)-7cwmw`` and ``safeDecoding(_:forKey:container:)-9b19z``. 43 | public enum CustomCodingEncoding { 44 | /// A closure that handles errors during coding and encoding operations. 45 | /// 46 | /// A closure that captures and handles any errors encountered during the encoding process. The handling might include logging the error for debugging purposes or taking other actions as needed. 47 | public static var errorHandler: ((Error) -> Void)? 48 | 49 | /// Logs an unhandled error during an encoding operation through ``errorHandler`` 50 | public static func logError(_ error: Error) { 51 | errorHandler?(error) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/CustomCoding/Predefined/SafeDecoding/SafeDecoding+Array.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafeDecoding+Array.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 01.10.23. 6 | // 7 | 8 | // MARK: - Decoding 9 | 10 | public extension CustomCodingDecoding { 11 | private struct AnyDecodableValue: Decodable {} 12 | 13 | /// Decodes array, suppressing individual element which failed to decode. 14 | /// 15 | /// It logs the error and continues to the next element, ensuring that all decodable elements are captured. 16 | /// 17 | /// - Parameters: 18 | /// - _: The type of the array elements. 19 | /// - forKey: The key to decode the array. 20 | /// - container: The container to decode from. 21 | /// 22 | /// - Throws: An error if decoding fails. 23 | /// 24 | /// - Returns: An optional array of decoded elements, or `nil` if the key is not present. 25 | static func safeDecoding( 26 | _: [Element].Type, 27 | forKey key: KeyedDecodingContainer.Key, 28 | container: KeyedDecodingContainer 29 | ) throws -> [Element] { 30 | var elements: [Element] = [] 31 | let decoder = try container.superDecoder(forKey: key) 32 | var container = try decoder.unkeyedContainer() 33 | 34 | while !container.isAtEnd { 35 | do { 36 | let value = try container.decode(Element.self) 37 | elements.append(value) 38 | } catch { 39 | _ = try? container.decode(AnyDecodableValue.self) 40 | 41 | logError(error) 42 | } 43 | } 44 | return elements 45 | } 46 | 47 | /// Decodes array, suppressing individual element which failed to decode if `key` exists. 48 | /// 49 | /// It logs the error and continues to the next element, ensuring that all decodable elements are captured. 50 | /// 51 | /// - Parameters: 52 | /// - _: The type of the array elements. 53 | /// - forKey: The key to decode the array. 54 | /// - container: The container to decode from. 55 | /// 56 | /// - Throws: An error if decoding fails. 57 | /// 58 | /// - Returns: An optional array of decoded elements, or `nil` if the key is not present. 59 | static func safeDecoding( 60 | _: [Element]?.Type, 61 | forKey key: KeyedDecodingContainer.Key, 62 | container: KeyedDecodingContainer 63 | ) throws -> [Element]? { 64 | if container.contains(key) { 65 | return try safeDecoding([Element].self, forKey: key, container: container) 66 | } else { 67 | return nil 68 | } 69 | } 70 | } 71 | 72 | // MARK: - Encoding 73 | 74 | public extension CustomCodingEncoding { 75 | /// Default encode implementation of an array 76 | static func safeDecoding( 77 | _ value: [Element], 78 | forKey key: KeyedEncodingContainer.Key, 79 | container: inout KeyedEncodingContainer 80 | ) throws { 81 | try container.encode(value, forKey: key) 82 | } 83 | 84 | /// Default encode implementation of an optional array 85 | static func safeDecoding( 86 | _ value: [Element]?, 87 | forKey key: KeyedEncodingContainer.Key, 88 | container: inout KeyedEncodingContainer 89 | ) throws { 90 | guard let value = value else { return } 91 | 92 | try safeDecoding(value, forKey: key, container: &container) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/CustomCoding/Predefined/SafeDecoding/SafeDecoding+Dictionary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafeDecoding+Dictionary.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 01.10.23. 6 | // 7 | 8 | #if canImport(Foundation) 9 | import Foundation 10 | 11 | protocol KeyDecodingStrategyProvider { 12 | var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy { get } 13 | } 14 | 15 | extension JSONDecoder: KeyDecodingStrategyProvider {} 16 | #endif 17 | 18 | // MARK: - Decoding 19 | 20 | public extension CustomCodingDecoding { 21 | private struct AnyDecodableValue: Decodable {} 22 | // Copy of https://github.com/apple/swift/blob/c0dc3173b6970466434770927aaaeaeb46f0cc10/stdlib/public/core/Codable.swift.gyb#L1974 23 | internal struct _DictionaryCodingKey: CodingKey { 24 | public var stringValue: String 25 | public var intValue: Int? 26 | 27 | public init?(stringValue: String) { 28 | self.stringValue = stringValue 29 | intValue = Int(stringValue) 30 | } 31 | 32 | public init?(intValue: Int) { 33 | self.intValue = intValue 34 | stringValue = String(intValue) 35 | } 36 | } 37 | 38 | /// Decodes dictionary suppressing individual value which failed to decode. 39 | /// 40 | /// Valid key types are `String` and `Int`. It tries to decode each value. If a value fails to decode, it logs the error and continues to the next key. 41 | /// Respects `JSONDecoder.KeyDecodingStrategy` 42 | static func safeDecoding( 43 | _: [Key: Value].Type, 44 | forKey dictKey: KeyedDecodingContainer.Key, 45 | container: KeyedDecodingContainer 46 | ) throws -> [Key: Value] { 47 | if Key.self == String.self { 48 | var result: [Key: Value] = [:] 49 | let decoder = try container.superDecoder(forKey: dictKey) 50 | let container = try decoder.container(keyedBy: _DictionaryCodingKey.self) 51 | 52 | #if canImport(Foundation) 53 | switch (decoder as? KeyDecodingStrategyProvider)?.keyDecodingStrategy { 54 | case .useDefaultKeys: 55 | for key in container.allKeys { 56 | do { 57 | let value = try container.decode(Value.self, forKey: key) 58 | result[key.stringValue as! Key] = value 59 | } catch { 60 | _ = try? container.decode(AnyDecodableValue.self, forKey: key) 61 | } 62 | } 63 | default: 64 | // We have to sort keys in order to match keys with snake case with camel case 65 | // keys which respect `keyDecodingStrategy` 66 | let resultKeys = try decoder.singleValueContainer() 67 | .decode([String: AnyDecodableValue].self) 68 | .keys 69 | .sorted() 70 | // keys as they appear in container 71 | let rawKeys = container 72 | .allKeys 73 | .sorted(by: { $0.stringValue < $1.stringValue }) 74 | 75 | assert(resultKeys.count == rawKeys.count) 76 | 77 | var resultKeyIndex = resultKeys.startIndex 78 | var rawKeyIndex = rawKeys.startIndex 79 | while resultKeyIndex < resultKeys.endIndex, rawKeyIndex < rawKeys.endIndex { 80 | let rawKey = rawKeys[rawKeyIndex] 81 | let resultKey = resultKeys[resultKeyIndex] 82 | 83 | do { 84 | let value = try container.decode(Value.self, forKey: rawKey) 85 | result[resultKey as! Key] = value 86 | } catch { 87 | _ = try? container.decode(AnyDecodableValue.self, forKey: rawKey) 88 | } 89 | 90 | rawKeyIndex = rawKeys.index(after: rawKeyIndex) 91 | resultKeyIndex = resultKeys.index(after: resultKeyIndex) 92 | } 93 | } 94 | #else 95 | for key in container.allKeys { 96 | do { 97 | let value = try container.decode(Value.self, forKey: key) 98 | result[key.stringValue as! Key] = value 99 | } catch { 100 | _ = try? container.decode(AnyDecodableValue.self, forKey: key) 101 | } 102 | } 103 | #endif 104 | return result 105 | } else if Key.self == Int.self { 106 | var result: [Key: Value] = [:] 107 | let decoder = try container.superDecoder(forKey: dictKey) 108 | let container = try decoder.container(keyedBy: _DictionaryCodingKey.self) 109 | for key in container.allKeys { 110 | guard key.intValue != nil else { 111 | var codingPath = decoder.codingPath 112 | codingPath.append(key) 113 | throw DecodingError.typeMismatch( 114 | Int.self, DecodingError.Context( 115 | codingPath: codingPath, 116 | debugDescription: "Expected Int key but found String key instead." 117 | ) 118 | ) 119 | } 120 | 121 | do { 122 | let value = try container.decode(Value.self, forKey: key) 123 | result[key.intValue! as! Key] = value 124 | } catch { 125 | _ = try? container.decode(AnyDecodableValue.self, forKey: key) 126 | } 127 | } 128 | return result 129 | } else { 130 | throw DecodingError.dataCorrupted( 131 | DecodingError.Context( 132 | codingPath: container.codingPath, 133 | debugDescription: "Couldn't decode key of type \(Key.self)" 134 | ) 135 | ) 136 | } 137 | } 138 | 139 | /// Decodes dictionary if provided `key` exists suppressing individual values which failed to decode. 140 | /// 141 | /// Valid key types are `String` and `Int`. It tries to decode each value. If a value fails to decode, it logs the error and continues to the next key. 142 | /// Respects `JSONDecoder.KeyDecodingStrategy` 143 | static func safeDecoding( 144 | _: [Key: Value]?.Type, 145 | forKey dictKey: KeyedDecodingContainer.Key, 146 | container: KeyedDecodingContainer 147 | ) throws -> [Key: Value]? { 148 | if container.contains(dictKey) { 149 | return try safeDecoding([Key: Value].self, forKey: dictKey, container: container) 150 | } else { 151 | return nil 152 | } 153 | } 154 | } 155 | 156 | // MARK: - Encoding 157 | 158 | public extension CustomCodingEncoding { 159 | /// Default dictionary encoding 160 | static func safeDecoding( 161 | _ value: [Key: Value], 162 | forKey key: KeyedEncodingContainer.Key, 163 | container: inout KeyedEncodingContainer 164 | ) throws { 165 | try container.encode(value, forKey: key) 166 | } 167 | 168 | /// Default dictionary encoding for an optional value 169 | static func safeDecoding( 170 | _ value: [Key: Value]?, 171 | forKey key: KeyedEncodingContainer.Key, 172 | container: inout KeyedEncodingContainer 173 | ) throws { 174 | guard let value else { return } 175 | 176 | try safeDecoding(value, forKey: key, container: &container) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/CustomCoding/Predefined/SafeDecoding/SafeDecoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafeDecoding.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 04.10.23. 6 | // 7 | 8 | /// Decodes arrays and dictionaries in a safe manner, while gracefully handling decoding errors during the process 9 | /// 10 | /// ``SafeDecoding`` provides a robust way to decode collections, ensuring that decodable elements are captured and errors are logged for further inspection. Suppressed errors are transfered to ``CustomCodingDecoding/logError(_:)`` 11 | /// 12 | /// To utilize this strategy, annotate the property with ``CustomCoding(_:)`` passing ``SafeDecoding``. Ensure the parent type is annotated with one of ``Codable()`` or ``Decodable()``. 13 | /// 14 | /// - ``SafeDecoding`` with Arrays: 15 | /// - **Usage**: 16 | /// ```swift 17 | /// @Codable 18 | /// struct ArrayExample: Equatable { 19 | /// @CustomCoding(SafeDecoding) 20 | /// var elements: [Element] 21 | /// } 22 | /// ``` 23 | /// - **Decoding** ``CustomCodingDecoding/safeDecoding(_:forKey:container:)-1qnwn`` or ``CustomCodingDecoding/safeDecoding(_:forKey:container:)-3ytig`` for optional 24 | /// - **Encoding**: Follows the standard encoding process for arrays, see``CustomCodingEncoding/safeDecoding(_:forKey:container:)-7cwmw`` and ``CustomCodingEncoding/safeDecoding(_:forKey:container:)-7cwmw`` for more details 25 | /// 26 | /// - ``SafeDecoding`` with Dictionary: 27 | /// - supports custom [`JSONDecoder.KeyDecodingStrategy`](https://developer.apple.com/documentation/foundation/jsondecoder/keydecodingstrategy) 28 | /// - only handles keys of type [`String`](https://developer.apple.com/documentation/swift/string) or [`Int`](https://developer.apple.com/documentation/swift/int) 29 | /// - **Usage**: 30 | /// ```swift 31 | /// @Codable 32 | /// struct DictionaryExample: Equatable { 33 | /// @CustomCoding(SafeDecoding) 34 | /// var entries: [String: Element] 35 | /// } 36 | /// ``` 37 | /// - **Decoding** ``CustomCodingDecoding/safeDecoding(_:forKey:container:)-5nus0`` and ``CustomCodingDecoding/safeDecoding(_:forKey:container:)-9nz0q`` for optional 38 | /// - **Encoding**: Follows the standard encoding process for dictionaries, see ``CustomCodingEncoding/safeDecoding(_:forKey:container:)-5sgwu`` and ``CustomCodingEncoding/safeDecoding(_:forKey:container:)-54c5h`` for more details 39 | public struct SafeDecoding: CustomDecodingName {} 40 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/CustomCoding/Types/CustomCodingEncoding+Types.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomCodingEncoding.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | public extension CustomCodingEncoding { 9 | // MARK: - Bool 10 | 11 | static func encode( 12 | _ value: Bool, 13 | forKey key: KeyedEncodingContainer.Key, 14 | container: inout KeyedEncodingContainer, 15 | strategy: Strategy.Type 16 | ) throws 17 | where Strategy: ValueCodableStrategy, 18 | Strategy.Value == Bool 19 | { 20 | let encoder = container.superEncoder(forKey: key) 21 | try strategy.encode(value: value, to: encoder) 22 | } 23 | 24 | // MARK: - String 25 | 26 | static func encode( 27 | _ value: String, 28 | forKey key: KeyedEncodingContainer.Key, 29 | container: inout KeyedEncodingContainer, 30 | strategy: Strategy.Type 31 | ) throws 32 | where Strategy: ValueCodableStrategy, 33 | Strategy.Value == String 34 | { 35 | let encoder = container.superEncoder(forKey: key) 36 | try strategy.encode(value: value, to: encoder) 37 | } 38 | 39 | // MARK: - Double 40 | 41 | static func encode( 42 | _ value: Double, 43 | forKey key: KeyedEncodingContainer.Key, 44 | container: inout KeyedEncodingContainer, 45 | strategy: Strategy.Type 46 | ) throws 47 | where Strategy: ValueCodableStrategy, 48 | Strategy.Value == Double 49 | { 50 | let encoder = container.superEncoder(forKey: key) 51 | try strategy.encode(value: value, to: encoder) 52 | } 53 | 54 | // MARK: - Float 55 | 56 | static func encode( 57 | _ value: Float, 58 | forKey key: KeyedEncodingContainer.Key, 59 | container: inout KeyedEncodingContainer, 60 | strategy: Strategy.Type 61 | ) throws 62 | where Strategy: ValueCodableStrategy, 63 | Strategy.Value == Float 64 | { 65 | let encoder = container.superEncoder(forKey: key) 66 | try strategy.encode(value: value, to: encoder) 67 | } 68 | 69 | // MARK: - Int 70 | 71 | static func encode( 72 | _ value: Int, 73 | forKey key: KeyedEncodingContainer.Key, 74 | container: inout KeyedEncodingContainer, 75 | strategy: Strategy.Type 76 | ) throws 77 | where Strategy: ValueCodableStrategy, 78 | Strategy.Value == Int 79 | { 80 | let encoder = container.superEncoder(forKey: key) 81 | try strategy.encode(value: value, to: encoder) 82 | } 83 | 84 | // MARK: - Int8 85 | 86 | static func encode( 87 | _ value: Int8, 88 | forKey key: KeyedEncodingContainer.Key, 89 | container: inout KeyedEncodingContainer, 90 | strategy: Strategy.Type 91 | ) throws 92 | where Strategy: ValueCodableStrategy, 93 | Strategy.Value == Int8 94 | { 95 | let encoder = container.superEncoder(forKey: key) 96 | try strategy.encode(value: value, to: encoder) 97 | } 98 | 99 | // MARK: - Int16 100 | 101 | static func encode( 102 | _ value: Int16, 103 | forKey key: KeyedEncodingContainer.Key, 104 | container: inout KeyedEncodingContainer, 105 | strategy: Strategy.Type 106 | ) throws 107 | where Strategy: ValueCodableStrategy, 108 | Strategy.Value == Int16 109 | { 110 | let encoder = container.superEncoder(forKey: key) 111 | try strategy.encode(value: value, to: encoder) 112 | } 113 | 114 | // MARK: - Int32 115 | 116 | static func encode( 117 | _ value: Int32, 118 | forKey key: KeyedEncodingContainer.Key, 119 | container: inout KeyedEncodingContainer, 120 | strategy: Strategy.Type 121 | ) throws 122 | where Strategy: ValueCodableStrategy, 123 | Strategy.Value == Int32 124 | { 125 | let encoder = container.superEncoder(forKey: key) 126 | try strategy.encode(value: value, to: encoder) 127 | } 128 | 129 | // MARK: - Int64 130 | 131 | static func encode( 132 | _ value: Int64, 133 | forKey key: KeyedEncodingContainer.Key, 134 | container: inout KeyedEncodingContainer, 135 | strategy: Strategy.Type 136 | ) throws 137 | where Strategy: ValueCodableStrategy, 138 | Strategy.Value == Int64 139 | { 140 | let encoder = container.superEncoder(forKey: key) 141 | try strategy.encode(value: value, to: encoder) 142 | } 143 | 144 | // MARK: - UInt 145 | 146 | static func encode( 147 | _ value: UInt, 148 | forKey key: KeyedEncodingContainer.Key, 149 | container: inout KeyedEncodingContainer, 150 | strategy: Strategy.Type 151 | ) throws 152 | where Strategy: ValueCodableStrategy, 153 | Strategy.Value == UInt 154 | { 155 | let encoder = container.superEncoder(forKey: key) 156 | try strategy.encode(value: value, to: encoder) 157 | } 158 | 159 | // MARK: - UInt8 160 | 161 | static func encode( 162 | _ value: UInt8, 163 | forKey key: KeyedEncodingContainer.Key, 164 | container: inout KeyedEncodingContainer, 165 | strategy: Strategy.Type 166 | ) throws 167 | where Strategy: ValueCodableStrategy, 168 | Strategy.Value == UInt8 169 | { 170 | let encoder = container.superEncoder(forKey: key) 171 | try strategy.encode(value: value, to: encoder) 172 | } 173 | 174 | // MARK: - UInt16 175 | 176 | static func encode( 177 | _ value: UInt16, 178 | forKey key: KeyedEncodingContainer.Key, 179 | container: inout KeyedEncodingContainer, 180 | strategy: Strategy.Type 181 | ) throws 182 | where Strategy: ValueCodableStrategy, 183 | Strategy.Value == UInt16 184 | { 185 | let encoder = container.superEncoder(forKey: key) 186 | try strategy.encode(value: value, to: encoder) 187 | } 188 | 189 | // MARK: - UInt32 190 | 191 | static func encode( 192 | _ value: UInt32, 193 | forKey key: KeyedEncodingContainer.Key, 194 | container: inout KeyedEncodingContainer, 195 | strategy: Strategy.Type 196 | ) throws 197 | where Strategy: ValueCodableStrategy, 198 | Strategy.Value == UInt32 199 | { 200 | let encoder = container.superEncoder(forKey: key) 201 | try strategy.encode(value: value, to: encoder) 202 | } 203 | 204 | // MARK: - UInt64 205 | 206 | static func encode( 207 | _ value: UInt64, 208 | forKey key: KeyedEncodingContainer.Key, 209 | container: inout KeyedEncodingContainer, 210 | strategy: Strategy.Type 211 | ) throws 212 | where Strategy: ValueCodableStrategy, 213 | Strategy.Value == UInt64 214 | { 215 | let encoder = container.superEncoder(forKey: key) 216 | try strategy.encode(value: value, to: encoder) 217 | } 218 | 219 | // MARK: - K: Encodable 220 | 221 | static func encode( 222 | _ value: T, 223 | forKey key: KeyedEncodingContainer.Key, 224 | container: inout KeyedEncodingContainer, 225 | strategy: Strategy.Type 226 | ) throws 227 | where Strategy: ValueCodableStrategy, 228 | Strategy.Value == T 229 | { 230 | let encoder = container.superEncoder(forKey: key) 231 | try strategy.encode(value: value, to: encoder) 232 | } 233 | 234 | static func encode( 235 | _ value: T, 236 | forKey key: KeyedEncodingContainer.Key, 237 | container: inout KeyedEncodingContainer, 238 | strategy: Strategy.Type 239 | ) throws 240 | where Strategy: ValueCodableStrategy, 241 | Strategy.Value == T.Wrapped, 242 | T: OptionalProtocol 243 | { 244 | guard let unwrappedValue = value.asOptional() else { return } 245 | 246 | let encoder = container.superEncoder(forKey: key) 247 | try strategy.encode(value: unwrappedValue, to: encoder) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/CustomDecodingName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomDecodingName.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | /// A marker protocol providing types in ``CustomCoding(_:)`` 9 | /// 10 | /// A marker protocol used to specify the name of custom encoding and decoding functions. Types conforming to this protocol don't functionally do anything, but their names are used to derive the name of custom functions in ``CustomCodingDecoding`` and ``CustomCodingEncoding`` namespaces. 11 | public protocol CustomDecodingName {} 12 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/DefaultProviders/BoolFalse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoolFalse.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | /// Provides a default `false` value for booleans in ``DefaultValue(_:)``. 9 | public typealias BoolFalse = DefaultFalseValueProvider 10 | 11 | /// A struct that provides a default value of `false` for booleans. 12 | public struct DefaultFalseValueProvider: DefaultValueProvider { 13 | /// The default value of `false`. 14 | public static var defaultValue: Bool { false } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/DefaultProviders/BoolTrue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoolTrue.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | /// Provides a default `true` value for booleans in ``DefaultValue(_:)``. 9 | public typealias BoolTrue = DefaultTrueValueProvider 10 | 11 | /// A struct that provides a default value of `true` for booleans. 12 | public struct DefaultTrueValueProvider: DefaultValueProvider { 13 | /// The default value of `true`. 14 | public static var defaultValue: Bool { true } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/DefaultProviders/DoubleZero.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoubleZero.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 01.10.23. 6 | // 7 | 8 | /// Provides a default `0` value for doubles in ``DefaultValue(_:)``. 9 | public typealias DoubleZero = DefaultDoubleZeroValueProvider 10 | 11 | /// A struct that provides a default value of `0` for doubles. 12 | public struct DefaultDoubleZeroValueProvider: DefaultValueProvider { 13 | /// The default value for double zero. 14 | public static var defaultValue: Double { 0 } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/DefaultProviders/EmptyString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyString.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 01.10.23. 6 | // 7 | 8 | /// Provides a default `""` value for strings in ``DefaultValue(_:)``. 9 | public typealias EmptyString = DefaultEmptyStringValueProvider 10 | 11 | /// A struct that provides a default value of `""` for string. 12 | public struct DefaultEmptyStringValueProvider: DefaultValueProvider { 13 | public static var defaultValue: String { "" } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/DefaultProviders/IntZero.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntZero.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 01.10.23. 6 | // 7 | 8 | /// Provides a default `0` value for ints in ``DefaultValue(_:)``. 9 | public typealias IntZero = DefaultIntZeroValueProvider 10 | 11 | /// A struct that provides a default value of `0` for ints. 12 | public struct DefaultIntZeroValueProvider: DefaultValueProvider { 13 | public static var defaultValue: Int { 0 } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/DefaultValueStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultValueStrategy.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | /// A protocol for providing default values in ``DefaultValue(_:)`` macro 9 | public protocol DefaultValueProvider { 10 | /// The type of the default value. 11 | associatedtype DefaultValue: Codable 12 | 13 | /// The default value. 14 | static var defaultValue: DefaultValue { get } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/OptionalProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalProtocol.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | /// A protocol that represents an optional value. 9 | /// 10 | /// This protocol is useful to handle optional types in decoding and encoding 11 | public protocol OptionalProtocol { 12 | associatedtype Wrapped 13 | 14 | /// Converts the value to an optional. 15 | /// 16 | /// - Returns: The wrapped value if it exists, otherwise `nil`. 17 | func asOptional() -> Wrapped? 18 | } 19 | 20 | extension Optional: OptionalProtocol { 21 | public func asOptional() -> Wrapped? { 22 | switch self { 23 | case let .some(wrapped): 24 | return wrapped 25 | case .none: 26 | return nil 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/ValueCodableStrategies/Base64Strategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Base64Strategy.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 02.10.23. 6 | // 7 | 8 | #if canImport(Foundation) 9 | import Foundation 10 | 11 | /// Decodes base64 `String` into `Data` and encodes `Data` to base64 `String` in ``ValueStrategy(_:)``. 12 | public typealias Base64Data = Base64Strategy 13 | 14 | /// Decodes base64 `String` into `Data` and encodes `Data` to base64 `String`. 15 | public struct Base64Strategy: ValueCodableStrategy { 16 | public typealias Value = Data 17 | 18 | /// Decodes a `Data` value from base64 `String`. 19 | /// 20 | /// - Throws: `DecodingError.dataCorrupted` if the Base64 string is invalid. 21 | /// 22 | /// - Returns: The decoded `Data` value from base64 String. 23 | public static func decode(from decoder: Decoder) throws -> Value { 24 | let container = try decoder.singleValueContainer() 25 | let base64Encoded = try container.decode(String.self) 26 | if let data = Data(base64Encoded: base64Encoded) { 27 | return data 28 | } else { 29 | throw DecodingError.dataCorrupted( 30 | DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid Base64 string: \(base64Encoded)") 31 | ) 32 | } 33 | } 34 | 35 | /// Encodes the given `Data` value to the given encoder as base64 String. 36 | public static func encode(value: Value, to encoder: Encoder) throws { 37 | var container = encoder.singleValueContainer() 38 | let base64EncodedString = value.base64EncodedString() 39 | try container.encode(base64EncodedString) 40 | } 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/ValueCodableStrategies/Date/DateFormatterStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatterStrategy.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 26.09.23. 6 | // 7 | 8 | /// Abstracts date formatters 9 | public protocol DateFormatterProtocol { 10 | /// Converts a `Date` object into a string representation. 11 | func string(from date: Date) -> String 12 | 13 | /// Converts a string representation of a date into a `Date` object. 14 | func date(from string: String) -> Date? 15 | } 16 | 17 | /// Provides a `DateFormatterProtocol` instance. 18 | public protocol DateFormatterProvider { 19 | static var dateFormatter: DateFormatterProtocol { get } 20 | } 21 | 22 | /// A strategy for encoding and decoding `Date` values using a `DateFormatterProvider`. 23 | public struct DateFormatterStrategy: ValueCodableStrategy { 24 | public typealias Value = Date 25 | 26 | /// Decodes a `String` value from the given decoder which then converts to `Date` using provided by date formatter from `Provider`. 27 | /// 28 | /// - Throws: `DecodingError.dataCorrupted` if formatting fails 29 | /// 30 | /// - Returns: The decoded `Data` value from base64 String. 31 | public static func decode(from decoder: Decoder) throws -> Value { 32 | let container = try decoder.singleValueContainer() 33 | let value = try container.decode(String.self) 34 | guard let date = Provider.dateFormatter.date(from: value) else { 35 | throw DecodingError.dataCorrupted( 36 | DecodingError.Context( 37 | codingPath: decoder.codingPath, 38 | debugDescription: "'\(value)' is an invalid date format. Provider = \(Provider.self)" 39 | ) 40 | ) 41 | } 42 | return date 43 | } 44 | 45 | /// Encodes the given date `Date` value to the given encoder. 46 | public static func encode(value: Value, to encoder: Encoder) throws { 47 | var container = encoder.singleValueContainer() 48 | try container.encode(Provider.dateFormatter.string(from: value)) 49 | } 50 | } 51 | 52 | #if canImport(Foundation) 53 | import Foundation 54 | 55 | extension ISO8601DateFormatter: DateFormatterProtocol {} 56 | 57 | extension DateFormatter: DateFormatterProtocol {} 58 | #endif 59 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/ValueCodableStrategies/Date/ISO8601DateFormatterProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ISO8601DateFormatterProvider.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 02.10.23. 6 | // 7 | 8 | #if canImport(Foundation) 9 | import Foundation 10 | 11 | // MARK: - ISO8601Default 12 | 13 | /// Decodes and encodes `yyyy-MM-dd'T'HH:mm:ssXXX` date in ``ValueStrategy(_:)`` 14 | /// 15 | /// Example: `2023-10-03T10:15:30+00:00` 16 | public typealias ISO8601Default = DateFormatterStrategy 17 | 18 | /// Provides `yyyy-MM-dd'T'HH:mm:ssXXX` date formatter 19 | /// 20 | /// Example: `2023-10-03T10:15:30+00:00` 21 | public struct ISO8601DefaultDateFormatterProvider: DateFormatterProvider { 22 | public static let dateFormatter: DateFormatterProtocol = ISO8601DateFormatter() 23 | } 24 | 25 | // MARK: - ISO8601WithFullDate 26 | 27 | /// Decodes and encodes `yyyy-MM-dd` date in ``ValueStrategy(_:)`` 28 | /// 29 | /// Example: `2023-10-03` 30 | public typealias ISO8601WithFullDate = DateFormatterStrategy 31 | 32 | /// Provides `yyyy-MM-dd` date formatter 33 | /// 34 | /// Example: `2023-10-03` 35 | public struct ISO8601FullDateDateFormatterProvider: DateFormatterProvider { 36 | public static let dateFormatter: DateFormatterProtocol = { 37 | let formatter = ISO8601DateFormatter() 38 | formatter.formatOptions = .withFullDate 39 | return formatter 40 | }() 41 | } 42 | 43 | // MARK: - ISO8601WithFractionalSeconds 44 | 45 | /// Decodes and encodes `yyyy-MM-dd'T'HH:mm:ss.SSSXXX` date in ``ValueStrategy(_:)`` 46 | /// 47 | /// Example: `2023-10-03T10:15:30.123+00:00` 48 | public typealias ISO8601WithFractionalSeconds = DateFormatterStrategy 49 | 50 | /// Provides `yyyy-MM-dd'T'HH:mm:ss.SSSXXX` date formatter 51 | /// 52 | /// Example: `2023-10-03T10:15:30.123+00:00` 53 | public struct ISO8601WithFractionalSecondsDateFormatterProvider: DateFormatterProvider { 54 | public static let dateFormatter: DateFormatterProtocol = { 55 | let formatter = ISO8601DateFormatter() 56 | formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] 57 | return formatter 58 | }() 59 | } 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/ValueCodableStrategies/Date/RFC2822DateFormatterProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFC2822DateFormatterProvider.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 02.10.23. 6 | // 7 | 8 | #if canImport(Foundation) 9 | import Foundation 10 | 11 | /// Decodes and encodes `EEE, d MMM y HH:mm:ss zzz` date in ``ValueStrategy(_:)`` 12 | /// 13 | /// Example: `Tue, 3 Oct 2023 10:15:30 GMT` 14 | public typealias RFC2822Date = DateFormatterStrategy 15 | 16 | /// Provides `EEE, d MMM y HH:mm:ss zzz` date formatter 17 | /// 18 | /// - Note: Uses `+0` time zone 19 | /// 20 | /// Example: `2023-10-03T10:15:30.123+00:00` 21 | public struct RFC2822DateFormatterProvider: DateFormatterProvider { 22 | public static let dateFormatter: DateFormatterProtocol = { 23 | let dateFormatter = DateFormatter() 24 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 25 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 26 | dateFormatter.dateFormat = "EEE, d MMM y HH:mm:ss zzz" 27 | return dateFormatter 28 | }() 29 | } 30 | #endif 31 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/ValueCodableStrategies/Date/RFC3339DateFormatterProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFC3339DateFormatterProvider.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 02.10.23. 6 | // 7 | 8 | #if canImport(Foundation) 9 | import Foundation 10 | 11 | /// Decodes and encodes `yyyy-MM-dd'T'HH:mm:ssZ` date in ``ValueStrategy(_:)`` 12 | /// 13 | /// Example: `2023-10-03T10:15:30Z` 14 | public typealias RFC3339Date = DateFormatterStrategy 15 | 16 | /// Provides `yyyy-MM-dd'T'HH:mm:ssZ` date formatter 17 | /// 18 | /// - Note: Uses `+0` time zone 19 | /// 20 | /// Example: `2023-10-03T10:15:30Z` 21 | public struct RFC3339DateFormatterProvider: DateFormatterProvider { 22 | public static let dateFormatter: DateFormatterProtocol = { 23 | let dateFormatter = DateFormatter() 24 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 25 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 26 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 27 | return dateFormatter 28 | }() 29 | } 30 | #endif 31 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/ValueCodableStrategies/Date/TimestampedDate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimestampedDate.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 02.10.23. 6 | // 7 | 8 | #if canImport(Foundation) 9 | import Foundation 10 | 11 | /// Decodes and encodes unix timestamp in ``ValueStrategy(_:)``. 12 | /// 13 | /// Works with `String` and `Double` representations. 14 | /// 15 | /// Example: `1696291200.0` or `"1696291200.0"` 16 | public typealias TimestampedDate = TimestampStrategy 17 | 18 | /// Decodes and encodes unix timestamp in ``ValueStrategy(_:)``. 19 | /// 20 | /// Works with `String` and `Double` representations. 21 | /// 22 | /// Example: `1696291200.0` or `"1696291200.0"` 23 | public struct TimestampStrategy: ValueCodableStrategy { 24 | public typealias Value = Date 25 | 26 | /// Decodes a `Date` value from the given decoder. 27 | /// 28 | /// - Parameter decoder: The decoder to read data from. 29 | /// 30 | /// - Throws: An error if coudn't decode either `String` or `TimeInterval` from decoder 31 | /// 32 | /// - Returns: The decoded `Date` value. 33 | public static func decode(from decoder: Decoder) throws -> Value { 34 | let container = try decoder.singleValueContainer() 35 | let timeInterval: TimeInterval? 36 | 37 | do { 38 | timeInterval = try container.decode(TimeInterval.self) 39 | } catch { 40 | timeInterval = (try? container.decode(String.self)).flatMap { Double($0) } 41 | } 42 | 43 | guard let timeInterval else { 44 | throw DecodingError.dataCorrupted( 45 | DecodingError.Context( 46 | codingPath: decoder.codingPath, 47 | debugDescription: "Couldn't decode neither TimeInterval nor String" 48 | ) 49 | ) 50 | } 51 | return Date(timeIntervalSince1970: timeInterval) 52 | } 53 | 54 | /// Encodes the given `Date` value into the given encoder as `TimeInterval`. 55 | public static func encode(value: Value, to encoder: Encoder) throws { 56 | var container = encoder.singleValueContainer() 57 | try container.encode(value.timeIntervalSince1970) 58 | } 59 | } 60 | #endif 61 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/ValueCodableStrategies/Date/YearMonthDayDateFormatterProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YearMonthDayDateFormatterProvider.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 02.10.23. 6 | // 7 | 8 | #if canImport(Foundation) 9 | import Foundation 10 | 11 | /// Decodes and encodes `yyyy-MM-dd` date in ``ValueStrategy(_:)`` 12 | /// 13 | /// Example: `2023-10-03` 14 | public typealias YearMonthDayDate = DateFormatterStrategy 15 | 16 | /// Provides `yyyy-MM-dd` date formatter 17 | /// 18 | /// Example: `2023-10-03` 19 | public struct YearMonthDayDateFormatterProvider: DateFormatterProvider { 20 | public static let dateFormatter: DateFormatterProtocol = { 21 | let dateFormatter = DateFormatter() 22 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 23 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 24 | dateFormatter.dateFormat = "yyyy-MM-dd" 25 | return dateFormatter 26 | }() 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/MacroCodableKit/Misc/ValueCodableStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValueCodableStrategy.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 30.09.23. 6 | // 7 | 8 | /// A strategy for encoding and decoding a property of type ``Value``. 9 | /// 10 | /// The ``Value`` is the same as it appears in the property it's applied to, for example: 11 | /// 12 | /// ```swift 13 | /// @Codable 14 | /// struct Example { 15 | /// @ValueStrategy(SomeConcreteValueStrategy) 16 | /// var property: ConcreteValue 17 | /// } 18 | /// ``` 19 | /// 20 | /// In this snippet above, **SomeConcreteValueStrategy** will have **ConcreteValue** type and could be applied only to properties of this type. 21 | public protocol ValueCodableStrategy { 22 | /// The type of value that this strategy encodes and decodes. 23 | associatedtype Value 24 | 25 | /// Decodes a value of the associated type from the given decoder. 26 | /// 27 | /// - Parameter decoder: The decoder to read data from. 28 | /// 29 | /// - Throws: An error if decoding fails. 30 | /// 31 | /// - Returns: The decoded value from decoder. 32 | static func decode(from decoder: Decoder) throws -> Value 33 | 34 | /// Encodes the given value using the associated type. 35 | /// 36 | /// - Parameter value: The value to encode. 37 | /// - Parameter encoder: The encoder to write data to. 38 | /// 39 | /// - Throws: An error if the value cannot be encoded. 40 | static func encode(value: Value, to encoder: Encoder) throws 41 | } 42 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/AllOfCodable/AllOfMacroDecodingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllOfMacroCodableTests.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 23.09.23. 6 | // 7 | 8 | import MacroCodableKit 9 | import XCTest 10 | 11 | final class AllOfMacroCodableTests: XCTestCase { 12 | func testValidPlainAllOfTestCases() throws { 13 | let testCases: [(String, A)] = [ 14 | (#"{ "int": 1, "boolean": true, "string": "some string", "anotherInt": 1 }"#, A(b: A.B(int: 1, boolean: true, string: "some string"), c: A.C(anotherInt: 1))), 15 | ] 16 | for testCase in testCases { 17 | guard let data = testCase.0.data(using: .utf8) else { 18 | XCTFail("Couldn't get data from \(testCase.0)") 19 | continue 20 | } 21 | 22 | XCTAssertEqual(try JSONDecoder().decode(A.self, from: data), testCase.1) 23 | XCTAssertEqual(try JSONDecoder().decode(A.self, from: try JSONEncoder().encode(testCase.1)), testCase.1) 24 | } 25 | } 26 | 27 | func testInvalidPlainAllOfTestCase() { 28 | let json = #"{ "int": 1, "boolean": true, "string": "some string" }"# 29 | do { 30 | _ = try json.data(using: .utf8).flatMap { try JSONDecoder().decode(A.self, from: $0) } 31 | XCTFail("Shouldn't decode invalid \(json)") 32 | } catch {} 33 | } 34 | } 35 | 36 | @AllOfCodable 37 | private struct A: Equatable { 38 | struct B: Codable, Equatable { 39 | let int: Int 40 | let boolean: Bool 41 | let string: String 42 | } 43 | 44 | struct C: Codable, Equatable { 45 | let anotherInt: Int 46 | } 47 | 48 | let b: B 49 | let c: C 50 | } 51 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/Annotations/AnnotationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnnotationTests.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | import Foundation 9 | import MacroCodableKit 10 | import XCTest 11 | 12 | final class AnnotationTests: XCTestCase { 13 | @Codable 14 | struct OmitCodingExample { 15 | @OmitCoding 16 | var a: Int? 17 | let b: Int 18 | } 19 | 20 | @Codable 21 | struct CodingKeyExample { 22 | @CodingKey("aaa") 23 | let a: Int 24 | } 25 | 26 | func test_OmitCoding() throws { 27 | let encodedA = try JSONEncoder().encode(OmitCodingExample(a: 0, b: 1)) 28 | let json = try JSONSerialization.jsonObject(with: encodedA) as? [String: Any] 29 | XCTAssertEqual(json?["a"] as? Int, nil) 30 | XCTAssertEqual(json?["b"] as? Int, 1) 31 | } 32 | 33 | func test_CodingKey() throws { 34 | let json = #"{"aaa": 100}"#.data(using: .utf8) 35 | let decoded = try JSONDecoder().decode(CodingKeyExample.self, from: json!) 36 | 37 | XCTAssertEqual(decoded.a, 100) 38 | } 39 | 40 | final class DoubleStringStrategy: ValueCodableStrategy { 41 | typealias Value = String 42 | 43 | static func decode(from decoder: Decoder) throws -> String { 44 | let container = try decoder.singleValueContainer() 45 | let string = try container.decode(Value.self) 46 | return string + string 47 | } 48 | 49 | static func encode(value: String, to encoder: Encoder) throws { 50 | var container = encoder.singleValueContainer() 51 | try container.encode(value) 52 | } 53 | } 54 | 55 | @Codable 56 | struct DoubleStringExample { 57 | @DefaultValue(EmptyString) 58 | @ValueStrategy(AnnotationTests.DoubleStringStrategy) 59 | let string1: String 60 | 61 | @DefaultValue(EmptyString) 62 | @ValueStrategy(AnnotationTests.DoubleStringStrategy) 63 | let string2: String 64 | } 65 | 66 | func test_DoubleStringStrategy() throws { 67 | let json = #"{"string1": "Hello"}"# 68 | let jsonData = Data(json.utf8) 69 | let expectedString = "HelloHello" 70 | 71 | let decoded = try JSONDecoder().decode(DoubleStringExample.self, from: jsonData) 72 | 73 | XCTAssertEqual(decoded.string1, expectedString, "Expected applied DoubleStringStrategy") 74 | XCTAssertEqual(decoded.string2, "", "Expected default to be an empty string") 75 | } 76 | 77 | final class NestedDoubleStringStrategy: ValueCodableStrategy { 78 | typealias Value = NestedDoubleStringExample.Nested 79 | 80 | static func decode(from decoder: Decoder) throws -> Value { 81 | let container = try decoder.singleValueContainer() 82 | let nested = try container.decode(Value.self) 83 | return Value(value: nested.value + nested.value) 84 | } 85 | 86 | static func encode(value: Value, to encoder: Encoder) throws { 87 | var container = encoder.singleValueContainer() 88 | try container.encode(value) 89 | } 90 | } 91 | 92 | final class Nested123DefaultValueProvider: DefaultValueProvider { 93 | static var defaultValue: NestedDoubleStringExample.Nested { 94 | NestedDoubleStringExample.Nested(value: "123") 95 | } 96 | } 97 | 98 | @Codable 99 | struct NestedDoubleStringExample { 100 | @Codable 101 | struct Nested: Equatable { 102 | let value: String 103 | } 104 | 105 | @DefaultValue(AnnotationTests.Nested123DefaultValueProvider) 106 | @ValueStrategy(AnnotationTests.NestedDoubleStringStrategy) 107 | let n1: Nested 108 | 109 | @DefaultValue(AnnotationTests.Nested123DefaultValueProvider) 110 | @ValueStrategy(AnnotationTests.NestedDoubleStringStrategy) 111 | let n2: Nested 112 | } 113 | 114 | func test_NestedDoubleStringExample() throws { 115 | let json = #"{"n1": { "value": "Hello" } }"# 116 | let jsonData = Data(json.utf8) 117 | let expectedNested = NestedDoubleStringExample.Nested(value: "HelloHello") 118 | 119 | let decoded = try JSONDecoder().decode(NestedDoubleStringExample.self, from: jsonData) 120 | 121 | XCTAssertEqual(decoded.n1, expectedNested, "Expected applied NestedDoubleStringStrategy") 122 | XCTAssertEqual(decoded.n2.value, "123", "Expected default to be an 123 as in Nested123DefaultValueProvider") 123 | } 124 | 125 | @Codable 126 | struct TwiceExample: Equatable { 127 | @CustomCoding(Twice) 128 | var int: Int 129 | 130 | @CustomCoding(Twice) 131 | var string: String 132 | } 133 | 134 | func test_TwiceExample() throws { 135 | let json = #"{"string": "Hello", "int": 10}"# 136 | let jsonData = Data(json.utf8) 137 | let expected = TwiceExample(int: 20, string: "HelloHello") 138 | 139 | let decoded = try JSONDecoder().decode(TwiceExample.self, from: jsonData) 140 | 141 | XCTAssertEqual(decoded, expected) 142 | } 143 | } 144 | 145 | // MARK: - Twice 146 | 147 | struct Twice: CustomDecodingName {} 148 | 149 | extension CustomCodingDecoding { 150 | static func twice( 151 | _: String.Type, 152 | forKey key: KeyedDecodingContainer.Key, 153 | container: KeyedDecodingContainer 154 | ) throws -> String { 155 | let result = try container.decode(String.self, forKey: key) 156 | return result + result 157 | } 158 | 159 | static func twice( 160 | _: Int.Type, 161 | forKey key: KeyedDecodingContainer.Key, 162 | container: KeyedDecodingContainer 163 | ) throws -> Int { 164 | let result = try container.decode(Int.self, forKey: key) 165 | return result * 2 166 | } 167 | 168 | // We don't implement optional handling 169 | } 170 | 171 | public extension CustomCodingEncoding { 172 | // Handle all types the same way 173 | 174 | static func twice( 175 | _ value: Element, 176 | forKey key: KeyedEncodingContainer.Key, 177 | container: inout KeyedEncodingContainer 178 | ) throws { 179 | try container.encode(value, forKey: key) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/Annotations/Base64StrategyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Base64StrategyTests.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | import MacroCodableKit 9 | import XCTest 10 | 11 | final class Base64StrategyTests: XCTestCase { 12 | @Codable 13 | struct Base64Struct { 14 | @ValueStrategy(Base64Data) 15 | let data: Data 16 | } 17 | 18 | func test_Base64Struct() throws { 19 | let expectedResult = "Hello world!" 20 | let json = #"{"data":"SGVsbG8gd29ybGQh"}"# 21 | let jsonData = Data(json.utf8) 22 | let decoded = try JSONDecoder().decode(Base64Struct.self, from: jsonData) 23 | 24 | XCTAssertEqual(String(data: decoded.data, encoding: .utf8), expectedResult) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/Annotations/DateValueStrategyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateValueStrategyTests.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | import Foundation 9 | import MacroCodableKit 10 | import XCTest 11 | 12 | final class DateValueStrategyTests: XCTestCase { 13 | @Codable 14 | struct IOS8601DefaultExample { 15 | @ValueStrategy(ISO8601Default) 16 | let date: Date 17 | } 18 | 19 | func test_IOS8601DefaultExample() throws { 20 | // Prepare the JSON string with ISO 8601 formatted date 21 | let jsonString = #"{"date": "2023-10-03T10:15:30+00:00"}"# 22 | let jsonData = Data(jsonString.utf8) 23 | let expectedDate = Date(timeIntervalSince1970: 1_696_328_130.0) 24 | 25 | let decodedExample = try JSONDecoder().decode(IOS8601DefaultExample.self, from: jsonData) 26 | 27 | XCTAssertEqual(decodedExample.date, expectedDate) 28 | } 29 | 30 | @Codable 31 | struct ISO8601FullDateExample { 32 | @ValueStrategy(ISO8601WithFullDate) 33 | let date: Date 34 | } 35 | 36 | func test_ISO8601WithFullDateExample() throws { 37 | let jsonString = #"{"date": "2023-10-03"}"# 38 | let jsonData = Data(jsonString.utf8) 39 | let expectedDate = Date(timeIntervalSince1970: 1_696_291_200.0) 40 | 41 | let decodedExample = try JSONDecoder().decode(ISO8601FullDateExample.self, from: jsonData) 42 | XCTAssertEqual(decodedExample.date, expectedDate) 43 | } 44 | 45 | @Codable 46 | struct ISO8601FractionalSecondsExample { 47 | @ValueStrategy(ISO8601WithFractionalSeconds) 48 | let date: Date 49 | } 50 | 51 | func test_ISO8601WithFractionalSecondsExample() throws { 52 | let jsonString = #"{"date": "2023-10-03T10:15:30.123+00:00"}"# 53 | let jsonData = Data(jsonString.utf8) 54 | let expectedDate = Date(timeIntervalSince1970: 1_696_328_130.123) 55 | 56 | let decodedExample = try JSONDecoder().decode(ISO8601FractionalSecondsExample.self, from: jsonData) 57 | XCTAssertEqual(decodedExample.date, expectedDate) 58 | } 59 | 60 | @Codable 61 | struct RFC2822DateExample { 62 | @ValueStrategy(RFC2822Date) 63 | let date: Date 64 | } 65 | 66 | func test_RFC2822DateExample() throws { 67 | let jsonString = #"{"date": "Tue, 3 Oct 2023 10:15:30 GMT"}"# 68 | let jsonData = Data(jsonString.utf8) 69 | let expectedDate = Date(timeIntervalSince1970: 1_696_328_130.0) 70 | 71 | // Decode and validate 72 | let decodedExample = try JSONDecoder().decode(RFC2822DateExample.self, from: jsonData) 73 | XCTAssertEqual(decodedExample.date, expectedDate) 74 | } 75 | 76 | @Codable 77 | struct RFC3339DateExample { 78 | @ValueStrategy(RFC3339Date) 79 | let date: Date 80 | } 81 | 82 | func test_RFC3339DateExample() throws { 83 | let jsonString = #"{"date": "2023-10-03T10:15:30Z"}"# 84 | let jsonData = Data(jsonString.utf8) 85 | let expectedDate = Date(timeIntervalSince1970: 1_696_328_130.0) 86 | 87 | let decodedExample = try JSONDecoder().decode(RFC3339DateExample.self, from: jsonData) 88 | XCTAssertEqual(decodedExample.date, expectedDate) 89 | } 90 | 91 | @Codable 92 | struct YearMonthDayDateExample { 93 | @ValueStrategy(YearMonthDayDate) 94 | let date: Date 95 | } 96 | 97 | func test_YearMonthDayDateExample() throws { 98 | let jsonString = #"{"date": "2023-10-03"}"# 99 | let jsonData = Data(jsonString.utf8) 100 | let expectedDate = Date(timeIntervalSince1970: 1_696_291_200.0) 101 | 102 | let decodedExample = try JSONDecoder().decode(YearMonthDayDateExample.self, from: jsonData) 103 | XCTAssertEqual(decodedExample.date, expectedDate) 104 | } 105 | 106 | @Codable 107 | struct TimestampedDateExample { 108 | @ValueStrategy(TimestampedDate) 109 | let date: Date 110 | } 111 | 112 | func test_TimestampedDateExample_Double() throws { 113 | let jsonString = #"{"date": 1696291200.0}"# 114 | let jsonData = Data(jsonString.utf8) 115 | let expectedDate = Date(timeIntervalSince1970: 1_696_291_200.0) 116 | 117 | let decodedExample = try JSONDecoder().decode(TimestampedDateExample.self, from: jsonData) 118 | XCTAssertEqual(decodedExample.date, expectedDate) 119 | } 120 | 121 | func test_TimestampedDateExample_String() throws { 122 | let jsonString = #"{"date": "1696291200.0"}"# 123 | let jsonData = Data(jsonString.utf8) 124 | let expectedDate = Date(timeIntervalSince1970: 1_696_291_200.0) 125 | 126 | let decodedExample = try JSONDecoder().decode(TimestampedDateExample.self, from: jsonData) 127 | XCTAssertEqual(decodedExample.date, expectedDate) 128 | } 129 | 130 | @Codable 131 | struct TimestampedDateOptionalExample { 132 | @ValueStrategy(TimestampedDate) 133 | let date: Date? 134 | } 135 | 136 | func test_TimestampedDateOptionalExample() throws { 137 | let jsonString = #"{"date": "1696291200.0"}"# 138 | let jsonData = Data(jsonString.utf8) 139 | let expectedDate = Date(timeIntervalSince1970: 1_696_291_200.0) 140 | 141 | let decodedExample = try JSONDecoder().decode(TimestampedDateExample.self, from: jsonData) 142 | XCTAssertEqual(decodedExample.date, expectedDate) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/Annotations/DefaultValueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultValueTests.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | import MacroCodableKit 9 | import XCTest 10 | 11 | final class DefaultValueTests: XCTestCase { 12 | @Codable 13 | struct DefaultValueBool { 14 | let boolean1: Bool 15 | 16 | @DefaultValue(BoolFalse) 17 | let boolean2: Bool 18 | 19 | @DefaultValue(BoolTrue) 20 | let boolean3: Bool 21 | 22 | @DefaultValue(BoolTrue) 23 | let boolean4: Bool? 24 | 25 | let boolean5: Bool? 26 | } 27 | 28 | @Codable 29 | struct DefaultValueInt { 30 | let int1: Int 31 | 32 | @DefaultValue(IntZero) 33 | let int2: Int 34 | 35 | @DefaultValue(IntZero) 36 | let int3: Int? 37 | 38 | let int4: Int? 39 | } 40 | 41 | @Codable 42 | struct DefaultValueDouble { 43 | let double1: Double 44 | 45 | @DefaultValue(DoubleZero) 46 | let double2: Double 47 | 48 | @DefaultValue(DoubleZero) 49 | let double3: Double? 50 | 51 | let double4: Double? 52 | } 53 | 54 | func test_DefaultValueBool() throws { 55 | let json = #"{"boolean1":true, "boolean2": 1234}"# 56 | let jsonData = Data(json.utf8) 57 | let decoded = try JSONDecoder().decode(DefaultValueBool.self, from: jsonData) 58 | 59 | XCTAssertTrue(decoded.boolean1) 60 | XCTAssertFalse(decoded.boolean2) 61 | XCTAssertTrue(decoded.boolean3) 62 | XCTAssertEqual(decoded.boolean4, true) 63 | XCTAssertNil(decoded.boolean5) 64 | } 65 | 66 | func test_DefaultValueInt() throws { 67 | let json = #"{"int1":100, "int2": true}"# 68 | let jsonData = Data(json.utf8) 69 | let decoded = try JSONDecoder().decode(DefaultValueInt.self, from: jsonData) 70 | 71 | XCTAssertEqual(decoded.int1, 100) 72 | XCTAssertEqual(decoded.int2, 0) 73 | XCTAssertEqual(decoded.int3, 0) 74 | XCTAssertNil(decoded.int4) 75 | } 76 | 77 | func test_DefaultValueDouble() throws { 78 | let json = #"{"double1":10.15, "double2": true}"# 79 | let jsonData = Data(json.utf8) 80 | let decoded = try JSONDecoder().decode(DefaultValueDouble.self, from: jsonData) 81 | 82 | XCTAssertEqual(decoded.double1, 10.15) 83 | XCTAssertEqual(decoded.double2, 0) 84 | XCTAssertEqual(decoded.double3, 0) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/Annotations/Macro/AnnotationsMixMacroTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnnotationsMixMacroTests.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | import Macro 9 | import MacroTesting 10 | import SwiftSyntaxMacros 11 | import SwiftSyntaxMacrosTestSupport 12 | import XCTest 13 | 14 | private let isRecording = false 15 | 16 | final class AnnotationsMixMacroTests: XCTestCase { 17 | func test_DefaultValueAndValueStrategy() { 18 | withMacroTesting( 19 | isRecording: isRecording, 20 | macros: [ 21 | "Codable": CodableMacro.self, 22 | "DefaultValue": DefaultValueMacro.self, 23 | "ValueStrategy": ValueStrategyMacro.self, 24 | ] 25 | ) { 26 | assertMacro { 27 | """ 28 | @Codable 29 | struct DefaultValueBool\(sutSuffix) { 30 | @ValueStrategy(SomeBoolStrategy) 31 | @DefaultValue(BoolFalse) 32 | let boolean1: Bool 33 | 34 | @ValueStrategy(SomeBoolStrategy) 35 | @DefaultValue(BoolFalse) 36 | let boolean2: Bool? 37 | } 38 | """ 39 | } expansion: { 40 | """ 41 | struct DefaultValueBool__testing__ { 42 | let boolean1: Bool 43 | let boolean2: Bool? 44 | } 45 | 46 | extension DefaultValueBool__testing__: Decodable, Encodable { 47 | enum CodingKeys: String, CodingKey { 48 | case boolean1 49 | case boolean2 50 | } 51 | init(from decoder: Decoder) throws { 52 | let container = try decoder.container(keyedBy: CodingKeys.self) 53 | self.boolean1 = try CustomCodingDecoding.decode( 54 | Bool.self, 55 | forKey: .boolean1, 56 | container: container, 57 | strategy: SomeBoolStrategy.self, 58 | provider: BoolFalse.self 59 | ) 60 | self.boolean2 = try CustomCodingDecoding.decode( 61 | Bool?.self, 62 | forKey: .boolean2, 63 | container: container, 64 | strategy: SomeBoolStrategy.self, 65 | provider: BoolFalse.self 66 | ) 67 | } 68 | func encode(to encoder: Encoder) throws { 69 | var container = encoder.container(keyedBy: CodingKeys.self) 70 | try CustomCodingEncoding.encode( 71 | self.boolean1, 72 | forKey: .boolean1, 73 | container: &container, 74 | strategy: SomeBoolStrategy.self 75 | ) 76 | try CustomCodingEncoding.encode( 77 | self.boolean2, 78 | forKey: .boolean2, 79 | container: &container, 80 | strategy: SomeBoolStrategy.self 81 | ) 82 | } 83 | } 84 | """ 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/Annotations/Macro/DiagnosticTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 07.10.23. 6 | // 7 | 8 | import Macro 9 | import MacroTesting 10 | import SwiftSyntaxMacros 11 | import SwiftSyntaxMacrosTestSupport 12 | import XCTest 13 | 14 | private let isRecording = false 15 | 16 | final class DiagnosticTests: XCTestCase { 17 | func testDuplicatedAnnotations() { 18 | withMacroTesting( 19 | isRecording: isRecording, 20 | macros: [ 21 | "Codable": CodableMacro.self, 22 | "OmitCoding": OmitCodingMacro.self, 23 | "CodingKey": CodingKeyMacro.self, 24 | "ValueStrategy": ValueStrategyMacro.self, 25 | "DefaultValue": DefaultValueMacro.self, 26 | "CustomCoding": CustomCodingMacro.self, 27 | ] 28 | ) { 29 | assertMacro { 30 | """ 31 | @Codable 32 | struct Example\(sutSuffix) { 33 | @OmitCoding 34 | @OmitCoding 35 | let a: Int 36 | } 37 | """ 38 | } diagnostics: { 39 | """ 40 | @Codable 41 | struct Example__testing__ { 42 | @OmitCoding 43 | ╰─ 🛑 '@Omitcoding' attribute has been applied more than once. Redundant attribute applications have no effect on the generated code and may cause confusion. 44 | @OmitCoding 45 | let a: Int 46 | } 47 | """ 48 | } 49 | 50 | assertMacro { 51 | """ 52 | @Codable 53 | struct Example\(sutSuffix) { 54 | @CodingKey("_a") 55 | @CodingKey("_a") 56 | let a: Int 57 | } 58 | """ 59 | } diagnostics: { 60 | """ 61 | @Codable 62 | struct Example__testing__ { 63 | @CodingKey("_a") 64 | ╰─ 🛑 '@Codingkey' attribute has been applied more than once. Redundant attribute applications have no effect on the generated code and may cause confusion. 65 | @CodingKey("_a") 66 | let a: Int 67 | } 68 | """ 69 | } 70 | 71 | assertMacro { 72 | """ 73 | @Codable 74 | struct Example\(sutSuffix) { 75 | @ValueStrategy(Base64Data) 76 | @ValueStrategy(Base64Data) 77 | let string: String 78 | } 79 | """ 80 | } diagnostics: { 81 | """ 82 | @Codable 83 | struct Example__testing__ { 84 | @ValueStrategy(Base64Data) 85 | ╰─ 🛑 '@Valuestrategy' attribute has been applied more than once. Redundant attribute applications have no effect on the generated code and may cause confusion. 86 | @ValueStrategy(Base64Data) 87 | let string: String 88 | } 89 | """ 90 | } 91 | 92 | assertMacro { 93 | """ 94 | @Codable 95 | struct Example\(sutSuffix) { 96 | @DefaultValue(BoolTrue) 97 | @DefaultValue(BoolFalse) 98 | let bool: Bool 99 | } 100 | """ 101 | } diagnostics: { 102 | """ 103 | @Codable 104 | struct Example__testing__ { 105 | @DefaultValue(BoolTrue) 106 | ╰─ 🛑 '@Defaultvalue' attribute has been applied more than once. Redundant attribute applications have no effect on the generated code and may cause confusion. 107 | @DefaultValue(BoolFalse) 108 | let bool: Bool 109 | } 110 | """ 111 | } 112 | 113 | assertMacro { 114 | """ 115 | @Codable 116 | struct Example\(sutSuffix) { 117 | @CustomCoding(SafeDecoding) 118 | @CustomCoding(SafeDecoding) 119 | let array: [Bool] 120 | } 121 | """ 122 | } diagnostics: { 123 | """ 124 | @Codable 125 | struct Example__testing__ { 126 | @CustomCoding(SafeDecoding) 127 | ╰─ 🛑 '@Customcoding' attribute has been applied more than once. Redundant attribute applications have no effect on the generated code and may cause confusion. 128 | @CustomCoding(SafeDecoding) 129 | let array: [Bool] 130 | } 131 | """ 132 | } 133 | } 134 | } 135 | 136 | func test_unsupportedAnnotations() { 137 | withMacroTesting( 138 | isRecording: isRecording, 139 | macros: [ 140 | "Codable": CodableMacro.self, 141 | ] 142 | ) { 143 | assertMacro { 144 | """ 145 | @Codable 146 | struct Example\(sutSuffix) { 147 | let a, b: Int 148 | } 149 | """ 150 | } diagnostics: { 151 | """ 152 | @Codable 153 | struct Example__testing__ { 154 | let a, b: Int 155 | ┬──────────── 156 | ╰─ 🛑 '@Codable' macro is only applicable to declarations with an identifier followed by a type 157 | } 158 | """ 159 | } 160 | 161 | assertMacro { 162 | """ 163 | @Codable 164 | struct Example\(sutSuffix) { 165 | let a: Int, b: Int 166 | } 167 | """ 168 | } diagnostics: { 169 | """ 170 | @Codable 171 | struct Example__testing__ { 172 | let a: Int, b: Int 173 | ┬───────────────── 174 | ╰─ 🛑 '@Codable' macro is not applicable to compound declarations, declare each variable on a new line 175 | } 176 | """ 177 | } 178 | 179 | assertMacro { 180 | """ 181 | @Codable 182 | struct Example\(sutSuffix) { 183 | @DefaultValue(IntZero) 184 | var a: Int { 0 } 185 | } 186 | """ 187 | } diagnostics: { 188 | """ 189 | @Codable 190 | struct Example__testing__ { 191 | @DefaultValue(IntZero) 192 | ╰─ 🛑 '@Codable' macro is only applicable to stored properties declared with an identifier followed by a type, example: `let variable: Int` 193 | var a: Int { 0 } 194 | } 195 | """ 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/Annotations/Macro/ValueStrategyMacroTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValueStrategyMacroTests.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | import Macro 9 | import MacroTesting 10 | import SwiftSyntaxMacros 11 | import SwiftSyntaxMacrosTestSupport 12 | import XCTest 13 | 14 | private let isRecording = false 15 | 16 | final class ValueStrategyMacroTests: XCTestCase { 17 | func test_ValueStrategy() { 18 | withMacroTesting( 19 | isRecording: isRecording, 20 | macros: [ 21 | "Codable": CodableMacro.self, 22 | "ValueStrategy": ValueStrategyMacro.self, 23 | ] 24 | ) { 25 | assertMacro { 26 | """ 27 | @Codable 28 | struct Base64Struct\(sutSuffix) { 29 | let data: Data 30 | 31 | @ValueStrategy(Base64Data) 32 | let data: Data 33 | 34 | @ValueStrategy(Base64Data) 35 | let data: Data? 36 | } 37 | """ 38 | } expansion: { 39 | """ 40 | struct Base64Struct__testing__ { 41 | let data: Data 42 | let data: Data 43 | let data: Data? 44 | } 45 | 46 | extension Base64Struct__testing__: Decodable, Encodable { 47 | enum CodingKeys: String, CodingKey { 48 | case data 49 | case data 50 | case data 51 | } 52 | init(from decoder: Decoder) throws { 53 | let container = try decoder.container(keyedBy: CodingKeys.self) 54 | self.data = try container.decode(Data.self, forKey: .data) 55 | self.data = try CustomCodingDecoding.decode( 56 | Data.self, 57 | forKey: .data, 58 | container: container, 59 | strategy: Base64Data.self 60 | ) 61 | self.data = try CustomCodingDecoding.decode( 62 | Data?.self, 63 | forKey: .data, 64 | container: container, 65 | strategy: Base64Data.self 66 | ) 67 | } 68 | func encode(to encoder: Encoder) throws { 69 | var container = encoder.container(keyedBy: CodingKeys.self) 70 | try container.encode(self.data, forKey: .data) 71 | try CustomCodingEncoding.encode(self.data, forKey: .data, container: &container, strategy: Base64Data.self) 72 | try CustomCodingEncoding.encode(self.data, forKey: .data, container: &container, strategy: Base64Data.self) 73 | } 74 | } 75 | """ 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/Annotations/SafeCodingArrayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafeCodingArrayTests.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | import MacroCodableKit 9 | import XCTest 10 | 11 | final class SafeCodingArrayTests: XCTestCase { 12 | @Codable 13 | struct SafeCodingArray1: Equatable { 14 | let strings: [String] 15 | 16 | @CustomCoding(SafeDecoding) 17 | let safeStrings: [String] 18 | } 19 | 20 | @Codable 21 | struct SafeCodingArray2: Equatable { 22 | @CustomCoding(SafeDecoding) 23 | let safeStrings: [String]? 24 | } 25 | 26 | func test_SafeCodingArray1_DiscardsInvalidArrayElements() throws { 27 | let json = """ 28 | { 29 | "strings": ["a", "b", "c"], 30 | "safeStrings": ["a", true, "b", "c", 0] 31 | } 32 | """ 33 | let jsonData = Data(json.utf8) 34 | let decoded = try JSONDecoder().decode(SafeCodingArray1.self, from: jsonData) 35 | 36 | XCTAssertEqual(decoded.strings, decoded.safeStrings) 37 | } 38 | 39 | func test_SafeCodingArray2_SafelyDecodsAbsentArray() throws { 40 | let json = "{}" 41 | let jsonData = Data(json.utf8) 42 | let decoded = try JSONDecoder().decode(SafeCodingArray2.self, from: jsonData) 43 | 44 | XCTAssertNil(decoded.safeStrings) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/Annotations/SafeCodingDictionaryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafeCodingDictionaryTests.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 03.10.23. 6 | // 7 | 8 | import MacroCodableKit 9 | import XCTest 10 | 11 | final class SafeCodingDictionaryTests: XCTestCase { 12 | @Codable 13 | struct SafeCodingDictionary1: Equatable { 14 | let intByString: [String: Int] 15 | 16 | @CustomCoding(SafeDecoding) 17 | let safeIntByString: [String: Int] 18 | } 19 | 20 | @Codable 21 | struct SafeCodingDictionary2 { 22 | let stringByInt: [Int: String] 23 | 24 | @CustomCoding(SafeDecoding) 25 | let safeStringByInt: [Int: String] 26 | } 27 | 28 | @Codable 29 | struct SafeCodingDictionary3 { 30 | let intByString: [String: Int] 31 | 32 | @CustomCoding(SafeDecoding) 33 | let safeIntByString: [String: Int]? 34 | } 35 | 36 | func test_SafeCodingDictionary1_IntByString() throws { 37 | let json = """ 38 | { 39 | "intByString": { 40 | "one": 1, 41 | "two": 2 42 | }, 43 | "safeIntByString": { 44 | "three": 3, 45 | "four": "invalid" 46 | } 47 | } 48 | """ 49 | let jsonData = Data(json.utf8) 50 | let decoded = try JSONDecoder().decode(SafeCodingDictionary1.self, from: jsonData) 51 | 52 | XCTAssertEqual(decoded.intByString["one"], 1) 53 | XCTAssertEqual(decoded.intByString["two"], 2) 54 | XCTAssertEqual(decoded.safeIntByString["three"], 3) 55 | XCTAssertEqual(decoded.safeIntByString.count, 1) 56 | } 57 | 58 | func test_SafeCodingDictionary2_StringByInt() throws { 59 | let json = """ 60 | { 61 | "stringByInt": { 62 | "1": "one", 63 | "2": "two" 64 | }, 65 | "safeStringByInt": { 66 | "3": "three", 67 | "4": false 68 | } 69 | } 70 | """ 71 | let jsonData = Data(json.utf8) 72 | let decoded = try JSONDecoder().decode(SafeCodingDictionary2.self, from: jsonData) 73 | 74 | XCTAssertEqual(decoded.stringByInt[1], "one") 75 | XCTAssertEqual(decoded.stringByInt[2], "two") 76 | XCTAssertEqual(decoded.safeStringByInt[3], "three") 77 | XCTAssertEqual(decoded.safeStringByInt.count, 1) 78 | } 79 | 80 | func test_SafeCodingDictionary3_OptionalDict() throws { 81 | let json = """ 82 | { 83 | "intByString": { 84 | "one": 1, 85 | "two": 2, 86 | "three": 3 87 | } 88 | } 89 | """ 90 | let jsonData = Data(json.utf8) 91 | let decoded = try JSONDecoder().decode(SafeCodingDictionary3.self, from: jsonData) 92 | 93 | XCTAssertEqual(decoded.intByString["one"], 1) 94 | XCTAssertEqual(decoded.intByString["two"], 2) 95 | XCTAssertEqual(decoded.intByString["three"], 3) 96 | XCTAssertNil(decoded.safeIntByString) 97 | } 98 | 99 | func test_DictionaryDecoderRespectsDecodingStrategy() throws { 100 | let decoder = JSONDecoder() 101 | decoder.keyDecodingStrategy = .convertFromSnakeCase 102 | 103 | let encoder = JSONEncoder() 104 | encoder.keyEncodingStrategy = .convertToSnakeCase 105 | 106 | let sut = SafeCodingDictionary1( 107 | intByString: [ 108 | "snake_case.with.dots_99.and_numbers": 1, 109 | "dots.and_2.00.1_numbers": 2, 110 | "key.1": 3, 111 | "normal key": 4, 112 | "another_key": 5, 113 | ], 114 | safeIntByString: [ 115 | "snake_case.with.dots_99.and_numbers": 1, 116 | "dots.and_2.00.1_numbers": 2, 117 | "key.1": 3, 118 | "normal key": 4, 119 | "another_key": 5, 120 | ] 121 | ) 122 | 123 | let result = try decoder.decode(SafeCodingDictionary1.self, from: encoder.encode(sut)) 124 | 125 | XCTAssertEqual(result.intByString, sut.intByString) 126 | XCTAssertEqual(result.safeIntByString, sut.safeIntByString) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/Codable/CodableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableTests.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 24.09.23. 6 | // 7 | 8 | import MacroCodableKit 9 | import XCTest 10 | 11 | final class CodableTests: XCTestCase { 12 | @Codable 13 | struct NestedCodable: Equatable { 14 | @Codable 15 | struct B: Equatable { 16 | let int: Int 17 | let boolean: Bool 18 | 19 | @CodingKey("string_string") 20 | let stringString: String 21 | } 22 | 23 | @Codable 24 | struct C: Equatable { 25 | let anotherInt: Int 26 | } 27 | 28 | let b: B 29 | let c: C 30 | } 31 | 32 | func test_ValidNestedCodable() throws { 33 | let testCases: [(String, NestedCodable)] = [ 34 | ( 35 | #"{"b":{"int":1,"boolean":true,"string_string":"string string"},"c":{"anotherInt":1}}"#, 36 | NestedCodable(b: NestedCodable.B(int: 1, boolean: true, stringString: "string string"), c: NestedCodable.C(anotherInt: 1)) 37 | ), 38 | ] 39 | for testCase in testCases { 40 | guard let data = testCase.0.data(using: .utf8) else { 41 | XCTFail("Couldn't get data from \(testCase.0)") 42 | continue 43 | } 44 | 45 | XCTAssertEqual(try JSONDecoder().decode(NestedCodable.self, from: data), testCase.1) 46 | XCTAssertEqual(try JSONDecoder().decode(NestedCodable.self, from: try JSONEncoder().encode(testCase.1)), testCase.1) 47 | } 48 | } 49 | 50 | func test_InvalidNestedCodable() { 51 | let json = "{\"int\": 1, \"boolean\": true, \"string\": \"some string\"}" 52 | do { 53 | _ = try json.data(using: .utf8).flatMap { try JSONDecoder().decode(NestedCodable.self, from: $0) } 54 | XCTFail("Shouldn't decode invalid \(json)") 55 | } catch {} 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/Helpers/Misc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Misc.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 08.10.23. 6 | // 7 | 8 | let sutSuffix = "__testing__" 9 | -------------------------------------------------------------------------------- /Tests/MacroCodableKitTests/OneOfCodable/OneOfMacroDecodingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OneOfMacroDecodingTests.swift 3 | // 4 | // 5 | // Created by Mikhail Maslo on 23.09.23. 6 | // 7 | 8 | import MacroCodableKit 9 | import XCTest 10 | 11 | final class OneOfMacroDecodingTests: XCTestCase { 12 | func testValidPlainOneOfTestCases() throws { 13 | let testCases: [(String, PlainOneOf)] = [ 14 | ("{\"int\": 1}", .int(1)), 15 | ("{\"boolean\": true}", .boolean(true)), 16 | ("{\"string\": \"Hello world\"}", .string("Hello world")), 17 | ] 18 | for testCase in testCases { 19 | guard let data = testCase.0.data(using: .utf8) else { 20 | XCTFail("Couldn't get data from \(testCase.0)") 21 | continue 22 | } 23 | 24 | XCTAssertEqual(try JSONDecoder().decode(PlainOneOf.self, from: data), testCase.1) 25 | XCTAssertEqual(try JSONDecoder().decode(PlainOneOf.self, from: try JSONEncoder().encode(testCase.1)), testCase.1) 26 | } 27 | } 28 | 29 | func testInvalidPlainOneOfTestCase() { 30 | let json = "{\"int\": true}" 31 | do { 32 | _ = try json.data(using: .utf8).flatMap { try JSONDecoder().decode(PlainOneOf.self, from: $0) } 33 | XCTFail("Shouldn't decode invalid \(json)") 34 | } catch {} 35 | } 36 | 37 | func testValidNestedOneOfTestCases() throws { 38 | let testCases: [(String, NestedOneOf)] = [ 39 | ("{\"plain\": {\"boolean\": true}}", .plain(.boolean(true))), 40 | ] 41 | for testCase in testCases { 42 | guard let data = testCase.0.data(using: .utf8) else { 43 | XCTFail("Couldn't get data from \(testCase.0)") 44 | continue 45 | } 46 | 47 | XCTAssertEqual(try JSONDecoder().decode(NestedOneOf.self, from: data), testCase.1) 48 | XCTAssertEqual(try JSONDecoder().decode(NestedOneOf.self, from: try JSONEncoder().encode(testCase.1)), testCase.1) 49 | } 50 | } 51 | 52 | func testInvalidNestedOneOfTestCase() { 53 | let json = "{\"plain\": true}" 54 | do { 55 | _ = try json.data(using: .utf8).flatMap { try JSONDecoder().decode(PlainOneOf.self, from: $0) } 56 | XCTFail("Shouldn't decode invalid \(json)") 57 | } catch {} 58 | } 59 | } 60 | 61 | @OneOfCodable 62 | private enum PlainOneOf: Equatable { 63 | case int(Int) 64 | case boolean(Bool) 65 | case string(String) 66 | } 67 | 68 | @OneOfCodable 69 | private enum NestedOneOf: Equatable { 70 | case plain(PlainOneOf) 71 | case int(Int) 72 | case boolean(Bool) 73 | case string(String) 74 | } 75 | --------------------------------------------------------------------------------