Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 60 additions & 11 deletions Sources/ReerCodableMacros/TypeInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,35 @@ extension TypeInfo {

return (typeStr, valueStr)
}

func isRangeValueString(_ value: String) -> Bool {
let trimmed = value.replacingOccurrences(of: " ", with: "")
return trimmed.contains("...") || trimmed.contains("..<")
}

func firstMatchValue(for enumCase: EnumCase) -> String? {
let orderedTypes = enumCase.matchOrder.isEmpty
? Array(enumCase.matches.keys)
: enumCase.matchOrder
for type in orderedTypes {
guard let values = enumCase.matches[type] else { continue }
for value in values {
if !isRangeValueString(value) {
return value
}
}
}
return nil
}

func firstKeyPathMatchValue(for enumCase: EnumCase) -> String? {
for match in enumCase.keyPathMatches {
if !isRangeValueString(match.value) {
return match.value
}
}
return nil
}
/*
@Codable
enum Phone {
Expand Down Expand Up @@ -645,7 +674,7 @@ extension TypeInfo {
needRequired = true
}
let hasCodingNested = codingContainer != nil
var container = isEnum && !hasEnumAssociatedValue
var container = isEnum && !hasEnumAssociatedValue && enumCases.contains(where: { $0.keyPathMatches.isEmpty })
? "let container = try decoder.singleValueContainer()"
: "\(hasCodingNested ? "var" : "let") container = try decoder.container(keyedBy: AnyCodingKey.self)"
if let codingContainer {
Expand Down Expand Up @@ -747,7 +776,7 @@ extension TypeInfo {
}
.joined(separator: "\n")
}
var container = isEnum && !hasEnumAssociatedValue
var container = isEnum && !hasEnumAssociatedValue && enumCases.allSatisfy({ $0.keyPathMatches.isEmpty })
? "var container = encoder.singleValueContainer()"
: "var container = encoder.container(keyedBy: AnyCodingKey.self)"
if codingContainerWorkForEncoding, let codingContainer {
Expand Down Expand Up @@ -867,7 +896,8 @@ extension TypeInfo {

/// Return: (assignments, shouldAddDidDecode)
private func generateEnumDecoderAssignments() -> (String, Bool) {
if hasEnumAssociatedValue {
if hasEnumAssociatedValue
|| enumCases.contains(where: { !$0.keyPathMatches.isEmpty }) {
let hasPathValue = enumCases.contains { !$0.keyPathMatches.isEmpty }
var index = -1
let findCase = enumCases.compactMap { theCase in
Expand All @@ -881,7 +911,9 @@ extension TypeInfo {
"""
} else {
var keys = theCase.matches["String"] ?? []
keys.append("\"\(theCase.caseName)\"")
if keys.isEmpty {
keys.append("\"\(theCase.caseName)\"")
}
keys.removeDuplicates()
condition = """
let \(hasAssociated ? "nestedContainer" : "_") = try? container.nestedContainer(forKeys: \(keys.joined(separator: ", ")))
Expand Down Expand Up @@ -940,15 +972,24 @@ extension TypeInfo {
}
"""
}
let tryRaw = """
let rawCases = enumCases.filter { $0.matches.isEmpty && $0.keyPathMatches.isEmpty }
let tryRaw: String? = rawCases.isEmpty
? nil
: """
let value = try container.decode(type: \(enumRawType ?? "String").self, enumName: String(describing: Self.self))
switch value {
\(enumCases.compactMap { "case \($0.rawValue): self = .\($0.caseName)" }.joined(separator: "\n"))
\(rawCases.compactMap { "case \($0.rawValue): self = .\($0.caseName)" }.joined(separator: "\n"))
default: throw ReerCodableError(text: "Cannot initialize \\(String(describing: Self.self)) from invalid value \\(value)")
}
try self.didDecode(from: decoder)
"""
return (tryDecode.joined(separator: "\n") + "\n" + tryRaw, false)
let notMatched = rawCases.isEmpty
? "throw ReerCodableError(text: \"No matching case found for \\\\(String(describing: Self.self)).\")"
: nil
let code = ([tryDecode.joined(separator: "\n"), tryRaw, notMatched]
.compactMap { $0?.isEmpty == false ? $0 : nil })
.joined(separator: "\n")
return (code, false)
} else {
return (
"""
Expand All @@ -963,21 +1004,26 @@ extension TypeInfo {
}

private func generateEnumEncoderEncoding() -> String {
if hasEnumAssociatedValue {
if hasEnumAssociatedValue || enumCases.contains(where: { !$0.keyPathMatches.isEmpty }) {
let hasPathValue = enumCases.contains { !$0.keyPathMatches.isEmpty }
let encodeCase = """
\(enumCases.compactMap {
let associated = "\($0.associated.compactMap { value in value.variableName }.joined(separator: ","))"
let postfix = $0.associated.isEmpty ? "\(associated)" : "(\(associated))"
let hasAssociated = !$0.associated.isEmpty
let matchValue = if hasPathValue {
firstKeyPathMatchValue(for: $0) ?? "\"\($0.caseName)\""
} else {
firstMatchValue(for: $0) ?? "\"\($0.caseName)\""
}
let encodeCase = if hasPathValue {
"""
try container.encode(keyPath: AnyCodingKey(\($0.keyPathMatches.first!.path), \($0.keyPathMatches.first!.path.hasDot)), value: "\($0.caseName)")
try container.encode(keyPath: AnyCodingKey(\($0.keyPathMatches.first!.path), \($0.keyPathMatches.first!.path.hasDot)), value: \(matchValue))
"""
}
else {
"""
var nestedContainer = container.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey("\($0.caseName)"))
var nestedContainer = container.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey(\(matchValue)))
"""
}
return """
Expand All @@ -999,7 +1045,10 @@ extension TypeInfo {
} else {
return """
switch self {
\(enumCases.compactMap { "case .\($0.caseName): try container.encode(\($0.rawValue))" }.joined(separator: "\n"))
\(enumCases.compactMap {
let encodeValue = firstMatchValue(for: $0) ?? $0.rawValue
return "case .\($0.caseName): try container.encode(\(encodeValue))"
}.joined(separator: "\n"))
}
"""
}
Expand Down
149 changes: 143 additions & 6 deletions Tests/ReerCodableTests/EnumTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ enum Phone: Codable {
case oppo
}

@Codable
enum ExplicitMatch: Codable {
@CodingCase(match: .string("Test"))
case test
}

struct UserExplicit: Codable {
let value: ExplicitMatch
}

struct User2: Codable {
let phone: Phone
}
Expand All @@ -112,7 +122,7 @@ extension TestReerCodable {
if let dict {
print(dict)
}
#expect(dict.string("phone") == "iPhone")
#expect(dict.bool("phone") == true)
}

@Test(
Expand All @@ -135,7 +145,7 @@ extension TestReerCodable {
if let dict {
print(dict)
}
#expect(dict.string("phone") == "xiaomi")
#expect(dict.int("phone") == 12)
}

@Test(
Expand All @@ -156,7 +166,24 @@ extension TestReerCodable {
if let dict {
print(dict)
}
#expect(dict.string("phone") == "oppo")
#expect(dict.bool("phone") == false)
}
}

extension TestReerCodable {
@Test
func enumExplicitMatch() throws {
let json = "{\"value\": \"Test\"}"
let model = try UserExplicit.decoded(from: json.data(using: .utf8)!)
#expect(model.value == .test)

// Encode
let modelData = try JSONEncoder().encode(model)
let dict = modelData.stringAnyDictionary
#expect(dict.string("value") == "Test")

let invalid = try? UserExplicit.decoded(from: "{\"value\": \"test\"}".data(using: .utf8)!)
#expect(invalid == nil)
}
}

Expand Down Expand Up @@ -224,7 +251,7 @@ extension TestReerCodable {
#expect(true)
// Encode
let modelData = try JSONEncoder().encode(model)
let index = modelData.stringAnyDictionary?.index(forKey: "youTube")
let index = modelData.stringAnyDictionary?.index(forKey: "youtube")
#expect(index != nil)
} else {
Issue.record("Expected youtube")
Expand Down Expand Up @@ -318,7 +345,7 @@ extension TestReerCodable {
// Encode
let modelData = try JSONEncoder().encode(model)
let dict = modelData.stringAnyDictionary?["type"] as? [String: Any]
#expect(dict.string("middle") == "youTube")
#expect(dict.string("middle") == "youtube")
} else {
Issue.record("Expected youtube")
}
Expand Down Expand Up @@ -402,7 +429,7 @@ extension TestReerCodable {
// Encode
let modelData = try JSONEncoder().encode(model)
let dict = modelData.stringAnyDictionary
#expect((dict?["type"] as! [String: Any]).string("middle") == "tiktok")
#expect((dict?["type"] as? [String: Any])?.string("middle") == "tiktok")
#expect(dict.string("url") == "https://example.com/video.mp4")
#expect(dict.string("tag") == "Art")
} else {
Expand All @@ -412,3 +439,113 @@ extension TestReerCodable {
}
}
}


@Codable
enum Foo123 {
@CodingCase(match: .string("Test123", at: "a.b"))
case test
}

extension TestReerCodable {

@Test
func enumWithPath() throws {
let json = """
{
"a": {
"b": "Test123"
}
}
"""
let model = try Foo123.decoded(from: json.data(using: .utf8)!)

switch model {
case .test:
if json.contains("Test123") {
#expect(true)

// Encode
let modelData = try JSONEncoder().encode(model)
let dict = modelData.stringAnyDictionary
#expect((dict?["a"] as? [String: Any])?.string("b") == "Test123")
} else {
Issue.record("Expected Test123")
}
}
}
}

@Codable
enum Foo333 {
@CodingCase(match: .string("test1", at: "a.b"))
case test

@CodingCase(match: .string("foo1", at: "f.d"))
case foo

@CodingCase(match: .string("bar1", at: "x"))
case bar
}
extension TestReerCodable {
@Test(arguments: [
"""
{
"a": {
"b": "test1"
}
}
""",
"""
{
"f": {
"d": "foo1"
}
}
""",
"""
{
"x": "bar1"
}
"""
])
func enumWithPath(json: String) throws {
let model = try Foo333.decoded(from: json.data(using: .utf8)!)

switch model {
case .test:
if json.contains("test1") {
#expect(true)

// Encode
let modelData = try JSONEncoder().encode(model)
let dict = modelData.stringAnyDictionary?["a"] as? [String: Any]
#expect(dict.string("b") == "test1")
} else {
Issue.record("Expected test1")
}
case .foo:
if json.contains("foo1") {
#expect(true)

// Encode
let modelData = try JSONEncoder().encode(model)
let dict = modelData.stringAnyDictionary?["f"] as? [String: Any]
#expect(dict.string("d") == "foo1")
} else {
Issue.record("Expected foo1")
}
case .bar:
if json.contains("bar1") {
#expect(true)

// Encode
let modelData = try JSONEncoder().encode(model)
let dict = modelData.stringAnyDictionary
#expect(dict.string("x") == "bar1")
} else {
Issue.record("Expected bar1")
}
}
}
}
21 changes: 5 additions & 16 deletions Tests/ReerCodableTests/MacroExpandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,18 +172,7 @@ final class ReerCodableTests: XCTestCase {
break
}
}
let value = try container.decode(type: String.self, enumName: String(describing: Self.self))
switch value {
case "apple":
self = .apple
case "mi":
self = .mi
case "oppo":
self = .oppo
default:
throw ReerCodableError(text: "Cannot initialize \(String(describing: Self.self)) from invalid value \(value)")
}
try self.didDecode(from: decoder)
throw ReerCodableError(text: "No matching case found for \\(String(describing: Self.self)).")

}

Expand All @@ -192,11 +181,11 @@ final class ReerCodableTests: XCTestCase {
var container = encoder.singleValueContainer()
switch self {
case .apple:
try container.encode("apple")
try container.encode(true)
case .mi:
try container.encode("mi")
try container.encode(12)
case .oppo:
try container.encode("oppo")
try container.encode(false)
}
}
}
Expand Down Expand Up @@ -271,7 +260,7 @@ final class ReerCodableTests: XCTestCase {
var container = encoder.container(keyedBy: AnyCodingKey.self)
switch self {
case .youTube:
try container.encode(keyPath: AnyCodingKey("type.middle", true), value: "youTube")
try container.encode(keyPath: AnyCodingKey("type.middle", true), value: "youtube")

case let .vimeo(id, duration, _2):
try container.encode(keyPath: AnyCodingKey("type", false), value: "vimeo")
Expand Down
Loading