XMLTools is a set APIs to parse, evaluate, manipulate and serialize complex XML structures. It is written written entirely in Swift programming language and designed to work on all platforms supporting Swift (e.g. macOS, iOS).
XMLTOOLS provides the following features:
Full Namespaces and QNames support
Lightweight DOM implementation
XPath like access to XML node tree (including axes support)
Subscript and Sequence support (like all other libraries)
Datatypes Support (e.g. Text, Data, Int, Double, Decimal)
Serializing XML Document to Data
XML creation and manipulation
Fully extensible to be used in specific use cases (e.g. SOAP)
Motivation
Since Apple only provides the low-level XMLParser
on all it’s Platforms (with exception of macOS, which has high level XML API), there are a lot of Open-Source Projects providing such APIs, most notably SWXMLHash and SwiftyXMLParser.
The problem with all projects I’ve found on GitHUB is that they only support the simplest XML structures and queries. Most of them take inspiration from SwiftyJSON and handle XML as JSON. There are two issues with that approach: 1) most of legacy XML Systems use rather complex XML structures with heavy use of namespaces; 2) if someone creates the new and simple protocols they use JSON anyway.
XMLTools tries to close this gap and provides the “old school XML” using modern features of Swift programming language.
Quick Start
let parser = XMLTools.Parser()
let xml: XMLTools.Infoset
do {
xml = try parser.parse(contentsOf: "https://ec.europa.eu/information_society/policy/esignature/trusted-list/tl-mp.xml")
} catch {
print (error)
return
}
xml.namespaceContext.declare(withNoPrefix: "http://uri.etsi.org/02231/v2#")
print(xml["TrustServiceStatusList", "SchemeInformation", "TSLType"].text)
// prints http://uri.etsi.org/TrstSvc/TrustedList/TSLType/EUlistofthelists
let wsdlSourceXML =
"""
<?xml version="1.0" encoding="UTF-8"?>
<description xmlns="http://www.w3.org/ns/wsdl"
xmlns:tns="http://www.tmsws.com/wsdl20sample"
xmlns:whttp="http://schemas.xmlsoap.org/wsdl/http/"
xmlns:wsoap="http://schemas.xmlsoap.org/wsdl/soap/"
targetNamespace="http://www.tmsws.com/wsdl20sample">
<documentation>
This is a sample WSDL 2.0 document.
</documentation>
<!-- Abstract type -->
<types>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://www.tmsws.com/wsdl20sample"
targetNamespace="http://www.example.com/wsdl20sample">
<xs:element name="request"> ... </xs:element>
<xs:element name="response"> ... </xs:element>
</xs:schema>
</types>
<!-- Abstract interfaces -->
<interface name="Interface1">
<fault name="Error1" element="tns:response"/>
<operation name="Get" pattern="http://www.w3.org/ns/wsdl/in-out">
<input messageLabel="In" element="tns:request"/>
<output messageLabel="Out" element="tns:response"/>
</operation>
</interface>
<!-- Concrete Binding Over HTTP -->
<binding name="HttpBinding" interface="tns:Interface1"
type="http://www.w3.org/ns/wsdl/http">
<operation ref="tns:Get" whttp:method="GET"/>
</binding>
<!-- Concrete Binding with SOAP-->
<binding name="SoapBinding" interface="tns:Interface1"
type="http://www.w3.org/ns/wsdl/soap"
wsoap:protocol="http://www.w3.org/2003/05/soap/bindings/HTTP/"
wsoap:mepDefault="http://www.w3.org/2003/05/soap/mep/request-response">
<operation ref="tns:Get" />
</binding>
<!-- Web Service offering endpoints for both bindings-->
<service name="Service1" interface="tns:Interface1">
<endpoint name="HttpEndpoint"
binding="tns:HttpBinding"
address="http://www.example.com/rest/"/>
<endpoint name="SoapEndpoint"
binding="tns:SoapBinding"
address="http://www.example.com/soap/"/>
</service>
</description>
"""
let parser = XMLTools.Parser()
let xml: XMLTools.Infoset
do {
xml = try parser.parse(string: wsdlSourceXML)
} catch {
print (error)
XCTFail("\(error)")
return
}
Since we didn’t specify any options when creating the “XMLTools.Parser” there are no namespace declarations in the current Infoset and every element must be accessed by using the qualified names:
The better way is to declare the namespace. Please note, that even if the source XML has no prefix defined we still should access the elements and attributes by using the prefix defined here. This way the code is independent of the source namespace prefixes, especially when sources are generated and use cryptic prefixes like ns0:
// equivalent to xmlns:wsdl="http://www.w3.org/ns/wsdl"
xml.namespaceContext.declare("wsdl", uri: "http://www.w3.org/ns/wsdl")
print (xml["wsdl:description", "wsdl:documentation"].text)
If we want to access WSDL elements without the prefix we can do it this way:
// equivalent to xmlns="http://www.w3.org/ns/wsdl"
xml.namespaceContext.declare(withNoPrefix: "http://www.w3.org/ns/wsdl")
print (xml["description", "documentation"].text)
Here is a more complex example demonstrating the extensibility of XMLTools API:
// somewhere on file level
extension NamespaceDeclaration {
public static let Wsdl = NamespaceDeclaration("wsdl", uri: "http://www.w3.org/ns/wsdl")
public static let WsdlSoap = NamespaceDeclaration("wsoap", uri: "http://schemas.xmlsoap.org/wsdl/soap/")
public static let WsdlHttp = NamespaceDeclaration("whttp", uri: "http://schemas.xmlsoap.org/wsdl/http/")
}
// declare the namespaces we want to use
xml.namespaceContext.declare(.Wsdl).declare(.WsdlSoap).declare(.WsdlHttp)
let httpBinding = xml.descendants("wsdl:binding").select {
$0.attr("name").text == "HttpBinding"
}
print (httpBinding["wsdl:operation"].attr("whttp:method").text) // "GET"
let soapBinding = xml.descendants("wsdl:binding").select {
$0.attr("name").text == "SoapBinding"
}
print (soapBinding.attr("wsoap:protocol").text) // "http://www.w3.org/2003/05/soap/bindings/HTTP/"
And finally we can just be lazy and tell the parser to preserve all namespace declarations exactly as they appear in the XML source
let anotherParser = XMLTools.Parser()
// tell the parser to preserve all namespace prefix declarations
anotherParser.options.preserveSourceNamespaceContexts = true
let anotherXML: XMLTools.Infoset
do {
anotherXML = try anotherParser.parse(string: wsdlSourceXML)
} catch {
print (error)
XCTFail("\(error)")
return
}
print (anotherXML["description"].name().namespaceURI) // "http://www.w3.org/ns/wsdl"
XCTAssertEqual(anotherXML["description"].name().namespaceURI, "http://www.w3.org/ns/wsdl")
Serializing XML
// Parse XML
let xmlLocation = "https://raw.githubusercontent.com/spilikin/SwiftXMLTools/master/Testfiles/xmldsig-core-schema.xsd"
let parser = XMLTools.Parser()
// tell the parser to preserve the namespace declarations (prefixes)
parser.options.preserveSourceNamespaceContexts = true
let xml: XMLTools.Infoset
do {
xml = try parser.parse(contentsOf: xmlLocation)
} catch {
print("\(error)")
return
}
if let indentedData = xml.document().data(.indent) {
print (String(data: indentedData, encoding:.utf8)! )
} else {
print ("Cannot convert XML to Data")
}
Creating XML from scratch
struct Book {
let title: String
let lang: String
let price: Decimal
let pages: Int
}
let bookstore = [
Book(title: "Harry Potter: The Philosopher's Stone", lang: "en", price: 24.99, pages: 223),
Book(title: "Harry Potter: The Chamber of Secrets", lang: "en", price: 29.99, pages: 251),
Book(title: "Learning XML", lang: "en", price: 39.95, pages: 432),
Book(title: "IT-Sicherheit: Konzepte - Verfahren - Protokolle", lang: "de", price: 69.95, pages: 932),
]
let builtXML = Document().select()
builtXML.appendElement("bookstore")
for book in bookstore {
builtXML["bookstore"].appendElement("book")
.appendElement("title")
.manipulate{ $0.text = book.title; $0.attr("lang", setValue: book.lang) }
.parent()
.appendElement("price").manipulate{ $0.number = book.price}.parent()
.appendElement("pages").manipulate{ $0.number = book.pages }
}
let xmlData = builtXML.document().data(.indent,.omitXMLDeclaration)
print ( String(data: xmlData!, encoding: .utf8)! )
XMLTools for Swift
XMLTools
is a set APIs to parse, evaluate, manipulate and serialize complex XML structures. It is written written entirely in Swift programming language and designed to work on all platforms supporting Swift (e.g. macOS, iOS).XMLTOOLS
provides the following features:Motivation
Since Apple only provides the low-level XMLParser on all it’s Platforms (with exception of macOS, which has high level XML API), there are a lot of Open-Source Projects providing such APIs, most notably SWXMLHash and SwiftyXMLParser.
The problem with all projects I’ve found on GitHUB is that they only support the simplest XML structures and queries. Most of them take inspiration from SwiftyJSON and handle XML as JSON. There are two issues with that approach: 1) most of legacy XML Systems use rather complex XML structures with heavy use of namespaces; 2) if someone creates the new and simple protocols they use JSON anyway.
XMLTools
tries to close this gap and provides the “old school XML” using modern features of Swift programming language.Quick Start
Integration
Swift Package Manager
TODO
XPath-Like Selection API
The following Example XML is based on w3schools.com XPath Tutorial
Parse String to Infoset
XPath equivalents for Swift XMLTools API
bookstore
xml["bookstore"]
xml.select("bookstore")
/bookstore
xml.selectDocument()["bookstore"]
xml.selectDocument().select("bookstore")
bookstore/book
xml["bookstore", "book"]
xml["bookstore"]["book"]
xml.select("bookstore", "book")
xml.select("bookstore").select("book")
//book
xml.descendants("book")
//@lang
xml.descendants().attr("lang")
/bookstore/book[1]
xml["bookstore", "book", 0]
xml["bookstore", "book"].item(0)
note the 0-based index in Swift
/bookstore/book[last()]
xml["bookstore", "book"].last()
/bookstore/book[position()<3]
xml["bookstore", "book"].select(byPosition: { $0 < 2 })
//title[@lang]
xml.descendants("title").select({ $0.attr("lang").text != "" })
//title[@lang='en']
xml.descendants("title").select({ $0.attr("lang").text == "en" })
/bookstore/book[pages>300]
xml["bookstore", "book"].select({ $0["pages"].number > 300 })
/bookstore/book[price>35.00]
xml["bookstore", "book"].select({ $0["price"].number > 35 })
/bookstore/book[price>40.00]/title
xml["bookstore", "book"].select({ $0["price"].number > 40 }).select("title")
*
xml.select()
/bookstore/book/title/@*
xml["bookstore", "book", "title"].attr()
/bookstore/book/title[0]/node()
xml["bookstore", "book", "title", 0].selectNode()
/bookstore/*
xml["bookstore"].select()
//*
xml.descendants()
count(//book)
xml.descendants("book").count
bookstore/book[starts-with(title,'Harry Potter')]
xml["bookstore", "book"].select({ $0["title"].text.starts(with: "Harry Potter") })
Using namespaces
Consider the example from Wikipedia article about WSDL
Since we didn’t specify any options when creating the “XMLTools.Parser” there are no namespace declarations in the current Infoset and every element must be accessed by using the qualified names:
Event if we make it shorter it’s still not very easy to read:
The better way is to declare the namespace. Please note, that even if the source XML has no prefix defined we still should access the elements and attributes by using the prefix defined here. This way the code is independent of the source namespace prefixes, especially when sources are generated and use cryptic prefixes like
ns0
:If we want to access WSDL elements without the prefix we can do it this way:
Here is a more complex example demonstrating the extensibility of
XMLTools
API:And finally we can just be lazy and tell the parser to preserve all namespace declarations exactly as they appear in the XML source
Serializing XML
Creating XML from scratch
Should produce the following output:
Developing XMLTools
XMLTools
uses the Swift package managerCreate release