Skip to content

Commit 908a14e

Browse files
authored
fix: ignore case name matching when declared a CaseMatcher (#37)
* fix: When the CaseMatcher is explicitly declared, the case name is no longer used. * fix: enum coding without associated value but with the path value
1 parent 3c31d0b commit 908a14e

3 files changed

Lines changed: 208 additions & 33 deletions

File tree

Sources/ReerCodableMacros/TypeInfo.swift

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,35 @@ extension TypeInfo {
404404

405405
return (typeStr, valueStr)
406406
}
407+
408+
func isRangeValueString(_ value: String) -> Bool {
409+
let trimmed = value.replacingOccurrences(of: " ", with: "")
410+
return trimmed.contains("...") || trimmed.contains("..<")
411+
}
412+
413+
func firstMatchValue(for enumCase: EnumCase) -> String? {
414+
let orderedTypes = enumCase.matchOrder.isEmpty
415+
? Array(enumCase.matches.keys)
416+
: enumCase.matchOrder
417+
for type in orderedTypes {
418+
guard let values = enumCase.matches[type] else { continue }
419+
for value in values {
420+
if !isRangeValueString(value) {
421+
return value
422+
}
423+
}
424+
}
425+
return nil
426+
}
427+
428+
func firstKeyPathMatchValue(for enumCase: EnumCase) -> String? {
429+
for match in enumCase.keyPathMatches {
430+
if !isRangeValueString(match.value) {
431+
return match.value
432+
}
433+
}
434+
return nil
435+
}
407436
/*
408437
@Codable
409438
enum Phone {
@@ -645,7 +674,7 @@ extension TypeInfo {
645674
needRequired = true
646675
}
647676
let hasCodingNested = codingContainer != nil
648-
var container = isEnum && !hasEnumAssociatedValue
677+
var container = isEnum && !hasEnumAssociatedValue && enumCases.contains(where: { $0.keyPathMatches.isEmpty })
649678
? "let container = try decoder.singleValueContainer()"
650679
: "\(hasCodingNested ? "var" : "let") container = try decoder.container(keyedBy: AnyCodingKey.self)"
651680
if let codingContainer {
@@ -747,7 +776,7 @@ extension TypeInfo {
747776
}
748777
.joined(separator: "\n")
749778
}
750-
var container = isEnum && !hasEnumAssociatedValue
779+
var container = isEnum && !hasEnumAssociatedValue && enumCases.allSatisfy({ $0.keyPathMatches.isEmpty })
751780
? "var container = encoder.singleValueContainer()"
752781
: "var container = encoder.container(keyedBy: AnyCodingKey.self)"
753782
if codingContainerWorkForEncoding, let codingContainer {
@@ -867,7 +896,8 @@ extension TypeInfo {
867896

868897
/// Return: (assignments, shouldAddDidDecode)
869898
private func generateEnumDecoderAssignments() -> (String, Bool) {
870-
if hasEnumAssociatedValue {
899+
if hasEnumAssociatedValue
900+
|| enumCases.contains(where: { !$0.keyPathMatches.isEmpty }) {
871901
let hasPathValue = enumCases.contains { !$0.keyPathMatches.isEmpty }
872902
var index = -1
873903
let findCase = enumCases.compactMap { theCase in
@@ -881,7 +911,9 @@ extension TypeInfo {
881911
"""
882912
} else {
883913
var keys = theCase.matches["String"] ?? []
884-
keys.append("\"\(theCase.caseName)\"")
914+
if keys.isEmpty {
915+
keys.append("\"\(theCase.caseName)\"")
916+
}
885917
keys.removeDuplicates()
886918
condition = """
887919
let \(hasAssociated ? "nestedContainer" : "_") = try? container.nestedContainer(forKeys: \(keys.joined(separator: ", ")))
@@ -940,15 +972,24 @@ extension TypeInfo {
940972
}
941973
"""
942974
}
943-
let tryRaw = """
975+
let rawCases = enumCases.filter { $0.matches.isEmpty && $0.keyPathMatches.isEmpty }
976+
let tryRaw: String? = rawCases.isEmpty
977+
? nil
978+
: """
944979
let value = try container.decode(type: \(enumRawType ?? "String").self, enumName: String(describing: Self.self))
945980
switch value {
946-
\(enumCases.compactMap { "case \($0.rawValue): self = .\($0.caseName)" }.joined(separator: "\n"))
981+
\(rawCases.compactMap { "case \($0.rawValue): self = .\($0.caseName)" }.joined(separator: "\n"))
947982
default: throw ReerCodableError(text: "Cannot initialize \\(String(describing: Self.self)) from invalid value \\(value)")
948983
}
949984
try self.didDecode(from: decoder)
950985
"""
951-
return (tryDecode.joined(separator: "\n") + "\n" + tryRaw, false)
986+
let notMatched = rawCases.isEmpty
987+
? "throw ReerCodableError(text: \"No matching case found for \\\\(String(describing: Self.self)).\")"
988+
: nil
989+
let code = ([tryDecode.joined(separator: "\n"), tryRaw, notMatched]
990+
.compactMap { $0?.isEmpty == false ? $0 : nil })
991+
.joined(separator: "\n")
992+
return (code, false)
952993
} else {
953994
return (
954995
"""
@@ -963,21 +1004,26 @@ extension TypeInfo {
9631004
}
9641005

9651006
private func generateEnumEncoderEncoding() -> String {
966-
if hasEnumAssociatedValue {
1007+
if hasEnumAssociatedValue || enumCases.contains(where: { !$0.keyPathMatches.isEmpty }) {
9671008
let hasPathValue = enumCases.contains { !$0.keyPathMatches.isEmpty }
9681009
let encodeCase = """
9691010
\(enumCases.compactMap {
9701011
let associated = "\($0.associated.compactMap { value in value.variableName }.joined(separator: ","))"
9711012
let postfix = $0.associated.isEmpty ? "\(associated)" : "(\(associated))"
9721013
let hasAssociated = !$0.associated.isEmpty
1014+
let matchValue = if hasPathValue {
1015+
firstKeyPathMatchValue(for: $0) ?? "\"\($0.caseName)\""
1016+
} else {
1017+
firstMatchValue(for: $0) ?? "\"\($0.caseName)\""
1018+
}
9731019
let encodeCase = if hasPathValue {
9741020
"""
975-
try container.encode(keyPath: AnyCodingKey(\($0.keyPathMatches.first!.path), \($0.keyPathMatches.first!.path.hasDot)), value: "\($0.caseName)")
1021+
try container.encode(keyPath: AnyCodingKey(\($0.keyPathMatches.first!.path), \($0.keyPathMatches.first!.path.hasDot)), value: \(matchValue))
9761022
"""
9771023
}
9781024
else {
9791025
"""
980-
var nestedContainer = container.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey("\($0.caseName)"))
1026+
var nestedContainer = container.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey(\(matchValue)))
9811027
"""
9821028
}
9831029
return """
@@ -999,7 +1045,10 @@ extension TypeInfo {
9991045
} else {
10001046
return """
10011047
switch self {
1002-
\(enumCases.compactMap { "case .\($0.caseName): try container.encode(\($0.rawValue))" }.joined(separator: "\n"))
1048+
\(enumCases.compactMap {
1049+
let encodeValue = firstMatchValue(for: $0) ?? $0.rawValue
1050+
return "case .\($0.caseName): try container.encode(\(encodeValue))"
1051+
}.joined(separator: "\n"))
10031052
}
10041053
"""
10051054
}

Tests/ReerCodableTests/EnumTests.swift

Lines changed: 143 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ enum Phone: Codable {
8686
case oppo
8787
}
8888

89+
@Codable
90+
enum ExplicitMatch: Codable {
91+
@CodingCase(match: .string("Test"))
92+
case test
93+
}
94+
95+
struct UserExplicit: Codable {
96+
let value: ExplicitMatch
97+
}
98+
8999
struct User2: Codable {
90100
let phone: Phone
91101
}
@@ -112,7 +122,7 @@ extension TestReerCodable {
112122
if let dict {
113123
print(dict)
114124
}
115-
#expect(dict.string("phone") == "iPhone")
125+
#expect(dict.bool("phone") == true)
116126
}
117127

118128
@Test(
@@ -135,7 +145,7 @@ extension TestReerCodable {
135145
if let dict {
136146
print(dict)
137147
}
138-
#expect(dict.string("phone") == "xiaomi")
148+
#expect(dict.int("phone") == 12)
139149
}
140150

141151
@Test(
@@ -156,7 +166,24 @@ extension TestReerCodable {
156166
if let dict {
157167
print(dict)
158168
}
159-
#expect(dict.string("phone") == "oppo")
169+
#expect(dict.bool("phone") == false)
170+
}
171+
}
172+
173+
extension TestReerCodable {
174+
@Test
175+
func enumExplicitMatch() throws {
176+
let json = "{\"value\": \"Test\"}"
177+
let model = try UserExplicit.decoded(from: json.data(using: .utf8)!)
178+
#expect(model.value == .test)
179+
180+
// Encode
181+
let modelData = try JSONEncoder().encode(model)
182+
let dict = modelData.stringAnyDictionary
183+
#expect(dict.string("value") == "Test")
184+
185+
let invalid = try? UserExplicit.decoded(from: "{\"value\": \"test\"}".data(using: .utf8)!)
186+
#expect(invalid == nil)
160187
}
161188
}
162189

@@ -224,7 +251,7 @@ extension TestReerCodable {
224251
#expect(true)
225252
// Encode
226253
let modelData = try JSONEncoder().encode(model)
227-
let index = modelData.stringAnyDictionary?.index(forKey: "youTube")
254+
let index = modelData.stringAnyDictionary?.index(forKey: "youtube")
228255
#expect(index != nil)
229256
} else {
230257
Issue.record("Expected youtube")
@@ -318,7 +345,7 @@ extension TestReerCodable {
318345
// Encode
319346
let modelData = try JSONEncoder().encode(model)
320347
let dict = modelData.stringAnyDictionary?["type"] as? [String: Any]
321-
#expect(dict.string("middle") == "youTube")
348+
#expect(dict.string("middle") == "youtube")
322349
} else {
323350
Issue.record("Expected youtube")
324351
}
@@ -402,7 +429,7 @@ extension TestReerCodable {
402429
// Encode
403430
let modelData = try JSONEncoder().encode(model)
404431
let dict = modelData.stringAnyDictionary
405-
#expect((dict?["type"] as! [String: Any]).string("middle") == "tiktok")
432+
#expect((dict?["type"] as? [String: Any])?.string("middle") == "tiktok")
406433
#expect(dict.string("url") == "https://example.com/video.mp4")
407434
#expect(dict.string("tag") == "Art")
408435
} else {
@@ -412,3 +439,113 @@ extension TestReerCodable {
412439
}
413440
}
414441
}
442+
443+
444+
@Codable
445+
enum Foo123 {
446+
@CodingCase(match: .string("Test123", at: "a.b"))
447+
case test
448+
}
449+
450+
extension TestReerCodable {
451+
452+
@Test
453+
func enumWithPath() throws {
454+
let json = """
455+
{
456+
"a": {
457+
"b": "Test123"
458+
}
459+
}
460+
"""
461+
let model = try Foo123.decoded(from: json.data(using: .utf8)!)
462+
463+
switch model {
464+
case .test:
465+
if json.contains("Test123") {
466+
#expect(true)
467+
468+
// Encode
469+
let modelData = try JSONEncoder().encode(model)
470+
let dict = modelData.stringAnyDictionary
471+
#expect((dict?["a"] as? [String: Any])?.string("b") == "Test123")
472+
} else {
473+
Issue.record("Expected Test123")
474+
}
475+
}
476+
}
477+
}
478+
479+
@Codable
480+
enum Foo333 {
481+
@CodingCase(match: .string("test1", at: "a.b"))
482+
case test
483+
484+
@CodingCase(match: .string("foo1", at: "f.d"))
485+
case foo
486+
487+
@CodingCase(match: .string("bar1", at: "x"))
488+
case bar
489+
}
490+
extension TestReerCodable {
491+
@Test(arguments: [
492+
"""
493+
{
494+
"a": {
495+
"b": "test1"
496+
}
497+
}
498+
""",
499+
"""
500+
{
501+
"f": {
502+
"d": "foo1"
503+
}
504+
}
505+
""",
506+
"""
507+
{
508+
"x": "bar1"
509+
}
510+
"""
511+
])
512+
func enumWithPath(json: String) throws {
513+
let model = try Foo333.decoded(from: json.data(using: .utf8)!)
514+
515+
switch model {
516+
case .test:
517+
if json.contains("test1") {
518+
#expect(true)
519+
520+
// Encode
521+
let modelData = try JSONEncoder().encode(model)
522+
let dict = modelData.stringAnyDictionary?["a"] as? [String: Any]
523+
#expect(dict.string("b") == "test1")
524+
} else {
525+
Issue.record("Expected test1")
526+
}
527+
case .foo:
528+
if json.contains("foo1") {
529+
#expect(true)
530+
531+
// Encode
532+
let modelData = try JSONEncoder().encode(model)
533+
let dict = modelData.stringAnyDictionary?["f"] as? [String: Any]
534+
#expect(dict.string("d") == "foo1")
535+
} else {
536+
Issue.record("Expected foo1")
537+
}
538+
case .bar:
539+
if json.contains("bar1") {
540+
#expect(true)
541+
542+
// Encode
543+
let modelData = try JSONEncoder().encode(model)
544+
let dict = modelData.stringAnyDictionary
545+
#expect(dict.string("x") == "bar1")
546+
} else {
547+
Issue.record("Expected bar1")
548+
}
549+
}
550+
}
551+
}

Tests/ReerCodableTests/MacroExpandTests.swift

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -172,18 +172,7 @@ final class ReerCodableTests: XCTestCase {
172172
break
173173
}
174174
}
175-
let value = try container.decode(type: String.self, enumName: String(describing: Self.self))
176-
switch value {
177-
case "apple":
178-
self = .apple
179-
case "mi":
180-
self = .mi
181-
case "oppo":
182-
self = .oppo
183-
default:
184-
throw ReerCodableError(text: "Cannot initialize \(String(describing: Self.self)) from invalid value \(value)")
185-
}
186-
try self.didDecode(from: decoder)
175+
throw ReerCodableError(text: "No matching case found for \\(String(describing: Self.self)).")
187176
188177
}
189178
@@ -192,11 +181,11 @@ final class ReerCodableTests: XCTestCase {
192181
var container = encoder.singleValueContainer()
193182
switch self {
194183
case .apple:
195-
try container.encode("apple")
184+
try container.encode(true)
196185
case .mi:
197-
try container.encode("mi")
186+
try container.encode(12)
198187
case .oppo:
199-
try container.encode("oppo")
188+
try container.encode(false)
200189
}
201190
}
202191
}
@@ -271,7 +260,7 @@ final class ReerCodableTests: XCTestCase {
271260
var container = encoder.container(keyedBy: AnyCodingKey.self)
272261
switch self {
273262
case .youTube:
274-
try container.encode(keyPath: AnyCodingKey("type.middle", true), value: "youTube")
263+
try container.encode(keyPath: AnyCodingKey("type.middle", true), value: "youtube")
275264
276265
case let .vimeo(id, duration, _2):
277266
try container.encode(keyPath: AnyCodingKey("type", false), value: "vimeo")

0 commit comments

Comments
 (0)