Skip to content

Commit b141d5e

Browse files
committed
Remove ElementName protocol in favor of explicit overloads
Replace the ElementName protocol with separate overloads for String and ExpandedName parameters. This simplifies the API and avoids exposing internal protocol conformances in the public interface. Also add HashableNode wrapper to avoid exposing pugi.xml_node Hashable conformance, and fix Node hash/equality to use internal_object().
1 parent 74ddab8 commit b141d5e

12 files changed

Lines changed: 229 additions & 73 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+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+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/PendingNameRecord.swift

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

44
internal class PendingNameRecord {
55
var elementName: ExpandedName?
66
var attributes: [ExpandedName: String] = [:] // value = qName
7-
var ancestors: Set<pugi.xml_node> = .init(minimumCapacity: 8) // Ancestors, including the element itself
7+
var ancestors: Set<HashableNode> = .init(minimumCapacity: 8) // Ancestors, including the element itself
88

99
private static let pendingPrefix = "__pending"
1010

@@ -18,7 +18,7 @@ internal class PendingNameRecord {
1818

1919
var node = element
2020
while !node.empty() {
21-
ancestors.insert(node)
21+
ancestors.insert(HashableNode(node))
2222
node = node.parent()
2323
}
2424
}
@@ -35,13 +35,13 @@ internal class PendingNameRecord {
3535
ancestors = []
3636
var node = element
3737
while !node.empty() {
38-
ancestors.insert(node)
38+
ancestors.insert(HashableNode(node))
3939
node = node.parent()
4040
}
4141
}
4242

4343
func belongsToTree(_ node: Node) -> Bool {
44-
ancestors.contains(node.node)
44+
ancestors.contains(HashableNode(node.node))
4545
}
4646

4747
private func pendingPlaceholder(for name: ExpandedName) -> String {

Sources/Nodal/Extensions/Pugi.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import pugixml
2-
import Bridge
1+
@_implementationOnly import pugixml
2+
@_implementationOnly import Bridge
33
import Foundation
44

5-
extension pugi.xml_node_type: Hashable {}
6-
75
extension pugi.xml_attribute {
86
var nonNull: pugi.xml_attribute? {
97
empty() ? nil : self

Sources/Nodal/Extensions/PugiNode.swift

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

4-
extension pugi.xml_node: Hashable {
5-
public func hash(into hasher: inout Hasher) {
6-
hasher.combine(internal_object())
4+
// Wrapper to make pugi.xml_node Hashable without exposing it in the public interface
5+
internal struct HashableNode: Hashable {
6+
let node: pugi.xml_node
7+
8+
init(_ node: pugi.xml_node) {
9+
self.node = node
10+
}
11+
12+
func hash(into hasher: inout Hasher) {
13+
hasher.combine(node.internal_object())
714
}
815

16+
static func == (lhs: HashableNode, rhs: HashableNode) -> Bool {
17+
lhs.node == rhs.node
18+
}
19+
}
20+
21+
internal extension pugi.xml_node {
922
var nonNull: pugi.xml_node? {
1023
empty() ? nil : self
1124
}

0 commit comments

Comments
 (0)