XMLTools

master

Swift XML API with XPath-Like Syntax and Namespaces support
spilikin/SwiftXMLTools

XMLTools for Swift

Swift 5.1 license

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

Integration

Swift Package Manager

TODO

XPath-Like Selection API

The following Example XML is based on w3schools.com XPath Tutorial

<?xml version="1.0" encoding="UTF-8"?>

<bookstore>

<book>
<title lang="en">Harry Potter: The Philosopher's Stone</title>
<price>24.99</price>
<pages>223</pages>
</book>

<book>
<title lang="en">Harry Potter: The Chamber of Secrets</title>
<price>29.99</price>
<pages>251</pages>
</book>

<book>
<title lang="en">Learning XML</title>
<price>39.95</price>
<pages>432</pages>
</book>

<book>
<title lang="de">IT-Sicherheit: Konzepte - Verfahren - Protokolle</title>
<price>69.95</price>
<pages>932</pages>
</book>

</bookstore>

Parse String to Infoset

let xmlString = """
<?xml version="1.0" encoding="UTF-8"?>

<bookstore>

<book>
<title lang="en">Harry Potter: The Philosopher's Stone</title>
<price>24.99</price>
<pages>223</pages>
</book>

<book>
<title lang="en">Harry Potter: The Chamber of Secrets</title>
<price>29.99</price>
<pages>251</pages>
</book>

<book>
<title lang="en">Learning XML</title>
<price>39.95</price>
<pages>432</pages>
</book>

<book>
<title lang="de">IT-Sicherheit: Konzepte - Verfahren - Protokolle</title>
<price>69.95</price>
<pages>932</pages>
</book>

</bookstore>
"""
let parser = XMLTools.Parser()

let xml: XMLTools.Infoset
do {
    xml = try parser.parse(string: xmlString, using: .utf8)
} catch {
    print (error)
    return
}

XPath equivalents for Swift XMLTools API

Xpath Swift
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

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:

print (xml[QName("description", uri: "http://www.w3.org/ns/wsdl"), QName("documentation", uri: "http://www.w3.org/ns/wsdl")].text)

Event if we make it shorter it's still not very easy to read:

let wsdlURI = "http://www.w3.org/ns/wsdl"
print (xml[QName("description", uri: wsdlURI), QName("documentation", uri: wsdlURI)].text)

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)! )

Should produce the following output:

<bookstore>
    <book>
        <title lang="en">Harry Potter: The Philosopher's Stone</title>
        <price>24.99</price>
        <pages>223</pages>
    </book>
    <book>
        <title lang="en">Harry Potter: The Chamber of Secrets</title>
        <price>29.99</price>
        <pages>251</pages>
    </book>
    <book>
        <title lang="en">Learning XML</title>
        <price>39.95</price>
        <pages>432</pages>
    </book>
    <book>
        <title lang="de">IT-Sicherheit: Konzepte - Verfahren - Protokolle</title>
        <price>69.95</price>
        <pages>932</pages>
    </book>
</bookstore>

Developing XMLTools

XMLTools uses the Swift package manager

cd SwiftXMLTools
swift package generate-xcodeproj
swift build
swift test

Create release

git tag <VERSION>
git push origin <VERSION>

Description

  • Swift Tools 5.0.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sun Apr 14 2024 01:21:18 GMT-0900 (Hawaii-Aleutian Daylight Time)