From ce65c842fee385bed2f6ee979456eb7c710332a7 Mon Sep 17 00:00:00 2001 From: PJ Fechner Date: Thu, 17 Apr 2025 23:55:09 -0500 Subject: [PATCH 01/12] Initial version of custom separators --- Sources/CodableMacros/CodableMacros.swift | 8 +-- .../CodingKeys/CodingKeyMacros.swift | 10 ++-- .../CodingKeys/CodingKeyTypes.swift | 51 ++++++++++--------- .../CodingKeys/CodingKeysGenerator.swift | 2 +- .../Convenience/ConvenienceExtensions.swift | 22 +++++--- .../CodingKeyMacroTests.swift | 32 ++++++++++++ 6 files changed, 85 insertions(+), 40 deletions(-) diff --git a/Sources/CodableMacros/CodableMacros.swift b/Sources/CodableMacros/CodableMacros.swift index 44a32de..4c9ceac 100644 --- a/Sources/CodableMacros/CodableMacros.swift +++ b/Sources/CodableMacros/CodableMacros.swift @@ -20,19 +20,19 @@ public macro CodingKeySuffix(_ name: StringLiteralType) = #externalMacro(module: /// CodingKey value will be `camelCase` @attached(peer) -public macro CamelCase() = #externalMacro(module: "CodableWrapperMacros", type: "CamelCase") +public macro CamelCase(separator: StringLiteralType = "") = #externalMacro(module: "CodableWrapperMacros", type: "CamelCase") /// CodingKey value will be` flatcase` @attached(peer) -public macro FlatCase() = #externalMacro(module: "CodableWrapperMacros", type: "FlatCase") +public macro FlatCase(separator: StringLiteralType = "") = #externalMacro(module: "CodableWrapperMacros", type: "FlatCase") /// CodingKey value will be `PascalCase` @attached(peer) -public macro PascalCase() = #externalMacro(module: "CodableWrapperMacros", type: "PascalCase") +public macro PascalCase(separator: StringLiteralType = "") = #externalMacro(module: "CodableWrapperMacros", type: "PascalCase") /// CodingKey value will be `UPPERCASE` @attached(peer) -public macro UpperCase() = #externalMacro(module: "CodableWrapperMacros", type: "UpperCase") +public macro UpperCase(separator: StringLiteralType = "") = #externalMacro(module: "CodableWrapperMacros", type: "UpperCase") /// CodingKey value will be `snake_case` @attached(peer) diff --git a/Sources/CodableWrapperMacros/CodingKeys/CodingKeyMacros.swift b/Sources/CodableWrapperMacros/CodingKeys/CodingKeyMacros.swift index cd76a82..2663934 100644 --- a/Sources/CodableWrapperMacros/CodingKeys/CodingKeyMacros.swift +++ b/Sources/CodableWrapperMacros/CodingKeys/CodingKeyMacros.swift @@ -153,12 +153,12 @@ enum CodingKeyAttribute: String, CaseIterable { /// CASED-LIKE-THIS case screamingKebabCase = "ScreamingKebabCase" - var codingKeyCase: CodingKeyCase { + func codingKeyCase(customSeparator: String? = nil) -> CodingKeyCase { switch self { - case .camelCase: .camelCase - case .flatCase: .flatCase - case .pascalCase: .pascalCase - case .upperCase: .upperCase + case .camelCase: .camelCase(separator: customSeparator ?? "") + case .flatCase: .flatCase(separator: customSeparator ?? "") + case .pascalCase: .pascalCase(separator: customSeparator ?? "") + case .upperCase: .upperCase(separator: customSeparator ?? "") case .snakeCase: .snakeCase case .camelSnakeCase: .camelSnakeCase case .pascalSnakeCase: .pascalSnakeCase diff --git a/Sources/CodableWrapperMacros/CodingKeys/CodingKeyTypes.swift b/Sources/CodableWrapperMacros/CodingKeys/CodingKeyTypes.swift index db506dc..f8e5833 100644 --- a/Sources/CodableWrapperMacros/CodingKeys/CodingKeyTypes.swift +++ b/Sources/CodableWrapperMacros/CodingKeys/CodingKeyTypes.swift @@ -10,9 +10,10 @@ import SwiftDiagnostics struct CodingAttributeInfo { let attributeType: CodingKeyAttribute + let customSeparator: String? var codingKeyCase: CodingKeyCase { - attributeType.codingKeyCase + attributeType.codingKeyCase(customSeparator: customSeparator) } func asCodingKeyInfo(named name: String) throws -> CodingKeyInfo { @@ -36,15 +37,18 @@ struct CodingAttributeInfo { struct CodingKeyInfo { let caseName: String var rawCaseValue: String + let customSeparator: String? - init(caseName: String, rawCaseValue: String) { + init(caseName: String, rawCaseValue: String, customSeparator: String?) { self.caseName = caseName self.rawCaseValue = rawCaseValue.replacingOccurrences(of: "\"", with: "") + self.customSeparator = customSeparator } init(caseName: String, rawCaseValue: String, keyCase: CodingKeyCase) { self.init(caseName: caseName, - rawCaseValue: keyCase.makeKeyValue(from: rawCaseValue.replacingOccurrences(of: "\"", with: ""))) + rawCaseValue: keyCase.makeKeyValue(from: rawCaseValue.replacingOccurrences(of: "\"", with: "")), + customSeparator: keyCase.separator) } var declaration: MemberBlockItemSyntax { @@ -56,40 +60,39 @@ enum CodingKeyCase { /// no changes case noChanges /// casedLikeThis - case camelCase + case camelCase(separator: String = "") /// casedlikethis - case flatCase + case flatCase(separator: String = "") /// CasedLikeThis - case pascalCase + case pascalCase(separator: String = "") /// CASEDLIKETHIS - case upperCase + case upperCase(separator: String = "") + /// cased_like_this - case snakeCase + static var snakeCase: Self { .flatCase(separator: "_") } /// cased_Like_This - case camelSnakeCase + static var camelSnakeCase: Self { .camelCase(separator: "_") } /// cased_Like_This - case pascalSnakeCase + static var pascalSnakeCase: Self { .pascalCase(separator: "_") } /// CASED_LIKE_THIS - case screamingSnakeCase + static var screamingSnakeCase: Self { .upperCase(separator: "_") } /// cased-like-this - case kebabCase + static var kebabCase: Self { .flatCase(separator: "-") } /// cased-Like-This - case camelKebabCase + static var camelKebabCase: Self { .camelCase(separator: "-") } /// Cased-Like-This - case pascalKebabCase + static var pascalKebabCase: Self { .pascalCase(separator: "-") } /// CASED-LIKE-THIS - case screamingKebabCase + static var screamingKebabCase: Self { .upperCase(separator: "-") } /// custom casing case custom((String) -> (String)) var separator: String? { switch self { - case .noChanges, .camelCase, .flatCase, .pascalCase, .upperCase: + case .noChanges: "" - case .snakeCase, .camelSnakeCase, .pascalSnakeCase, .screamingSnakeCase: - "_" - case .kebabCase, .camelKebabCase, .pascalKebabCase, .screamingKebabCase: - "-" + case .camelCase(let separator), .flatCase(let separator), .pascalCase(let separator), .upperCase(let separator): + separator case .custom: nil } @@ -97,13 +100,13 @@ enum CodingKeyCase { var caseVariant: CaseVariant? { switch self { - case .flatCase, .snakeCase, .kebabCase: + case .flatCase: .lowerCase - case .camelCase, .camelSnakeCase, .camelKebabCase: + case .camelCase: .camelCase - case .pascalCase, .pascalSnakeCase, .pascalKebabCase: + case .pascalCase: .pascalCase - case .upperCase, .screamingSnakeCase, .screamingKebabCase: + case .upperCase: .upperCase case .custom(_), .noChanges: nil diff --git a/Sources/CodableWrapperMacros/CodingKeys/CodingKeysGenerator.swift b/Sources/CodableWrapperMacros/CodingKeys/CodingKeysGenerator.swift index 253069b..e7234c6 100644 --- a/Sources/CodableWrapperMacros/CodingKeys/CodingKeysGenerator.swift +++ b/Sources/CodableWrapperMacros/CodingKeys/CodingKeysGenerator.swift @@ -76,7 +76,7 @@ class CodingKeysGenerator { if !property.codableAttributes.isEmpty { context.diagnose(.init(node: member, syntaxWarning: .defaultingToCodingKey)) } - return .init(caseName: propertyName, rawCaseValue: codingKey) + return .init(caseName: propertyName, rawCaseValue: codingKey, customSeparator: nil) } if let codingAttribute = property.codableAttributes.first { return try codingAttribute.asCodingKeyInfo(named: propertyName) diff --git a/Sources/CodableWrapperMacros/Convenience/ConvenienceExtensions.swift b/Sources/CodableWrapperMacros/Convenience/ConvenienceExtensions.swift index d608d75..0b1cc0f 100644 --- a/Sources/CodableWrapperMacros/Convenience/ConvenienceExtensions.swift +++ b/Sources/CodableWrapperMacros/Convenience/ConvenienceExtensions.swift @@ -18,10 +18,12 @@ extension MemberBlockItemListSyntax.Element { } var codingAttributes: [CodingAttributeInfo] { - attributes(matching: CodingKeyAttribute.self).map(CodingAttributeInfo.init(attributeType:)) + attributes(matching: CodingKeyAttribute.self).map { + CodingAttributeInfo.init(attributeType: $0.0, customSeparator: $0.1) + } } - func attributes(matching rawType: T.Type) -> [T] where T.RawValue == String { + func attributes(matching rawType: T.Type) -> [(T, String?)] where T.RawValue == String { decl.as(VariableDeclSyntax.self)?.attributes.matching(matching: T.self) ?? [] } @@ -36,9 +38,10 @@ extension MemberBlockItemListSyntax.Element { } extension AttributeListSyntax { - var codingAttributes: [CodingAttributeInfo] { - matching(matching: CodingKeyAttribute.self).map(CodingAttributeInfo.init(attributeType:)) + matching(matching: CodingKeyAttribute.self).map { + CodingAttributeInfo.init(attributeType: $0.0, customSeparator: $0.1) + } } func attribute(named attributeName: String) -> AttributeListSyntax.Element? { @@ -49,12 +52,13 @@ extension AttributeListSyntax { attribute(named: attributeName)?.as(AttributeSyntax.self) } - func matching(matching rawType: T.Type) -> [T] where T.RawValue == String { + func matching(matching rawType: T.Type) -> [(T, String?)] where T.RawValue == String { compactMap { guard let attributeName = $0.identifierName?.trimmingCharacters(in: .whitespacesAndNewlines), let type = T(rawValue: attributeName) else { return nil } - return type + + return (type, try? $0.parameterValue()) } } @@ -74,6 +78,12 @@ extension AttributeListSyntax.Element { } } +extension AttributeListSyntax.Element { + func parameterValue() throws -> String? { + try self.as(AttributeSyntax.self)?.parameterValue() + } +} + extension DeclGroupSyntax { func hasEnum(named name: String) -> Bool { memberBlock.members.first(where: { $0.decl.as(EnumDeclSyntax.self)?.name.text == name }) != nil diff --git a/Tests/CodableWrapperMacrosTests/CodingKeyMacroTests.swift b/Tests/CodableWrapperMacrosTests/CodingKeyMacroTests.swift index 571cbb3..ad2707c 100644 --- a/Tests/CodableWrapperMacrosTests/CodingKeyMacroTests.swift +++ b/Tests/CodableWrapperMacrosTests/CodingKeyMacroTests.swift @@ -11,6 +11,38 @@ import XCTest @testable import CodableWrapperMacros final class CodingKeyMacroTests: XCTestCase { + func testCustomCaseSeparators() throws { + assertMacroExpansion( + """ + @CustomCodable() @CamelCase(separator: "~") + struct TestCodable: Codable { + let camelCaseKey: String + @FlatCase(seprator: "~") + let flatCaseKey: String + @PascalCase(seprator: "~") + let pascalCaseKey: String + @UpperCase(seprator: "~") + let upperCaseKey: String + } + """, + expandedSource: """ + struct TestCodable: Codable { + let camelCaseKey: String + let flatCaseKey: String + let pascalCaseKey: String + let upperCaseKey: String + + private enum CodingKeys: String, CodingKey { + case camelCaseKey = "camel~Case~Key" + case flatCaseKey = "flat~case~key" + case pascalCaseKey = "Pascal~Case~Key" + case upperCaseKey = "UPPER~CASE~KEY" + } + } + """, + macros: testMacros) + } + func testCustomCodingWorks() throws { assertMacroExpansion( """ From 5cff3d2277be1d2da366692d8c13ddfcc044193b Mon Sep 17 00:00:00 2001 From: PJ Fechner Date: Fri, 18 Apr 2025 00:09:31 -0500 Subject: [PATCH 02/12] Removing unused test target --- Package.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Package.swift b/Package.swift index ca2b43f..99cb3a7 100644 --- a/Package.swift +++ b/Package.swift @@ -76,15 +76,15 @@ let package = Package( .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), ] ), - .testTarget( - name: "IntegrationTests", - dependencies: [ - "CodableWrapperMacros", - "Quick", "Nimble", - "CodableWrappers", - .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), - ] - ), +// .testTarget( +// name: "IntegrationTests", +// dependencies: [ +// "CodableWrapperMacros", +// "Quick", "Nimble", +// "CodableWrappers", +// .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), +// ] +// ), ], swiftLanguageVersions: [.version("6"), .v5] ) From 1d89b4f642312a109bdcb3aa0a06f90707449723 Mon Sep 17 00:00:00 2001 From: PJ Fechner Date: Mon, 14 Jul 2025 11:43:31 -0500 Subject: [PATCH 03/12] Adding workflow_dispatch to workflows --- .github/workflows/code-coverage.yml | 1 + .github/workflows/experimental-tests.yml | 3 ++- .github/workflows/ios-tests.yml | 1 + .github/workflows/multiplatform-tests.yml | 2 ++ 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 609ae4c..f9a32bb 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -1,5 +1,6 @@ name: Code Coverage on: + workflow_dispatch: push: branches: [ "main" ] pull_request: diff --git a/.github/workflows/experimental-tests.yml b/.github/workflows/experimental-tests.yml index a19fb20..4849a46 100644 --- a/.github/workflows/experimental-tests.yml +++ b/.github/workflows/experimental-tests.yml @@ -4,6 +4,7 @@ name: Experimental Tests on: + workflow_dispatch: push: branches: [ "experimental*" ] pull_request: @@ -22,4 +23,4 @@ jobs: - name: Build run: swift build -v - name: Run swift tests - run: swift test -v \ No newline at end of file + run: swift test -v diff --git a/.github/workflows/ios-tests.yml b/.github/workflows/ios-tests.yml index 1b3fced..a0c60f2 100644 --- a/.github/workflows/ios-tests.yml +++ b/.github/workflows/ios-tests.yml @@ -4,6 +4,7 @@ name: iOS Tests on: + workflow_dispatch: push: branches: [ "main" ] pull_request: diff --git a/.github/workflows/multiplatform-tests.yml b/.github/workflows/multiplatform-tests.yml index a40652d..446981d 100644 --- a/.github/workflows/multiplatform-tests.yml +++ b/.github/workflows/multiplatform-tests.yml @@ -4,11 +4,13 @@ name: Multi-platform Tests on: + workflow_dispatch: push: branches: [ "main" ] pull_request: branches: [ "main" ] + jobs: check-macro-compatibility: name: Check Macro Compatibility From 88aca21e09ef68697187efe26c8efc70ce61d745 Mon Sep 17 00:00:00 2001 From: PJ Fechner Date: Mon, 14 Jul 2025 12:00:17 -0500 Subject: [PATCH 04/12] Fixing workflow? --- .github/workflows/ios-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios-tests.yml b/.github/workflows/ios-tests.yml index a0c60f2..88c8a1f 100644 --- a/.github/workflows/ios-tests.yml +++ b/.github/workflows/ios-tests.yml @@ -27,7 +27,7 @@ jobs: ios-17-test: name: iOS 17 tests - runs-on: macos-15 + runs-on: macos-14 steps: - uses: actions/checkout@v4 - name: iOS 17 From 2e75c42a5bdc786a87af15219f0253578e5dd4e5 Mon Sep 17 00:00:00 2001 From: PJ Fechner Date: Mon, 14 Jul 2025 12:35:24 -0500 Subject: [PATCH 05/12] Testing --- Tests/LinuxMain.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 92c9f47..5dcb4e0 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -28,10 +28,10 @@ let allTestClasses = [ CompositionTests.self, PartialImplementationTests.self, ] -#if os(Linux) +//#if os(Linux) @main struct Main { static func main() { QCKMain(allTestClasses) } } -#endif +//#endif From 66d56795d54b512231537bae86c710f7099ee1dc Mon Sep 17 00:00:00 2001 From: PJ Fechner Date: Mon, 14 Jul 2025 13:12:41 -0500 Subject: [PATCH 06/12] Test --- .github/workflows/multiplatform-tests.yml | 2 ++ Tests/LinuxMain.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/multiplatform-tests.yml b/.github/workflows/multiplatform-tests.yml index 446981d..7dd5d90 100644 --- a/.github/workflows/multiplatform-tests.yml +++ b/.github/workflows/multiplatform-tests.yml @@ -47,6 +47,8 @@ jobs: ubuntu-swift-test: name: latest Ubuntu swift tests runs-on: ubuntu-latest + container: + image: swift:latest steps: - uses: actions/checkout@v4 - name: Build diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 5dcb4e0..2c3b279 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -2,6 +2,7 @@ import XCTest import Quick @testable import CodableWrappersTests +@testable import CodableWrappers //@testable import CodableWrapperMacrosTests let allTestClasses = [ From 65e2d33326ef7d3bd48a0ac466bcacde5101347e Mon Sep 17 00:00:00 2001 From: PJ Fechner Date: Mon, 14 Jul 2025 13:19:33 -0500 Subject: [PATCH 07/12] Test --- .github/workflows/multiplatform-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/multiplatform-tests.yml b/.github/workflows/multiplatform-tests.yml index 7dd5d90..f3fe207 100644 --- a/.github/workflows/multiplatform-tests.yml +++ b/.github/workflows/multiplatform-tests.yml @@ -47,10 +47,10 @@ jobs: ubuntu-swift-test: name: latest Ubuntu swift tests runs-on: ubuntu-latest - container: - image: swift:latest steps: - uses: actions/checkout@v4 + with: + swift-version: 6.0.3 - name: Build run: swift build -v - name: Run swift tests From 3259c04d0193115a48cb2c1f118637e47ac06527 Mon Sep 17 00:00:00 2001 From: PJ Fechner Date: Mon, 14 Jul 2025 13:21:30 -0500 Subject: [PATCH 08/12] Test --- .github/workflows/multiplatform-tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/multiplatform-tests.yml b/.github/workflows/multiplatform-tests.yml index f3fe207..fe38aff 100644 --- a/.github/workflows/multiplatform-tests.yml +++ b/.github/workflows/multiplatform-tests.yml @@ -48,9 +48,10 @@ jobs: name: latest Ubuntu swift tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: swift-actions/setup-swift@65540b95f51493d65f5e59e97dcef9629ddf11bf with: - swift-version: 6.0.3 + swift-version: "6.0.3" + - uses: actions/checkout@v4 - name: Build run: swift build -v - name: Run swift tests From dd2aff1443d429c77c31cbfe86db644f714a47c3 Mon Sep 17 00:00:00 2001 From: PJ Fechner Date: Mon, 14 Jul 2025 13:23:27 -0500 Subject: [PATCH 09/12] test --- .github/workflows/multiplatform-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/multiplatform-tests.yml b/.github/workflows/multiplatform-tests.yml index fe38aff..7eaf509 100644 --- a/.github/workflows/multiplatform-tests.yml +++ b/.github/workflows/multiplatform-tests.yml @@ -48,7 +48,7 @@ jobs: name: latest Ubuntu swift tests runs-on: ubuntu-latest steps: - - uses: swift-actions/setup-swift@65540b95f51493d65f5e59e97dcef9629ddf11bf + - uses: swift-actions/setup-swift@v2 with: swift-version: "6.0.3" - uses: actions/checkout@v4 From 1b8b5404ce383c8ec905efa7c653b354a0c660ad Mon Sep 17 00:00:00 2001 From: PJ Fechner Date: Mon, 14 Jul 2025 13:27:39 -0500 Subject: [PATCH 10/12] test --- .github/workflows/multiplatform-tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/multiplatform-tests.yml b/.github/workflows/multiplatform-tests.yml index 7eaf509..1502d5b 100644 --- a/.github/workflows/multiplatform-tests.yml +++ b/.github/workflows/multiplatform-tests.yml @@ -49,8 +49,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: swift-actions/setup-swift@v2 - with: - swift-version: "6.0.3" - uses: actions/checkout@v4 - name: Build run: swift build -v From a1a64646d80e760997b1f87c06b5ed92af3ec295 Mon Sep 17 00:00:00 2001 From: PJ Fechner Date: Mon, 14 Jul 2025 13:43:54 -0500 Subject: [PATCH 11/12] test --- .github/workflows/multiplatform-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/multiplatform-tests.yml b/.github/workflows/multiplatform-tests.yml index 1502d5b..6fdb275 100644 --- a/.github/workflows/multiplatform-tests.yml +++ b/.github/workflows/multiplatform-tests.yml @@ -51,6 +51,7 @@ jobs: - uses: swift-actions/setup-swift@v2 - uses: actions/checkout@v4 - name: Build + run: swift --version run: swift build -v - name: Run swift tests run: swift test -v From 3455172437dc44f0780d488b596308f60a4b32d2 Mon Sep 17 00:00:00 2001 From: PJ Fechner Date: Mon, 14 Jul 2025 13:57:58 -0500 Subject: [PATCH 12/12] test --- .github/workflows/multiplatform-tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/multiplatform-tests.yml b/.github/workflows/multiplatform-tests.yml index 6fdb275..3d1305c 100644 --- a/.github/workflows/multiplatform-tests.yml +++ b/.github/workflows/multiplatform-tests.yml @@ -50,8 +50,9 @@ jobs: steps: - uses: swift-actions/setup-swift@v2 - uses: actions/checkout@v4 - - name: Build + - name: Version run: swift --version + - name: Build run: swift build -v - name: Run swift tests run: swift test -v