Skip to content

Commit 3340436

Browse files
committed
add case style
1 parent b5eba73 commit 3340436

6 files changed

Lines changed: 433 additions & 23 deletions

File tree

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,17 @@ The `caseStyle` parameter of `@CodedAt` controls how Swift case names map to the
210210

211211
| Style | Swift case | JSON tag |
212212
|---|---|---|
213-
| `.screamingSnakeCase` *(default)* | `applePay` | `"APPLE_PAY"` |
213+
| `.verbatim` *(default)* | `send_message` | `"send_message"` |
214+
| `.screamingSnakeCase` | `applePay` | `"APPLE_PAY"` |
214215
| `.snakeCase` | `applePay` | `"apple_pay"` |
215216
| `.camelCase` | `applePay` | `"applePay"` |
216-
| `.verbatim` | `applePay` | `"applePay"` |
217+
| `.pascalCase` | `applePay` | `"ApplePay"` |
218+
| `.kebabCase` | `applePay` | `"apple-pay"` |
219+
220+
For `.camelCase`, `.pascalCase`, `.snakeCase`, `.screamingSnakeCase`, and `.kebabCase`,
221+
case names are normalized through tokenization first, so `sendMessage`,
222+
`SendMessage`, `send_message`, and `send-message` all map consistently.
223+
`.verbatim` skips normalization and preserves the original case name as-is.
217224

218225
#### Custom keys
219226

Sources/Macro/Macro/TaggedEnum/TaggedEnumMacroBase.swift

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import MacroToolkit
2+
import Foundation
23
import SwiftDiagnostics
34
import SwiftSyntax
45
import SwiftSyntaxMacros
@@ -21,23 +22,61 @@ enum TaggedEnumMacroBase {
2122
func transform(_ caseName: String) -> String {
2223
switch memberName {
2324
case "verbatim": return caseName
24-
case "camelCase": return caseName
25-
case "snakeCase": return toSnakeCase(caseName)
26-
case "screamingSnakeCase": return toSnakeCase(caseName).uppercased()
27-
default: return toSnakeCase(caseName).uppercased()
25+
case "camelCase": return CaseConverter.format(caseName, to: .camelCase)
26+
case "pascalCase": return CaseConverter.format(caseName, to: .pascalCase)
27+
case "snakeCase": return CaseConverter.format(caseName, to: .snakeCase)
28+
case "screamingSnakeCase": return CaseConverter.format(caseName, to: .screamingSnakeCase)
29+
case "kebabCase": return CaseConverter.format(caseName, to: .kebabCase)
30+
default: return caseName
2831
}
2932
}
33+
}
34+
35+
struct CaseConverter {
36+
static func tokenize(_ input: String) -> [String] {
37+
let splitCamel = input.replacingOccurrences(
38+
of: "([a-z])([A-Z])",
39+
with: "$1 $2",
40+
options: .regularExpression
41+
)
42+
43+
let separators = CharacterSet.alphanumerics.inverted
44+
let components = splitCamel.components(separatedBy: separators)
3045

31-
private func toSnakeCase(_ s: String) -> String {
32-
var result = ""
33-
for (i, c) in s.enumerated() {
34-
if c.isUppercase && i > 0 { result += "_" }
35-
result += String(c).lowercased()
46+
return components
47+
.filter { !$0.isEmpty }
48+
.map { $0.lowercased() }
49+
}
50+
51+
static func format(_ string: String, to style: Style) -> String {
52+
let tokens = tokenize(string)
53+
guard !tokens.isEmpty else { return string }
54+
55+
switch style {
56+
case .camelCase:
57+
let first = tokens[0]
58+
let rest = tokens.dropFirst().map(\.capitalized)
59+
return first + rest.joined()
60+
case .pascalCase:
61+
return tokens.map(\.capitalized).joined()
62+
case .snakeCase:
63+
return tokens.joined(separator: "_")
64+
case .screamingSnakeCase:
65+
return tokens.joined(separator: "_").uppercased()
66+
case .kebabCase:
67+
return tokens.joined(separator: "-")
3668
}
37-
return result
3869
}
3970
}
4071

72+
enum Style {
73+
case camelCase
74+
case pascalCase
75+
case snakeCase
76+
case screamingSnakeCase
77+
case kebabCase
78+
}
79+
4180
// MARK: - Entry Point
4281

4382
static func expansion(
@@ -151,7 +190,7 @@ enum TaggedEnumMacroBase {
151190

152191
let tagKey = segment.content.text
153192

154-
var styleName = "screamingSnakeCase"
193+
var styleName = "verbatim"
155194
if let styleArg = args.first(where: { $0.label?.text == "caseStyle" }),
156195
let member = styleArg.expression.as(MemberAccessExprSyntax.self) {
157196
styleName = member.declName.baseName.text
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import Foundation
2+
3+
public enum CaseStyle {
4+
case verbatim
5+
case camelCase
6+
case pascalCase
7+
case snakeCase
8+
case screamingSnakeCase
9+
case kebabCase
10+
}
11+
12+
public struct CaseConverter {
13+
public static func tokenize(_ input: String) -> [String] {
14+
let splitCamel = input.replacingOccurrences(
15+
of: "([a-z])([A-Z])",
16+
with: "$1 $2",
17+
options: .regularExpression
18+
)
19+
20+
let separators = CharacterSet.alphanumerics.inverted
21+
let components = splitCamel.components(separatedBy: separators)
22+
23+
return components
24+
.filter { !$0.isEmpty }
25+
.map { $0.lowercased() }
26+
}
27+
28+
public static func format(_ string: String, to style: CaseStyle) -> String {
29+
let tokens = tokenize(string)
30+
guard !tokens.isEmpty else { return string }
31+
32+
switch style {
33+
case .verbatim:
34+
return string
35+
case .camelCase:
36+
let first = tokens[0]
37+
let rest = tokens.dropFirst().map(\.capitalized)
38+
return first + rest.joined()
39+
case .pascalCase:
40+
return tokens.map(\.capitalized).joined()
41+
case .snakeCase:
42+
return tokens.joined(separator: "_")
43+
case .screamingSnakeCase:
44+
return tokens.joined(separator: "_").uppercased()
45+
case .kebabCase:
46+
return tokens.joined(separator: "-")
47+
}
48+
}
49+
}
50+
51+
@available(*, deprecated, renamed: "CaseStyle")
52+
public typealias TaggedCodableCaseStyle = CaseStyle

Sources/MacroCodableKit/Macros/TaggedAnnotations.swift

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
/// The strategy used to transform enum case names into the serialized tag value.
2-
public enum TaggedCodableCaseStyle {
3-
case verbatim
4-
case camelCase
5-
case snakeCase
6-
case screamingSnakeCase
7-
}
8-
91
/// Generates `Decodable` and `Encodable` conformances for an enum using an
102
/// adjacently tagged format: one key for the discriminator tag, another for
113
/// the associated-value parameters.
@@ -37,9 +29,9 @@ public macro TaggedCodable() = #externalMacro(module: "Macro", type: "TaggedCoda
3729
///
3830
/// - Parameters:
3931
/// - key: The JSON key used as the discriminator (e.g. `"intent"`).
40-
/// - caseStyle: How enum case names map to tag values. Defaults to `.screamingSnakeCase`.
32+
/// - caseStyle: How enum case names map to tag values. Defaults to `.verbatim`.
4133
@attached(peer)
42-
public macro CodedAt(_ key: String, caseStyle: TaggedCodableCaseStyle = .screamingSnakeCase) = #externalMacro(module: "Macro", type: "CodedAtMacro")
34+
public macro CodedAt(_ key: String, caseStyle: CaseStyle = .verbatim) = #externalMacro(module: "Macro", type: "CodedAtMacro")
4335

4436
/// Specifies the JSON key under which associated values are nested for
4537
/// ``TaggedCodable()``.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import MacroCodableKit
2+
import XCTest
3+
4+
final class CaseStyleTests: XCTestCase {
5+
func test_tokenize() {
6+
XCTAssertEqual(CaseConverter.tokenize("sampleEntry"), ["sample", "entry"])
7+
XCTAssertEqual(CaseConverter.tokenize("SampleEntry"), ["sample", "entry"])
8+
XCTAssertEqual(CaseConverter.tokenize("sample_entry"), ["sample", "entry"])
9+
XCTAssertEqual(CaseConverter.tokenize("sample-entry"), ["sample", "entry"])
10+
}
11+
12+
func test_format() {
13+
XCTAssertEqual(CaseConverter.format("sample_entry", to: .verbatim), "sample_entry")
14+
XCTAssertEqual(CaseConverter.format("sample_entry", to: .camelCase), "sampleEntry")
15+
XCTAssertEqual(CaseConverter.format("sample_entry", to: .pascalCase), "SampleEntry")
16+
XCTAssertEqual(CaseConverter.format("sampleEntry", to: .snakeCase), "sample_entry")
17+
XCTAssertEqual(CaseConverter.format("sampleEntry", to: .screamingSnakeCase), "SAMPLE_ENTRY")
18+
XCTAssertEqual(CaseConverter.format("sampleEntry", to: .kebabCase), "sample-entry")
19+
}
20+
}

0 commit comments

Comments
 (0)