From caa4645241d65d9869d636a52fe71c15bffadd8f Mon Sep 17 00:00:00 2001 From: Davide Date: Wed, 21 Jan 2026 17:01:43 +0100 Subject: [PATCH 1/2] Support enums with associated values Except optionals and default values for now. Follow Swift/ObjC encoding convention, where unlabeled arguments are encoded with the "_#" key based on the argument order. Fixes #2 --- README.md | 2 +- Sources/SWONMacros/Decoding.swift | 86 +++++++++++--- Sources/SWONMacros/Encoding.swift | 142 ++++++++++++++++++----- Sources/SWONMacros/Helpers.swift | 17 +++ Sources/SWON_C/swon.c | 1 + Tests/SWONTests/ComplexStruct.swift | 11 ++ Tests/SWONTests/Resources/populated.json | 42 ++++++- Tests/SWONTests/SWONTests.swift | 76 +++++++++--- 8 files changed, 317 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index ca1d40f..872d77c 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ SWON uses the amazingly written [cJSON][credits-cjson] parser (C) under the hood ### Things to do -- Enums with associated types. +- Enums with optional or default associated values. - Do not suppress errors inside an optional field (silently made nil). ## License diff --git a/Sources/SWONMacros/Decoding.swift b/Sources/SWONMacros/Decoding.swift index 8f17ea7..f164f4d 100644 --- a/Sources/SWONMacros/Decoding.swift +++ b/Sources/SWONMacros/Decoding.swift @@ -28,23 +28,74 @@ struct SWONDecodeMacro: MemberMacro { var assignments: [String] = [] - // Parse enum from raw value if let enumDecl = declaration.as(EnumDeclSyntax.self) { - guard let inheritance = enumDecl.inheritanceClause, - let inheritedType = inheritance.inheritedTypes.first else { - fatalError("Enumeration must inherit a raw type") - } let enumType = enumDecl.name.text - let rawType = inheritedType.type.description -// context.diagnose( -// Diagnostic( -// node: Syntax(node), -// message: SWONMessage(message: "Raw type of enum \(enumType) is \(rawType)") + + // Enum with associated values + if enumDecl.hasAssociatedValues { + let cases = enumDecl.cases +// context.diagnose( +// Diagnostic( +// node: Syntax(node), +// message: SWONMessage(message: "Associated cases of enum \(enumType): \(cases.map(\.description))") +// ) // ) -// ) - switch rawType { - case "String": + // FIXME: ###, Ensure root is an actual object assignments.append(""" + let child = swon_get_map_first(root) + guard let key = swon_get_map_key(child) else { + throw SWONError.invalid("Unable to find enum dictionary") + } + """) + + assignments.append("switch String(cString: key) {") + cases.forEach { + $0.elements.forEach { el in + assignments.append("case \"\(el.name)\":") + guard let parms = el.parameterClause?.parameters, !parms.isEmpty else { + assignments.append("self = .\(el.name)") + return + } + var parmAssignments: [String] = [] + parms.enumerated().forEach { i, p in + let name: String + let type = p.type.decodedType(context: context) + // FIXME: ###, Handle optional (replace with nil) + if let firstName = p.firstName { + name = firstName.description + parmAssignments.append("\(name): \(name)") + } else { + name = "_\(i)" + parmAssignments.append(name) + } + assignments.append(""" + let \(name) = try { + var dict = swon_t() + let dictResult = swon_get_object(&dict, child, "\(name)") + try dictResult.check("\(name) in \(el.name)") + \(mapItem(to: type, fieldName: "\(name) in \(el.name)", varName: "dict", nesting: 0, isOptional: false)) + }() + """) + } + let parmAssignmentList = parmAssignments.joined(separator: ",") + assignments.append("self = .\(el.name)(\(parmAssignmentList))") + } + } + assignments.append("default: throw SWONError.invalid(\"Unknown enum case '\\(key)'\")") + assignments.append("}") + } + // Enum with raw value + else if let inheritedType = enumDecl.inheritanceClause?.inheritedTypes.first { + let rawType = inheritedType.type.description +// context.diagnose( +// Diagnostic( +// node: Syntax(node), +// message: SWONMessage(message: "Raw type of enum \(enumType) is \(rawType)") +// ) +// ) + switch rawType { + case "String": + assignments.append(""" var str: UnsafePointer! try swon_get_string(&str, root).check("\(enumType)") let rawValue = String(cString: str) @@ -53,8 +104,8 @@ struct SWONDecodeMacro: MemberMacro { } self = value """) - case "Int": - assignments.append(""" + case "Int": + assignments.append(""" var num: Int32 = 0 try swon_get_integer(&num, root).check("\(enumType)") guard let value = \(enumType)(rawValue: Int(num)) else { @@ -62,8 +113,9 @@ struct SWONDecodeMacro: MemberMacro { } self = value """) - default: - fatalError("Unsupported raw type '\(rawType)'") + default: + fatalError("Unsupported raw type '\(rawType)'") + } } } else { // Parse struct fields diff --git a/Sources/SWONMacros/Encoding.swift b/Sources/SWONMacros/Encoding.swift index 4124495..68326ad 100644 --- a/Sources/SWONMacros/Encoding.swift +++ b/Sources/SWONMacros/Encoding.swift @@ -29,42 +29,121 @@ struct SWONEncodeMacro: MemberMacro { var assignments: [String] = [] - // Parse enum from raw value if let enumDecl = declaration.as(EnumDeclSyntax.self) { - guard let inheritance = enumDecl.inheritanceClause, - let inheritedType = inheritance.inheritedTypes.first else { - fatalError("Enumeration must inherit a raw type") - } let enumType = enumDecl.name.text - let rawType = inheritedType.type.description -// context.diagnose( -// Diagnostic( -// node: Syntax(node), -// message: SWONMessage(message: "Raw type of enum \(enumType) is \(rawType)") + + // Enum with associated values + if enumDecl.hasAssociatedValues { + let cases = enumDecl.cases +// context.diagnose( +// Diagnostic( +// node: Syntax(node), +// message: SWONMessage(message: "Associated cases of enum \(enumType): \(cases.map(\.description))") +// ) // ) -// ) - switch rawType { - case "String": - assignments.append(""" - var item = swon_t() - guard rawValue.withCString({ swon_create_string(&item, $0) }) else { + assignments.append("switch self {") + cases.forEach { + for el in $0.elements { + // Parameters of the case + guard let parms = el.parameterClause?.parameters, !parms.isEmpty else { + assignments.append("case .\(el.name):") + // Empty dictionary with el name + assignments.append(""" + var empty = swon_t() + guard swon_create_object(&empty) else { + throw SWONError.invalid("Unable to create empty dictionary (\(el.name))") + } + guard swon_object_add_item(&root, "\(el.name)", empty) else { + swon_free(&empty) + throw SWONError.invalid("Unable to set root[\(el.name)]") + } + """) + continue + } + // Iterate through parameters + var parmVars: [String] = [] + var parmDeclarations: [String] = [] + var parmAssignments: [String] = [] + parms.enumerated().forEach { i, p in + let name = p.firstName?.description ?? "_\(i)" + let type = p.type.decodedType(context: context) + parmVars.append(name) + parmDeclarations.append("let \(name)") + parmAssignments.append( + contentsOf: type.statements( + forName: "\(name)JSON", + element: name, + pair: false, + withDeclaration: false + ) + ) + } + let parmDeclarationList = parmDeclarations.joined(separator: ",") + assignments.append("case .\(el.name)(\(parmDeclarationList)):") + assignments.append("var item = swon_t()") + parmVars.forEach { + assignments.append("var \($0)JSON = swon_t()") + } + assignments.append("do {") + assignments.append(""" + guard swon_create_object(&item) else { + throw SWONError.invalid("Unable to create root[\(el.name)]") + } + """) + assignments.append(contentsOf: parmAssignments) + parmVars.forEach { + assignments.append(""" + guard swon_object_add_item(&item, \"\($0)\", \($0)JSON) else { + throw SWONError.invalid("Unable to set root[\(el.name)][\($0)]") + } + """) + } + assignments.append(""" + guard swon_object_add_item(&root, \"\(el.name)\", item) else { + throw SWONError.invalid("Unable to set root[\(el.name)]") + } + """) + assignments.append("} catch {") + assignments.append("swon_free(&item)") + parmVars.forEach { + assignments.append("swon_free(&\($0)JSON)") + } + assignments.append("throw error") + assignments.append("}") + } + } + assignments.append("}") + } + // Enum with raw value + else if let inheritedType = enumDecl.inheritanceClause?.inheritedTypes.first { + let rawType = inheritedType.type.description +// context.diagnose( +// Diagnostic( +// node: Syntax(node), +// message: SWONMessage(message: "Raw type of enum \(enumType) is \(rawType)") +// ) +// ) + switch rawType { + case "String": + assignments.append(""" + var root = swon_t() + guard swon_create_string(&root, rawValue) else { throw SWONError.invalid("\(enumType)(String)") } - return item """) - case "Int": - assignments.append(""" - var item = swon_t() - guard swon_create_number(&item, Double(rawValue)) else { + case "Int": + assignments.append(""" + var root = swon_t() + guard swon_create_number(&root, Double(rawValue)) else { throw SWONError.invalid("\(enumType)(Int)") } - return item """) - default: - fatalError("Unsupported raw type '\(rawType)'") + default: + fatalError("Unsupported raw type '\(rawType)'") + } } } else { - // Parse struct fields + // Struct fields for prop in properties { guard let field = prop.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else { continue @@ -87,8 +166,8 @@ struct SWONEncodeMacro: MemberMacro { ) assignments.append(contentsOf: stmts) } - assignments.append("return root") } + assignments.append("return root") // context.diagnose( // Diagnostic( // node: Syntax(node), @@ -283,11 +362,18 @@ private extension SWONEncodeMacro { } private extension DecodedType { - func statements(forName name: String, element: String, pair: Bool) -> [String] { + func statements( + forName name: String, + element: String, + pair: Bool, + withDeclaration: Bool = true + ) -> [String] { var stmts: [String] = [] let suffix = pair ? ".value" : "" var isScalar = true - stmts.append("var \(name) = swon_t()") + if withDeclaration { + stmts.append("var \(name) = swon_t()") + } switch description { case "Bool": stmts.append("guard swon_create_bool(&\(name), \(element)\(suffix)) else {") diff --git a/Sources/SWONMacros/Helpers.swift b/Sources/SWONMacros/Helpers.swift index 056567e..c5e316c 100644 --- a/Sources/SWONMacros/Helpers.swift +++ b/Sources/SWONMacros/Helpers.swift @@ -62,6 +62,23 @@ extension TypeSyntax { } } +extension EnumDeclSyntax { + var cases: [EnumCaseDeclSyntax] { + memberBlock.members + .compactMap { member in + member.decl.as(EnumCaseDeclSyntax.self) + } + } + + var hasAssociatedValues: Bool { + cases.contains { caseDecl in + caseDecl.elements.contains { element in + element.parameterClause != nil + } + } + } +} + struct SWONMessage: DiagnosticMessage { let severity: DiagnosticSeverity = .note let message: String diff --git a/Sources/SWON_C/swon.c b/Sources/SWON_C/swon.c index cfc3b6e..bf861a9 100644 --- a/Sources/SWON_C/swon.c +++ b/Sources/SWON_C/swon.c @@ -45,6 +45,7 @@ void swon_free(swon_t *dst) { swon_result swon_get_object(swon_t *dst, swon_t src, const char *field) { cJSON *item = cJSON_GetObjectItemCaseSensitive(src.impl, field); SWON_RETURN_IF_NULL(item) + SWON_RETURN_INVALID_IF(!cJSON_IsObject(src.impl)) dst->impl = item; return SWONResultValid; } diff --git a/Tests/SWONTests/ComplexStruct.swift b/Tests/SWONTests/ComplexStruct.swift index 309720d..0d8cd48 100644 --- a/Tests/SWONTests/ComplexStruct.swift +++ b/Tests/SWONTests/ComplexStruct.swift @@ -74,4 +74,15 @@ struct SubStruct: Equatable, Codable { let statusHistory: [Status] let colorToStatus: [String: Status] let optionalColorArray: [Color]? + let assEnum: [AssociatedEnum]? +} + +@SWON +enum AssociatedEnum: Equatable, Codable { + case single + case singleFlat(String) + case singleKeyed(i: Int) + case multipleFlat(Int, Bool) + case multipleKeyed(d: Double, b: Bool) + case multipleMixed(d: Double, Bool, foo: SubStruct) } diff --git a/Tests/SWONTests/Resources/populated.json b/Tests/SWONTests/Resources/populated.json index c9d962a..cde5242 100644 --- a/Tests/SWONTests/Resources/populated.json +++ b/Tests/SWONTests/Resources/populated.json @@ -88,7 +88,47 @@ "optionalSize": null, "statusHistory": ["active", "pending"], "colorToStatus": {"blue": "pending"}, - "optionalColorArray": null + "optionalColorArray": null, + "assEnum": [ + { + "single": {} + }, + { + "singleFlat": { + "_0": "Value" + } + }, + { + "singleKeyed": { + "i": 42 + } + }, + { + "multipleFlat": { + "_0": 100, + "_1": true + } + }, + { + "multipleKeyed": { + "d": 3.14, + "b": false + } + }, + { + "multipleMixed": { + "d": 1.5, + "_1": true, + "foo": { + "favoriteColor": "blue", + "optionalSize": null, + "statusHistory": ["active", "pending"], + "colorToStatus": {"blue": "pending"}, + "optionalColorArray": null + } + } + } + ] }] } } diff --git a/Tests/SWONTests/SWONTests.swift b/Tests/SWONTests/SWONTests.swift index d41b22a..ea82f92 100644 --- a/Tests/SWONTests/SWONTests.swift +++ b/Tests/SWONTests/SWONTests.swift @@ -22,22 +22,47 @@ struct SWONTests { } @Test - func encodingStruct() throws { + func encodingAssociatedEnums() throws { @SWON - struct LocalStruct { - let favoriteColor: String - let optionalSize: Int? - let statusHistory: [String] - let colorToStatus: [String: String] - let another: AnotherStruct? - let nestedStrings: [[String]] - let nestedColors: [[Color]] + struct LocalContainer { + let enums: [AssociatedEnum] + init(enums: [AssociatedEnum]) { + self.enums = enums + } } - @SWON - struct AnotherStruct { - let one: Int - let two: Double + let sub = try SubStruct(fromJSON: """ + { + "favoriteColor": "red", + "optionalSize": 0, + "statusHistory": ["active"], + "colorToStatus": {"red": "active"}, + "optionalColorArray": ["blue"] } + """) + let sut = LocalContainer(enums: [ + .single, + .singleFlat("flat"), + .singleKeyed(i: 100), + .multipleFlat(50, true), + .multipleKeyed(d: 70.0, b: false), + .multipleMixed(d: 600.0, true, foo: sub) + ]) + let json = try sut.toJSON() + print(json) + } + + @Test + func decodingAssociatedEnums() throws { + let json = """ +{"enums":[{"single":{}},{"singleFlat":{"_0":"flat"}},{"singleKeyed":{"i":100}},{"multipleFlat":{"_0":50,"_1":true}},{"multipleKeyed":{"d":70,"b":false}},{"multipleMixed":{"d":600,"_1":true}}]} +""" + let sut = try LocalContainer(fromJSON: json) + print(json) + print(sut) + } + + @Test + func encodingStruct() throws { let sut = try LocalStruct(fromJSON: """ { "favoriteColor": "green", @@ -74,6 +99,31 @@ struct SWONTests { } } +@SWON +struct LocalContainer { + let enums: [AssociatedEnum] + init(enums: [AssociatedEnum]) { + self.enums = enums + } +} + +@SWON +struct LocalStruct { + let favoriteColor: String + let optionalSize: Int? + let statusHistory: [String] + let colorToStatus: [String: String] + let another: AnotherStruct? + let nestedStrings: [[String]] + let nestedColors: [[Color]] +} + +@SWON +struct AnotherStruct { + let one: Int + let two: Double +} + func jsonString(fromFileNamed name: String) throws -> String { guard let url = Bundle.module.url(forResource: name, withExtension: "json") else { fatalError() From fa4392c42bf0c28dc4ac368c1f2e7abadc3633f4 Mon Sep 17 00:00:00 2001 From: Davide Date: Mon, 26 Jan 2026 09:31:19 +0100 Subject: [PATCH 2/2] Fix test --- Tests/SWONTests/SWONTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SWONTests/SWONTests.swift b/Tests/SWONTests/SWONTests.swift index ea82f92..c31619e 100644 --- a/Tests/SWONTests/SWONTests.swift +++ b/Tests/SWONTests/SWONTests.swift @@ -54,7 +54,7 @@ struct SWONTests { @Test func decodingAssociatedEnums() throws { let json = """ -{"enums":[{"single":{}},{"singleFlat":{"_0":"flat"}},{"singleKeyed":{"i":100}},{"multipleFlat":{"_0":50,"_1":true}},{"multipleKeyed":{"d":70,"b":false}},{"multipleMixed":{"d":600,"_1":true}}]} +{"enums":[{"single":{}},{"singleFlat":{"_0":"flat"}},{"singleKeyed":{"i":100}},{"multipleFlat":{"_0":50,"_1":true}},{"multipleKeyed":{"d":70,"b":false}},{"multipleMixed":{"d":600,"_1":true,"foo":{"favoriteColor": "blue", "statusHistory": [], "colorToStatus": {}}}}]} """ let sut = try LocalContainer(fromJSON: json) print(json)