Skip to content

Commit 8cd1c2d

Browse files
committed
Merge branch 'dev'
2 parents 275b8ef + b141d5e commit 8cd1c2d

34 files changed

Lines changed: 254 additions & 98 deletions

Sources/Nodal/Codable/Element/Node+XMLElementCodable.swift

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,25 @@ public extension Node {
66
/// This method searches for a child element with the given name and attempts to decode it.
77
/// If the element is missing, it returns `nil`.
88
///
9-
/// - Parameter name: The name of the element to decode; either a `String` or an `ExpandedName`.
9+
/// - Parameter name: The name of the element to decode.
1010
/// - Returns: A decoded instance of `T`, or `nil` if the element is not present.
1111
/// - Throws: `XMLElementCodableError.invalidFormat` if the element cannot be parsed.
1212
///
13-
func decode<T: XMLElementDecodable>(elementName name: any ElementName) throws -> T? {
13+
func decode<T: XMLElementDecodable>(elementName name: String) throws -> T? {
14+
guard let element = self[element: name] else { return nil }
15+
return try T.init(from: element)
16+
}
17+
18+
/// Decodes an optional XML element into the specified type.
19+
///
20+
/// This method searches for a child element with the given expanded name and attempts to decode it.
21+
/// If the element is missing, it returns `nil`.
22+
///
23+
/// - Parameter name: The expanded name of the element to decode.
24+
/// - Returns: A decoded instance of `T`, or `nil` if the element is not present.
25+
/// - Throws: `XMLElementCodableError.invalidFormat` if the element cannot be parsed.
26+
///
27+
func decode<T: XMLElementDecodable>(elementName name: ExpandedName) throws -> T? {
1428
guard let element = self[element: name] else { return nil }
1529
return try T.init(from: element)
1630
}
@@ -19,30 +33,70 @@ public extension Node {
1933
///
2034
/// If the element is missing, this method *throws an error*.
2135
///
22-
/// - Parameter name: The name of the element to decode; either a `String` or an `ExpandedName`.
36+
/// - Parameter name: The name of the element to decode.
2337
/// - Returns: A decoded instance of `T`.
2438
/// - Throws:
2539
/// - `XMLElementCodableError.elementMissing` if the element is missing.
2640
/// - `XMLElementCodableError.invalidFormat` if the element cannot be parsed.
2741
///
28-
func decode<T: XMLElementDecodable>(elementName name: any ElementName) throws -> T {
42+
func decode<T: XMLElementDecodable>(elementName name: String) throws -> T {
2943
guard let element: T = try decode(elementName: name) else { throw XMLElementCodableError.elementMissing(name) }
3044
return element
3145
}
3246

47+
/// Decodes an XML element into the specified type.
48+
///
49+
/// If the element is missing, this method *throws an error*.
50+
///
51+
/// - Parameter name: The expanded name of the element to decode.
52+
/// - Returns: A decoded instance of `T`.
53+
/// - Throws:
54+
/// - `XMLElementCodableError.elementMissing` if the element is missing.
55+
/// - `XMLElementCodableError.invalidFormat` if the element cannot be parsed.
56+
///
57+
func decode<T: XMLElementDecodable>(elementName name: ExpandedName) throws -> T {
58+
guard let element: T = try decode(elementName: name) else { throw XMLElementCodableError.expandedElementMissing(name) }
59+
return element
60+
}
61+
3362
/// Decodes an array of XML elements into the specified type.
3463
///
3564
/// This method searches for all child elements matching the given name and decodes them.
3665
/// If a `containerName` is provided, it looks inside that container element first.
3766
///
3867
/// - Parameters:
39-
/// - name: The name of the elements to decode; either a `String` or an `ExpandedName`.
68+
/// - name: The name of the elements to decode.
4069
/// - containerName: The optional container element name. If provided, the method searches inside this container.
4170
/// - Returns: An array of decoded values.
4271
/// - Throws:
4372
/// - `XMLElementCodableError.invalidFormat` if any element cannot be parsed.
4473
///
45-
func decode<T: XMLElementDecodable>(elementName name: any ElementName, containedIn containerName: ElementName? = nil) throws -> [T] {
74+
func decode<T: XMLElementDecodable>(elementName name: String, containedIn containerName: String? = nil) throws -> [T] {
75+
let parent: Node
76+
if let containerName {
77+
guard let container = self[element: containerName] else {
78+
return []
79+
}
80+
parent = container
81+
} else {
82+
parent = self
83+
}
84+
return try parent[elements: name].map { try T.init(from: $0) }
85+
}
86+
87+
/// Decodes an array of XML elements into the specified type.
88+
///
89+
/// This method searches for all child elements matching the given expanded name and decodes them.
90+
/// If a `containerName` is provided, it looks inside that container element first.
91+
///
92+
/// - Parameters:
93+
/// - name: The expanded name of the elements to decode.
94+
/// - containerName: The optional container element name. If provided, the method searches inside this container.
95+
/// - Returns: An array of decoded values.
96+
/// - Throws:
97+
/// - `XMLElementCodableError.invalidFormat` if any element cannot be parsed.
98+
///
99+
func decode<T: XMLElementDecodable>(elementName name: ExpandedName, containedIn containerName: ExpandedName? = nil) throws -> [T] {
46100
let parent: Node
47101
if let containerName {
48102
guard let container = self[element: containerName] else {
@@ -63,23 +117,58 @@ public extension Node {
63117
///
64118
/// - Parameters:
65119
/// - item: The value to encode.
66-
/// - name: The name of the XML element to create; either a `String` or an `ExpandedName`.
120+
/// - name: The name of the XML element to create.
67121
///
68-
func encode<T: XMLElementEncodable>(_ item: T?, elementName name: any ElementName) {
122+
func encode<T: XMLElementEncodable>(_ item: T?, elementName name: String) {
69123
guard let item else { return }
70124
item.encode(to: addElement(name))
71125
}
72126

127+
/// Encodes an optional value as an XML element.
128+
///
129+
/// If the provided value is `nil`, no element is added.
130+
///
131+
/// - Parameters:
132+
/// - item: The value to encode.
133+
/// - name: The expanded name of the XML element to create.
134+
///
135+
func encode<T: XMLElementEncodable>(_ item: T?, elementName name: ExpandedName) {
136+
guard let item else { return }
137+
item.encode(to: addElement(name))
138+
}
139+
140+
/// Encodes an array of values as XML elements.
141+
///
142+
/// This method creates an element for each value in `items`. If `containerName` is provided, all elements are wrapped inside that container.
143+
///
144+
/// - Parameters:
145+
/// - items: The array of values to encode. If this is empty, this method does nothing.
146+
/// - name: The name of each XML element.
147+
/// - containerName: An optional container element name. If provided, the elements are placed inside this container.
148+
///
149+
func encode<T: XMLElementEncodable>(_ items: [T], elementName name: String, containedIn containerName: String? = nil) {
150+
guard items.isEmpty == false else { return }
151+
let parent: Node
152+
if let containerName {
153+
parent = addElement(containerName)
154+
} else {
155+
parent = self
156+
}
157+
for item in items {
158+
parent.encode(item, elementName: name)
159+
}
160+
}
161+
73162
/// Encodes an array of values as XML elements.
74163
///
75164
/// This method creates an element for each value in `items`. If `containerName` is provided, all elements are wrapped inside that container.
76165
///
77166
/// - Parameters:
78167
/// - items: The array of values to encode. If this is empty, this method does nothing.
79-
/// - name: The name of each XML element; either a `String` or an `ExpandedName`.
80-
/// - containerName: An optional container element name; either a `String` or an `ExpandedName`. If provided, the elements are placed inside this container.
168+
/// - name: The expanded name of each XML element.
169+
/// - containerName: An optional container element name. If provided, the elements are placed inside this container.
81170
///
82-
func encode<T: XMLElementEncodable>(_ items: [T], elementName name: any ElementName, containedIn containerName: (any ElementName)? = nil) {
171+
func encode<T: XMLElementEncodable>(_ items: [T], elementName name: ExpandedName, containedIn containerName: ExpandedName? = nil) {
83172
guard items.isEmpty == false else { return }
84173
let parent: Node
85174
if let containerName {

Sources/Nodal/Codable/Element/XMLElementCodable.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public protocol XMLElementDecodable {
3535
public typealias XMLElementCodable = XMLElementEncodable & XMLElementDecodable
3636

3737
public enum XMLElementCodableError: Error {
38-
case elementMissing (any ElementName)
38+
case elementMissing (String)
39+
case expandedElementMissing (ExpandedName)
3940
case documentElementMissing
4041
}

Sources/Nodal/Document/Document+Errors.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Foundation
2-
import pugixml
2+
@_implementationOnly import pugixml
33

44
public extension Document {
55
/// Represents an error that occurs during the parsing of an XML document.

Sources/Nodal/Document/Document+Input.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Foundation
2-
import pugixml
2+
@_implementationOnly import pugixml
33

44
public extension Document {
55
/// Creates an XML document by parsing the given XML string.

Sources/Nodal/Document/Document+Namespaces.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Foundation
2-
import pugixml
2+
@_implementationOnly import pugixml
33

44
public extension Document {
55
/// A set of namespace names that are referenced in the document but have not been declared.
@@ -117,22 +117,22 @@ internal extension Document {
117117
}
118118
}
119119

120-
private func removeNamespaceDeclarations(for nodes: Set<pugi.xml_node>) {
120+
private func removeNamespaceDeclarations(for nodes: Set<HashableNode>) {
121121
namespaceDeclarationsByName = namespaceDeclarationsByName.mapValues {
122-
$0.filter { !nodes.contains($0.node) }
122+
$0.filter { !nodes.contains(HashableNode($0.node)) }
123123
}
124124
namespaceDeclarationsByPrefix = namespaceDeclarationsByPrefix.mapValues {
125-
$0.filter { !nodes.contains($0.node) }
125+
$0.filter { !nodes.contains(HashableNode($0.node)) }
126126
}
127127
}
128128

129129
func removeNamespaceDeclarations(for tree: pugi.xml_node, excludingTarget: Bool = false) {
130-
let descendants = Set(tree.descendants.filter { $0.type() == pugi.node_element && (!excludingTarget || $0 != tree) })
130+
let descendants = Set(tree.descendants.filter { $0.type() == pugi.node_element && (!excludingTarget || $0 != tree) }.map { HashableNode($0) })
131131
removeNamespaceDeclarations(for: descendants)
132132
}
133133

134134
func rebuildNamespaceDeclarationCache(for element: Node) {
135-
removeNamespaceDeclarations(for: [element.node])
135+
removeNamespaceDeclarations(for: [HashableNode(element.node)])
136136
addNamespaceDeclarations(for: element.node)
137137
}
138138

Sources/Nodal/Document/Document+Output.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
2-
import pugixml
3-
import Bridge
2+
@_implementationOnly import pugixml
3+
@_implementationOnly import Bridge
44

55
internal extension Document {
66
private func save(encoding: String.Encoding = .utf8,

Sources/Nodal/Document/Document+PendingNameRecords.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Foundation
2-
import pugixml
2+
@_implementationOnly import pugixml
33

44
internal extension Document {
55
func pendingNameRecord(for element: Node) -> PendingNameRecord? {
@@ -49,7 +49,7 @@ internal extension Document {
4949
if excludingTarget && node == nodePointer {
5050
return false
5151
}
52-
return record.ancestors.contains(ancestor.node)
52+
return record.ancestors.contains(HashableNode(ancestor.node))
5353
}.map(\.key)
5454

5555
for key in keys { pendingNamespaceRecords[key] = nil }

Sources/Nodal/Document/Document+RootElement.swift

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Foundation
2-
import pugixml
2+
@_implementationOnly import pugixml
33

44
internal extension Document {
55
func clearDocumentElement() -> Node {
@@ -23,18 +23,36 @@ public extension Document {
2323
/// Creates a new document (root) element for the document with the specified name and optional default namespace URI.
2424
///
2525
/// - Parameters:
26-
/// - name: The name of the new document element; either a String or an ExpandedName
26+
/// - name: The name of the new document element.
2727
/// - uri: The default namespace URI to associate with the document element. Defaults to `nil`.
2828
/// - Returns: The newly created element.
2929
///
3030
/// - Note: If the document already has a document element, it is removed before creating the new one.
3131
@discardableResult
32-
func makeDocumentElement(name: ElementName, defaultNamespace uri: String? = nil) -> Node {
32+
func makeDocumentElement(name: String, defaultNamespace uri: String? = nil) -> Node {
3333
let element = clearDocumentElement()
3434
if let uri {
3535
element.declareNamespace(uri, forPrefix: nil)
3636
}
37-
element.name = name.requestQualifiedName(for: element)
37+
element.name = name
38+
return element
39+
}
40+
41+
/// Creates a new document (root) element for the document with the specified expanded name and optional default namespace URI.
42+
///
43+
/// - Parameters:
44+
/// - name: The expanded name of the new document element.
45+
/// - uri: The default namespace URI to associate with the document element. Defaults to `nil`.
46+
/// - Returns: The newly created element.
47+
///
48+
/// - Note: If the document already has a document element, it is removed before creating the new one.
49+
@discardableResult
50+
func makeDocumentElement(name: ExpandedName, defaultNamespace uri: String? = nil) -> Node {
51+
let element = clearDocumentElement()
52+
if let uri {
53+
element.declareNamespace(uri, forPrefix: nil)
54+
}
55+
element.name = name.requestQualifiedElementName(for: element)
3856
return element
3957
}
4058

@@ -94,10 +112,25 @@ public extension Document {
94112
///
95113
/// - Parameters:
96114
/// - item: The object to encode into XML. Must conform to `XMLElementEncodable`.
97-
/// - elementName: The name of the root element in the XML document; either a String or an ExpandedName
115+
/// - elementName: The name of the root element in the XML document.
116+
/// - SeeAlso: `decoded(as:)`
117+
///
118+
convenience init<T: XMLElementEncodable>(_ item: T, elementName: String) {
119+
self.init()
120+
let root = makeDocumentElement(name: elementName)
121+
item.encode(to: root)
122+
}
123+
124+
/// Creates an XML document from an instance of `XMLElementEncodable`.
125+
///
126+
/// This initializes a new XML document with the specified *root element name*, then encodes the given object into it.
127+
///
128+
/// - Parameters:
129+
/// - item: The object to encode into XML. Must conform to `XMLElementEncodable`.
130+
/// - elementName: The expanded name of the root element in the XML document.
98131
/// - SeeAlso: `decoded(as:)`
99132
///
100-
convenience init<T: XMLElementEncodable>(_ item: T, elementName: ElementName) {
133+
convenience init<T: XMLElementEncodable>(_ item: T, elementName: ExpandedName) {
101134
self.init()
102135
let root = makeDocumentElement(name: elementName)
103136
item.encode(to: root)

Sources/Nodal/Document/Document.ParseOptions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Foundation
2-
import pugixml
2+
@_implementationOnly import pugixml
33

44
public extension Document {
55
struct ParseOptions: OptionSet, Sendable {

Sources/Nodal/Document/Document.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
2-
import pugixml
3-
import Bridge
2+
@_implementationOnly import pugixml
3+
@_implementationOnly import Bridge
44

55
/// Represents an XML document node, providing methods for working with the document structure and serialization.
66
public class Document {

0 commit comments

Comments
 (0)