diff --git a/README.md b/README.md index 2b8ba12..ac019eb 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,9 @@ SWON uses the amazingly written [cJSON][credits-cjson] parser (C) under the hood ### Things to do +- Handle `RawRepresentable` automatically. - Do not suppress errors inside an optional field (silently made nil). -- Add thorough tests add meaningful examples. +- Add thorough tests and meaningful examples. - Clean up some messy code. ## License diff --git a/Sources/SWON/Extensions.swift b/Sources/SWON/Extensions.swift new file mode 100644 index 0000000..ce3b263 --- /dev/null +++ b/Sources/SWON/Extensions.swift @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2026 Davide De Rosa +// +// SPDX-License-Identifier: MIT + +extension Array: SWONDecodable, SWONEncodable where Element: SWONDecodable & SWONEncodable { + public init(fromSWON root: swon_t) throws { + var list: [Element] = [] + var array = swon_t() + guard swon_get_array(&array, root) == SWONResultValid else { + throw SWONError.required("Array") + } + let size = swon_get_array_size(array) + for i in 0.. swon_t { + let list = Array(self) + var root = swon_t() + guard swon_create_array(&root) else { throw SWONError.invalid("Array") } + for el in list { + let item = try el.toSWON() + guard swon_array_add_item(&root, item) else { + throw SWONError.invalid("Array: Element") + } + } + return root + } +} + +extension Set: SWONDecodable, SWONEncodable where Element: SWONDecodable & SWONEncodable { + public init(fromSWON root: swon_t) throws { + self = try Set(Array(fromSWON: root)) + } + + public func toSWON() throws -> swon_t { + try Array(self).toSWON() + } +} diff --git a/Sources/SWON/Protocols.swift b/Sources/SWON/Protocols.swift new file mode 100644 index 0000000..27189f2 --- /dev/null +++ b/Sources/SWON/Protocols.swift @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2026 Davide De Rosa +// +// SPDX-License-Identifier: MIT + +public protocol SWONEncodable { + func toSWON() throws -> swon_t +} + +public protocol SWONDecodable { + init(fromSWON root: swon_t) throws +} diff --git a/Sources/SWON/SWON.swift b/Sources/SWON/SWON.swift index 24f7562..81a9b6a 100644 --- a/Sources/SWON/SWON.swift +++ b/Sources/SWON/SWON.swift @@ -10,6 +10,10 @@ named(toSWON), named(toJSON) ) +@attached( + extension, + conformances: SWONEncodable, SWONDecodable +) public macro SWON() = #externalMacro( module: "SWONMacros", type: "SWONCompoundMacro" diff --git a/Sources/SWONMacros/Decoding.swift b/Sources/SWONMacros/Decoding.swift index fcff0f2..1207af5 100644 --- a/Sources/SWONMacros/Decoding.swift +++ b/Sources/SWONMacros/Decoding.swift @@ -151,7 +151,7 @@ struct SWONDecodeMacro: MemberMacro { // context.diagnose( // Diagnostic( // node: Syntax(node), -// message: SWONMessage(message: "Assigning \(field) of type \(type)") +// message: SWONMessage(message: "Assigning \(field) of type \(type) (decoded: \(type.decodedType(context: context)))") // ) // ) assignments.append(""" diff --git a/Sources/SWONMacros/Macros.swift b/Sources/SWONMacros/Macros.swift index 42c9bb7..08aaa7b 100644 --- a/Sources/SWONMacros/Macros.swift +++ b/Sources/SWONMacros/Macros.swift @@ -13,7 +13,7 @@ struct SWONMacrosPlugin: CompilerPlugin { ] } -public struct SWONCompoundMacro: MemberMacro { +public struct SWONCompoundMacro: MemberMacro, ExtensionMacro { public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, @@ -35,4 +35,41 @@ public struct SWONCompoundMacro: MemberMacro { ) return result } + + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + // Extract the type name (struct/class/enum identifier) + guard let named = declaration.asProtocol(NamedDeclSyntax.self) else { + return [] + } + let typeName = named.name.text + + // Optional: avoid duplicate conformances if already declared + let alreadyConforms: Set = + declaration.inheritanceClause? + .inheritedTypes + .compactMap { $0.type.as(IdentifierTypeSyntax.self)?.name.text } + .reduce(into: Set()) { $0.insert($1) } + ?? [] + let needsEncodable = !alreadyConforms.contains("SWONEncodable") + let needsDecodable = !alreadyConforms.contains("SWONDecodable") + guard needsEncodable || needsDecodable else { + return [] + } + + // Build the conformance list dynamically + var conformances: [String] = [] + if needsEncodable { conformances.append("SWONEncodable") } + if needsDecodable { conformances.append("SWONDecodable") } + let conformanceList = conformances.joined(separator: ", ") + let ext = try ExtensionDeclSyntax( + "extension \(raw: typeName): \(raw: conformanceList) {}" + ) + return [ext] + } } diff --git a/Sources/SWON_C/swon.c b/Sources/SWON_C/swon.c index bf861a9..335dbca 100644 --- a/Sources/SWON_C/swon.c +++ b/Sources/SWON_C/swon.c @@ -119,7 +119,7 @@ swon_t swon_get_map_next(swon_t src) { bool swon_create_array(swon_t *dst) { dst->impl = cJSON_CreateArray(); - return dst->impl; + return dst->impl != NULL; } bool swon_array_add_item(swon_t *dst, swon_t item) { diff --git a/Tests/SWONTests/ComplexStruct.swift b/Tests/SWONTests/ComplexStruct.swift index 1f8c9ab..6a6b187 100644 --- a/Tests/SWONTests/ComplexStruct.swift +++ b/Tests/SWONTests/ComplexStruct.swift @@ -52,6 +52,9 @@ struct ComplexStruct: Equatable, Codable { let optionalStringToSubStructArray: [String: [SubStruct]]? let uint16: UInt16? + + // Set + let someSet: Set? } @SWON diff --git a/Tests/SWONTests/Resources/populated.json b/Tests/SWONTests/Resources/populated.json index 3459b9b..9185e84 100644 --- a/Tests/SWONTests/Resources/populated.json +++ b/Tests/SWONTests/Resources/populated.json @@ -131,5 +131,6 @@ ] }] }, - "uint16": 60000 + "uint16": 60000, + "someSet": ["blue", "red", "green"] } diff --git a/Tests/SWONTests/SWONTests.swift b/Tests/SWONTests/SWONTests.swift index cf9df80..268a564 100644 --- a/Tests/SWONTests/SWONTests.swift +++ b/Tests/SWONTests/SWONTests.swift @@ -23,13 +23,6 @@ struct SWONTests { @Test func encodingAssociatedEnums() throws { - @SWON - struct LocalContainer { - let enums: [AssociatedEnum] - init(enums: [AssociatedEnum]) { - self.enums = enums - } - } let sub = try SubStruct(fromJSON: """ { "favoriteColor": "red", @@ -81,7 +74,8 @@ struct SWONTests { "nestedColors": [ ["green", "blue"], ["red", "red", "green"] - ] + ], + "colorSet": ["red", "green"] } """) print(try sut.toJSON()) @@ -116,6 +110,7 @@ struct LocalStruct { let another: AnotherStruct? let nestedStrings: [[String]] let nestedColors: [[Color]] + let colorSet: Set } @SWON