diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index 805a0d0e..9402dc7e 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -293,21 +293,23 @@ struct XMLCoderElement: Equatable { // MARK: - Convenience Initializers extension XMLCoderElement { - init(key: String, box: UnkeyedBox) { + init(key: String, box: UnkeyedBox, attributes: [Attribute] = []) { if let containsChoice = box as? [ChoiceBox] { - self.init(key: key, elements: containsChoice.map { - XMLCoderElement(key: $0.key, box: $0.element) - }) + self.init( + key: key, + elements: containsChoice.map { XMLCoderElement(key: $0.key, box: $0.element) }, + attributes: attributes + ) } else { - self.init(key: key, elements: box.map { XMLCoderElement(key: key, box: $0) }) + self.init(key: key, elements: box.map { XMLCoderElement(key: key, box: $0) }, attributes: attributes) } } - init(key: String, box: ChoiceBox) { - self.init(key: key, elements: [XMLCoderElement(key: box.key, box: box.element)]) + init(key: String, box: ChoiceBox, attributes: [Attribute] = []) { + self.init(key: key, elements: [XMLCoderElement(key: box.key, box: box.element)], attributes: attributes) } - init(key: String, box: KeyedBox) { + init(key: String, box: KeyedBox, attributes: [Attribute] = []) { var elements: [XMLCoderElement] = [] for (key, box) in box.elements { @@ -338,7 +340,7 @@ extension XMLCoderElement { } } - let attributes: [Attribute] = box.attributes.compactMap { key, box in + let attributes: [Attribute] = attributes + box.attributes.compactMap { key, box in guard let value = box.xmlString else { return nil } @@ -356,20 +358,20 @@ extension XMLCoderElement { } } - init(key: String, box: Box) { + init(key: String, box: Box, attributes: [Attribute] = []) { switch box { case let sharedUnkeyedBox as SharedBox: - self.init(key: key, box: sharedUnkeyedBox.unboxed) + self.init(key: key, box: sharedUnkeyedBox.unboxed, attributes: attributes) case let sharedKeyedBox as SharedBox: - self.init(key: key, box: sharedKeyedBox.unboxed) + self.init(key: key, box: sharedKeyedBox.unboxed, attributes: attributes) case let sharedChoiceBox as SharedBox: - self.init(key: key, box: sharedChoiceBox.unboxed) + self.init(key: key, box: sharedChoiceBox.unboxed, attributes: attributes) case let unkeyedBox as UnkeyedBox: - self.init(key: key, box: unkeyedBox) + self.init(key: key, box: unkeyedBox, attributes: attributes) case let keyedBox as KeyedBox: - self.init(key: key, box: keyedBox) + self.init(key: key, box: keyedBox, attributes: attributes) case let choiceBox as ChoiceBox: - self.init(key: key, box: choiceBox) + self.init(key: key, box: choiceBox, attributes: attributes) case let simpleBox as SimpleBox: self.init(key: key, box: simpleBox) case let box: diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index a1b4a3e0..233802b5 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -318,12 +318,16 @@ open class XMLEncoder { /// /// - parameter value: The value to encode. /// - parameter withRootKey: the key used to wrap the encoded values. + /// - parameter rootAttributes: the list of attributes to be added to the root node /// - returns: A new `Data` value containing the encoded XML data. /// - throws: `EncodingError.invalidValue` if a non-conforming /// floating-point value is encountered during encoding, and the encoding /// strategy is `.throw`. /// - throws: An error if any value throws an error during encoding. - open func encode(_ value: T, withRootKey rootKey: String, header: XMLHeader? = nil) throws -> Data { + open func encode(_ value: T, + withRootKey rootKey: String? = nil, + rootAttributes: [String: String]? = nil, + header: XMLHeader? = nil) throws -> Data { let encoder = XMLEncoderImplementation( options: options, nodeEncodings: [] @@ -331,15 +335,18 @@ open class XMLEncoder { encoder.nodeEncodings.append(options.nodeEncodingStrategy.nodeEncodings(forType: T.self, with: encoder)) let topLevel = try encoder.box(value) + let attributes = rootAttributes?.map(Attribute.init) ?? [] let elementOrNone: XMLCoderElement? + let rootKey = rootKey ?? "\(T.self)".convert(for: keyEncodingStrategy) + if let keyedBox = topLevel as? KeyedBox { - elementOrNone = XMLCoderElement(key: rootKey, box: keyedBox) + elementOrNone = XMLCoderElement(key: rootKey, box: keyedBox, attributes: attributes) } else if let unkeyedBox = topLevel as? UnkeyedBox { - elementOrNone = XMLCoderElement(key: rootKey, box: unkeyedBox) + elementOrNone = XMLCoderElement(key: rootKey, box: unkeyedBox, attributes: attributes) } else if let choiceBox = topLevel as? ChoiceBox { - elementOrNone = XMLCoderElement(key: rootKey, box: choiceBox) + elementOrNone = XMLCoderElement(key: rootKey, box: choiceBox, attributes: attributes) } else { fatalError("Unrecognized top-level element of type: \(type(of: topLevel))") } @@ -358,3 +365,24 @@ open class XMLEncoder { .data(using: .utf8, allowLossyConversion: true)! } } + +private extension String { + func convert(for encodingStrategy: XMLEncoder.KeyEncodingStrategy) -> String { + switch encodingStrategy { + case .useDefaultKeys: + return self + case .convertToSnakeCase: + return XMLEncoder.KeyEncodingStrategy._convertToSnakeCase(self) + case .convertToKebabCase: + return XMLEncoder.KeyEncodingStrategy._convertToKebabCase(self) + case .custom: + return self + case .capitalized: + return XMLEncoder.KeyEncodingStrategy._convertToCapitalized(self) + case .uppercased: + return XMLEncoder.KeyEncodingStrategy._convertToUppercased(self) + case .lowercased: + return XMLEncoder.KeyEncodingStrategy._convertToLowercased(self) + } + } +} diff --git a/Tests/XMLCoderTests/RootLevetExtraAttributesTests.swift b/Tests/XMLCoderTests/RootLevetExtraAttributesTests.swift new file mode 100644 index 00000000..f5ff76c1 --- /dev/null +++ b/Tests/XMLCoderTests/RootLevetExtraAttributesTests.swift @@ -0,0 +1,56 @@ +import XCTest +@testable import XMLCoder + +final class RootLevetExtraAttributesTests: XCTestCase { + private let encoder = XMLEncoder() + + func testExtraAttributes() { + let policy = Policy(name: "test", initial: "extra root attributes") + + let extraRootAttributes = [ + "xmlns": "http://www.nrf-arts.org/IXRetail/namespace", + "xmlns:xsd": "http://www.w3.org/2001/XMLSchema", + "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", + ] + + encoder.keyEncodingStrategy = .lowercased + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + + do { + let data = try encoder.encode(policy, + rootAttributes: extraRootAttributes) + + let dataString = String(data: data, encoding: .utf8) + XCTAssertNotNil(dataString, "failed to encode object") + + let expected = """ + + extra root attributes + + """ + + XCTAssertEqual(dataString!, expected, "") + } catch { + XCTAssertThrowsError(error) + } + } +} + +private struct Policy: Encodable, DynamicNodeEncoding { + var name: String + var initial: String + + enum CodingKeys: String, CodingKey { + case name, initial + } + + static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case Policy.CodingKeys.name: return .attribute + default: return .element + } + } +} diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index f01e6870..fe92a340 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ /* Begin PBXBuildFile section */ 07E441BA2340F14B00890F46 /* EmptyElementEmptyStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E441B92340F14B00890F46 /* EmptyElementEmptyStringTests.swift */; }; 4A062D4F2341924E009BCAC1 /* CombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A062D4E2341924E009BCAC1 /* CombineTests.swift */; }; + 970FA9DC2422EFAE0023C1EC /* RootLevetExtraAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970FA9DB2422EFAE0023C1EC /* RootLevetExtraAttributesTests.swift */; }; B54555BC2343F5C1000D4128 /* EmptyArrayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54555BB2343F5C1000D4128 /* EmptyArrayTest.swift */; }; B5E67533238B47E5006C8548 /* MixedChoiceAndNonChoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E67532238B47E5006C8548 /* MixedChoiceAndNonChoiceTests.swift */; }; B5E67535238B4960006C8548 /* IntOrString.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E67534238B4960006C8548 /* IntOrString.swift */; }; @@ -164,6 +165,7 @@ /* Begin PBXFileReference section */ 07E441B92340F14B00890F46 /* EmptyElementEmptyStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyElementEmptyStringTests.swift; sourceTree = ""; }; 4A062D4E2341924E009BCAC1 /* CombineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombineTests.swift; sourceTree = ""; }; + 970FA9DB2422EFAE0023C1EC /* RootLevetExtraAttributesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootLevetExtraAttributesTests.swift; sourceTree = ""; }; B54555BB2343F5C1000D4128 /* EmptyArrayTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyArrayTest.swift; sourceTree = ""; }; B5E67532238B47E5006C8548 /* MixedChoiceAndNonChoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MixedChoiceAndNonChoiceTests.swift; sourceTree = ""; }; B5E67534238B4960006C8548 /* IntOrString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntOrString.swift; sourceTree = ""; }; @@ -452,30 +454,31 @@ OBJ_90 /* DecodingContainerTests.swift */, OBJ_91 /* DynamicNodeDecodingTest.swift */, OBJ_92 /* DynamicNodeEncodingTest.swift */, + B54555BB2343F5C1000D4128 /* EmptyArrayTest.swift */, + 07E441B92340F14B00890F46 /* EmptyElementEmptyStringTests.swift */, OBJ_93 /* ErrorContextTest.swift */, + B5E67534238B4960006C8548 /* IntOrString.swift */, OBJ_94 /* KeyDecodingAndEncodingStrategyTests.swift */, + B5E67532238B47E5006C8548 /* MixedChoiceAndNonChoiceTests.swift */, OBJ_113 /* MixedContainerTest.swift */, OBJ_114 /* NamespaceTest.swift */, OBJ_115 /* NestedAttributeChoiceTests.swift */, + B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */, OBJ_116 /* NestedChoiceTests.swift */, OBJ_117 /* NestingTests.swift */, OBJ_118 /* NodeEncodingStrategyTests.swift */, OBJ_119 /* NoteTest.swift */, OBJ_120 /* PlantCatalog.swift */, OBJ_121 /* PlantTest.swift */, - 07E441B92340F14B00890F46 /* EmptyElementEmptyStringTests.swift */, D18FBFB72348FAE500FA4F65 /* QuoteDecodingTest.swift */, OBJ_124 /* RelationshipsTest.swift */, OBJ_122 /* RJISample.swift */, OBJ_123 /* RJITest.swift */, B5F74471233F74E400BBDB15 /* RootLevelAttributeTest.swift */, + 970FA9DB2422EFAE0023C1EC /* RootLevetExtraAttributesTests.swift */, OBJ_125 /* SimpleChoiceTests.swift */, OBJ_126 /* SingleChildTests.swift */, OBJ_127 /* SpacePreserveTest.swift */, - B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */, - B54555BB2343F5C1000D4128 /* EmptyArrayTest.swift */, - B5E67532238B47E5006C8548 /* MixedChoiceAndNonChoiceTests.swift */, - B5E67534238B4960006C8548 /* IntOrString.swift */, ); name = XMLCoderTests; path = Tests/XMLCoderTests; @@ -792,6 +795,7 @@ OBJ_268 /* RJISample.swift in Sources */, B5E67535238B4960006C8548 /* IntOrString.swift in Sources */, OBJ_269 /* RJITest.swift in Sources */, + 970FA9DC2422EFAE0023C1EC /* RootLevetExtraAttributesTests.swift in Sources */, OBJ_270 /* RelationshipsTest.swift in Sources */, OBJ_271 /* SimpleChoiceTests.swift in Sources */, 4A062D4F2341924E009BCAC1 /* CombineTests.swift in Sources */,