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..88c8a1f 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: @@ -26,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 diff --git a/.github/workflows/multiplatform-tests.yml b/.github/workflows/multiplatform-tests.yml index a40652d..3d1305c 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 @@ -46,7 +48,10 @@ jobs: name: latest Ubuntu swift tests runs-on: ubuntu-latest steps: + - uses: swift-actions/setup-swift@v2 - uses: actions/checkout@v4 + - name: Version + run: swift --version - name: Build run: swift build -v - name: Run swift tests 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] ) 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( """ diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 92c9f47..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 = [ @@ -28,10 +29,10 @@ let allTestClasses = [ CompositionTests.self, PartialImplementationTests.self, ] -#if os(Linux) +//#if os(Linux) @main struct Main { static func main() { QCKMain(allTestClasses) } } -#endif +//#endif