Skip to content

Commit 5cc1a93

Browse files
authored
feat: added options for custom key case style (#18)
1 parent 6cd519e commit 5cc1a93

File tree

18 files changed

+1277
-68
lines changed

18 files changed

+1277
-68
lines changed

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@
55
"**/.docc-build": true,
66
"**/node_modules": true,
77
"Package.resolved": true
8-
}
8+
},
9+
"editor.unicodeHighlight.allowedCharacters": {
10+
"-": true
11+
},
912
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Supercharge `Swift`'s `Codable` implementations with macros.
2424
- Generates member-wise initializer(s) considering the above default value syntax as well.
2525
- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` etc.
2626
- Allows to ignore specific properties from decoding/encoding with ``IgnoreCoding()``, ``IgnoreDecoding()`` and ``@IgnoreEncoding()``.
27+
- Allows to use camel-case names for variables according to [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/#general-conventions), while enabling a type to work with different case style keys with ``CodingKeys(_:)``.
2728
- Allows to ignore all initialized properties of a type from decoding/encoding with ``IgnoreCodingInitialized()`` unless explicitly asked to decode/encode by attaching any coding attributes, i.e. ``CodedIn(_:)``, ``CodedAt(_:)``,
2829
``CodedBy(_:)``, ``Default(_:)`` etc.
2930

Sources/CodableMacroPlugin/Attributes/Codable/Codable+Expansion.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ extension Codable: ConformanceMacro, MemberMacro {
5858
else { return }
5959

6060
// builder
61-
let builder = IgnoreCodingInitialized(from: declaration)
61+
let builder = CodingKeys(from: declaration)
62+
|> IgnoreCodingInitialized(from: declaration)
6263
|> KeyPathRegistrationBuilder(
6364
provider: CodedAt(from: decl)
6465
?? CodedIn(from: decl)
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import Foundation
2+
3+
/// A type performing transformation on provided `CodingKey`.
4+
///
5+
/// Performs transformation on provided `CodingKey`
6+
/// based on the strategy passed during initialization.
7+
/// The separation letter and separated words capitalization
8+
/// style is adjusted according to the provided case style.
9+
struct CodingKeyTransformer {
10+
/// The key transformation strategy provided.
11+
let strategy: CodingKeys.Strategy
12+
13+
/// Transform provided `CodingKey` string according
14+
/// to current strategy.
15+
///
16+
/// Adjusts elements in provided `CodingKey` to match
17+
/// the current casing strategy.
18+
///
19+
/// - Parameter key: The `CodingKey` to transform.
20+
/// - Returns: The transformed `CodingKey`.
21+
func transform(key: String) -> String {
22+
guard !key.isEmpty else { return key }
23+
24+
let interimKey: String
25+
if #available(
26+
macOS 13, iOS 16, macCatalyst 16,
27+
tvOS 16, watchOS 9, *
28+
) {
29+
let regex = #/([a-z0-9])([A-Z])/#
30+
interimKey = key.replacing(regex) { match in
31+
let (_, first, second) = match.output
32+
return "\(first)@\(second)"
33+
}.lowercased()
34+
} else {
35+
let regex = try! NSRegularExpression(pattern: "([a-z0-9])([A-Z])")
36+
let range = NSRange(location: 0, length: key.count)
37+
interimKey = regex.stringByReplacingMatches(
38+
in: key,
39+
range: range,
40+
withTemplate: "$1@$2"
41+
).lowercased()
42+
}
43+
44+
let parts = interimKey.components(separatedBy: .alphanumerics.inverted)
45+
return strategy.capitalization
46+
.transform(parts: parts)
47+
.joined(separator: strategy.separator)
48+
}
49+
}
50+
51+
fileprivate extension CodingKeys.Strategy {
52+
/// The separator being used by current case style.
53+
///
54+
/// There might not be any separator for current case style,
55+
/// in such case empty string is returned. Otherwise the separator
56+
/// character corresponding to current case is returned.
57+
var separator: String {
58+
switch self {
59+
case .camelCase, .PascalCase:
60+
return ""
61+
case .snake_case, .camel_Snake_Case, .SCREAMING_SNAKE_CASE:
62+
return "_"
63+
case .kebab-case, .Train-Case, .SCREAMING-KEBAB-CASE:
64+
return "-"
65+
}
66+
}
67+
}
68+
69+
fileprivate extension CodingKeys.Strategy {
70+
/// Represents capitalization style
71+
/// of each token in a casing style.
72+
///
73+
/// Indicates capitalization style preferred
74+
/// by each separated word in a casing style,
75+
/// i.e. upper, lower, only first letter is capitalized etc.
76+
enum Capitalization {
77+
/// Represents all the separated
78+
/// words are in upper case.
79+
///
80+
/// Typically used for screaming
81+
/// style cases with separators.
82+
case upper
83+
/// Represents all the separated words
84+
/// have only first letter capitalized.
85+
///
86+
/// Typically used for default
87+
/// style cases with separators.
88+
case lower
89+
/// Represents all the separated
90+
/// words are in lower case.
91+
///
92+
/// Typically used for default
93+
/// style cases with separators.
94+
case all
95+
/// Represents first word is in lower case
96+
/// and subsequent words have only
97+
/// first letter capitalized.
98+
///
99+
/// Typically used for styles that are variation
100+
/// on top of default styles.
101+
case exceptFirst
102+
103+
/// Converts provided string tokens according
104+
/// to current casing style.
105+
///
106+
/// Adjusts capitalization style of provided string tokens
107+
/// according to current casing style.
108+
///
109+
/// - Parameter parts: The string tokens to transform.
110+
/// - Returns: The transformed string tokens.
111+
func transform(parts: [String]) -> [String] {
112+
guard !parts.isEmpty else { return parts }
113+
switch self {
114+
case .upper:
115+
return parts.map { $0.uppercased() }
116+
case .lower:
117+
return parts.map { $0.lowercased() }
118+
case .all:
119+
return parts.map { $0.uppercasingFirst }
120+
case .exceptFirst:
121+
let first = parts.first!.lowercasingFirst
122+
let rest = parts.dropFirst().map { $0.uppercasingFirst }
123+
return [first] + rest
124+
}
125+
}
126+
}
127+
128+
/// The capitalization casing style of each pattern
129+
/// corresponding to current strategy.
130+
///
131+
/// Depending on the current style it might be upper,
132+
/// lower or capitalizing first word etc.
133+
var capitalization: Capitalization {
134+
switch self {
135+
case .camelCase, .camel_Snake_Case:
136+
return .exceptFirst
137+
case .snake_case, .kebab-case:
138+
return .lower
139+
case .SCREAMING_SNAKE_CASE, .SCREAMING-KEBAB-CASE:
140+
return .upper
141+
case .PascalCase, .Train-Case:
142+
return .all
143+
}
144+
}
145+
}
146+
147+
/// Helps converting any string to camel case
148+
///
149+
/// Picked up from:
150+
/// https://gist.github.com/reitzig/67b41e75176ddfd432cb09392a270218
151+
extension String {
152+
/// Makes the first letter lowercase.
153+
var lowercasingFirst: String { prefix(1).lowercased() + dropFirst() }
154+
/// Makes the first letter uppercase.
155+
var uppercasingFirst: String { prefix(1).uppercased() + dropFirst() }
156+
157+
/// Convert any string to camel case
158+
///
159+
/// Removes non-alphanumeric characters
160+
/// and makes the letters just after these
161+
/// characters uppercase.
162+
///
163+
/// First letter is made lowercase.
164+
var camelCased: String {
165+
return CodingKeyTransformer(strategy: .camelCase).transform(key: self)
166+
}
167+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import SwiftSyntax
2+
3+
/// Attribute type for `CodingKeys` macro-attribute.
4+
///
5+
/// This type can validate`CodingKeys` macro-attribute
6+
/// usage and extract data for `Codable` macro to
7+
/// generate implementation.
8+
///
9+
/// Attaching this macro to type declaration indicates all the
10+
/// property names will be converted to `CodingKey` value
11+
/// using the strategy provided.
12+
struct CodingKeys: PeerAttribute {
13+
/// The node syntax provided
14+
/// during initialization.
15+
let node: AttributeSyntax
16+
17+
/// The key transformation strategy provided.
18+
var strategy: Strategy {
19+
let expr = node.argument!
20+
.as(TupleExprElementListSyntax.self)!.first!.expression
21+
return .init(with: expr)
22+
}
23+
24+
/// Creates a new instance with the provided node
25+
///
26+
/// The initializer fails to create new instance if the name
27+
/// of the provided node is different than this attribute.
28+
///
29+
/// - Parameter node: The attribute syntax to create with.
30+
/// - Returns: Newly created attribute instance.
31+
init?(from node: AttributeSyntax) {
32+
guard
33+
node.attributeName.as(SimpleTypeIdentifierSyntax.self)!
34+
.description == Self.name
35+
else { return nil }
36+
self.node = node
37+
}
38+
39+
/// Builds diagnoser that can validate this macro
40+
/// attached declaration.
41+
///
42+
/// Builds diagnoser that validates attached declaration
43+
/// has `Codable` macro attached and macro usage
44+
/// is not duplicated for the same declaration.
45+
///
46+
/// - Returns: The built diagnoser instance.
47+
func diagnoser() -> DiagnosticProducer {
48+
return AggregatedDiagnosticProducer {
49+
mustBeCombined(with: Codable.self)
50+
cantDuplicate()
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)