Skip to content
This repository was archived by the owner on Feb 2, 2022. It is now read-only.

Commit dda2773

Browse files
author
Jakob Mygind
committed
Add support for underscore notation
1 parent 5c72c96 commit dda2773

File tree

3 files changed

+111
-5
lines changed

3 files changed

+111
-5
lines changed

Model Boiler/Classes/Helpers/CodableGenerator.swift

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import SwiftSemantics
1313
class Generator {
1414

1515
let source: String
16+
let mapUnderscoreToCamelCase: Bool
1617

17-
init(source: String) {
18+
init(source: String, mapUnderscoreToCamelCase: Bool = false) {
1819
self.source = source
20+
self.mapUnderscoreToCamelCase = mapUnderscoreToCamelCase
1921
}
2022

2123
var encode: [String] = ["""
@@ -35,17 +37,43 @@ class Generator {
3537
"""
3638
]
3739

38-
func addNode(name: String, type: String, isOptional: Bool = false) {
40+
func addNode(name: String, type: String, isOptional: Bool = false) {
3941
encode.append(" try container.encode(\(name), forKey: .\(name))")
4042
if isOptional {
4143
initStrings.append(" \(name) = try container.decodeIfPresent(\(type.trimmingCharacters(in: .init(charactersIn: "?"))).self, forKey: .\(name))")
4244
} else {
4345
initStrings.append(" \(name) = try container.decode(\(type).self, forKey: .\(name))")
4446
}
45-
codingKeys.append(" case \(name) = \"\(name)\"")
47+
48+
if mapUnderscoreToCamelCase {
49+
codingKeys.append(" case \(name) = \"\(mapCamelToUnderscore(name))\"")
50+
} else {
51+
codingKeys.append(" case \(name) = \"\(name)\"")
52+
}
53+
}
54+
55+
func mapCamelToUnderscore(_ string: String) -> String {
56+
var res = ""
57+
58+
var strCopy = string[...]
59+
while let match = parseWord(str: &strCopy) {
60+
res += "_" + match.lowercased()
61+
}
62+
return res.trimmingCharacters(in: CharacterSet.init(charactersIn: "_"))
4663
}
4764

48-
/// Generation is based on dumb pattern matchint
65+
func parseWord(str: inout Substring) -> String? {
66+
67+
if let lowerMatch = Parser.lower.run(&str) {
68+
return lowerMatch
69+
}
70+
if let upperThenLower = zip(Parser.upper, Parser.lower).run(&str) {
71+
return upperThenLower.0 + upperThenLower.1
72+
}
73+
return Parser.upper.run(&str)
74+
}
75+
76+
/// Generation is based on dumb pattern matchint
4977
func generate() throws -> String {
5078
var collector = DeclarationCollector()
5179
let tree = try SyntaxParser.parse(source: source)
@@ -82,3 +110,37 @@ class Generator {
82110
}
83111
}
84112

113+
struct Parser<A> {
114+
let run: (inout Substring) -> A?
115+
}
116+
117+
extension Parser where A == String {
118+
119+
static func predicate(_ predicate: @escaping (Character) -> Bool) -> Parser {
120+
Parser { str in
121+
let match = str.prefix(while: predicate)
122+
guard !match.isEmpty else { return nil }
123+
str.removeFirst(match.count)
124+
return String(match)
125+
}
126+
}
127+
128+
static let upper: Parser = .predicate { $0.isUppercase }
129+
130+
static let lower: Parser = .predicate { $0.isLowercase }
131+
}
132+
133+
func zip<A, B>(_ pa: Parser<A>, _ pb: Parser<B>) -> Parser<(A, B)> {
134+
Parser { str in
135+
let originalString = str
136+
guard let a = pa.run(&str) else {
137+
return nil
138+
}
139+
guard let b = pb.run(&str) else {
140+
str = originalString
141+
return nil
142+
}
143+
return (a, b)
144+
}
145+
}
146+

Model Boiler/Classes/Service/Service.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ struct Service {
3535
//
3636
do {
3737
// Try to generate the code
38-
let code = try Generator(source: source).generate()
38+
let code = try Generator(source: source, mapUnderscoreToCamelCase: !SettingsManager.isSettingEnabled(.NoCamelCaseConversion)).generate()
3939

4040
// Play success sound
4141
playSound(Service.successSound)

ModelBoilerTests/ModelBoilerTests.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,48 @@ class ModelBoilerTests: XCTestCase {
150150
let res = try XCTUnwrap(try Generator(source: str).generate())
151151
XCTAssertEqual(res, expected)
152152
}
153+
154+
func testCamelCaseToUnderscore() throws {
155+
156+
let str = """
157+
struct Test {
158+
var intVal = 1
159+
var doubleVal = 2.33
160+
var stringVal = "Hello"
161+
var boolVal = true
162+
var imageURL: URL
163+
}
164+
"""
165+
166+
let expected = """
167+
enum CodingKeys: String, CodingKey {
168+
case intVal = "int_val"
169+
case doubleVal = "double_val"
170+
case stringVal = "string_val"
171+
case boolVal = "bool_val"
172+
case imageURL = "image_url"
173+
}
174+
175+
public func encode(to encoder: Encoder) throws {
176+
var container = encoder.container(keyedBy: CodingKeys.self)
177+
try container.encode(intVal, forKey: .intVal)
178+
try container.encode(doubleVal, forKey: .doubleVal)
179+
try container.encode(stringVal, forKey: .stringVal)
180+
try container.encode(boolVal, forKey: .boolVal)
181+
try container.encode(imageURL, forKey: .imageURL)
182+
}
183+
184+
public init(from decoder: Decoder) throws {
185+
let container = try decoder.container(keyedBy: CodingKeys.self)
186+
intVal = try container.decode(Int.self, forKey: .intVal)
187+
doubleVal = try container.decode(Double.self, forKey: .doubleVal)
188+
stringVal = try container.decode(String.self, forKey: .stringVal)
189+
boolVal = try container.decode(Bool.self, forKey: .boolVal)
190+
imageURL = try container.decode(URL.self, forKey: .imageURL)
191+
}
192+
"""
193+
194+
let res = try XCTUnwrap(try Generator(source: str, mapUnderscoreToCamelCase: true).generate())
195+
XCTAssertEqual(res, expected)
196+
}
153197
}

0 commit comments

Comments
 (0)