├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── MacroLibrary │ └── MacroLibrary.swift ├── MacroLibraryClient │ └── main.swift └── MacroLibraryMacros │ └── MacroLibraryMacro.swift └── Tests └── MacroLibraryTests └── MacroLibraryTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-syntax", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-syntax.git", 7 | "state" : { 8 | "revision" : "165fc6d22394c1168ff76ab5d951245971ef07e5", 9 | "version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /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 PackageDescription 5 | import CompilerPluginSupport 6 | 7 | let package = Package( 8 | name: "MacroLibrary", 9 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], 10 | products: [ 11 | // Products define the executables and libraries a package produces, making them visible to other packages. 12 | .library( 13 | name: "MacroLibrary", 14 | targets: ["MacroLibrary"] 15 | ), 16 | .executable( 17 | name: "MacroLibraryClient", 18 | targets: ["MacroLibraryClient"] 19 | ), 20 | ], 21 | dependencies: [ 22 | // Depend on the latest Swift 5.9 prerelease of SwiftSyntax 23 | .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"), 24 | ], 25 | targets: [ 26 | // Targets are the basic building blocks of a package, defining a module or a test suite. 27 | // Targets can depend on other targets in this package and products from dependencies. 28 | // Macro implementation that performs the source transformation of a macro. 29 | .macro( 30 | name: "MacroLibraryMacros", 31 | dependencies: [ 32 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 33 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 34 | ] 35 | ), 36 | 37 | // Library that exposes a macro as part of its API, which is used in client programs. 38 | .target(name: "MacroLibrary", dependencies: ["MacroLibraryMacros"]), 39 | 40 | // A client of the library, which is able to use the macro in its own code. 41 | .executableTarget(name: "MacroLibraryClient", dependencies: ["MacroLibrary"]), 42 | 43 | // A test target used to develop the macro implementation. 44 | .testTarget( 45 | name: "MacroLibraryTests", 46 | dependencies: [ 47 | "MacroLibraryMacros", 48 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 49 | ] 50 | ), 51 | ] 52 | ) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodingKeysMacro-swift 2 | 3 | CodingKeysMacro-swift is an incredibly powerful Swift Compiler Plugin that introduces an automated way to generate `CodingKeys` for Codable structs in Swift. This eliminates the need for manually declaring `CodingKeys` enumeration for every Codable struct, saving you valuable time and reducing the possibility of human errors. 4 | 5 | ## Installation 6 | 7 | ### Swift Package Manager 8 | 9 | `.package(url: "https://github.com/sasha-riabchuk/CodingKeysMacro-swift", from: "1.0.0")` 10 | 11 | ## Why CodingKeysMacro-swift is Super 12 | 13 | 1\. **Automation**: No need to manually write `CodingKeys` for each property. This library does that for you! 14 | 15 | 2\. **Reduced Errors**: Manual errors that occur while writing `CodingKeys` manually are significantly reduced. 16 | 17 | 3\. **Time-Saving**: Less time spent on boilerplate means more time for writing the code that matters. 18 | 19 | 4\. **Flexibility**: You have the freedom to specify custom string representation for any property. 20 | 21 | 22 | ## How to Use 23 | 24 | Using this library is super simple. Here's an example: 25 | 26 | ```swift 27 | 28 | @CodingKeysMacro
([ 29 |     \.buildingNumber: "building_number" 30 | ]) 31 | 32 | struct Address: Codable { 33 |     var buildingNumber: String? 34 |     let city: String? 35 |     let cityPart: String? 36 |     let district: String? 37 |     let country: String? 38 |     let registryBuildingNumber: String? 39 |     let street: String? 40 |     let zipCode: String? 41 | } 42 | 43 | ``` 44 | 45 | Just apply the `@CodingKeysMacro` annotation to your Codable struct and the library will take care of the rest. It will generate the `CodingKeys` enumeration for your Codable struct. 46 | 47 | The example above will generate the following `CodingKeys`: 48 | 49 | ```swift 50 | 51 | enum CodingKeys: String, CodingKey { 52 |     case buildingNumber = "building_number" 53 |     case city 54 |     case cityPart 55 |     case district 56 |     case country 57 |     case registryBuildingNumber 58 |     case street 59 |     case zipCode 60 | } 61 | 62 | ``` 63 | -------------------------------------------------------------------------------- /Sources/MacroLibrary/MacroLibrary.swift: -------------------------------------------------------------------------------- 1 | /// A macro annotation for creating a `CodingKeysMacro`. 2 | /// This macro provides a shorthand way to generate a `CodingKeys` enumeration for a structure. 3 | /// The macro uses a dictionary where each key-value pair represents a key path to a property and 4 | /// its respective string representation. 5 | /// The generated `CodingKeys` enumeration conforms to `String` and `CodingKey`. 6 | /// - Parameter _: A dictionary with `PartialKeyPath` as key and `String` as value. 7 | /// The dictionary represents the mapping of properties to their respective string representation. 8 | /// The default value of this parameter is an empty dictionary. 9 | /// - Note: This macro requires the external macro provided by "MacroLibraryMacros". 10 | @attached(member, names: arbitrary) 11 | public macro CodingKeysMacro(_ : [PartialKeyPath: String] = [:]) = #externalMacro( 12 | module: "MacroLibraryMacros", 13 | type: "CodingKeysMacro" 14 | ) 15 | -------------------------------------------------------------------------------- /Sources/MacroLibraryClient/main.swift: -------------------------------------------------------------------------------- 1 | import MacroLibrary 2 | 3 | @CodingKeysMacro
([ 4 | \.buildingNumber: "building_number" 5 | ]) 6 | struct Address: Codable { 7 | var buildingNumber: String? 8 | let city: String? 9 | let cityPart: String? 10 | let district: String? 11 | let country: String? 12 | let registryBuildingNumber: String? 13 | let street: String? 14 | let zipCode: String? 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Sources/MacroLibraryMacros/MacroLibraryMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | 6 | /// A struct that defines a macro for automatically generating CodingKeys 7 | public struct CodingKeysMacro: MemberMacro { 8 | 9 | /// Defines the types of errors that can occur in CodingKeysMacro 10 | private enum CodingKeysError: Error { 11 | case invalidDeclarationSyntax 12 | case keyPathNotFound 13 | } 14 | 15 | /// A function that generates an array of `DeclSyntax` based on the given attribute syntax, 16 | /// declaration and context. 17 | /// - Parameters: 18 | /// - node: An `AttributeSyntax` that represents the syntax of an attribute. 19 | /// - declaration: A `Declaration` that represents the syntax of a declaration. 20 | /// - context: A `Context` that represents the context of the macro expansion. 21 | /// - Throws: Throws an error of type `CodingKeysError` if an invalid declaration syntax is encountered. 22 | /// - Returns: Returns an array of `DeclSyntax`. 23 | public static func expansion( 24 | of node: AttributeSyntax, 25 | providingMembersOf declaration: Declaration, 26 | in context: Context 27 | ) throws -> [DeclSyntax] where Declaration : DeclGroupSyntax, Context : MacroExpansionContext { 28 | guard let structDecl = declaration.as(StructDeclSyntax.self) else { 29 | throw CodingKeysError.invalidDeclarationSyntax 30 | } 31 | 32 | let attributeSyntax = declaration.attributes?.first?.as(AttributeSyntax.self) 33 | let tupleSyntax = attributeSyntax?.argument?.as(TupleExprElementListSyntax.self) 34 | let content = tupleSyntax?.first?.expression.as(DictionaryExprSyntax.self) 35 | let dictionaryList = content?.content.as(DictionaryElementListSyntax.self) 36 | let valueExpression = dictionaryList?.compactMap({$0.valueExpression.as(StringLiteralExprSyntax.self)}) 37 | let keyPathExpression = dictionaryList?.compactMap({$0.keyExpression.as(KeyPathExprSyntax.self)}) 38 | 39 | guard let keyPathSyntax = keyPathExpression?.compactMap({ $0.components }), 40 | let stringSegmentSyntax = valueExpression?.compactMap({$0.segments.first?.as(StringSegmentSyntax.self)}) 41 | else { 42 | return Self.generateDefaultCodingKeys(structDecl) 43 | } 44 | 45 | let strings = stringSegmentSyntax.compactMap({ $0.content }) 46 | let members = structDecl.memberBlock.members 47 | let variableDecl = members.compactMap { $0.decl.as(VariableDeclSyntax.self)?.bindings } 48 | let variables = variableDecl.compactMap({$0.first?.pattern}) 49 | var valueTuple: [(KeyPathComponentListSyntax, TokenSyntax)] = [] 50 | 51 | for (key, value) in zip(keyPathSyntax, strings) { 52 | valueTuple.append((key, value)) 53 | } 54 | 55 | let parameters = variables 56 | .map { variable in 57 | if valueTuple.contains(where: { (key, value) in 58 | guard let key = key.first?.component else { return false } 59 | return "\(key)" == "\(variable)" 60 | }){ 61 | guard let index = valueTuple.firstIndex(where: { (key, value) in 62 | guard let key = key.first?.component else { return false } 63 | return "\(key)" == "\(variable)" 64 | }) else { return "case \(variable)" } 65 | return """ 66 | case \(variable) = "\(valueTuple[index].1.text)" 67 | """ 68 | } else { 69 | return "case \(variable)" 70 | } 71 | } 72 | .joined(separator: "\n") 73 | 74 | return [ 75 | """ 76 | enum CodingKeys: String, CodingKey { 77 | \(raw: parameters) 78 | } 79 | """ 80 | ] 81 | } 82 | 83 | /// A function that generates default coding keys. 84 | /// - Parameter structDecSyntax: A `StructDeclSyntax` that represents the syntax of a struct declaration. 85 | /// - Returns: Returns an array of `DeclSyntax`. 86 | public static func generateDefaultCodingKeys(_ structDecSyntax: StructDeclSyntax) -> [DeclSyntax] { 87 | let members = structDecSyntax.memberBlock.members 88 | let variableDecl = members.compactMap { $0.decl.as(VariableDeclSyntax.self)?.bindings } 89 | let variables = variableDecl.compactMap({$0.first?.pattern}) 90 | let parameters = variables 91 | .map { variable in 92 | return "case \(variable)" 93 | } 94 | .joined(separator: "\n") 95 | 96 | return [ 97 | """ 98 | enum CodingKeys: String, CodingKey { 99 | \(raw: parameters) 100 | } 101 | """ 102 | ] 103 | } 104 | } 105 | 106 | /// A struct that defines a compiler plugin. 107 | /// The plugin provides macros. 108 | @main 109 | struct MacroLibraryPlugin: CompilerPlugin { 110 | /// An array of Macro types provided by the plugin. 111 | let providingMacros: [Macro.Type] = [ 112 | CodingKeysMacro.self 113 | ] 114 | } 115 | -------------------------------------------------------------------------------- /Tests/MacroLibraryTests/MacroLibraryTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntaxMacros 2 | import SwiftSyntaxMacrosTestSupport 3 | import XCTest 4 | import MacroLibraryMacros 5 | import XCTest 6 | import SwiftSyntax 7 | import SwiftSyntaxBuilder 8 | import SwiftSyntaxMacros 9 | 10 | final class MacroLibraryTests: XCTestCase { 11 | let testMacros: [String: Macro.Type] = [ 12 | "CodingKeysMacro": CodingKeysMacro.self 13 | ] 14 | 15 | func testApiObjects() throws { 16 | 17 | let sf: SourceFileSyntax = """ 18 | @Init 19 | public struct Something: Codable { 20 | let foo: String 21 | let bar: Int 22 | let hello: Bool? 23 | } 24 | """ 25 | 26 | let expectation = """ 27 | 28 | public struct Something: Codable { 29 | let foo: String 30 | let bar: Int 31 | let hello: Bool? 32 | public init( 33 | foo: String, 34 | bar: Int, 35 | hello: Bool? 36 | ) { 37 | self.foo = foo 38 | self.bar = bar 39 | self.hello = hello 40 | } 41 | } 42 | """ 43 | 44 | let context = BasicMacroExpansionContext( 45 | sourceFiles: [ 46 | sf: .init( 47 | moduleName: "TestModule", 48 | fullFilePath: "test.swift" 49 | ) 50 | ] 51 | ) 52 | 53 | let transformed = sf.expand(macros: testMacros, in: context) 54 | XCTAssertEqual(transformed.formatted().description, expectation) 55 | } 56 | 57 | func testApiObjects1() throws { 58 | 59 | let sf: SourceFileSyntax = #""" 60 | @CodingKeysMacro([ 61 | \Something.newUser: "new_user", 62 | \Something.bar: "Value", 63 | \Something.foo: "fo_o" 64 | ]) 65 | public struct Something: Codable { 66 | let foo: String 67 | let bar: Int 68 | let newUser: String 69 | } 70 | """# 71 | 72 | let expectation = """ 73 | 74 | public struct Something: Codable { 75 | let foo: String 76 | let bar: Int 77 | let newUser: String 78 | enum CodingKeys: String, CodingKey { 79 | case foo = "fo_o" 80 | case bar = "Value" 81 | case newUser = "new_user" 82 | } 83 | } 84 | """ 85 | 86 | let context = BasicMacroExpansionContext( 87 | sourceFiles: [ 88 | sf: .init( 89 | moduleName: "TestModule", 90 | fullFilePath: "test.swift" 91 | ) 92 | ] 93 | ) 94 | 95 | let transformed = sf.expand(macros: testMacros, in: context) 96 | XCTAssertEqual(transformed.formatted().description, expectation) 97 | } 98 | 99 | func testCodingKeysWithoutPartialKeyPaths() throws { 100 | 101 | let sf: SourceFileSyntax = #""" 102 | @CodingKeysMacro() 103 | public struct Something: Codable { 104 | let foo: String 105 | let bar: Int 106 | let newUser: String 107 | } 108 | """# 109 | 110 | let expectation = """ 111 | 112 | public struct Something: Codable { 113 | let foo: String 114 | let bar: Int 115 | let newUser: String 116 | enum CodingKeys: String, CodingKey { 117 | case foo 118 | case bar 119 | case newUser 120 | } 121 | } 122 | """ 123 | 124 | let context = BasicMacroExpansionContext( 125 | sourceFiles: [ 126 | sf: .init( 127 | moduleName: "TestModule", 128 | fullFilePath: "test.swift" 129 | ) 130 | ] 131 | ) 132 | 133 | let transformed = sf.expand(macros: testMacros, in: context) 134 | XCTAssertEqual(transformed.formatted().description, expectation) 135 | } 136 | } 137 | --------------------------------------------------------------------------------