# SWXMLHash [![Swift](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdrmohundro%2FSWXMLHash%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/drmohundro/SWXMLHash) [![Platforms](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdrmohundro%2FSWXMLHash%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/drmohundro/SWXMLHash) [![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square) [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/SWXMLHash)](https://cocoapods.org/pods/SWXMLHash) [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) SWXMLHash is a relatively simple way to parse XML in Swift. If you're familiar with `XMLParser` (formerly `NSXMLParser`), this library is a wrapper around it. Conceptually, it provides a translation from XML to a dictionary of arrays (aka hash). The API takes a lot of inspiration from [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON). ## Contents - [Requirements](#requirements) - [Installation](#installation) - [Getting Started](#getting-started) - [Configuration](#configuration) - [Examples](#examples) - [FAQ](#faq) - [Changelog](#changelog) - [Contributing](#contributing) - [License](#license) ## Requirements - iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ - Xcode 16.0+ / Swift 6.0+ ## Installation SWXMLHash can be installed using [Swift Package Manager](https://swift.org/package-manager/), [CocoaPods](http://cocoapods.org/), [Carthage](https://github.com/Carthage/Carthage), or manually. ### Swift Package Manager The [Swift Package Manager](https://swift.org/package-manager/) is a tool built by Apple as part of [the Swift project](https://swift.org/) for integrating libraries and frameworks into your Swift apps. To add SWXMLHash as a dependency, update the `dependencies` in your `Package.swift` to include a reference like so: ```swift dependencies: [ .package(url: "https://github.com/drmohundro/SWXMLHash.git", from: "7.0.0") ] ``` `swift build` should then pull in and compile SWXMLHash to begin using. ### CocoaPods To install CocoaPods, run: ```bash gem install cocoapods ``` Then create a `Podfile` with the following contents: ```ruby platform :ios, '10.0' use_frameworks! target 'YOUR_TARGET_NAME' do pod 'SWXMLHash', '~> 7.0.0' end ``` Finally, run the following command to install it: ```bash pod install ``` ### Carthage To install Carthage, run (using Homebrew): ```bash brew update brew install carthage ``` Then add the following line to your `Cartfile`: ``` github "drmohundro/SWXMLHash" ~> 7.0 ``` ### Manual Installation To install manually, you'll need to clone the SWXMLHash repository. You can do this in a separate directory, or you can make use of git submodules - in this case, git submodules are recommended so that your repository has details about which commit of SWXMLHash you're using. Once this is done, you can just drop all of the relevant swift files into your project. If you're using a workspace, though, you can include the entire `SWXMLHash.xcodeproj`. ## Getting Started If you're just getting started with SWXMLHash, I'd recommend cloning the repository down and opening the workspace. I've included a Swift playground in the workspace which makes it easy to experiment with the API and the calls. ![Swift Playground](https://raw.githubusercontent.com/drmohundro/SWXMLHash/assets/swift-playground@2x.png) ## Configuration SWXMLHash allows for limited configuration in terms of its approach to parsing. To set any of the configuration options, you use the `configure` method, like so: ```swift let xml = XMLHash.config { config in // set any config options here }.parse(xmlToParse) ``` The available options at this time are: - `shouldProcessLazily` - This determines whether not to use lazy loading of the XML. It can significantly increase the performance of parsing if your XML is large. - Defaults to `false` - `shouldProcessNamespaces` - This setting is forwarded on to the internal `NSXMLParser` instance. It will return any XML elements without their namespace parts (i.e. "\" will be returned as "\") - Defaults to `false` - `caseInsensitive` - This setting allows for key lookups to be case-insensitive. Typically, XML is a case-sensitive language, but this option lets you bypass this if necessary. - Defaults to `false` - `encoding` - This setting allows for explicitly specifying the character encoding when an XML string is passed to `parse`. - Defaults to `String.encoding.utf8` - `userInfo` - This setting mimics `Codable`'s `userInfo` property to allow the user to add contextual information that will be used for deserialization. - See [Codable's userInfo docs](https://developer.apple.com/documentation/swift/encoder/2894907-userinfo) - The default is [:] - `detectParsingErrors` - This setting attempts to detect XML parsing errors. `parse` will return an `XMLIndexer.parsingError` if any parsing issues are found. - Defaults to `false` (because of backwards compatibility and because many users attempt to parse HTML with this library) ## Examples All examples below can be found in the included [specs](https://github.com/drmohundro/SWXMLHash/blob/main/Tests/). ### Initialization ```swift let xml = XMLHash.parse(xmlToParse) ``` Alternatively, if you're parsing a large XML file and need the best performance, you may wish to configure the parsing to be processed lazily. Lazy processing avoids loading the entire XML document into memory, so it could be preferable for performance reasons. See the error handling for one caveat regarding lazy loading. ```swift let xml = XMLHash.config { config in config.shouldProcessLazily = true }.parse(xmlToParse) ``` The above approach uses the config method, but there is also a `lazy` method directly off of `XMLHash`. ```swift let xml = XMLHash.lazy(xmlToParse) ``` ### Single Element Lookup Given: ```xml
Foo
...
``` Will return "Foo". ```swift xml["root"]["header"]["title"].element?.text ``` ### Multiple Elements Lookup Given: ```xml ... Bob John Mark ... ``` The below will return "John". ```swift xml["root"]["catalog"]["book"][1]["author"].element?.text ``` ### Attributes Usage Given: ```xml ... Bob John Mark ... ``` The below will return "123". ```swift xml["root"]["catalog"]["book"][1].element?.attribute(by: "id")?.text ``` Alternatively, you can look up an element with specific attributes. The below will return "John". ```swift xml["root"]["catalog"]["book"].withAttribute("id", "123")["author"].element?.text ``` ### Returning All Elements At Current Level Given: ```xml ... Fiction Non-fiction Technical ... ``` The `all` method will iterate over all nodes at the indexed level. The code below will return "Fiction, Non-fiction, Technical". ```swift ", ".join(xml["root"]["catalog"]["book"].all.map { elem in elem["genre"].element!.text! }) ``` You can also iterate over the `all` method: ```swift for elem in xml["root"]["catalog"]["book"].all { print(elem["genre"].element!.text!) } ``` ### Returning All Child Elements At Current Level Given: ```xml Fiction Book 1/1/2015 ``` The below will `print` "root", "catalog", "book", "genre", "title", and "date" (note the `children` method). ```swift func enumerate(indexer: XMLIndexer) { for child in indexer.children { print(child.element!.name) enumerate(child) } } enumerate(indexer: xml) ``` ### Filtering elements Given: ```xml Gambardella, Matthew XML Developer's Guide Computer44.95 2000-10-01 Ralls, Kim Midnight Rain Fantasy 5.95 2000-12-16 Corets, Eva Maeve Ascendant Fantasy 5.95 2000-11-17 ``` The following will return "Midnight Rain". Filtering can be by any part of the `XMLElement` class or by index as well. ```swift let subIndexer = xml!["root"]["catalog"]["book"] .filterAll { elem, _ in elem.attribute(by: "id")!.text == "bk102" } .filterChildren { _, index in index >= 1 && index <= 3 } print(subIndexer.children[0].element?.text) ``` ### Error Handling Using Do-Catch with Errors: ```swift do { try xml!.byKey("root").byKey("what").byKey("header").byKey("foo") } catch let error as IndexingError { // error is an IndexingError instance that you can deal with } ``` **Or** using the existing indexing functionality: ```swift switch xml["root"]["what"]["header"]["foo"] { case .element(let elem): // everything is good, code away! case .xmlError(let error): // error is an IndexingError instance that you can deal with } ``` Note that error handling as shown above will not work with lazy loaded XML. The lazy parsing doesn't actually occur until the `element` or `all` method are called - as a result, there isn't any way to know prior to asking for an element if it exists or not. ### XML Deserialization Into Objects Even more often, you'll want to deserialize an XML tree into an array of custom types. This is where `XMLObjectDeserialization` comes into play. Given: ```xml Book A 12.5 2015 C1 C2 Book B 10 1988 C2 C3 Book C 8.33 1990 10 C1 C3 ``` with `Book` struct implementing `XMLObjectDeserialization`: ```swift struct Book: XMLObjectDeserialization { let title: String let price: Double let year: Int let amount: Int? let isbn: Int let category: [String] static func deserialize(_ node: XMLIndexer) throws -> Book { return try Book( title: node["title"].value(), price: node["price"].value(), year: node["year"].value(), amount: node["amount"].value(), isbn: node.value(ofAttribute: "isbn"), category : node["categories"]["category"].value() ) } } ``` The below will return an array of `Book` structs: ```swift let books: [Book] = try xml["root"]["books"]["book"].value() ``` ![Types Conversion](https://raw.githubusercontent.com/ncreated/SWXMLHash/assets/types-conversion%402x.png) You can convert any XML to your custom type by implementing `XMLObjectDeserialization` for any non-leaf node (e.g. `` in the example above). For leaf nodes (e.g. `` in the example above), built-in converters support `Int`, `Double`, `Float`, `Bool`, and `String` values (both non- and -optional variants). Custom converters can be added by implementing `XMLElementDeserializable`. For attributes (e.g. `isbn=` in the example above), built-in converters support the same types as above, and additional converters can be added by implementing `XMLAttributeDeserializable`. Types conversion supports error handling, optionals and arrays. For more examples, look into `SWXMLHashTests.swift` or play with types conversion directly in the Swift playground. ### Custom Value Conversion Value deserialization is where a specific string value needs to be deserialized into a custom type. So, date is a good example here - you'd rather deal with date types than doing string parsing, right? That's what the `XMLValueDeserialization` attribute is for. Given: ```xml <root> <elem>Monday, 23 January 2016 12:01:12 111</elem> </root> ``` With the following implementation for `Date` value deserialization: ```swift extension Date: XMLValueDeserialization { public static func deserialize(_ element: XMLHash.XMLElement) throws -> Date { let date = stringToDate(element.text) guard let validDate = date else { throw XMLDeserializationError.typeConversionFailed(type: "Date", element: element) } return validDate } public static func deserialize(_ attribute: XMLAttribute) throws -> Date { let date = stringToDate(attribute.text) guard let validDate = date else { throw XMLDeserializationError.attributeDeserializationFailed(type: "Date", attribute: attribute) } return validDate } public func validate() throws { // empty validate... only necessary for custom validation logic after parsing } private static func stringToDate(_ dateAsString: String) -> Date? { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "EEEE, dd MMMM yyyy HH:mm:ss SSS" return dateFormatter.date(from: dateAsString) } } ``` The below will return a date value: ```swift let dt: Date = try xml["root"]["elem"].value() ``` ## FAQ ### Does SWXMLHash handle URLs for me? No - SWXMLHash only handles parsing of XML. If you have a URL that has XML content on it, I'd recommend using a library like [AlamoFire](https://github.com/Alamofire/Alamofire) to download the content into a string and then parsing it. ### Does SWXMLHash support writing XML content? No, not at the moment - SWXMLHash only supports parsing XML (via indexing, deserialization, etc.). ### I'm getting an "Ambiguous reference to member 'subscript'" when I call `.value()` `.value()` is used for deserialization - you have to have something that implements `XMLObjectDeserialization` (or `XMLElementDeserializable` if it is a single element versus a group of elements) and that can handle deserialization to the left-hand side of expression. For example, given the following: ```swift let dateValue: Date = try! xml["root"]["date"].value() ``` You'll get an error because there isn't any built-in deserializer for `Date`. See the above documentation on adding your own deserialization support. In this case, you would create your own `XMLElementDeserializable` implementation for `Date`. See above for an example of how to add your own `Date` deserialization support. ### I'm getting an `EXC_BAD_ACCESS (SIGSEGV)` when I call `parse()` Chances are very good that your XML content has what is called a "byte order mark" or BOM. SWXMLHash uses `NSXMLParser` for its parsing logic and there are issues with it and handling BOM characters. See [issue #65](https://github.com/drmohundro/SWXMLHash/issues/65) for more details. Others who have run into this problem have just stripped the BOM out of their content prior to parsing. ### How do I handle deserialization with a class versus a struct (such as with `NSDate`)? Using extensions on classes instead of structs can result in some odd catches that might give you a little trouble. For example, see [this question on StackOverflow](http://stackoverflow.com/questions/38174669/how-to-deserialize-nsdate-with-swxmlhash) where someone was trying to write their own `XMLElementDeserializable` for `NSDate` which is a class and not a struct. The `XMLElementDeserializable` protocol expects a method that returns `Self` - this is the part that gets a little odd. See below for the code snippet to get this to work and note in particular the `private static func value<T>() -> T` line - that is the key. ```swift extension NSDate: XMLElementDeserializable { public static func deserialize(_ element: XMLElement) throws -> Self { guard let dateAsString = element.text else { throw XMLDeserializationError.nodeHasNoValue } let dateFormatter = NSDateFormatter() dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" let date = dateFormatter.dateFromString(dateAsString) guard let validDate = date else { throw XMLDeserializationError.typeConversionFailed(type: "Date", element: element) } // NOTE THIS return value(validDate) } // AND THIS private static func value<T>(date: NSDate) -> T { return date as! T } } ``` ### How do I handle deserialization with an enum? Check out this great suggestion/example from @woolie up at <https://github.com/drmohundro/SWXMLHash/discussions/245>. ### I'm seeing an ""'XMLElement' is ambiguous" Error This is related to <https://github.com/drmohundro/SWXMLHash/issues/256> - `XMLElement` has actually been renamed multiple times to attempt to avoid conflicts, but the easiest approach is to just scope it via `XMLHash.XMLElement`. ### Will SWXMLHash work in a web context (e.g. Vapor)? See <https://github.com/drmohundro/SWXMLHash/discussions/264> where this is discussed. The only change needed is to add the following import logic: ```swift #if canImport(FoundationNetworking) import FoundationNetworking #endif ``` ### Have a different question? Feel free to shoot me an email, post a [question on StackOverflow](http://stackoverflow.com/questions/tagged/swxmlhash), or open an issue if you think you've found a bug. I'm happy to try to help! Another alternative is to post a question in the [Discussions](https://github.com/drmohundro/SWXMLHash/discussions). ## Changelog See [CHANGELOG](CHANGELOG.md) for a list of all changes and their corresponding versions. ## Contributing See [CONTRIBUTING](CONTRIBUTING.md) for guidelines to contribute back to SWXMLHash. ## License SWXMLHash is released under the MIT license. See [LICENSE](LICENSE) for details.