├── .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 |
--------------------------------------------------------------------------------