Commit 479ba0f2 by Demid Merzlyakov

Add XMLCoder pod.

parent 6ea6fb52
......@@ -9,4 +9,5 @@ target '1Weather' do
pod 'SnapKit'
pod 'BezierKit'
pod 'Localize-Swift'
pod 'XMLCoder', '~> 0.12.0'
end
......@@ -7,23 +7,27 @@ PODS:
- Localize-Swift/UIKit (3.2.0):
- Localize-Swift/LocalizeSwiftCore
- SnapKit (5.0.1)
- XMLCoder (0.12.0)
DEPENDENCIES:
- BezierKit
- Localize-Swift
- SnapKit
- XMLCoder (~> 0.12.0)
SPEC REPOS:
trunk:
- BezierKit
- Localize-Swift
- SnapKit
- XMLCoder
SPEC CHECKSUMS:
BezierKit: 1828aca1675d68f0659c5353bc5b0d3399a3910c
Localize-Swift: 6f4475136bdb0d7b2882ea3d4ea919d70142b232
SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
XMLCoder: f884dfa894a6f8b7dce465e4f6c02963bf17e028
PODFILE CHECKSUM: 06893793de13524c28b9afdc6dca649b77af3601
PODFILE CHECKSUM: 1f2e35e8ddbf05711e7cbbc0504177213177b924
COCOAPODS: 1.10.1
......@@ -7,23 +7,27 @@ PODS:
- Localize-Swift/UIKit (3.2.0):
- Localize-Swift/LocalizeSwiftCore
- SnapKit (5.0.1)
- XMLCoder (0.12.0)
DEPENDENCIES:
- BezierKit
- Localize-Swift
- SnapKit
- XMLCoder (~> 0.12.0)
SPEC REPOS:
trunk:
- BezierKit
- Localize-Swift
- SnapKit
- XMLCoder
SPEC CHECKSUMS:
BezierKit: 1828aca1675d68f0659c5353bc5b0d3399a3910c
Localize-Swift: 6f4475136bdb0d7b2882ea3d4ea919d70142b232
SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
XMLCoder: f884dfa894a6f8b7dce465e4f6c02963bf17e028
PODFILE CHECKSUM: 06893793de13524c28b9afdc6dca649b77af3601
PODFILE CHECKSUM: 1f2e35e8ddbf05711e7cbbc0504177213177b924
COCOAPODS: 1.10.1
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -70,4 +70,29 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
## XMLCoder
MIT License
Copyright (c) 2018-2019 Shawn Moore and XMLCoder contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Generated by CocoaPods - https://cocoapods.org
......@@ -102,6 +102,37 @@ THE SOFTWARE.
</dict>
<dict>
<key>FooterText</key>
<string>MIT License
Copyright (c) 2018-2019 Shawn Moore and XMLCoder contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</string>
<key>License</key>
<string>MIT</string>
<key>Title</key>
<string>XMLCoder</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Generated by CocoaPods - https://cocoapods.org</string>
<key>Title</key>
<string></string>
......
......@@ -2,3 +2,4 @@ ${PODS_ROOT}/Target Support Files/Pods-1Weather/Pods-1Weather-frameworks.sh
${BUILT_PRODUCTS_DIR}/BezierKit/BezierKit.framework
${BUILT_PRODUCTS_DIR}/Localize-Swift/Localize_Swift.framework
${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework
${BUILT_PRODUCTS_DIR}/XMLCoder/XMLCoder.framework
\ No newline at end of file
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BezierKit.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Localize_Swift.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/XMLCoder.framework
\ No newline at end of file
......@@ -2,3 +2,4 @@ ${PODS_ROOT}/Target Support Files/Pods-1Weather/Pods-1Weather-frameworks.sh
${BUILT_PRODUCTS_DIR}/BezierKit/BezierKit.framework
${BUILT_PRODUCTS_DIR}/Localize-Swift/Localize_Swift.framework
${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework
${BUILT_PRODUCTS_DIR}/XMLCoder/XMLCoder.framework
\ No newline at end of file
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BezierKit.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Localize_Swift.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/XMLCoder.framework
\ No newline at end of file
......@@ -178,11 +178,13 @@ if [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/BezierKit/BezierKit.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Localize-Swift/Localize_Swift.framework"
install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
install_framework "${BUILT_PRODUCTS_DIR}/XMLCoder/XMLCoder.framework"
fi
if [[ "$CONFIGURATION" == "Release" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/BezierKit/BezierKit.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Localize-Swift/Localize_Swift.framework"
install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
install_framework "${BUILT_PRODUCTS_DIR}/XMLCoder/XMLCoder.framework"
fi
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
wait
......
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/BezierKit" "${PODS_CONFIGURATION_BUILD_DIR}/Localize-Swift" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit"
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/BezierKit" "${PODS_CONFIGURATION_BUILD_DIR}/Localize-Swift" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/XMLCoder"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/BezierKit/BezierKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Localize-Swift/Localize_Swift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers"
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/BezierKit/BezierKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Localize-Swift/Localize_Swift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/XMLCoder/XMLCoder.framework/Headers"
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
OTHER_LDFLAGS = $(inherited) -framework "BezierKit" -framework "CoreGraphics" -framework "Localize_Swift" -framework "SnapKit" -framework "UIKit"
OTHER_LDFLAGS = $(inherited) -framework "BezierKit" -framework "CoreGraphics" -framework "Localize_Swift" -framework "SnapKit" -framework "UIKit" -framework "XMLCoder"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
......
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/BezierKit" "${PODS_CONFIGURATION_BUILD_DIR}/Localize-Swift" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit"
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/BezierKit" "${PODS_CONFIGURATION_BUILD_DIR}/Localize-Swift" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/XMLCoder"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/BezierKit/BezierKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Localize-Swift/Localize_Swift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers"
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/BezierKit/BezierKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Localize-Swift/Localize_Swift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/XMLCoder/XMLCoder.framework/Headers"
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
OTHER_LDFLAGS = $(inherited) -framework "BezierKit" -framework "CoreGraphics" -framework "Localize_Swift" -framework "SnapKit" -framework "UIKit"
OTHER_LDFLAGS = $(inherited) -framework "BezierKit" -framework "CoreGraphics" -framework "Localize_Swift" -framework "SnapKit" -framework "UIKit" -framework "XMLCoder"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>0.12.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${CURRENT_PROJECT_VERSION}</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
#import <Foundation/Foundation.h>
@interface PodsDummy_XMLCoder : NSObject
@end
@implementation PodsDummy_XMLCoder
@end
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
FOUNDATION_EXPORT double XMLCoderVersionNumber;
FOUNDATION_EXPORT const unsigned char XMLCoderVersionString[];
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/XMLCoder
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/XMLCoder
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
framework module XMLCoder {
umbrella header "XMLCoder-umbrella.h"
export *
module * { export * }
}
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/XMLCoder
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/XMLCoder
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
MIT License
Copyright (c) 2018-2019 Shawn Moore and XMLCoder contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# XMLCoder
Encoder &amp; Decoder for XML using Swift's `Codable` protocols.
[![Build Status](https://dev.azure.com/max0484/max/_apis/build/status/MaxDesiatov.XMLCoder?branchName=master)](https://dev.azure.com/max0484/max/_build/latest?definitionId=4&branchName=master)
[![Version](https://img.shields.io/cocoapods/v/XMLCoder.svg?style=flat)](https://cocoapods.org/pods/XMLCoder)
[![License](https://img.shields.io/cocoapods/l/XMLCoder.svg?style=flat)](https://cocoapods.org/pods/XMLCoder)
[![Platform](https://img.shields.io/badge/platform-watchos%20%7C%20ios%20%7C%20tvos%20%7C%20macos%20%7C%20linux-lightgrey.svg?style=flat)](https://cocoapods.org/pods/XMLCoder)
[![Coverage](https://img.shields.io/codecov/c/github/MaxDesiatov/XMLCoder/master.svg?style=flat)](https://codecov.io/gh/maxdesiatov/XMLCoder)
This package is a fork of the original
[ShawnMoore/XMLParsing](https://github.com/ShawnMoore/XMLParsing)
with more features and improved test coverage. Automatically generated documentation is available on [our GitHub Pages](https://maxdesiatov.github.io/XMLCoder/).
## Example
```swift
import XMLCoder
let xmlStr = """
<note>
<to>Bob</to>
<from>Jane</from>
<heading>Reminder</heading>
<body>Don't forget to use XMLCoder!</body>
</note>
"""
struct Note: Codable {
let to: String
let from: String
let heading: String
let body: String
}
guard let data = xmlStr.data(using: .utf8) else { return }
let note = try? XMLDecoder().decode(Note.self, from: data)
let returnData = try? XMLEncoder().encode(note, withRootKey: "note")
```
## Advanced features
The following features are available in [0.4.0
release](https://github.com/MaxDesiatov/XMLCoder/releases/tag/0.4.0) or later
(unless stated otherwise):
### Stripping namespace prefix
Sometimes you need to handle an XML namespace prefix, like in the XML below:
```xml
<h:table xmlns:h="http://www.w3.org/TR/html4/">
<h:tr>
<h:td>Apples</h:td>
<h:td>Bananas</h:td>
</h:tr>
</h:table>
```
Stripping the prefix from element names is enabled with
`shouldProcessNamespaces` property:
```swift
struct Table: Codable, Equatable {
struct TR: Codable, Equatable {
let td: [String]
}
let tr: [TR]
}
let decoder = XMLDecoder()
// Setting this property to `true` for the namespace prefix to be stripped
// during decoding so that key names could match.
decoder.shouldProcessNamespaces = true
let decoded = try decoder.decode(Table.self, from: xmlData)
```
### Dynamic node coding
XMLCoder provides two helper protocols that allow you to customize whether nodes
are encoded and decoded as attributes or elements: `DynamicNodeEncoding` and
`DynamicNodeDecoding`.
The declarations of the protocols are very simple:
```swift
protocol DynamicNodeEncoding: Encodable {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding
}
protocol DynamicNodeDecoding: Decodable {
static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
}
```
The values returned by corresponding `static` functions look like this:
```swift
enum NodeDecoding {
// decodes a value from an attribute
case attribute
// decodes a value from an element
case element
// the default, attempts to decode as an element first,
// otherwise reads from an attribute
case elementOrAttribute
}
enum NodeEncoding {
// encodes a value in an attribute
case attribute
// the default, encodes a value in an element
case element
// encodes a value in both attribute and element
case both
}
```
Add conformance to an appropriate protocol for types you'd like to customize.
Accordingly, this example code:
```swift
struct Book: Codable, Equatable, DynamicNodeEncoding {
let id: UInt
let title: String
let categories: [Category]
enum CodingKeys: String, CodingKey {
case id
case title
case categories = "category"
}
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case Book.CodingKeys.id: return .both
default: return .element
}
}
}
```
works for this XML:
```xml
<book id="123">
<id>123</id>
<title>Cat in the Hat</title>
<category>Kids</category>
<category>Wildlife</category>
</book>
```
Please refer to PR [\#70](https://github.com/MaxDesiatov/XMLCoder/pull/70) by
[@JoeMatt](https://github.com/JoeMatt) for more details.
### Coding key value intrinsic
Suppose that you need to decode an XML that looks similar to this:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<foo id="123">456</foo>
```
By default you'd be able to decode `foo` as an element, but then it's not
possible to decode the `id` attribute. `XMLCoder` handles certain `CodingKey`
values in a special way to allow proper coding for this XML. Just add a coding
key with `stringValue` that equals `""` (empty string). What
follows is an example type declaration that encodes the XML above, but special
handling of coding keys with those values works for both encoding and decoding.
```swift
struct Foo: Codable, DynamicNodeEncoding {
let id: String
let value: String
enum CodingKeys: String, CodingKey {
case id
case value = ""
}
static func nodeEncoding(forKey key: CodingKey)
-> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.id:
return .attribute
default:
return .element
}
}
}
```
Thanks to [@JoeMatt](https://github.com/JoeMatt) for implementing this in
in PR [\#73](https://github.com/MaxDesiatov/XMLCoder/pull/73).
### Preserving whitespaces in element content
By default whitespaces are trimmed in element content during decoding. This
includes string values decoded with [value intrinsic keys](#coding-key-value-intrinsic).
Starting with [version 0.5](https://github.com/MaxDesiatov/XMLCoder/releases/tag/0.5.0)
you can now set a property `trimValueWhitespaces` to `false` (the default value is `true`) on
`XMLDecoder` instance to preserve all whitespaces in decoded strings.
### Choice element coding
Starting with [version 0.8](https://github.com/MaxDesiatov/XMLCoder/releases/tag/0.8.0),
you can encode and decode `enum`s with associated values by conforming your
`CodingKey` type additionally to `XMLChoiceCodingKey`. This allows decoding
XML elements similar in structure to this example:
```xml
<container>
<int>1</int>
<string>two</string>
<string>three</string>
<int>4</int>
<int>5</int>
</container>
```
To decode these elements you can use this type:
```swift
enum IntOrString: Equatable {
case int(Int)
case string(String)
}
extension IntOrString: Codable {
enum CodingKeys: String, XMLChoiceCodingKey {
case int
case string
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .int(value):
try container.encode(value, forKey: .int)
case let .string(value):
try container.encode(value, forKey: .string)
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self = .int(try container.decode(Int.self, forKey: .int))
} catch {
self = .string(try container.decode(String.self, forKey: .string))
}
}
}
```
This is described in more details in PR [\#119](https://github.com/MaxDesiatov/XMLCoder/pull/119)
by [@jsbean](https://github.com/jsbean) and [@bwetherfield](https://github.com/bwetherfield).
### Integrating with [Combine](https://developer.apple.com/documentation/combine)
Starting with XMLCoder [version 0.9](https://github.com/MaxDesiatov/XMLCoder/releases/tag/0.9.0),
when Apple's Combine framework is available, `XMLDecoder` conforms to the
`TopLevelDecoder` protocol, which allows it to be used with the
`decode(type:decoder:)` operator:
```swift
import Combine
import Foundation
import XMLCoder
func fetchBook(from url: URL) -> AnyPublisher<Book, Error> {
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Book.self, decoder: XMLDecoder())
.eraseToAnyPublisher()
}
```
This was implemented in PR [\#132](https://github.com/MaxDesiatov/XMLCoder/pull/132)
by [@sharplet](https://github.com/sharplet).
Additionally, starting with [XMLCoder
0.11](https://github.com/MaxDesiatov/XMLCoder/releases/tag/0.11.0) `XMLEncoder`
conforms to the `TopLevelEncoder` protocol:
```swift
import Combine
import XMLCoder
func encode(book: Book) -> AnyPublisher<Data, Error> {
return Just(book)
.encode(encoder: XMLEncoder())
.eraseToAnyPublisher()
}
```
The resulting XML in the example above will start with `<book`, to customize
capitalization of the root element (e.g. `<Book`) you'll need to set an
appropriate `keyEncoding` strategy on the encoder. To change the element name
altogether you'll have to change the name of the type, which is an unfortunate
limitation of the `TopLevelEncoder` API.
### Root element attributes
Sometimes you need to set attributes on the root element, which aren't
directly related to your model type. Starting with [XMLCoder
0.11](https://github.com/MaxDesiatov/XMLCoder/releases/tag/0.11.0) the `encode`
function on `XMLEncoder` accepts a new `rootAttributes` argument to help with
this:
```swift
struct Policy: Encodable {
var name: String
}
let encoder = XMLEncoder()
let data = try encoder.encode(Policy(name: "test"), rootAttributes: [
"xmlns": "http://www.nrf-arts.org/IXRetail/namespace",
"xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
"xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
])
```
The resulting XML will look like this:
```xml
<policy xmlns="http://www.nrf-arts.org/IXRetail/namespace"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<name>test</name>
</policy>
```
This was implemented in PR [\#160](https://github.com/MaxDesiatov/XMLCoder/pull/160)
by [@portellaa](https://github.com/portellaa).
## Installation
### Requirements
**Apple Platforms**
- Xcode 10.0 or later
- **IMPORTANT**: compiling XMLCoder with Xcode 11.2.0 (11B52) and 11.2.1 (11B500) is not recommended due to crashes with `EXC_BAD_ACCESS` caused by [a compiler bug](https://bugs.swift.org/browse/SR-11564), please use Xcode 11.3 or later instead. Please refer to [\#150](https://github.com/MaxDesiatov/XMLCoder/issues/150) for more details.
- Swift 4.2 or later
- iOS 9.0 / watchOS 2.0 / tvOS 9.0 / macOS 10.10 or later deployment targets
**Linux**
- Ubuntu 14.04 or later
- Swift 5.0.1 or later
### Swift Package Manager
[Swift Package Manager](https://swift.org/package-manager/) is a tool for
managing the distribution of Swift code. It’s integrated with the Swift build
system to automate the process of downloading, compiling, and linking
dependencies on all platforms.
Once you have your Swift package set up, adding `XMLCoder` as a dependency is as
easy as adding it to the `dependencies` value of your `Package.swift`.
```swift
dependencies: [
.package(url: "https://github.com/MaxDesiatov/XMLCoder.git", from: "0.12.0")
]
```
If you're using XMLCoder in an app built with Xcode, you can also add it as a direct
dependency [using Xcode's
GUI](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app).
### CocoaPods
[CocoaPods](https://cocoapods.org) is a dependency manager for Swift and Objective-C
Cocoa projects for Apple's platfoms. You can install it with the following command:
```bash
$ gem install cocoapods
```
Navigate to the project directory and create `Podfile` with the following command:
```bash
$ pod install
```
Inside of your `Podfile`, specify the `XMLCoder` pod:
```ruby
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'YourApp' do
# Comment the next line if you're not using Swift or don't want
# to use dynamic frameworks
use_frameworks!
# Pods for YourApp
pod 'XMLCoder', '~> 0.12.0'
end
```
Then, run the following command:
```bash
$ pod install
```
Open the the `YourApp.xcworkspace` file that was created. This should be the
file you use everyday to create your app, instead of the `YourApp.xcodeproj`
file.
### Carthage
[Carthage](https://github.com/Carthage/Carthage) is a dependency manager for Apple's
platfoms that builds your dependencies and provides you with binary frameworks.
Carthage can be installed with [Homebrew](https://brew.sh/) using the following command:
```bash
$ brew update
$ brew install carthage
```
Inside of your `Cartfile`, add GitHub path to `XMLCoder`:
```ogdl
github "MaxDesiatov/XMLCoder" ~> 0.12.0
```
Then, run the following command to build the framework:
```bash
$ carthage update
```
Drag the built framework into your Xcode project.
## Contributing
This project adheres to the [Contributor Covenant Code of
Conduct](https://github.com/MaxDesiatov/XMLCoder/blob/master/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report
unacceptable behavior to xmlcoder@desiatov.com.
### Sponsorship
If this library saved you any amount of time or money, please consider [sponsoring
the work of its maintainer](https://github.com/sponsors/MaxDesiatov). While some of the
sponsorship tiers give you priority support or even consulting time, any amount is
appreciated and helps in maintaining the project.
### Coding Style
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat)
and [SwiftLint](https://github.com/realm/SwiftLint) to
enforce formatting and coding style. We encourage you to run SwiftFormat within
a local clone of the repository in whatever way works best for you either
manually or automatically via an [Xcode
extension](https://github.com/nicklockwood/SwiftFormat#xcode-source-editor-extension),
[build phase](https://github.com/nicklockwood/SwiftFormat#xcode-build-phase) or
[git pre-commit
hook](https://github.com/nicklockwood/SwiftFormat#git-pre-commit-hook) etc.
To guarantee that these tools run before you commit your changes on macOS, you're encouraged
to run this once to set up the [pre-commit](https://pre-commit.com/) hook:
```
brew bundle # installs SwiftLint, SwiftFormat and pre-commit
pre-commit install # installs pre-commit hook to run checks before you commit
```
Refer to [the pre-commit documentation page](https://pre-commit.com/) for more details
and installation instructions for other platforms.
SwiftFormat and SwiftLint also run on CI for every PR and thus a CI build can
fail with incosistent formatting or style. We require CI builds to pass for all
PRs before merging.
### Test Coverage
Our goal is to keep XMLCoder stable and to serialize any XML correctly according
to [XML 1.0 standard](https://www.w3.org/TR/2008/REC-xml-20081126/). All of this
can be easily tested automatically and we're slowly improving [test coverage of
XMLCoder](https://codecov.io/gh/MaxDesiatov/XMLCoder) and don't expect it to
decrease. PRs that decrease the test coverage have a much lower chance of being
merged. If you add any new features, please make sure to add tests, likewise for
changes and any refactoring in existing code.
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
struct BoolBox: Equatable {
typealias Unboxed = Bool
let unboxed: Unboxed
init(_ unboxed: Unboxed) {
self.unboxed = unboxed
}
init?(xmlString: String) {
switch xmlString.lowercased() {
case "false", "0", "n", "no": self.init(false)
case "true", "1", "y", "yes": self.init(true)
case _: return nil
}
}
}
extension BoolBox: Box {
var isNull: Bool {
return false
}
/// # Lexical representation
/// Boolean has a lexical representation consisting of the following
/// legal literals {`true`, `false`, `1`, `0`}.
///
/// # Canonical representation
/// The canonical representation for boolean is the set of literals {`true`, `false`}.
///
/// ---
///
/// [Schema definition](https://www.w3.org/TR/xmlschema-2/#boolean)
var xmlString: String? {
return (unboxed) ? "true" : "false"
}
}
extension BoolBox: SimpleBox {}
extension BoolBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
protocol Box {
var isNull: Bool { get }
var xmlString: String? { get }
}
/// A box that only describes a single atomic value.
protocol SimpleBox: Box {
// A simple tagging protocol, for now.
}
protocol TypeErasedSharedBoxProtocol {
func typeErasedUnbox() -> Box
}
protocol SharedBoxProtocol: TypeErasedSharedBoxProtocol {
associatedtype B: Box
func unbox() -> B
}
extension SharedBoxProtocol {
func typeErasedUnbox() -> Box {
return unbox()
}
}
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by James Bean on 7/18/19.
//
/// A `Box` which represents an element which is known to contain an XML choice element.
struct ChoiceBox {
var key: String = ""
var element: Box = NullBox()
}
extension ChoiceBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return nil
}
}
extension ChoiceBox: SimpleBox {}
extension ChoiceBox {
init?(_ keyedBox: KeyedBox) {
guard
let firstKey = keyedBox.elements.keys.first,
let firstElement = keyedBox.elements[firstKey].first
else {
return nil
}
self.init(key: firstKey, element: firstElement)
}
init(_ singleKeyedBox: SingleKeyedBox) {
self.init(key: singleKeyedBox.key, element: singleKeyedBox.element)
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/19/18.
//
import Foundation
struct DataBox: Equatable {
enum Format: Equatable {
case base64
}
typealias Unboxed = Data
let unboxed: Unboxed
let format: Format
init(_ unboxed: Unboxed, format: Format) {
self.unboxed = unboxed
self.format = format
}
init?(base64 string: String) {
guard let data = Data(base64Encoded: string) else {
return nil
}
self.init(data, format: .base64)
}
func xmlString(format: Format) -> String {
switch format {
case .base64:
return unboxed.base64EncodedString()
}
}
}
extension DataBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return xmlString(format: format)
}
}
extension DataBox: SimpleBox {}
extension DataBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/18/18.
//
import Foundation
struct DateBox: Equatable {
enum Format: Equatable {
case secondsSince1970
case millisecondsSince1970
case iso8601
case formatter(DateFormatter)
}
typealias Unboxed = Date
let unboxed: Unboxed
let format: Format
init(_ unboxed: Unboxed, format: Format) {
self.unboxed = unboxed
self.format = format
}
init?(secondsSince1970 string: String) {
guard let seconds = TimeInterval(string) else {
return nil
}
let unboxed = Date(timeIntervalSince1970: seconds)
self.init(unboxed, format: .secondsSince1970)
}
init?(millisecondsSince1970 string: String) {
guard let milliseconds = TimeInterval(string) else {
return nil
}
let unboxed = Date(timeIntervalSince1970: milliseconds / 1000.0)
self.init(unboxed, format: .millisecondsSince1970)
}
init?(iso8601 string: String) {
if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
guard let unboxed = _iso8601Formatter.date(from: string) else {
return nil
}
self.init(unboxed, format: .iso8601)
} else {
fatalError("ISO8601DateFormatter is unavailable on this platform.")
}
}
init?(xmlString: String, formatter: DateFormatter) {
guard let date = formatter.date(from: xmlString) else {
return nil
}
self.init(date, format: .formatter(formatter))
}
func xmlString(format: Format) -> String {
switch format {
case .secondsSince1970:
let seconds = unboxed.timeIntervalSince1970
return seconds.description
case .millisecondsSince1970:
let milliseconds = unboxed.timeIntervalSince1970 * 1000.0
return milliseconds.description
case .iso8601:
if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
return _iso8601Formatter.string(from: self.unboxed)
} else {
fatalError("ISO8601DateFormatter is unavailable on this platform.")
}
case let .formatter(formatter):
return formatter.string(from: unboxed)
}
}
}
extension DateBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return xmlString(format: format)
}
}
extension DateBox: SimpleBox {}
extension DateBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
import Foundation
struct DecimalBox: Equatable {
typealias Unboxed = Decimal
let unboxed: Unboxed
init(_ unboxed: Unboxed) {
self.unboxed = unboxed
}
init?(xmlString: String) {
guard let unboxed = Unboxed(string: xmlString) else {
return nil
}
self.init(unboxed)
}
}
extension DecimalBox: Box {
var isNull: Bool {
return false
}
/// # Lexical representation
/// Decimal has a lexical representation consisting of a finite-length sequence of
/// decimal digits separated by a period as a decimal indicator.
/// An optional leading sign is allowed. If the sign is omitted, `"+"` is assumed.
/// Leading and trailing zeroes are optional. If the fractional part is zero,
/// the period and following zero(es) can be omitted.
/// For example: `-1.23`, `12678967.543233`, `+100000.00`, `210`.
///
/// # Canonical representation
/// The canonical representation for decimal is defined by prohibiting certain
/// options from the Lexical representation. Specifically, the preceding optional
/// `"+"` sign is prohibited. The decimal point is required. Leading and trailing
/// zeroes are prohibited subject to the following: there must be at least one
/// digit to the right and to the left of the decimal point which may be a zero.
///
/// ---
///
/// [Schema definition](https://www.w3.org/TR/xmlschema-2/#decimal)
var xmlString: String? {
return "\(unboxed)"
}
}
extension DecimalBox: SimpleBox {}
extension DecimalBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Max Desiatov on 05/10/2019.
//
struct DoubleBox: Equatable, ValueBox {
typealias Unboxed = Double
let unboxed: Unboxed
init(_ value: Unboxed) {
unboxed = value
}
init?(xmlString: String) {
guard let unboxed = Double(xmlString) else { return nil }
self.init(unboxed)
}
}
extension DoubleBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
guard !unboxed.isNaN else {
return "NaN"
}
guard !unboxed.isInfinite else {
return (unboxed > 0.0) ? "INF" : "-INF"
}
return unboxed.description
}
}
extension DoubleBox: SimpleBox {}
extension DoubleBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
struct FloatBox: Equatable, ValueBox {
typealias Unboxed = Float
let unboxed: Unboxed
init<Float: BinaryFloatingPoint>(_ unboxed: Float) {
self.unboxed = Unboxed(unboxed)
}
init?(xmlString: String) {
guard let unboxed = Unboxed(xmlString) else {
return nil
}
self.init(unboxed)
}
}
extension FloatBox: Box {
var isNull: Bool {
return false
}
/// # Lexical representation
/// float values have a lexical representation consisting of a mantissa followed, optionally,
/// by the character `"E"` or `"e"`, followed by an exponent. The exponent **must** be an integer.
/// The mantissa **must** be a decimal number. The representations for exponent and mantissa **must**
/// follow the lexical rules for integer and decimal. If the `"E"` or `"e"` and the following
/// exponent are omitted, an exponent value of `0` is assumed.
///
/// The special values positive and negative infinity and not-a-number have lexical
/// representations `INF`, `-INF` and `NaN`, respectively. Lexical representations for zero
/// may take a positive or negative sign.
///
/// For example, `-1E4`, `1267.43233E12`, `12.78e-2`, `12` , `-0`, `0` and `INF` are all
/// legal literals for float.
///
/// # Canonical representation
/// The canonical representation for float is defined by prohibiting certain options from the
/// Lexical representation. Specifically, the exponent must be indicated by `"E"`.
/// Leading zeroes and the preceding optional `"+"` sign are prohibited in the exponent.
/// If the exponent is zero, it must be indicated by `"E0"`. For the mantissa, the preceding
/// optional `"+"` sign is prohibited and the decimal point is required. Leading and trailing
/// zeroes are prohibited subject to the following: number representations must be normalized
/// such that there is a single digit which is non-zero to the left of the decimal point and
/// at least a single digit to the right of the decimal point unless the value being represented
/// is zero. The canonical representation for zero is `0.0E0`.
///
/// ---
///
/// [Schema definition](https://www.w3.org/TR/xmlschema-2/#float)
var xmlString: String? {
guard !unboxed.isNaN else {
return "NaN"
}
guard !unboxed.isInfinite else {
return (unboxed > 0.0) ? "INF" : "-INF"
}
return unboxed.description
}
}
extension FloatBox: SimpleBox {}
extension FloatBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
struct IntBox: Equatable {
typealias Unboxed = Int64
let unboxed: Unboxed
init<Integer: SignedInteger>(_ unboxed: Integer) {
self.unboxed = Unboxed(unboxed)
}
init?(xmlString: String) {
guard let unboxed = Unboxed(xmlString) else {
return nil
}
self.init(unboxed)
}
func unbox<Integer: BinaryInteger>() -> Integer? {
return Integer(exactly: unboxed)
}
}
extension IntBox: Box {
var isNull: Bool {
return false
}
/// # Lexical representation
/// Integer has a lexical representation consisting of a finite-length sequence of
/// decimal digits with an optional leading sign. If the sign is omitted, `"+"` is assumed.
/// For example: `-1`, `0`, `12678967543233`, `+100000`.
///
/// # Canonical representation
/// The canonical representation for integer is defined by prohibiting certain
/// options from the Lexical representation. Specifically, the preceding optional
/// `"+"` sign is prohibited and leading zeroes are prohibited.
///
/// ---
///
/// [Schema definition](https://www.w3.org/TR/xmlschema-2/#integer)
var xmlString: String? {
return unboxed.description
}
}
extension IntBox: SimpleBox {}
extension IntBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 11/19/18.
//
struct KeyedBox {
typealias Key = String
typealias Attribute = SimpleBox
typealias Element = Box
typealias Attributes = KeyedStorage<Key, Attribute>
typealias Elements = KeyedStorage<Key, Element>
var elements = Elements()
var attributes = Attributes()
var unboxed: (elements: Elements, attributes: Attributes) {
return (
elements: elements,
attributes: attributes
)
}
var value: SimpleBox? {
return elements.values.first as? SimpleBox
}
}
extension KeyedBox {
init<E, A>(elements: E, attributes: A)
where E: Sequence, E.Element == (Key, Element),
A: Sequence, A.Element == (Key, Attribute)
{
let elements = Elements(elements)
let attributes = Attributes(attributes)
self.init(elements: elements, attributes: attributes)
}
}
extension KeyedBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return nil
}
}
extension KeyedBox: CustomStringConvertible {
var description: String {
return "{attributes: \(attributes), elements: \(elements)}"
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
struct NullBox {}
extension NullBox: Box {
var isNull: Bool {
return true
}
var xmlString: String? {
return nil
}
}
extension NullBox: SimpleBox {}
extension NullBox: Equatable {
static func ==(_: NullBox, _: NullBox) -> Bool {
return true
}
}
extension NullBox: CustomStringConvertible {
var description: String {
return "null"
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/22/18.
//
class SharedBox<Unboxed: Box> {
private(set) var unboxed: Unboxed
init(_ wrapped: Unboxed) {
unboxed = wrapped
}
func withShared<T>(_ body: (inout Unboxed) throws -> T) rethrows -> T {
return try body(&unboxed)
}
}
extension SharedBox: Box {
var isNull: Bool {
return unboxed.isNull
}
var xmlString: String? {
return unboxed.xmlString
}
}
extension SharedBox: SharedBoxProtocol {
func unbox() -> Unboxed {
return unboxed
}
}
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by James Bean on 7/15/19.
//
/// A `Box` which contains a single `key` and `element` pair. This is useful for disambiguating elements which could either represent
/// an element nested in a keyed or unkeyed container, or an choice between multiple known-typed values (implemented in Swift using
/// enums with associated values).
struct SingleKeyedBox: SimpleBox {
var key: String
var element: Box
}
extension SingleKeyedBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return nil
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
struct StringBox: Equatable {
typealias Unboxed = String
let unboxed: Unboxed
init(_ unboxed: Unboxed) {
self.unboxed = unboxed
}
init(xmlString: Unboxed) {
self.init(xmlString)
}
}
extension StringBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return unboxed.description
}
}
extension StringBox: SimpleBox {}
extension StringBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
struct UIntBox: Equatable {
typealias Unboxed = UInt64
let unboxed: Unboxed
init<Integer: UnsignedInteger>(_ unboxed: Integer) {
self.unboxed = Unboxed(unboxed)
}
init?(xmlString: String) {
guard let unboxed = Unboxed(xmlString) else {
return nil
}
self.init(unboxed)
}
func unbox<Integer: BinaryInteger>() -> Integer? {
return Integer(exactly: unboxed)
}
}
extension UIntBox: Box {
var isNull: Bool {
return false
}
/// # Lexical representation
/// Unsigned integer has a lexical representation consisting of an optional
/// sign followed by a finite-length sequence of decimal digits.
/// If the sign is omitted, the positive sign (`"+"`) is assumed.
/// If the sign is present, it must be `"+"` except for lexical forms denoting zero,
/// which may be preceded by a positive (`"+"`) or a negative (`"-"`) sign.
/// For example: `1`, `0`, `12678967543233`, `+100000`.
///
/// # Canonical representation
/// The canonical representation for nonNegativeInteger is defined by prohibiting
/// certain options from the Lexical representation. Specifically,
/// the the optional `"+"` sign is prohibited and leading zeroes are prohibited.
///
/// ---
///
/// [Schema definition](https://www.w3.org/TR/xmlschema-2/#nonNegativeInteger)
var xmlString: String? {
return unboxed.description
}
}
extension UIntBox: SimpleBox {}
extension UIntBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/21/18.
//
import Foundation
struct URLBox: Equatable {
typealias Unboxed = URL
let unboxed: Unboxed
init(_ unboxed: Unboxed) {
self.unboxed = unboxed
}
init?(xmlString: String) {
guard let unboxed = Unboxed(string: xmlString) else {
return nil
}
self.init(unboxed)
}
}
extension URLBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return unboxed.absoluteString
}
}
extension URLBox: SimpleBox {}
extension URLBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 11/20/18.
//
typealias UnkeyedBox = [Box]
extension Array: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return nil
}
}
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Max Desiatov on 05/10/2019.
//
protocol ValueBox: SimpleBox {
associatedtype Unboxed
init(_ value: Unboxed)
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/21/17.
//
import Foundation
/// Shared ISO8601 Date Formatter
/// NOTE: This value is implicitly lazy and _must_ be lazy. We're compiled
/// against the latest SDK (w/ ISO8601DateFormatter), but linked against
/// whichever Foundation the user has. ISO8601DateFormatter might not exist, so
/// we better not hit this code path on an older OS.
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
var _iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withInternetDateTime
return formatter
}()
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Max Desiatov on 07/04/2019.
//
struct KeyedStorage<Key: Hashable & Comparable, Value> {
typealias Buffer = [(Key, Value)]
typealias KeyMap = [Key: [Int]]
fileprivate var keyMap = KeyMap()
fileprivate var buffer = Buffer()
var isEmpty: Bool {
return buffer.isEmpty
}
var count: Int {
return buffer.count
}
var keys: [Key] {
return buffer.map { $0.0 }
}
var values: [Value] {
return buffer.map { $0.1 }
}
init<S>(_ sequence: S) where S: Sequence, S.Element == (Key, Value) {
buffer = Buffer()
keyMap = KeyMap()
sequence.forEach { key, value in append(value, at: key) }
}
subscript(key: Key) -> [Value] {
return keyMap[key]?.map { buffer[$0].1 } ?? []
}
mutating func append(_ value: Value, at key: Key) {
let i = buffer.count
buffer.append((key, value))
if keyMap[key] != nil {
keyMap[key]?.append(i)
} else {
keyMap[key] = [i]
}
}
func map<T>(_ transform: (Key, Value) throws -> T) rethrows -> [T] {
return try buffer.map(transform)
}
func compactMap<T>(
_ transform: ((Key, Value)) throws -> T?
) rethrows -> [T] {
return try buffer.compactMap(transform)
}
init() {}
}
extension KeyedStorage: Sequence {
func makeIterator() -> Buffer.Iterator {
return buffer.makeIterator()
}
}
extension KeyedStorage: CustomStringConvertible {
var description: String {
let result = buffer.map { "\"\($0)\": \($1)" }.joined(separator: ", ")
return "[\(result)]"
}
}
extension KeyedStorage where Key == String, Value == Box {
func merge(element: XMLCoderElement) -> KeyedStorage<String, Box> {
var result = self
let hasElements = !element.elements.isEmpty
let hasAttributes = !element.attributes.isEmpty
let hasText = element.stringValue != nil
if hasElements || hasAttributes {
result.append(element.transformToBoxTree(), at: element.key)
} else if hasText {
result.append(element.transformToBoxTree(), at: element.key)
} else {
result.append(SingleKeyedBox(key: element.key, element: NullBox()), at: element.key)
}
return result
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Max Desiatov on 30/12/2018.
//
/// Type-erased protocol helper for a metatype check in generic `decode`
/// overload.
protocol AnySequence {
init()
}
extension Array: AnySequence {}
extension Dictionary: AnySequence {}
/// Type-erased protocol helper for a metatype check in generic `decode`
/// overload.
protocol AnyOptional {
init()
}
extension Optional: AnyOptional {
init() {
self = nil
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/18/18.
//
import Foundation
extension StringProtocol where Self.Index == String.Index {
func escape(_ characterSet: [(character: String, escapedCharacter: String)]) -> String {
var string = String(self)
for set in characterSet {
string = string.replacingOccurrences(of: set.character, with: set.escapedCharacter, options: .literal)
}
return string
}
}
extension StringProtocol {
func capitalizingFirstLetter() -> Self {
guard !isEmpty else {
return self
}
return Self(prefix(1).uppercased() + dropFirst())!
}
mutating func capitalizeFirstLetter() {
self = capitalizingFirstLetter()
}
func lowercasingFirstLetter() -> Self {
// avoid lowercasing single letters (I), or capitalized multiples (AThing ! to aThing, leave as AThing)
guard count > 1, !(String(prefix(2)) == prefix(2).lowercased()) else {
return self
}
return Self(prefix(1).lowercased() + dropFirst())!
}
mutating func lowercaseFirstLetter() {
self = lowercasingFirstLetter()
}
}
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Benjamin Wetherfield on 7/17/19.
//
/// An empty marker protocol that can be used in place of `CodingKey`. It must be used when
/// attempting to encode and decode union-type–like enums with associated values to and from `XML`
/// choice elements.
///
/// - Important: In order for your `XML`-destined `Codable` type to be encoded and/or decoded
/// properly, you must conform your custom `CodingKey` type additionally to `XMLChoiceCodingKey`.
///
/// For example, say you have defined a type which can hold _either_ an `Int` _or_ a `String`:
///
/// enum IntOrString {
/// case int(Int)
/// case string(String)
/// }
///
/// Implementing the requirements for the `Codable` protocol like this:
///
/// extension IntOrString: Codable {
/// enum CodingKeys: String, XMLChoiceCodingKey {
/// case int
/// case string
/// }
///
/// func encode(to encoder: Encoder) throws {
/// var container = encoder.container(keyedBy: CodingKeys.self)
/// switch self {
/// case let .int(value):
/// try container.encode(value, forKey: .int)
/// case let .string(value):
/// try container.encode(value, forKey: .string)
/// }
/// }
///
/// init(from decoder: Decoder) throws {
/// let container = try decoder.container(keyedBy: CodingKeys.self)
/// do {
/// self = .int(try container.decode(Int.self, forKey: .int))
/// } catch {
/// self = .string(try container.decode(String.self, forKey: .string))
/// }
/// }
/// }
///
/// Retroactively conform the `CodingKeys` enum to `XMLChoiceCodingKey` when targeting `XML` as your
/// encoded format.
///
/// extension IntOrString.CodingKeys: XMLChoiceCodingKey {}
///
/// - Note: The `XMLChoiceCodingKey` marker protocol allows the `XMLEncoder` / `XMLDecoder` to
/// resolve ambiguities particular to the `XML` format between nested unkeyed container elements and
/// choice elements.
public protocol XMLChoiceCodingKey: CodingKey {}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/18/18.
//
import Foundation
struct Attribute: Equatable {
let key: String
let value: String
}
struct XMLCoderElement: Equatable {
let key: String
private(set) var stringValue: String?
private(set) var elements: [XMLCoderElement] = []
private(set) var attributes: [Attribute] = []
private(set) var containsTextNodes: Bool = false
var isStringNode: Bool {
return key == ""
}
var isCDATANode: Bool {
return key == "#CDATA"
}
var isTextNode: Bool {
return isStringNode || isCDATANode
}
init(
key: String,
elements: [XMLCoderElement] = [],
attributes: [Attribute] = []
) {
self.key = key
stringValue = nil
self.elements = elements
self.attributes = attributes
}
init(
key: String,
stringValue string: String,
attributes: [Attribute] = []
) {
self.key = key
elements = [XMLCoderElement(stringValue: string)]
self.attributes = attributes
containsTextNodes = true
}
init(
key: String,
cdataValue string: String,
attributes: [Attribute] = []
) {
self.key = key
elements = [XMLCoderElement(cdataValue: string)]
self.attributes = attributes
containsTextNodes = true
}
init(stringValue string: String) {
key = ""
stringValue = string
}
init(cdataValue string: String) {
key = "#CDATA"
stringValue = string
}
mutating func append(element: XMLCoderElement, forKey key: String) {
elements.append(element)
containsTextNodes = containsTextNodes || element.isTextNode
}
mutating func append(string: String) {
if elements.last?.isTextNode == true {
let oldValue = elements[elements.count - 1].stringValue ?? ""
elements[elements.count - 1].stringValue = oldValue + string
} else {
elements.append(XMLCoderElement(stringValue: string))
}
containsTextNodes = true
}
mutating func append(cdata string: String) {
if elements.last?.isCDATANode == true {
let oldValue = elements[elements.count - 1].stringValue ?? ""
elements[elements.count - 1].stringValue = oldValue + string
} else {
elements.append(XMLCoderElement(cdataValue: string))
}
containsTextNodes = true
}
func transformToBoxTree() -> Box {
if isTextNode {
return StringBox(stringValue!)
}
let attributes = KeyedStorage(self.attributes.map { attribute in
(key: attribute.key, value: StringBox(attribute.value) as SimpleBox)
})
let storage = KeyedStorage<String, Box>()
let elements = self.elements.reduce(storage) { $0.merge(element: $1) }
return KeyedBox(elements: elements, attributes: attributes)
}
func toXMLString(
with header: XMLHeader? = nil,
escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
formatting: XMLEncoder.OutputFormatting,
indentation: XMLEncoder.PrettyPrintIndentation
) -> String {
if let header = header, let headerXML = header.toXML() {
return headerXML + _toXMLString(escapedCharacters, formatting, indentation)
}
return _toXMLString(escapedCharacters, formatting, indentation)
}
private func formatUnsortedXMLElements(
_ string: inout String,
_ level: Int,
_ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
_ formatting: XMLEncoder.OutputFormatting,
_ indentation: XMLEncoder.PrettyPrintIndentation,
_ prettyPrinted: Bool
) {
formatXMLElements(
from: elements,
into: &string,
at: level,
escapedCharacters: escapedCharacters,
formatting: formatting,
indentation: indentation,
prettyPrinted: prettyPrinted
)
}
fileprivate func elementString(
for element: XMLCoderElement,
at level: Int,
formatting: XMLEncoder.OutputFormatting,
indentation: XMLEncoder.PrettyPrintIndentation,
escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
prettyPrinted: Bool
) -> String {
if let stringValue = element.stringValue {
if element.isCDATANode {
return "<![CDATA[\(stringValue)]]>"
} else {
return stringValue.escape(escapedCharacters.elements)
}
}
var string = ""
string += element._toXMLString(indented: level + 1, escapedCharacters, formatting, indentation)
string += prettyPrinted ? "\n" : ""
return string
}
fileprivate func formatSortedXMLElements(
_ string: inout String,
_ level: Int,
_ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
_ formatting: XMLEncoder.OutputFormatting,
_ indentation: XMLEncoder.PrettyPrintIndentation,
_ prettyPrinted: Bool
) {
formatXMLElements(from: elements.sorted { $0.key < $1.key },
into: &string,
at: level,
escapedCharacters: escapedCharacters,
formatting: formatting,
indentation: indentation,
prettyPrinted: prettyPrinted)
}
fileprivate func formatXMLAttributes(
from attributes: [Attribute],
into string: inout String,
charactersEscapedInAttributes: [(String, String)]
) {
for attribute in attributes {
string += " \(attribute.key)=\"\(attribute.value.escape(charactersEscapedInAttributes))\""
}
}
fileprivate func formatXMLElements(
from elements: [XMLCoderElement],
into string: inout String,
at level: Int,
escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
formatting: XMLEncoder.OutputFormatting,
indentation: XMLEncoder.PrettyPrintIndentation,
prettyPrinted: Bool
) {
for element in elements {
string += elementString(for: element,
at: level,
formatting: formatting,
indentation: indentation,
escapedCharacters: escapedCharacters,
prettyPrinted: prettyPrinted && !containsTextNodes)
}
}
private func formatXMLAttributes(
_ formatting: XMLEncoder.OutputFormatting,
_ string: inout String,
_ charactersEscapedInAttributes: [(String, String)]
) {
let attributes = formatting.contains(.sortedKeys) ?
self.attributes.sorted(by: { $0.key < $1.key }) :
self.attributes
formatXMLAttributes(
from: attributes,
into: &string,
charactersEscapedInAttributes: charactersEscapedInAttributes
)
}
private func formatXMLElements(
_ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
_ formatting: XMLEncoder.OutputFormatting,
_ indentation: XMLEncoder.PrettyPrintIndentation,
_ string: inout String,
_ level: Int,
_ prettyPrinted: Bool
) {
if formatting.contains(.sortedKeys) {
formatSortedXMLElements(
&string, level, escapedCharacters, formatting, indentation, prettyPrinted
)
return
}
formatUnsortedXMLElements(
&string, level, escapedCharacters, formatting, indentation, prettyPrinted
)
}
private func _toXMLString(
indented level: Int = 0,
_ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
_ formatting: XMLEncoder.OutputFormatting,
_ indentation: XMLEncoder.PrettyPrintIndentation
) -> String {
let prettyPrinted = formatting.contains(.prettyPrinted)
let prefix: String
switch indentation {
case let .spaces(count) where prettyPrinted:
prefix = String(repeating: " ", count: level * count)
case let .tabs(count) where prettyPrinted:
prefix = String(repeating: "\t", count: level * count)
default:
prefix = ""
}
var string = prefix
if !key.isEmpty {
string += "<\(key)"
}
formatXMLAttributes(formatting, &string, escapedCharacters.attributes)
if !elements.isEmpty {
let prettyPrintElements = prettyPrinted && !containsTextNodes
if !key.isEmpty {
string += prettyPrintElements ? ">\n" : ">"
}
formatXMLElements(escapedCharacters, formatting, indentation, &string, level, prettyPrintElements)
if prettyPrintElements { string += prefix }
if !key.isEmpty {
string += "</\(key)>"
}
} else {
string += " />"
}
return string
}
}
// MARK: - Convenience Initializers
extension XMLCoderElement {
init(key: String, isStringBoxCDATA isCDATA: Bool, box: UnkeyedBox, attributes: [Attribute] = []) {
if let containsChoice = box as? [ChoiceBox] {
self.init(
key: key,
elements: containsChoice.map {
XMLCoderElement(key: $0.key, isStringBoxCDATA: isCDATA, box: $0.element)
},
attributes: attributes
)
} else {
self.init(
key: key,
elements: box.map { XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: $0) },
attributes: attributes
)
}
}
init(key: String, isStringBoxCDATA: Bool, box: ChoiceBox, attributes: [Attribute] = []) {
self.init(
key: key,
elements: [
XMLCoderElement(key: box.key, isStringBoxCDATA: isStringBoxCDATA, box: box.element),
],
attributes: attributes
)
}
init(key: String, isStringBoxCDATA isCDATA: Bool, box: KeyedBox, attributes: [Attribute] = []) {
var elements: [XMLCoderElement] = []
for (key, box) in box.elements {
let fail = {
preconditionFailure("Unclassified box: \(type(of: box))")
}
switch box {
case let sharedUnkeyedBox as SharedBox<UnkeyedBox>:
let box = sharedUnkeyedBox.unboxed
elements.append(contentsOf: box.map {
XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: $0)
})
case let unkeyedBox as UnkeyedBox:
// This basically injects the unkeyed children directly into self:
elements.append(contentsOf: unkeyedBox.map {
XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: $0)
})
case let sharedKeyedBox as SharedBox<KeyedBox>:
let box = sharedKeyedBox.unboxed
elements.append(XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: box))
case let keyedBox as KeyedBox:
elements.append(XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: keyedBox))
case let simpleBox as SimpleBox:
elements.append(XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: simpleBox))
default:
fail()
}
}
let attributes: [Attribute] = attributes + box.attributes.compactMap { key, box in
guard let value = box.xmlString else {
return nil
}
return Attribute(key: key, value: value)
}
self.init(key: key, elements: elements, attributes: attributes)
}
init(key: String, isStringBoxCDATA: Bool, box: SimpleBox) {
if isStringBoxCDATA, let stringBox = box as? StringBox {
self.init(key: key, cdataValue: stringBox.unboxed)
} else if let value = box.xmlString {
self.init(key: key, stringValue: value)
} else {
self.init(key: key)
}
}
init(key: String, isStringBoxCDATA isCDATA: Bool, box: Box, attributes: [Attribute] = []) {
switch box {
case let sharedUnkeyedBox as SharedBox<UnkeyedBox>:
self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedUnkeyedBox.unboxed, attributes: attributes)
case let sharedKeyedBox as SharedBox<KeyedBox>:
self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedKeyedBox.unboxed, attributes: attributes)
case let sharedChoiceBox as SharedBox<ChoiceBox>:
self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedChoiceBox.unboxed, attributes: attributes)
case let unkeyedBox as UnkeyedBox:
self.init(key: key, isStringBoxCDATA: isCDATA, box: unkeyedBox, attributes: attributes)
case let keyedBox as KeyedBox:
self.init(key: key, isStringBoxCDATA: isCDATA, box: keyedBox, attributes: attributes)
case let choiceBox as ChoiceBox:
self.init(key: key, isStringBoxCDATA: isCDATA, box: choiceBox, attributes: attributes)
case let simpleBox as SimpleBox:
self.init(key: key, isStringBoxCDATA: isCDATA, box: simpleBox)
case let box:
preconditionFailure("Unclassified box: \(type(of: box))")
}
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/18/18.
//
import Foundation
public struct XMLHeader {
/// The XML standard that the produced document conforms to.
public let version: Double?
/// The encoding standard used to represent the characters in the produced document.
public let encoding: String?
/// Indicates whether a document relies on information from an external source.
public let standalone: String?
public init(version: Double? = nil, encoding: String? = nil, standalone: String? = nil) {
self.version = version
self.encoding = encoding
self.standalone = standalone
}
func isEmpty() -> Bool {
return version == nil && encoding == nil && standalone == nil
}
func toXML() -> String? {
guard !isEmpty() else {
return nil
}
var string = "<?xml"
if let version = version {
string += " version=\"\(version)\""
}
if let encoding = encoding {
string += " encoding=\"\(encoding)\""
}
if let standalone = standalone {
string += " standalone=\"\(standalone)\""
}
string += "?>\n"
return string
}
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/21/17.
//
import Foundation
/// Shared Key Types
struct XMLKey: CodingKey {
public let stringValue: String
public let intValue: Int?
public init?(stringValue: String) {
self.init(key: stringValue)
}
public init?(intValue: Int) {
self.init(index: intValue)
}
public init(stringValue: String, intValue: Int?) {
self.stringValue = stringValue
self.intValue = intValue
}
init(key: String) {
self.init(stringValue: key, intValue: nil)
}
init(index: Int) {
self.init(stringValue: "\(index)", intValue: index)
}
static let `super` = XMLKey(stringValue: "super")!
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/14/17.
//
import Foundation
#if canImport(FoundationXML)
import FoundationXML
#endif
class XMLStackParser: NSObject {
var root: XMLCoderElement?
private var stack: [XMLCoderElement] = []
private let trimValueWhitespaces: Bool
init(trimValueWhitespaces: Bool = true) {
self.trimValueWhitespaces = trimValueWhitespaces
super.init()
}
static func parse(
with data: Data,
errorContextLength length: UInt,
shouldProcessNamespaces: Bool,
trimValueWhitespaces: Bool
) throws -> Box {
let parser = XMLStackParser(trimValueWhitespaces: trimValueWhitespaces)
let node = try parser.parse(
with: data,
errorContextLength: length,
shouldProcessNamespaces: shouldProcessNamespaces
)
return node.transformToBoxTree()
}
func parse(
with data: Data,
errorContextLength: UInt,
shouldProcessNamespaces: Bool
) throws -> XMLCoderElement {
let xmlParser = XMLParser(data: data)
xmlParser.shouldProcessNamespaces = shouldProcessNamespaces
xmlParser.delegate = self
guard !xmlParser.parse() || root == nil else {
return root!
}
guard let error = xmlParser.parserError else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: [],
debugDescription: "The given data could not be parsed into XML."
))
}
// `lineNumber` isn't 0-indexed, so 0 is an invalid value for context
guard errorContextLength > 0 && xmlParser.lineNumber > 0 else {
throw error
}
let string = String(data: data, encoding: .utf8) ?? ""
let lines = string.split(separator: "\n")
var errorPosition = 0
let offset = Int(errorContextLength / 2)
for i in 0..<xmlParser.lineNumber - 1 {
errorPosition += lines[i].count
}
errorPosition += xmlParser.columnNumber
var lowerBoundIndex = 0
if errorPosition - offset > 0 {
lowerBoundIndex = errorPosition - offset
}
var upperBoundIndex = string.count
if errorPosition + offset < string.count {
upperBoundIndex = errorPosition + offset
}
#if compiler(>=5.0)
let lowerBound = String.Index(utf16Offset: lowerBoundIndex, in: string)
let upperBound = String.Index(utf16Offset: upperBoundIndex, in: string)
#else
let lowerBound = String.Index(encodedOffset: lowerBoundIndex)
let upperBound = String.Index(encodedOffset: upperBoundIndex)
#endif
let context = string[lowerBound..<upperBound]
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: [],
debugDescription: """
\(error.localizedDescription) \
at line \(xmlParser.lineNumber), column \(xmlParser.columnNumber):
`\(context)`
""",
underlyingError: error
))
}
func withCurrentElement(_ body: (inout XMLCoderElement) throws -> ()) rethrows {
guard !stack.isEmpty else {
return
}
try body(&stack[stack.count - 1])
}
/// Trim whitespaces for a given string if needed.
func process(string: String) -> String {
return trimValueWhitespaces
? string.trimmingCharacters(in: .whitespacesAndNewlines)
: string
}
}
extension XMLStackParser: XMLParserDelegate {
func parserDidStartDocument(_: XMLParser) {
root = nil
stack = []
}
func parser(_: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName: String?,
attributes attributeDict: [String: String] = [:])
{
#if os(Linux) && !compiler(>=5.1)
// For some reason, element names on linux are coming out with the namespace after the name
// https://bugs.swift.org/browse/SR-11191
let elementName = elementName.components(separatedBy: ":").reversed().joined(separator: ":")
#endif
let attributes = attributeDict.map { key, value in
Attribute(key: key, value: value)
}
let element = XMLCoderElement(key: elementName, attributes: attributes)
stack.append(element)
}
func parser(_: XMLParser,
didEndElement _: String,
namespaceURI _: String?,
qualifiedName _: String?)
{
guard let element = stack.popLast() else {
return
}
withCurrentElement { currentElement in
currentElement.append(element: element, forKey: element.key)
}
if stack.isEmpty {
root = element
}
}
func parser(_: XMLParser, foundCharacters string: String) {
let processedString = process(string: string)
guard processedString.count > 0, string.count != 0 else {
return
}
withCurrentElement { currentElement in
currentElement.append(string: processedString)
}
}
func parser(_: XMLParser, foundCDATA CDATABlock: Data) {
guard let string = String(data: CDATABlock, encoding: .utf8) else {
return
}
withCurrentElement { currentElement in
currentElement.append(cdata: string)
}
}
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/21/17.
//
import Foundation
//===----------------------------------------------------------------------===//
// Error Utilities
//===----------------------------------------------------------------------===//
extension DecodingError {
/// Returns a `.typeMismatch` error describing the expected type.
///
/// - parameter path: The path of `CodingKey`s taken to decode a value of this type.
/// - parameter expectation: The type expected to be encountered.
/// - parameter reality: The value that was encountered instead of the expected type.
/// - returns: A `DecodingError` with the appropriate path and debug description.
static func typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Box) -> DecodingError {
let description = "Expected to decode \(expectation) but found \(_typeDescription(of: reality)) instead."
return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description))
}
/// Returns a description of the type of `value` appropriate for an error message.
///
/// - parameter value: The value whose type to describe.
/// - returns: A string describing `value`.
/// - precondition: `value` is one of the types below.
static func _typeDescription(of box: Box) -> String {
switch box {
case is NullBox:
return "a null value"
case is BoolBox:
return "a boolean value"
case is DecimalBox:
return "a decimal value"
case is IntBox:
return "a signed integer value"
case is UIntBox:
return "an unsigned integer value"
case is FloatBox:
return "a floating-point value"
case is DoubleBox:
return "a double floating-point value"
case is UnkeyedBox:
return "a array value"
case is KeyedBox:
return "a dictionary value"
case _:
return "\(type(of: box))"
}
}
}
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Max Desiatov on 01/03/2019.
//
public protocol DynamicNodeDecoding: Decodable {
static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/20/17.
//
import Foundation
extension XMLDecoderImplementation: SingleValueDecodingContainer {
// MARK: SingleValueDecodingContainer Methods
public func decodeNil() -> Bool {
return (try? topContainer().isNull) ?? true
}
public func decode(_: Bool.Type) throws -> Bool {
return try unbox(try topContainer())
}
public func decode(_: Decimal.Type) throws -> Decimal {
return try unbox(try topContainer())
}
public func decode<T: BinaryInteger & SignedInteger & Decodable>(_: T.Type) throws -> T {
return try unbox(try topContainer())
}
public func decode<T: BinaryInteger & UnsignedInteger & Decodable>(_: T.Type) throws -> T {
return try unbox(try topContainer())
}
public func decode(_: Float.Type) throws -> Float {
return try unbox(try topContainer())
}
public func decode(_: Double.Type) throws -> Double {
return try unbox(try topContainer())
}
public func decode(_: String.Type) throws -> String {
return try unbox(try topContainer())
}
public func decode(_: String.Type) throws -> Date {
return try unbox(try topContainer())
}
public func decode(_: String.Type) throws -> Data {
return try unbox(try topContainer())
}
public func decode<T: Decodable>(_: T.Type) throws -> T {
return try unbox(try topContainer())
}
}
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by James Bean on 7/18/19.
//
/// Container specialized for decoding XML choice elements.
struct XMLChoiceDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
typealias Key = K
// MARK: Properties
/// A reference to the decoder we're reading from.
private let decoder: XMLDecoderImplementation
/// A reference to the container we're reading from.
private let container: SharedBox<ChoiceBox>
/// The path of coding keys taken to get to this point in decoding.
public private(set) var codingPath: [CodingKey]
// MARK: - Initialization
/// Initializes `self` by referencing the given decoder and container.
init(referencing decoder: XMLDecoderImplementation, wrapping container: SharedBox<ChoiceBox>) {
self.decoder = decoder
container.withShared { $0.key = decoder.keyTransform($0.key) }
self.container = container
codingPath = decoder.codingPath
}
// MARK: - KeyedDecodingContainerProtocol Methods
public var allKeys: [Key] {
return container.withShared { [Key(stringValue: $0.key)!] }
}
public func contains(_ key: Key) -> Bool {
return container.withShared { $0.key == key.stringValue }
}
public func decodeNil(forKey key: Key) throws -> Bool {
return container.withShared { $0.element.isNull }
}
public func decode<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T {
guard container.withShared({ $0.key == key.stringValue }), key is XMLChoiceCodingKey else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: type,
reality: container
)
}
return try decoder.unbox(container.withShared { $0.element })
}
public func nestedContainer<NestedKey>(
keyedBy _: NestedKey.Type, forKey key: Key
) throws -> KeyedDecodingContainer<NestedKey> {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: NestedKey.self,
reality: container
)
}
public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: Key.self,
reality: container
)
}
public func superDecoder() throws -> Decoder {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: Key.self,
reality: container
)
}
public func superDecoder(forKey key: Key) throws -> Decoder {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: Key.self,
reality: container
)
}
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/20/17.
//
import Foundation
//===----------------------------------------------------------------------===//
// XML Decoder
//===----------------------------------------------------------------------===//
/// `XMLDecoder` facilitates the decoding of XML into semantic `Decodable` types.
open class XMLDecoder {
// MARK: Options
/// The strategy to use for decoding `Date` values.
public enum DateDecodingStrategy {
/// Defer to `Date` for decoding. This is the default strategy.
case deferredToDate
/// Decode the `Date` as a UNIX timestamp from a XML number. This is the default strategy.
case secondsSince1970
/// Decode the `Date` as UNIX millisecond timestamp from a XML number.
case millisecondsSince1970
/// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
case iso8601
/// Decode the `Date` as a string parsed by the given formatter.
case formatted(DateFormatter)
/// Decode the `Date` as a custom box decoded by the given closure.
case custom((_ decoder: Decoder) throws -> Date)
/// Decode the `Date` as a string parsed by the given formatter for the give key.
static func keyFormatted(
_ formatterForKey: @escaping (CodingKey) throws -> DateFormatter?
) -> XMLDecoder.DateDecodingStrategy {
return .custom { (decoder) -> Date in
guard let codingKey = decoder.codingPath.last else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "No Coding Path Found"
))
}
guard let container = try? decoder.singleValueContainer(),
let text = try? container.decode(String.self)
else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Could not decode date text"
))
}
guard let dateFormatter = try formatterForKey(codingKey) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "No date formatter for date text"
)
}
if let date = dateFormatter.date(from: text) {
return date
} else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Cannot decode date string \(text)"
)
}
}
}
}
/// The strategy to use for decoding `Data` values.
public enum DataDecodingStrategy {
/// Defer to `Data` for decoding.
case deferredToData
/// Decode the `Data` from a Base64-encoded string. This is the default strategy.
case base64
/// Decode the `Data` as a custom box decoded by the given closure.
case custom((_ decoder: Decoder) throws -> Data)
/// Decode the `Data` as a custom box by the given closure for the give key.
static func keyFormatted(
_ formatterForKey: @escaping (CodingKey) throws -> Data?
) -> XMLDecoder.DataDecodingStrategy {
return .custom { (decoder) -> Data in
guard let codingKey = decoder.codingPath.last else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "No Coding Path Found"
))
}
guard let container = try? decoder.singleValueContainer(),
let text = try? container.decode(String.self)
else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Could not decode date text"
))
}
guard let data = try formatterForKey(codingKey) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Cannot decode data string \(text)"
)
}
return data
}
}
}
/// The strategy to use for non-XML-conforming floating-point values (IEEE 754 infinity and NaN).
public enum NonConformingFloatDecodingStrategy {
/// Throw upon encountering non-conforming values. This is the default strategy.
case `throw`
/// Decode the values from the given representation strings.
case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
}
/// The strategy to use for automatically changing the box of keys before decoding.
public enum KeyDecodingStrategy {
/// Use the keys specified by each type. This is the default strategy.
case useDefaultKeys
/// Convert from "snake_case_keys" to "camelCaseKeys" before attempting
/// to match a key with the one specified by each type.
///
/// The conversion to upper case uses `Locale.system`, also known as
/// the ICU "root" locale. This means the result is consistent
/// regardless of the current user's locale and language preferences.
///
/// Converting from snake case to camel case:
/// 1. Capitalizes the word starting after each `_`
/// 2. Removes all `_`
/// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata).
/// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`.
///
/// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character.
case convertFromSnakeCase
/// Convert from "kebab-case" to "kebabCase" before attempting
/// to match a key with the one specified by each type.
case convertFromKebabCase
/// Convert from "CodingKey" to "codingKey"
case convertFromCapitalized
/// Provide a custom conversion from the key in the encoded XML to the
/// keys specified by the decoded types.
/// The full path to the current decoding position is provided for
/// context (in case you need to locate this key within the payload).
/// The returned key is used in place of the last component in the
/// coding path before decoding.
/// If the result of the conversion is a duplicate key, then only one
/// box will be present in the container for the type to decode from.
case custom((_ codingPath: [CodingKey]) -> CodingKey)
static func _convertFromCapitalized(_ stringKey: String) -> String {
guard !stringKey.isEmpty else {
return stringKey
}
let firstLetter = stringKey.prefix(1).lowercased()
let result = firstLetter + stringKey.dropFirst()
return result
}
static func _convertFromSnakeCase(_ stringKey: String) -> String {
return _convert(stringKey, usingSeparator: "_")
}
static func _convertFromKebabCase(_ stringKey: String) -> String {
return _convert(stringKey, usingSeparator: "-")
}
static func _convert(_ stringKey: String, usingSeparator separator: Character) -> String {
guard !stringKey.isEmpty else {
return stringKey
}
// Find the first non-separator character
guard let firstNonSeparator = stringKey.firstIndex(where: { $0 != separator }) else {
// Reached the end without finding a separator character
return stringKey
}
// Find the last non-separator character
var lastNonSeparator = stringKey.index(before: stringKey.endIndex)
while lastNonSeparator > firstNonSeparator, stringKey[lastNonSeparator] == separator {
stringKey.formIndex(before: &lastNonSeparator)
}
let keyRange = firstNonSeparator...lastNonSeparator
let leadingSeparatorRange = stringKey.startIndex..<firstNonSeparator
let trailingSeparatorRange = stringKey.index(after: lastNonSeparator)..<stringKey.endIndex
let components = stringKey[keyRange].split(separator: separator)
let joinedString: String
if components.count == 1 {
// No separators in key, leave the word as is - maybe it is already good
joinedString = String(stringKey[keyRange])
} else {
joinedString = ([components[0].lowercased()] + components[1...].map { $0.capitalized }).joined()
}
// Do a cheap isEmpty check before creating and appending potentially empty strings
let result: String
if leadingSeparatorRange.isEmpty, trailingSeparatorRange.isEmpty {
result = joinedString
} else if !leadingSeparatorRange.isEmpty, !trailingSeparatorRange.isEmpty {
// Both leading and trailing underscores
result = String(stringKey[leadingSeparatorRange]) + joinedString + String(stringKey[trailingSeparatorRange])
} else if !leadingSeparatorRange.isEmpty {
// Just leading
result = String(stringKey[leadingSeparatorRange]) + joinedString
} else {
// Just trailing
result = joinedString + String(stringKey[trailingSeparatorRange])
}
return result
}
}
/// The strategy to use in decoding dates. Defaults to `.secondsSince1970`.
open var dateDecodingStrategy: DateDecodingStrategy = .secondsSince1970
/// The strategy to use in decoding binary data. Defaults to `.base64`.
open var dataDecodingStrategy: DataDecodingStrategy = .base64
/// The strategy to use in decoding non-conforming numbers. Defaults to `.throw`.
open var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .throw
/// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys
/// A node's decoding type
public enum NodeDecoding {
case attribute
case element
case elementOrAttribute
}
/// The strategy to use in encoding encoding attributes. Defaults to `.deferredToEncoder`.
open var nodeDecodingStrategy: NodeDecodingStrategy = .deferredToDecoder
/// Set of strategies to use for encoding of nodes.
public enum NodeDecodingStrategy {
/// Defer to `Encoder` for choosing an encoding. This is the default strategy.
case deferredToDecoder
/// Return a closure computing the desired node encoding for the value by its coding key.
case custom((Decodable.Type, Decoder) -> ((CodingKey) -> NodeDecoding))
func nodeDecodings(
forType codableType: Decodable.Type,
with decoder: Decoder
) -> ((CodingKey) -> NodeDecoding) {
switch self {
case .deferredToDecoder:
guard let dynamicType = codableType as? DynamicNodeDecoding.Type else {
return { _ in .elementOrAttribute }
}
return dynamicType.nodeDecoding(for:)
case let .custom(closure):
return closure(codableType, decoder)
}
}
}
/// Contextual user-provided information for use during decoding.
open var userInfo: [CodingUserInfoKey: Any] = [:]
/// The error context length. Non-zero length makes an error thrown from
/// the XML parser with line/column location repackaged with a context
/// around that location of specified length. For example, if an error was
/// thrown indicating that there's an unexpected character at line 3, column
/// 15 with `errorContextLength` set to 10, a new error type is rethrown
/// containing 5 characters before column 15 and 5 characters after, all on
/// line 3. Line wrapping should be handled correctly too as the context can
/// span more than a few lines.
open var errorContextLength: UInt = 0
/** A boolean value that determines whether the parser reports the
namespaces and qualified names of elements. The default value is `false`.
*/
open var shouldProcessNamespaces: Bool = false
/** A boolean value that determines whether the parser trims whitespaces
and newlines from the end and the beginning of string values. The default
value is `true`.
*/
open var trimValueWhitespaces: Bool
/// Options set on the top-level encoder to pass down the decoding hierarchy.
struct Options {
let dateDecodingStrategy: DateDecodingStrategy
let dataDecodingStrategy: DataDecodingStrategy
let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
let keyDecodingStrategy: KeyDecodingStrategy
let nodeDecodingStrategy: NodeDecodingStrategy
let userInfo: [CodingUserInfoKey: Any]
}
/// The options set on the top-level decoder.
var options: Options {
return Options(
dateDecodingStrategy: dateDecodingStrategy,
dataDecodingStrategy: dataDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
keyDecodingStrategy: keyDecodingStrategy,
nodeDecodingStrategy: nodeDecodingStrategy,
userInfo: userInfo
)
}
// MARK: - Constructing a XML Decoder
/// Initializes `self` with default strategies.
public init(trimValueWhitespaces: Bool = true) {
self.trimValueWhitespaces = trimValueWhitespaces
}
// MARK: - Decoding Values
/// Decodes a top-level box of the given type from the given XML representation.
///
/// - parameter type: The type of the box to decode.
/// - parameter data: The data to decode from.
/// - returns: A box of the requested type.
/// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid XML.
/// - throws: An error if any box throws an error during decoding.
open func decode<T: Decodable>(
_ type: T.Type,
from data: Data
) throws -> T {
let topLevel: Box = try XMLStackParser.parse(
with: data,
errorContextLength: errorContextLength,
shouldProcessNamespaces: shouldProcessNamespaces,
trimValueWhitespaces: trimValueWhitespaces
)
let decoder = XMLDecoderImplementation(
referencing: topLevel,
options: options,
nodeDecodings: []
)
decoder.nodeDecodings = [
options.nodeDecodingStrategy.nodeDecodings(
forType: T.self,
with: decoder
),
]
defer {
_ = decoder.nodeDecodings.removeLast()
}
return try decoder.unbox(topLevel)
}
}
// MARK: TopLevelDecoder
#if canImport(Combine)
import protocol Combine.TopLevelDecoder
import protocol Combine.TopLevelEncoder
#elseif canImport(OpenCombine)
import protocol OpenCombine.TopLevelDecoder
import protocol OpenCombine.TopLevelEncoder
#endif
#if canImport(Combine) || canImport(OpenCombine)
extension XMLDecoder: TopLevelDecoder {}
extension XMLEncoder: TopLevelEncoder {}
#endif
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/20/17.
//
import Foundation
class XMLDecoderImplementation: Decoder {
// MARK: Properties
/// The decoder's storage.
var storage = XMLDecodingStorage()
/// Options set on the top-level decoder.
let options: XMLDecoder.Options
/// The path to the current point in encoding.
public internal(set) var codingPath: [CodingKey]
public var nodeDecodings: [(CodingKey) -> XMLDecoder.NodeDecoding]
/// Contextual user-provided information for use during encoding.
public var userInfo: [CodingUserInfoKey: Any] {
return options.userInfo
}
// The error context length
open var errorContextLength: UInt = 0
// MARK: - Initialization
/// Initializes `self` with the given top-level container and options.
init(
referencing container: Box,
options: XMLDecoder.Options,
nodeDecodings: [(CodingKey) -> XMLDecoder.NodeDecoding],
codingPath: [CodingKey] = []
) {
storage.push(container: container)
self.codingPath = codingPath
self.nodeDecodings = nodeDecodings
self.options = options
}
// MARK: - Decoder Methods
internal func topContainer() throws -> Box {
guard let topContainer = storage.topContainer() else {
throw DecodingError.valueNotFound(Box.self, DecodingError.Context(
codingPath: codingPath,
debugDescription: "Cannot get decoding container -- empty container stack."
))
}
return topContainer
}
private func popContainer() throws -> Box {
guard let topContainer = storage.popContainer() else {
throw DecodingError.valueNotFound(Box.self, DecodingError.Context(
codingPath: codingPath,
debugDescription:
"""
Cannot get decoding container -- empty container stack.
"""
))
}
return topContainer
}
public func container<Key>(keyedBy keyType: Key.Type) throws -> KeyedDecodingContainer<Key> {
if let keyed = try topContainer() as? SharedBox<KeyedBox> {
return KeyedDecodingContainer(XMLKeyedDecodingContainer<Key>(
referencing: self,
wrapping: keyed
))
}
if Key.self is XMLChoiceCodingKey.Type {
return try choiceContainer(keyedBy: keyType)
} else {
return try keyedContainer(keyedBy: keyType)
}
}
public func unkeyedContainer() throws -> UnkeyedDecodingContainer {
let topContainer = try self.topContainer()
guard !topContainer.isNull else {
throw DecodingError.valueNotFound(
UnkeyedDecodingContainer.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription:
"""
Cannot get unkeyed decoding container -- found null box instead.
"""
)
)
}
switch topContainer {
case let unkeyed as SharedBox<UnkeyedBox>:
return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed)
case let keyed as SharedBox<KeyedBox>:
return XMLUnkeyedDecodingContainer(
referencing: self,
wrapping: SharedBox(keyed.withShared { $0.elements.map(SingleKeyedBox.init) })
)
default:
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [Any].self,
reality: topContainer
)
}
}
public func singleValueContainer() throws -> SingleValueDecodingContainer {
return self
}
private func keyedContainer<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
let topContainer = try self.topContainer()
let keyedBox: KeyedBox
switch topContainer {
case _ where topContainer.isNull:
throw DecodingError.valueNotFound(
KeyedDecodingContainer<Key>.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription:
"""
Cannot get keyed decoding container -- found null box instead.
"""
)
)
case let string as StringBox:
keyedBox = KeyedBox(
elements: KeyedStorage([("", string)]),
attributes: KeyedStorage()
)
case let containsEmpty as SingleKeyedBox where containsEmpty.element is NullBox:
keyedBox = KeyedBox(
elements: KeyedStorage([("", StringBox(""))]),
attributes: KeyedStorage()
)
case let unkeyed as SharedBox<UnkeyedBox>:
guard let keyed = unkeyed.withShared({ $0.first }) as? KeyedBox else {
fallthrough
}
keyedBox = keyed
default:
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [String: Any].self,
reality: topContainer
)
}
let container = XMLKeyedDecodingContainer<Key>(
referencing: self,
wrapping: SharedBox(keyedBox)
)
return KeyedDecodingContainer(container)
}
/// - Returns: A `KeyedDecodingContainer` for an XML choice element.
private func choiceContainer<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
let topContainer = try self.topContainer()
let choiceBox: ChoiceBox
switch topContainer {
case let choice as ChoiceBox:
choiceBox = choice
case let singleKeyed as SingleKeyedBox:
choiceBox = ChoiceBox(singleKeyed)
default:
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [String: Any].self,
reality: topContainer
)
}
let container = XMLChoiceDecodingContainer<Key>(
referencing: self,
wrapping: SharedBox(choiceBox)
)
return KeyedDecodingContainer(container)
}
}
// MARK: - Concrete Value Representations
extension XMLDecoderImplementation {
/// Returns the given box unboxed from a container.
private func typedBox<T, B: Box>(_ box: Box, for valueType: T.Type) throws -> B {
let error = DecodingError.valueNotFound(valueType, DecodingError.Context(
codingPath: codingPath,
debugDescription: "Expected \(valueType) but found null instead."
))
switch box {
case let typedBox as B:
return typedBox
case let unkeyedBox as SharedBox<UnkeyedBox>:
guard let value = unkeyedBox.withShared({
$0.first as? B
}) else { throw error }
return value
case let keyedBox as SharedBox<KeyedBox>:
guard
let value = keyedBox.withShared({ $0.value as? B })
else { throw error }
return value
case let singleKeyedBox as SingleKeyedBox:
if let value = singleKeyedBox.element as? B {
return value
} else if let box = singleKeyedBox.element as? KeyedBox, let value = box.elements[""].first as? B {
return value
} else {
throw error
}
case is NullBox:
throw error
case let keyedBox as KeyedBox:
guard
let value = keyedBox.value as? B
else { fallthrough }
return value
default:
throw DecodingError.typeMismatch(
at: codingPath,
expectation: valueType,
reality: box
)
}
}
func unbox(_ box: Box) throws -> Bool {
let stringBox: StringBox = try typedBox(box, for: Bool.self)
let string = stringBox.unboxed
guard let boolBox = BoolBox(xmlString: string) else {
throw DecodingError.typeMismatch(at: codingPath, expectation: Bool.self, reality: box)
}
return boolBox.unboxed
}
func unbox(_ box: Box) throws -> Decimal {
let stringBox: StringBox = try typedBox(box, for: Decimal.self)
let string = stringBox.unboxed
guard let decimalBox = DecimalBox(xmlString: string) else {
throw DecodingError.typeMismatch(at: codingPath, expectation: Decimal.self, reality: box)
}
return decimalBox.unboxed
}
func unbox<T: BinaryInteger & SignedInteger & Decodable>(_ box: Box) throws -> T {
let stringBox: StringBox = try typedBox(box, for: T.self)
let string = stringBox.unboxed
guard let intBox = IntBox(xmlString: string) else {
throw DecodingError.typeMismatch(at: codingPath, expectation: T.self, reality: box)
}
guard let int: T = intBox.unbox() else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: codingPath,
debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)."
))
}
return int
}
func unbox<T: BinaryInteger & UnsignedInteger & Decodable>(_ box: Box) throws -> T {
let stringBox: StringBox = try typedBox(box, for: T.self)
let string = stringBox.unboxed
guard let uintBox = UIntBox(xmlString: string) else {
throw DecodingError.typeMismatch(at: codingPath, expectation: T.self, reality: box)
}
guard let uint: T = uintBox.unbox() else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: codingPath,
debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)."
))
}
return uint
}
func unbox(_ box: Box) throws -> Float {
let stringBox: StringBox = try typedBox(box, for: Float.self)
let string = stringBox.unboxed
guard let floatBox = FloatBox(xmlString: string) else {
throw DecodingError.typeMismatch(at: codingPath, expectation: Float.self, reality: box)
}
return floatBox.unboxed
}
func unbox(_ box: Box) throws -> Double {
let stringBox: StringBox = try typedBox(box, for: Double.self)
let string = stringBox.unboxed
guard let doubleBox = DoubleBox(xmlString: string) else {
throw DecodingError.typeMismatch(at: codingPath, expectation: Double.self, reality: box)
}
return doubleBox.unboxed
}
func unbox(_ box: Box) throws -> String {
do {
let stringBox: StringBox = try typedBox(box, for: String.self)
return stringBox.unboxed
} catch {
if box is NullBox {
return ""
}
}
return ""
}
func unbox(_ box: Box) throws -> Date {
switch options.dateDecodingStrategy {
case .deferredToDate:
storage.push(container: box)
defer { storage.popContainer() }
return try Date(from: self)
case .secondsSince1970:
let stringBox: StringBox = try typedBox(box, for: Date.self)
let string = stringBox.unboxed
guard let dateBox = DateBox(secondsSince1970: string) else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: codingPath,
debugDescription: "Expected date string to be formatted in seconds since 1970."
))
}
return dateBox.unboxed
case .millisecondsSince1970:
let stringBox: StringBox = try typedBox(box, for: Date.self)
let string = stringBox.unboxed
guard let dateBox = DateBox(millisecondsSince1970: string) else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: codingPath,
debugDescription: "Expected date string to be formatted in milliseconds since 1970."
))
}
return dateBox.unboxed
case .iso8601:
let stringBox: StringBox = try typedBox(box, for: Date.self)
let string = stringBox.unboxed
guard let dateBox = DateBox(iso8601: string) else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: codingPath,
debugDescription: "Expected date string to be ISO8601-formatted."
))
}
return dateBox.unboxed
case let .formatted(formatter):
let stringBox: StringBox = try typedBox(box, for: Date.self)
let string = stringBox.unboxed
guard let dateBox = DateBox(xmlString: string, formatter: formatter) else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: codingPath,
debugDescription: "Date string does not match format expected by formatter."
))
}
return dateBox.unboxed
case let .custom(closure):
storage.push(container: box)
defer { storage.popContainer() }
return try closure(self)
}
}
func unbox(_ box: Box) throws -> Data {
switch options.dataDecodingStrategy {
case .deferredToData:
storage.push(container: box)
defer { storage.popContainer() }
return try Data(from: self)
case .base64:
let stringBox: StringBox = try typedBox(box, for: Data.self)
let string = stringBox.unboxed
guard let dataBox = DataBox(base64: string) else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: codingPath,
debugDescription: "Encountered Data is not valid Base64"
))
}
return dataBox.unboxed
case let .custom(closure):
storage.push(container: box)
defer { storage.popContainer() }
return try closure(self)
}
}
func unbox(_ box: Box) throws -> URL {
let stringBox: StringBox = try typedBox(box, for: URL.self)
let string = stringBox.unboxed
guard let urlBox = URLBox(xmlString: string) else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: codingPath,
debugDescription: "Encountered Data is not valid Base64"
))
}
return urlBox.unboxed
}
func unbox<T: Decodable>(_ box: Box) throws -> T {
let decoded: T?
let type = T.self
if type == Date.self || type == NSDate.self {
let date: Date = try unbox(box)
decoded = date as? T
} else if type == Data.self || type == NSData.self {
let data: Data = try unbox(box)
decoded = data as? T
} else if type == URL.self || type == NSURL.self {
let data: URL = try unbox(box)
decoded = data as? T
} else if type == Decimal.self || type == NSDecimalNumber.self {
let decimal: Decimal = try unbox(box)
decoded = decimal as? T
} else if
type == String.self || type == NSString.self,
let value = (try unbox(box) as String) as? T
{
decoded = value
} else {
storage.push(container: box)
defer {
storage.popContainer()
}
do {
decoded = try type.init(from: self)
} catch {
guard case DecodingError.valueNotFound = error,
let type = type as? AnyOptional.Type,
let result = type.init() as? T
else {
throw error
}
return result
}
}
guard let result = decoded else {
throw DecodingError.typeMismatch(
at: codingPath, expectation: type, reality: box
)
}
return result
}
}
extension XMLDecoderImplementation {
var keyTransform: (String) -> String {
switch options.keyDecodingStrategy {
case .convertFromSnakeCase:
return XMLDecoder.KeyDecodingStrategy._convertFromSnakeCase
case .convertFromCapitalized:
return XMLDecoder.KeyDecodingStrategy._convertFromCapitalized
case .convertFromKebabCase:
return XMLDecoder.KeyDecodingStrategy._convertFromKebabCase
case .useDefaultKeys:
return { key in key }
case let .custom(converter):
return { key in
converter(self.codingPath + [XMLKey(stringValue: key, intValue: nil)]).stringValue
}
}
}
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/20/17.
//
import Foundation
// MARK: - Decoding Storage
struct XMLDecodingStorage {
// MARK: Properties
/// The container stack.
/// Elements may be any one of the XML types (StringBox, KeyedBox).
private var containers: [Box] = []
// MARK: - Initialization
/// Initializes `self` with no containers.
init() {}
// MARK: - Modifying the Stack
var count: Int {
return containers.count
}
func topContainer() -> Box? {
return containers.last
}
mutating func push(container: Box) {
if let keyedBox = container as? KeyedBox {
containers.append(SharedBox(keyedBox))
} else if let unkeyedBox = container as? UnkeyedBox {
containers.append(SharedBox(unkeyedBox))
} else {
containers.append(container)
}
}
@discardableResult
mutating func popContainer() -> Box? {
guard !containers.isEmpty else {
return nil
}
return containers.removeLast()
}
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/21/17.
//
import Foundation
// MARK: Decoding Containers
struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
typealias Key = K
typealias KeyedContainer = SharedBox<KeyedBox>
typealias UnkeyedContainer = SharedBox<UnkeyedBox>
// MARK: Properties
/// A reference to the decoder we're reading from.
private let decoder: XMLDecoderImplementation
/// A reference to the container we're reading from.
private let container: KeyedContainer
/// The path of coding keys taken to get to this point in decoding.
public private(set) var codingPath: [CodingKey]
// MARK: - Initialization
/// Initializes `self` by referencing the given decoder and container.
init(
referencing decoder: XMLDecoderImplementation,
wrapping container: KeyedContainer
) {
self.decoder = decoder
container.withShared {
$0.elements = .init($0.elements.map { (decoder.keyTransform($0), $1) })
$0.attributes = .init($0.attributes.map { (decoder.keyTransform($0), $1) })
}
self.container = container
codingPath = decoder.codingPath
}
// MARK: - KeyedDecodingContainerProtocol Methods
public var allKeys: [Key] {
let elementKeys = container.withShared { keyedBox in
keyedBox.elements.keys.compactMap { Key(stringValue: $0) }
}
let attributeKeys = container.withShared { keyedBox in
keyedBox.attributes.keys.compactMap { Key(stringValue: $0) }
}
return attributeKeys + elementKeys
}
public func contains(_ key: Key) -> Bool {
let elements = container.withShared { keyedBox in
keyedBox.elements[key.stringValue]
}
let attributes = container.withShared { keyedBox in
keyedBox.attributes[key.stringValue]
}
return !elements.isEmpty || !attributes.isEmpty
}
public func decodeNil(forKey key: Key) throws -> Bool {
let elements = container.withShared { keyedBox in
keyedBox.elements[key.stringValue]
}
let attributes = container.withShared { keyedBox in
keyedBox.attributes[key.stringValue]
}
let box = elements.first ?? attributes.first
if box is SingleKeyedBox {
return false
}
return box?.isNull ?? true
}
public func decode<T: Decodable>(
_ type: T.Type, forKey key: Key
) throws -> T {
let attributeFound = container.withShared { keyedBox in
!keyedBox.attributes[key.stringValue].isEmpty
}
let elementFound = container.withShared { keyedBox in
!keyedBox.elements[key.stringValue].isEmpty || keyedBox.value != nil
}
if let type = type as? AnySequence.Type,
!attributeFound,
!elementFound,
let result = type.init() as? T
{
return result
}
return try decodeConcrete(type, forKey: key)
}
public func nestedContainer<NestedKey>(
keyedBy _: NestedKey.Type, forKey key: Key
) throws -> KeyedDecodingContainer<NestedKey> {
decoder.codingPath.append(key)
defer { decoder.codingPath.removeLast() }
let elements = self.container.withShared { keyedBox in
keyedBox.elements[key.stringValue]
}
let attributes = self.container.withShared { keyedBox in
keyedBox.attributes[key.stringValue]
}
guard let value = elements.first ?? attributes.first else {
throw DecodingError.keyNotFound(key, DecodingError.Context(
codingPath: codingPath,
debugDescription:
"""
Cannot get \(KeyedDecodingContainer<NestedKey>.self) -- \
no value found for key \"\(key.stringValue)\"
"""
))
}
let container: XMLKeyedDecodingContainer<NestedKey>
if let keyedContainer = value as? KeyedContainer {
container = XMLKeyedDecodingContainer<NestedKey>(
referencing: decoder,
wrapping: keyedContainer
)
} else if let keyedContainer = value as? KeyedBox {
container = XMLKeyedDecodingContainer<NestedKey>(
referencing: decoder,
wrapping: SharedBox(keyedContainer)
)
} else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [String: Any].self,
reality: value
)
}
return KeyedDecodingContainer(container)
}
public func nestedUnkeyedContainer(
forKey key: Key
) throws -> UnkeyedDecodingContainer {
decoder.codingPath.append(key)
defer { decoder.codingPath.removeLast() }
let elements = container.unboxed.elements[key.stringValue]
if let containsKeyed = elements as? [KeyedBox], containsKeyed.count == 1, let keyed = containsKeyed.first {
return XMLUnkeyedDecodingContainer(
referencing: decoder,
wrapping: SharedBox(keyed.elements.map(SingleKeyedBox.init))
)
} else {
return XMLUnkeyedDecodingContainer(
referencing: decoder,
wrapping: SharedBox(elements)
)
}
}
public func superDecoder() throws -> Decoder {
return try _superDecoder(forKey: XMLKey.super)
}
public func superDecoder(forKey key: Key) throws -> Decoder {
return try _superDecoder(forKey: key)
}
}
/// Private functions
extension XMLKeyedDecodingContainer {
private func _errorDescription(of key: CodingKey) -> String {
switch decoder.options.keyDecodingStrategy {
case .convertFromSnakeCase:
// In this case we can attempt to recover the original value by
// reversing the transform
let original = key.stringValue
let converted = XMLEncoder.KeyEncodingStrategy
._convertToSnakeCase(original)
if converted == original {
return "\(key) (\"\(original)\")"
} else {
return "\(key) (\"\(original)\"), converted to \(converted)"
}
default:
// Otherwise, just report the converted string
return "\(key) (\"\(key.stringValue)\")"
}
}
private func decodeSignedInteger<T>(_ type: T.Type,
forKey key: Key) throws -> T
where T: BinaryInteger & SignedInteger & Decodable
{
return try decodeConcrete(type, forKey: key)
}
private func decodeUnsignedInteger<T>(_ type: T.Type,
forKey key: Key) throws -> T
where T: BinaryInteger & UnsignedInteger & Decodable
{
return try decodeConcrete(type, forKey: key)
}
private func decodeFloatingPoint<T>(_ type: T.Type,
forKey key: Key) throws -> T
where T: BinaryFloatingPoint & Decodable
{
return try decodeConcrete(type, forKey: key)
}
private func decodeConcrete<T: Decodable>(
_ type: T.Type,
forKey key: Key
) throws -> T {
guard let strategy = decoder.nodeDecodings.last else {
preconditionFailure(
"""
Attempt to access node decoding strategy from empty stack.
"""
)
}
let elements = container
.withShared { keyedBox -> [KeyedBox.Element] in
keyedBox.elements[key.stringValue].map {
if let singleKeyed = $0 as? SingleKeyedBox {
return singleKeyed.element.isNull ? singleKeyed : singleKeyed.element
} else {
return $0
}
}
}
let attributes = container.withShared { keyedBox in
keyedBox.attributes[key.stringValue]
}
decoder.codingPath.append(key)
let nodeDecodings = decoder.options.nodeDecodingStrategy.nodeDecodings(
forType: T.self,
with: decoder
)
decoder.nodeDecodings.append(nodeDecodings)
defer {
_ = decoder.nodeDecodings.removeLast()
decoder.codingPath.removeLast()
}
let box: Box
// You can't decode sequences from attributes, but other strategies
// need special handling for empty sequences.
if strategy(key) != .attribute && elements.isEmpty,
let empty = (type as? AnySequence.Type)?.init() as? T
{
return empty
}
// If we are looking at a coding key value intrinsic where the expected type is `String` and
// the value is empty, return `""`.
if strategy(key) != .attribute, elements.isEmpty, attributes.isEmpty, type == String.self, key.stringValue == "", let emptyString = "" as? T {
return emptyString
}
switch strategy(key) {
case .attribute:
guard
let attributeBox = attributes.first
else {
throw DecodingError.keyNotFound(key, DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription:
"""
No attribute found for key \(_errorDescription(of: key)).
"""
))
}
box = attributeBox
case .element:
box = elements
case .elementOrAttribute:
guard
let anyBox = elements.isEmpty ? attributes.first : elements as Box?
else {
throw DecodingError.keyNotFound(key, DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription:
"""
No attribute or element found for key \
\(_errorDescription(of: key)).
"""
))
}
box = anyBox
}
let value: T?
if !(type is AnySequence.Type), let unkeyedBox = box as? UnkeyedBox,
let first = unkeyedBox.first
{
// Handle case where we have held onto a `SingleKeyedBox`
if let singleKeyed = first as? SingleKeyedBox {
if singleKeyed.element.isNull {
value = try decoder.unbox(singleKeyed)
} else {
value = try decoder.unbox(singleKeyed.element)
}
} else {
value = try decoder.unbox(first)
}
} else {
value = try decoder.unbox(box)
}
if value == nil, let type = type as? AnyOptional.Type,
let result = type.init() as? T
{
return result
}
guard let unwrapped = value else {
throw DecodingError.valueNotFound(type, DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription:
"Expected \(type) value but found null instead."
))
}
return unwrapped
}
private func _superDecoder(forKey key: CodingKey) throws -> Decoder {
decoder.codingPath.append(key)
defer { decoder.codingPath.removeLast() }
let elements = container.withShared { keyedBox in
keyedBox.elements[key.stringValue]
}
let attributes = container.withShared { keyedBox in
keyedBox.attributes[key.stringValue]
}
let box: Box = elements.first ?? attributes.first ?? NullBox()
return XMLDecoderImplementation(
referencing: box,
options: decoder.options,
nodeDecodings: decoder.nodeDecodings,
codingPath: decoder.codingPath
)
}
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/21/17.
//
import Foundation
struct XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer {
// MARK: Properties
/// A reference to the decoder we're reading from.
private let decoder: XMLDecoderImplementation
/// A reference to the container we're reading from.
private let container: SharedBox<UnkeyedBox>
/// The path of coding keys taken to get to this point in decoding.
public private(set) var codingPath: [CodingKey]
/// The index of the element we're about to decode.
public private(set) var currentIndex: Int
// MARK: - Initialization
/// Initializes `self` by referencing the given decoder and container.
init(referencing decoder: XMLDecoderImplementation, wrapping container: SharedBox<UnkeyedBox>) {
self.decoder = decoder
self.container = container
codingPath = decoder.codingPath
currentIndex = 0
}
// MARK: - UnkeyedDecodingContainer Methods
public var count: Int? {
return container.withShared { unkeyedBox in
unkeyedBox.count
}
}
public var isAtEnd: Bool {
return currentIndex >= count!
}
public mutating func decodeNil() throws -> Bool {
guard !isAtEnd else {
throw DecodingError.valueNotFound(Any?.self, DecodingError.Context(
codingPath: decoder.codingPath + [XMLKey(index: currentIndex)],
debugDescription: "Unkeyed container is at end."
))
}
let isNull = container.withShared { unkeyedBox in
unkeyedBox[self.currentIndex].isNull
}
if isNull {
currentIndex += 1
return true
} else {
return false
}
}
public mutating func decode<T: Decodable>(_ type: T.Type) throws -> T {
return try decode(type) { decoder, box in
try decoder.unbox(box)
}
}
private mutating func decode<T: Decodable>(
_ type: T.Type,
decode: (XMLDecoderImplementation, Box) throws -> T?
) throws -> T {
decoder.codingPath.append(XMLKey(index: currentIndex))
let nodeDecodings = decoder.options.nodeDecodingStrategy.nodeDecodings(
forType: T.self,
with: decoder
)
decoder.nodeDecodings.append(nodeDecodings)
defer {
_ = decoder.nodeDecodings.removeLast()
_ = decoder.codingPath.removeLast()
}
guard !isAtEnd else {
throw DecodingError.valueNotFound(type, DecodingError.Context(
codingPath: decoder.codingPath + [XMLKey(index: currentIndex)],
debugDescription: "Unkeyed container is at end."
))
}
decoder.codingPath.append(XMLKey(index: currentIndex))
defer { self.decoder.codingPath.removeLast() }
let box = container.withShared { unkeyedBox in
unkeyedBox[self.currentIndex]
}
var value: T?
if let singleKeyed = box as? SingleKeyedBox {
do {
value = try decode(decoder, singleKeyed)
} catch {
do {
// Drill down to the element in the case of an nested unkeyed element
value = try decode(decoder, singleKeyed.element)
} catch {
// Specialize for choice elements
value = try decode(decoder, ChoiceBox(key: singleKeyed.key, element: singleKeyed.element))
}
}
} else {
value = try decode(decoder, box)
}
defer { currentIndex += 1 }
if value == nil, let type = type as? AnyOptional.Type,
let result = type.init() as? T
{
return result
}
guard let decoded: T = value else {
throw DecodingError.valueNotFound(type, DecodingError.Context(
codingPath: decoder.codingPath + [XMLKey(index: currentIndex)],
debugDescription: "Expected \(type) but found null instead."
))
}
return decoded
}
public mutating func nestedContainer<NestedKey>(
keyedBy _: NestedKey.Type
) throws -> KeyedDecodingContainer<NestedKey> {
decoder.codingPath.append(XMLKey(index: currentIndex))
defer { self.decoder.codingPath.removeLast() }
guard !isAtEnd else {
throw DecodingError.valueNotFound(
KeyedDecodingContainer<NestedKey>.self, DecodingError.Context(
codingPath: codingPath,
debugDescription: "Cannot get nested keyed container -- unkeyed container is at end."
)
)
}
let value = self.container.withShared { unkeyedBox in
unkeyedBox[self.currentIndex]
}
guard !value.isNull else {
throw DecodingError.valueNotFound(KeyedDecodingContainer<NestedKey>.self, DecodingError.Context(
codingPath: codingPath,
debugDescription: "Cannot get keyed decoding container -- found null value instead."
))
}
guard let keyedContainer = value as? SharedBox<KeyedBox> else {
throw DecodingError.typeMismatch(at: codingPath,
expectation: [String: Any].self,
reality: value)
}
currentIndex += 1
let container = XMLKeyedDecodingContainer<NestedKey>(
referencing: decoder,
wrapping: keyedContainer
)
return KeyedDecodingContainer(container)
}
public mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
decoder.codingPath.append(XMLKey(index: currentIndex))
defer { self.decoder.codingPath.removeLast() }
guard !isAtEnd else {
throw DecodingError.valueNotFound(
UnkeyedDecodingContainer.self, DecodingError.Context(
codingPath: codingPath,
debugDescription: "Cannot get nested keyed container -- unkeyed container is at end."
)
)
}
let value = container.withShared { unkeyedBox in
unkeyedBox[self.currentIndex]
}
guard !value.isNull else {
throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self, DecodingError.Context(
codingPath: codingPath,
debugDescription: "Cannot get keyed decoding container -- found null value instead."
))
}
guard let unkeyedContainer = value as? SharedBox<UnkeyedBox> else {
throw DecodingError.typeMismatch(at: codingPath,
expectation: UnkeyedBox.self,
reality: value)
}
currentIndex += 1
return XMLUnkeyedDecodingContainer(referencing: decoder, wrapping: unkeyedContainer)
}
public mutating func superDecoder() throws -> Decoder {
decoder.codingPath.append(XMLKey(index: currentIndex))
defer { self.decoder.codingPath.removeLast() }
guard !isAtEnd else {
throw DecodingError.valueNotFound(Decoder.self, DecodingError.Context(
codingPath: codingPath,
debugDescription: "Cannot get superDecoder() -- unkeyed container is at end."
))
}
let value = container.withShared { unkeyedBox in
unkeyedBox[self.currentIndex]
}
currentIndex += 1
return XMLDecoderImplementation(
referencing: value,
options: decoder.options,
nodeDecodings: decoder.nodeDecodings,
codingPath: decoder.codingPath
)
}
}
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Joseph Mattiello on 1/24/19.
//
public protocol DynamicNodeEncoding: Encodable {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding
}
extension Array: DynamicNodeEncoding where Element: DynamicNodeEncoding {
public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
return Element.nodeEncoding(for: key)
}
}
public extension DynamicNodeEncoding where Self: Collection, Self.Iterator.Element: DynamicNodeEncoding {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
return Element.nodeEncoding(for: key)
}
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/22/17.
//
import Foundation
/// Error Utilities
extension EncodingError {
/// Returns a `.invalidValue` error describing the given invalid floating-point value.
///
///
/// - parameter value: The value that was invalid to encode.
/// - parameter path: The path of `CodingKey`s taken to encode this value.
/// - returns: An `EncodingError` with the appropriate path and debug description.
static func _invalidFloatingPointValue<T: FloatingPoint>(_ value: T, at codingPath: [CodingKey]) -> EncodingError {
let valueDescription: String
if value == T.infinity {
valueDescription = "\(T.self).infinity"
} else if value == -T.infinity {
valueDescription = "-\(T.self).infinity"
} else {
valueDescription = "\(T.self).nan"
}
let debugDescription = """
Unable to encode \(valueDescription) directly in XML. \
Use XMLEncoder.NonConformingFloatEncodingStrategy.convertToString \
to specify how the value should be encoded.
"""
return .invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: debugDescription))
}
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/22/17.
//
import Foundation
extension XMLEncoderImplementation: SingleValueEncodingContainer {
// MARK: - SingleValueEncodingContainer Methods
func assertCanEncodeNewValue() {
precondition(
canEncodeNewValue,
"""
Attempt to encode value through single value container when \
previously value already encoded.
"""
)
}
public func encodeNil() throws {
assertCanEncodeNewValue()
storage.push(container: box())
}
public func encode(_ value: Bool) throws {
assertCanEncodeNewValue()
storage.push(container: box(value))
}
public func encode(_ value: Int) throws {
assertCanEncodeNewValue()
storage.push(container: box(value))
}
public func encode(_ value: Int8) throws {
assertCanEncodeNewValue()
storage.push(container: box(value))
}
public func encode(_ value: Int16) throws {
assertCanEncodeNewValue()
storage.push(container: box(value))
}
public func encode(_ value: Int32) throws {
assertCanEncodeNewValue()
storage.push(container: box(value))
}
public func encode(_ value: Int64) throws {
assertCanEncodeNewValue()
storage.push(container: box(value))
}
public func encode(_ value: UInt) throws {
assertCanEncodeNewValue()
storage.push(container: box(value))
}
public func encode(_ value: UInt8) throws {
assertCanEncodeNewValue()
storage.push(container: box(value))
}
public func encode(_ value: UInt16) throws {
assertCanEncodeNewValue()
storage.push(container: box(value))
}
public func encode(_ value: UInt32) throws {
assertCanEncodeNewValue()
storage.push(container: box(value))
}
public func encode(_ value: UInt64) throws {
assertCanEncodeNewValue()
storage.push(container: box(value))
}
public func encode(_ value: String) throws {
assertCanEncodeNewValue()
storage.push(container: box(value))
}
public func encode(_ value: Float) throws {
assertCanEncodeNewValue()
try storage.push(container: box(value))
}
public func encode(_ value: Double) throws {
assertCanEncodeNewValue()
try storage.push(container: box(value))
}
public func encode<T: Encodable>(_ value: T) throws {
assertCanEncodeNewValue()
try storage.push(container: box(value))
}
}
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Benjamin Wetherfield on 7/17/19.
//
struct XMLChoiceEncodingContainer<K: CodingKey>: KeyedEncodingContainerProtocol {
typealias Key = K
// MARK: Properties
/// A reference to the encoder we're writing to.
private let encoder: XMLEncoderImplementation
/// A reference to the container we're writing to.
private var container: SharedBox<ChoiceBox>
/// The path of coding keys taken to get to this point in encoding.
public private(set) var codingPath: [CodingKey]
// MARK: - Initialization
/// Initializes `self` with the given references.
init(
referencing encoder: XMLEncoderImplementation,
codingPath: [CodingKey],
wrapping container: SharedBox<ChoiceBox>
) {
self.encoder = encoder
self.codingPath = codingPath
self.container = container
}
// MARK: - Coding Path Operations
private func converted(_ key: CodingKey) -> CodingKey {
switch encoder.options.keyEncodingStrategy {
case .useDefaultKeys:
return key
case .convertToSnakeCase:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToSnakeCase(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
case .convertToKebabCase:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToKebabCase(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
case let .custom(converter):
return converter(codingPath + [key])
case .capitalized:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToCapitalized(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
case .uppercased:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToUppercased(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
case .lowercased:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToLowercased(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
}
}
// MARK: - KeyedEncodingContainerProtocol Methods
public mutating func encodeNil(forKey key: Key) throws {
container.withShared {
$0.key = converted(key).stringValue
$0.element = NullBox()
}
}
public mutating func encode<T: Encodable>(
_ value: T,
forKey key: Key
) throws {
return try encode(value, forKey: key) { encoder, value in
try encoder.box(value)
}
}
private mutating func encode<T: Encodable>(
_ value: T,
forKey key: Key,
encode: (XMLEncoderImplementation, T) throws -> Box
) throws {
defer {
_ = self.encoder.nodeEncodings.removeLast()
self.encoder.codingPath.removeLast()
}
encoder.codingPath.append(key)
let nodeEncodings = encoder.options.nodeEncodingStrategy.nodeEncodings(
forType: T.self,
with: encoder
)
encoder.nodeEncodings.append(nodeEncodings)
let box = try encode(encoder, value)
let oldSelf = self
let elementEncoder: (T, Key, Box) throws -> () = { _, key, box in
oldSelf.container.withShared { container in
container.element = box
container.key = oldSelf.converted(key).stringValue
}
}
defer {
self = oldSelf
}
try elementEncoder(value, key, box)
}
public mutating func nestedContainer<NestedKey>(
keyedBy _: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> {
if NestedKey.self is XMLChoiceCodingKey.Type {
return nestedChoiceContainer(keyedBy: NestedKey.self, forKey: key)
} else {
return nestedKeyedContainer(keyedBy: NestedKey.self, forKey: key)
}
}
mutating func nestedKeyedContainer<NestedKey>(
keyedBy _: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> {
let sharedKeyed = SharedBox(KeyedBox())
self.container.withShared { container in
container.element = sharedKeyed
container.key = converted(key).stringValue
}
codingPath.append(key)
defer { self.codingPath.removeLast() }
let container = XMLKeyedEncodingContainer<NestedKey>(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedKeyed
)
return KeyedEncodingContainer(container)
}
mutating func nestedChoiceContainer<NestedKey>(
keyedBy _: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> {
let sharedChoice = SharedBox(ChoiceBox())
self.container.withShared { container in
container.element = sharedChoice
container.key = converted(key).stringValue
}
codingPath.append(key)
defer { self.codingPath.removeLast() }
let container = XMLChoiceEncodingContainer<NestedKey>(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedChoice
)
return KeyedEncodingContainer(container)
}
public mutating func nestedUnkeyedContainer(
forKey key: Key
) -> UnkeyedEncodingContainer {
let sharedUnkeyed = SharedBox(UnkeyedBox())
container.withShared { container in
container.element = sharedUnkeyed
container.key = converted(key).stringValue
}
codingPath.append(key)
defer { self.codingPath.removeLast() }
return XMLUnkeyedEncodingContainer(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedUnkeyed
)
}
public mutating func superEncoder() -> Encoder {
return XMLReferencingEncoder(
referencing: encoder,
key: XMLKey.super,
convertedKey: converted(XMLKey.super),
wrapping: container
)
}
public mutating func superEncoder(forKey key: Key) -> Encoder {
return XMLReferencingEncoder(
referencing: encoder,
key: key,
convertedKey: converted(key),
wrapping: container
)
}
}
// Copyright © 2017-2020 Shawn Moore and XMLCoder contributors.
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/22/17.
//
import Foundation
//===----------------------------------------------------------------------===//
// XML Encoder
//===----------------------------------------------------------------------===//
/// `XMLEncoder` facilitates the encoding of `Encodable` values into XML.
open class XMLEncoder {
// MARK: Options
/// The formatting of the output XML data.
public struct OutputFormatting: OptionSet {
/// The format's default value.
public let rawValue: UInt
/// Creates an OutputFormatting value with the given raw value.
public init(rawValue: UInt) {
self.rawValue = rawValue
}
/// Produce human-readable XML with indented output.
public static let prettyPrinted = OutputFormatting(rawValue: 1 << 0)
/// Produce XML with keys sorted in lexicographic order.
public static let sortedKeys = OutputFormatting(rawValue: 1 << 1)
}
/// The identation to use when XML is pretty-printed.
public enum PrettyPrintIndentation {
case spaces(Int)
case tabs(Int)
}
/// A node's encoding type
public enum NodeEncoding {
case attribute
case element
case both
public static let `default`: NodeEncoding = .element
}
/// The strategy to use for encoding `Date` values.
public enum DateEncodingStrategy {
/// Defer to `Date` for choosing an encoding. This is the default strategy.
case deferredToDate
/// Encode the `Date` as a UNIX timestamp (as a XML number).
case secondsSince1970
/// Encode the `Date` as UNIX millisecond timestamp (as a XML number).
case millisecondsSince1970
/// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
case iso8601
/// Encode the `Date` as a string formatted by the given formatter.
case formatted(DateFormatter)
/// Encode the `Date` as a custom value encoded by the given closure.
///
/// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
case custom((Date, Encoder) throws -> ())
}
/// The strategy to use for encoding `String` values.
public enum StringEncodingStrategy {
/// Defer to `String` for choosing an encoding. This is the default strategy.
case deferredToString
/// Encode the `String` as a CData-encoded string.
case cdata
}
/// The strategy to use for encoding `Data` values.
public enum DataEncodingStrategy {
/// Defer to `Data` for choosing an encoding.
case deferredToData
/// Encoded the `Data` as a Base64-encoded string. This is the default strategy.
case base64
/// Encode the `Data` as a custom value encoded by the given closure.
///
/// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
case custom((Data, Encoder) throws -> ())
}
/// The strategy to use for non-XML-conforming floating-point values (IEEE 754 infinity and NaN).
public enum NonConformingFloatEncodingStrategy {
/// Throw upon encountering non-conforming values. This is the default strategy.
case `throw`
/// Encode the values using the given representation strings.
case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String)
}
/// The strategy to use for automatically changing the value of keys before encoding.
public enum KeyEncodingStrategy {
/// Use the keys specified by each type. This is the default strategy.
case useDefaultKeys
/// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to XML payload.
///
/// Capital characters are determined by testing membership in
/// `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters`
/// (Unicode General Categories Lu and Lt).
/// The conversion to lower case uses `Locale.system`, also known as
/// the ICU "root" locale. This means the result is consistent
/// regardless of the current user's locale and language preferences.
///
/// Converting from camel case to snake case:
/// 1. Splits words at the boundary of lower-case to upper-case
/// 2. Inserts `_` between words
/// 3. Lowercases the entire string
/// 4. Preserves starting and ending `_`.
///
/// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`.
///
/// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted.
case convertToSnakeCase
/// Same as convertToSnakeCase, but using `-` instead of `_`
/// For example, `oneTwoThree` becomes `one-two-three`.
case convertToKebabCase
/// Capitalize the first letter only
/// `oneTwoThree` becomes `OneTwoThree`
case capitalized
/// Uppercase ize all letters
/// `oneTwoThree` becomes `ONETWOTHREE`
case uppercased
/// Lowercase all letters
/// `oneTwoThree` becomes `onetwothree`
case lowercased
/// Provide a custom conversion to the key in the encoded XML from the
/// keys specified by the encoded types.
/// The full path to the current encoding position is provided for
/// context (in case you need to locate this key within the payload).
/// The returned key is used in place of the last component in the
/// coding path before encoding.
/// If the result of the conversion is a duplicate key, then only one
/// value will be present in the result.
case custom((_ codingPath: [CodingKey]) -> CodingKey)
static func _convertToSnakeCase(_ stringKey: String) -> String {
return _convert(stringKey, usingSeparator: "_")
}
static func _convertToKebabCase(_ stringKey: String) -> String {
return _convert(stringKey, usingSeparator: "-")
}
static func _convert(_ stringKey: String, usingSeparator separator: String) -> String {
guard !stringKey.isEmpty else {
return stringKey
}
var words: [Range<String.Index>] = []
// The general idea of this algorithm is to split words on
// transition from lower to upper case, then on transition of >1
// upper case characters to lowercase
//
// myProperty -> my_property
// myURLProperty -> my_url_property
//
// We assume, per Swift naming conventions, that the first character of the key is lowercase.
var wordStart = stringKey.startIndex
var searchRange = stringKey.index(after: wordStart)..<stringKey.endIndex
// Find next uppercase character
while let upperCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.uppercaseLetters, options: [], range: searchRange) {
let untilUpperCase = wordStart..<upperCaseRange.lowerBound
words.append(untilUpperCase)
// Find next lowercase character
searchRange = upperCaseRange.lowerBound..<searchRange.upperBound
guard let lowerCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.lowercaseLetters, options: [], range: searchRange) else {
// There are no more lower case letters. Just end here.
wordStart = searchRange.lowerBound
break
}
// Is the next lowercase letter more than 1 after the uppercase?
// If so, we encountered a group of uppercase letters that we
// should treat as its own word
let nextCharacterAfterCapital = stringKey.index(after: upperCaseRange.lowerBound)
if lowerCaseRange.lowerBound == nextCharacterAfterCapital {
// The next character after capital is a lower case character and therefore not a word boundary.
// Continue searching for the next upper case for the boundary.
wordStart = upperCaseRange.lowerBound
} else {
// There was a range of >1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound)
words.append(upperCaseRange.lowerBound..<beforeLowerIndex)
// Next word starts at the capital before the lowercase we just found
wordStart = beforeLowerIndex
}
searchRange = lowerCaseRange.upperBound..<searchRange.upperBound
}
words.append(wordStart..<searchRange.upperBound)
let result = words.map { range in
stringKey[range].lowercased()
}.joined(separator: separator)
return result
}
static func _convertToCapitalized(_ stringKey: String) -> String {
return stringKey.capitalizingFirstLetter()
}
static func _convertToLowercased(_ stringKey: String) -> String {
return stringKey.lowercased()
}
static func _convertToUppercased(_ stringKey: String) -> String {
return stringKey.uppercased()
}
}
@available(*, deprecated, renamed: "NodeEncodingStrategy")
public typealias NodeEncodingStrategies = NodeEncodingStrategy
public typealias XMLNodeEncoderClosure = ((CodingKey) -> NodeEncoding)
public typealias XMLEncodingClosure = (Encodable.Type, Encoder) -> XMLNodeEncoderClosure
/// Set of strategies to use for encoding of nodes.
public enum NodeEncodingStrategy {
/// Defer to `Encoder` for choosing an encoding. This is the default strategy.
case deferredToEncoder
/// Return a closure computing the desired node encoding for the value by its coding key.
case custom(XMLEncodingClosure)
func nodeEncodings(
forType codableType: Encodable.Type,
with encoder: Encoder
) -> ((CodingKey) -> NodeEncoding) {
return encoderClosure(codableType, encoder)
}
var encoderClosure: XMLEncodingClosure {
switch self {
case .deferredToEncoder: return NodeEncodingStrategy.defaultEncoder
case let .custom(closure): return closure
}
}
static let defaultEncoder: XMLEncodingClosure = { codableType, _ in
guard let dynamicType = codableType as? DynamicNodeEncoding.Type else {
return { _ in .default }
}
return dynamicType.nodeEncoding(for:)
}
}
/// Characters and their escaped representations to be escaped in attributes
open var charactersEscapedInAttributes = [
("&", "&amp;"),
("<", "&lt;"),
(">", "&gt;"),
("'", "&apos;"),
("\"", "&quot;"),
]
/// Characters and their escaped representations to be escaped in elements
open var charactersEscapedInElements = [
("&", "&amp;"),
("<", "&lt;"),
(">", "&gt;"),
("'", "&apos;"),
("\"", "&quot;"),
]
/// The output format to produce. Defaults to `[]`.
open var outputFormatting: OutputFormatting = []
/// The indentation to use when XML is printed. Defaults to `.spaces(4)`.
open var prettyPrintIndentation: PrettyPrintIndentation = .spaces(4)
/// The strategy to use in encoding dates. Defaults to `.deferredToDate`.
open var dateEncodingStrategy: DateEncodingStrategy = .deferredToDate
/// The strategy to use in encoding binary data. Defaults to `.base64`.
open var dataEncodingStrategy: DataEncodingStrategy = .base64
/// The strategy to use in encoding non-conforming numbers. Defaults to `.throw`.
open var nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy = .throw
/// The strategy to use for encoding keys. Defaults to `.useDefaultKeys`.
open var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys
/// The strategy to use in encoding encoding attributes. Defaults to `.deferredToEncoder`.
open var nodeEncodingStrategy: NodeEncodingStrategy = .deferredToEncoder
/// The strategy to use in encoding strings. Defaults to `.deferredToString`.
open var stringEncodingStrategy: StringEncodingStrategy = .deferredToString
/// Contextual user-provided information for use during encoding.
open var userInfo: [CodingUserInfoKey: Any] = [:]
/// Options set on the top-level encoder to pass down the encoding hierarchy.
struct Options {
let dateEncodingStrategy: DateEncodingStrategy
let dataEncodingStrategy: DataEncodingStrategy
let nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy
let keyEncodingStrategy: KeyEncodingStrategy
let nodeEncodingStrategy: NodeEncodingStrategy
let stringEncodingStrategy: StringEncodingStrategy
let userInfo: [CodingUserInfoKey: Any]
}
/// The options set on the top-level encoder.
var options: Options {
return Options(dateEncodingStrategy: dateEncodingStrategy,
dataEncodingStrategy: dataEncodingStrategy,
nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy,
keyEncodingStrategy: keyEncodingStrategy,
nodeEncodingStrategy: nodeEncodingStrategy,
stringEncodingStrategy: stringEncodingStrategy,
userInfo: userInfo)
}
// MARK: - Constructing a XML Encoder
/// Initializes `self` with default strategies.
public init() {}
// MARK: - Encoding Values
/// Encodes the given top-level value and returns its XML representation.
///
/// - parameter value: The value to encode.
/// - parameter withRootKey: the key used to wrap the encoded values. The
/// default value is inferred from the name of the root type.
/// - parameter rootAttributes: the list of attributes to be added to the root node
/// - returns: A new `Data` value containing the encoded XML data.
/// - throws: `EncodingError.invalidValue` if a non-conforming
/// floating-point value is encountered during encoding, and the encoding
/// strategy is `.throw`.
/// - throws: An error if any value throws an error during encoding.
open func encode<T: Encodable>(_ value: T,
withRootKey rootKey: String? = nil,
rootAttributes: [String: String]? = nil,
header: XMLHeader? = nil) throws -> Data
{
let encoder = XMLEncoderImplementation(options: options, nodeEncodings: [])
encoder.nodeEncodings.append(options.nodeEncodingStrategy.nodeEncodings(forType: T.self, with: encoder))
let topLevel = try encoder.box(value)
let attributes = rootAttributes?.map(Attribute.init) ?? []
let elementOrNone: XMLCoderElement?
let rootKey = rootKey ?? "\(T.self)".convert(for: keyEncodingStrategy)
let isStringBoxCDATA = stringEncodingStrategy == .cdata
if let keyedBox = topLevel as? KeyedBox {
elementOrNone = XMLCoderElement(
key: rootKey,
isStringBoxCDATA: isStringBoxCDATA,
box: keyedBox,
attributes: attributes
)
} else if let unkeyedBox = topLevel as? UnkeyedBox {
elementOrNone = XMLCoderElement(
key: rootKey,
isStringBoxCDATA: isStringBoxCDATA,
box: unkeyedBox,
attributes: attributes
)
} else if let choiceBox = topLevel as? ChoiceBox {
elementOrNone = XMLCoderElement(
key: rootKey,
isStringBoxCDATA: isStringBoxCDATA,
box: choiceBox,
attributes: attributes
)
} else {
fatalError("Unrecognized top-level element of type: \(type(of: topLevel))")
}
guard let element = elementOrNone else {
throw EncodingError.invalidValue(value, EncodingError.Context(
codingPath: [],
debugDescription: "Unable to encode the given top-level value to XML."
))
}
return element.toXMLString(
with: header,
escapedCharacters: (
elements: charactersEscapedInElements,
attributes: charactersEscapedInAttributes
),
formatting: outputFormatting,
indentation: prettyPrintIndentation
).data(using: .utf8, allowLossyConversion: true)!
}
// MARK: - TopLevelEncoder
#if canImport(Combine) || canImport(OpenCombine)
open func encode<T>(_ value: T) throws -> Data where T: Encodable {
return try encode(value, withRootKey: nil, rootAttributes: nil, header: nil)
}
#endif
}
private extension String {
func convert(for encodingStrategy: XMLEncoder.KeyEncodingStrategy) -> String {
switch encodingStrategy {
case .useDefaultKeys:
return self
case .convertToSnakeCase:
return XMLEncoder.KeyEncodingStrategy._convertToSnakeCase(self)
case .convertToKebabCase:
return XMLEncoder.KeyEncodingStrategy._convertToKebabCase(self)
case .custom:
return self
case .capitalized:
return XMLEncoder.KeyEncodingStrategy._convertToCapitalized(self)
case .uppercased:
return XMLEncoder.KeyEncodingStrategy._convertToUppercased(self)
case .lowercased:
return XMLEncoder.KeyEncodingStrategy._convertToLowercased(self)
}
}
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/22/17.
//
import Foundation
class XMLEncoderImplementation: Encoder {
// MARK: Properties
/// The encoder's storage.
var storage: XMLEncodingStorage
/// Options set on the top-level encoder.
let options: XMLEncoder.Options
/// The path to the current point in encoding.
public var codingPath: [CodingKey]
public var nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding]
/// Contextual user-provided information for use during encoding.
public var userInfo: [CodingUserInfoKey: Any] {
return options.userInfo
}
// MARK: - Initialization
/// Initializes `self` with the given top-level encoder options.
init(
options: XMLEncoder.Options,
nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding],
codingPath: [CodingKey] = []
) {
self.options = options
storage = XMLEncodingStorage()
self.codingPath = codingPath
self.nodeEncodings = nodeEncodings
}
/// Returns whether a new element can be encoded at this coding path.
///
/// `true` if an element has not yet been encoded at this coding path; `false` otherwise.
var canEncodeNewValue: Bool {
// Every time a new value gets encoded, the key it's encoded for is
// pushed onto the coding path (even if it's a nil key from an unkeyed container).
// At the same time, every time a container is requested, a new value
// gets pushed onto the storage stack.
// If there are more values on the storage stack than on the coding path,
// it means the value is requesting more than one container, which
// violates the precondition.
//
// This means that anytime something that can request a new container
// goes onto the stack, we MUST push a key onto the coding path.
// Things which will not request containers do not need to have the
// coding path extended for them (but it doesn't matter if it is,
// because they will not reach here).
return storage.count == codingPath.count
}
// MARK: - Encoder Methods
public func container<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
guard canEncodeNewValue else {
return mergeWithExistingKeyedContainer(keyedBy: Key.self)
}
if Key.self is XMLChoiceCodingKey.Type {
return choiceContainer(keyedBy: Key.self)
} else {
return keyedContainer(keyedBy: Key.self)
}
}
public func unkeyedContainer() -> UnkeyedEncodingContainer {
// If an existing unkeyed container was already requested, return that one.
let topContainer: SharedBox<UnkeyedBox>
if canEncodeNewValue {
// We haven't yet pushed a container at this level; do so here.
topContainer = storage.pushUnkeyedContainer()
} else {
guard let container = storage.lastContainer as? SharedBox<UnkeyedBox> else {
preconditionFailure(
"""
Attempt to push new unkeyed encoding container when already previously encoded \
at this path.
"""
)
}
topContainer = container
}
return XMLUnkeyedEncodingContainer(referencing: self, codingPath: codingPath, wrapping: topContainer)
}
public func singleValueContainer() -> SingleValueEncodingContainer {
return self
}
private func keyedContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
let container = XMLKeyedEncodingContainer<Key>(
referencing: self,
codingPath: codingPath,
wrapping: storage.pushKeyedContainer()
)
return KeyedEncodingContainer(container)
}
private func choiceContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
let container = XMLChoiceEncodingContainer<Key>(
referencing: self,
codingPath: codingPath,
wrapping: storage.pushChoiceContainer()
)
return KeyedEncodingContainer(container)
}
private func mergeWithExistingKeyedContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
switch storage.lastContainer {
case let keyed as SharedBox<KeyedBox>:
let container = XMLKeyedEncodingContainer<Key>(
referencing: self,
codingPath: codingPath,
wrapping: keyed
)
return KeyedEncodingContainer(container)
case let choice as SharedBox<ChoiceBox>:
_ = storage.popContainer()
let keyed = KeyedBox(
elements: KeyedBox.Elements([choice.withShared { ($0.key, $0.element) }]),
attributes: []
)
let container = XMLKeyedEncodingContainer<Key>(
referencing: self,
codingPath: codingPath,
wrapping: storage.pushKeyedContainer(keyed)
)
return KeyedEncodingContainer(container)
default:
preconditionFailure(
"""
No existing keyed encoding container to merge with.
"""
)
}
}
}
extension XMLEncoderImplementation {
/// Returns the given value boxed in a container appropriate for pushing onto the container stack.
func box() -> SimpleBox {
return NullBox()
}
func box(_ value: Bool) -> SimpleBox {
return BoolBox(value)
}
func box(_ value: Decimal) -> SimpleBox {
return DecimalBox(value)
}
func box<T: BinaryInteger & SignedInteger & Encodable>(_ value: T) -> SimpleBox {
return IntBox(value)
}
func box<T: BinaryInteger & UnsignedInteger & Encodable>(_ value: T) -> SimpleBox {
return UIntBox(value)
}
func box(_ value: Float) throws -> SimpleBox {
return try box(value, FloatBox.self)
}
func box(_ value: Double) throws -> SimpleBox {
return try box(value, DoubleBox.self)
}
func box<T: BinaryFloatingPoint & Encodable, B: ValueBox>(
_ value: T,
_: B.Type
) throws -> SimpleBox where B.Unboxed == T {
guard value.isInfinite || value.isNaN else {
return B(value)
}
guard case let .convertToString(
positiveInfinity: posInfString,
negativeInfinity: negInfString,
nan: nanString
) = options.nonConformingFloatEncodingStrategy else {
throw EncodingError._invalidFloatingPointValue(value, at: codingPath)
}
if value == T.infinity {
return StringBox(posInfString)
} else if value == -T.infinity {
return StringBox(negInfString)
} else {
return StringBox(nanString)
}
}
func box(_ value: String) -> SimpleBox {
return StringBox(value)
}
func box(_ value: Date) throws -> Box {
switch options.dateEncodingStrategy {
case .deferredToDate:
try value.encode(to: self)
return storage.popContainer()
case .secondsSince1970:
return DateBox(value, format: .secondsSince1970)
case .millisecondsSince1970:
return DateBox(value, format: .millisecondsSince1970)
case .iso8601:
return DateBox(value, format: .iso8601)
case let .formatted(formatter):
return DateBox(value, format: .formatter(formatter))
case let .custom(closure):
let depth = storage.count
try closure(value, self)
guard storage.count > depth else {
return KeyedBox()
}
return storage.popContainer()
}
}
func box(_ value: Data) throws -> Box {
switch options.dataEncodingStrategy {
case .deferredToData:
try value.encode(to: self)
return storage.popContainer()
case .base64:
return DataBox(value, format: .base64)
case let .custom(closure):
let depth = storage.count
try closure(value, self)
guard storage.count > depth else {
return KeyedBox()
}
return storage.popContainer()
}
}
func box(_ value: URL) -> SimpleBox {
return URLBox(value)
}
func box<T: Encodable>(_ value: T) throws -> Box {
if T.self == Date.self || T.self == NSDate.self,
let value = value as? Date
{
return try box(value)
} else if T.self == Data.self || T.self == NSData.self,
let value = value as? Data
{
return try box(value)
} else if T.self == URL.self || T.self == NSURL.self,
let value = value as? URL
{
return box(value)
} else if T.self == Decimal.self || T.self == NSDecimalNumber.self,
let value = value as? Decimal
{
return box(value)
}
let depth = storage.count
try value.encode(to: self)
// The top container should be a new container.
guard storage.count > depth else {
return KeyedBox()
}
let lastContainer = storage.popContainer()
guard let sharedBox = lastContainer as? TypeErasedSharedBoxProtocol else {
return lastContainer
}
return sharedBox.typeErasedUnbox()
}
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/22/17.
//
import Foundation
// MARK: - Encoding Storage and Containers
struct XMLEncodingStorage {
// MARK: Properties
/// The container stack.
private var containers: [Box] = []
// MARK: - Initialization
/// Initializes `self` with no containers.
init() {}
// MARK: - Modifying the Stack
var count: Int {
return containers.count
}
var lastContainer: Box? {
return containers.last
}
mutating func pushKeyedContainer(_ keyedBox: KeyedBox = KeyedBox()) -> SharedBox<KeyedBox> {
let container = SharedBox(keyedBox)
containers.append(container)
return container
}
mutating func pushChoiceContainer() -> SharedBox<ChoiceBox> {
let container = SharedBox(ChoiceBox())
containers.append(container)
return container
}
mutating func pushUnkeyedContainer() -> SharedBox<UnkeyedBox> {
let container = SharedBox(UnkeyedBox())
containers.append(container)
return container
}
mutating func push(container: Box) {
if let keyedBox = container as? KeyedBox {
containers.append(SharedBox(keyedBox))
} else if let unkeyedBox = container as? UnkeyedBox {
containers.append(SharedBox(unkeyedBox))
} else {
containers.append(container)
}
}
mutating func popContainer() -> Box {
precondition(!containers.isEmpty, "Empty container stack.")
return containers.popLast()!
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 11/20/18.
//
import Foundation
struct XMLKeyedEncodingContainer<K: CodingKey>: KeyedEncodingContainerProtocol {
typealias Key = K
// MARK: Properties
/// A reference to the encoder we're writing to.
private let encoder: XMLEncoderImplementation
/// A reference to the container we're writing to.
private var container: SharedBox<KeyedBox>
/// The path of coding keys taken to get to this point in encoding.
public private(set) var codingPath: [CodingKey]
// MARK: - Initialization
/// Initializes `self` with the given references.
init(
referencing encoder: XMLEncoderImplementation,
codingPath: [CodingKey],
wrapping container: SharedBox<KeyedBox>
) {
self.encoder = encoder
self.codingPath = codingPath
self.container = container
}
// MARK: - Coding Path Operations
private func converted(_ key: CodingKey) -> CodingKey {
switch encoder.options.keyEncodingStrategy {
case .useDefaultKeys:
return key
case .convertToSnakeCase:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToSnakeCase(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
case .convertToKebabCase:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToKebabCase(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
case let .custom(converter):
return converter(codingPath + [key])
case .capitalized:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToCapitalized(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
case .uppercased:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToUppercased(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
case .lowercased:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToLowercased(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
}
}
// MARK: - KeyedEncodingContainerProtocol Methods
public mutating func encodeNil(forKey key: Key) throws {
container.withShared {
$0.elements.append(NullBox(), at: converted(key).stringValue)
}
}
public mutating func encode<T: Encodable>(
_ value: T,
forKey key: Key
) throws {
return try encode(value, forKey: key) { encoder, value in
try encoder.box(value)
}
}
private mutating func encode<T: Encodable>(
_ value: T,
forKey key: Key,
encode: (XMLEncoderImplementation, T) throws -> Box
) throws {
defer {
_ = self.encoder.nodeEncodings.removeLast()
self.encoder.codingPath.removeLast()
}
guard let strategy = encoder.nodeEncodings.last else {
preconditionFailure(
"Attempt to access node encoding strategy from empty stack."
)
}
encoder.codingPath.append(key)
let nodeEncodings = encoder.options.nodeEncodingStrategy.nodeEncodings(
forType: T.self,
with: encoder
)
encoder.nodeEncodings.append(nodeEncodings)
let box = try encode(encoder, value)
let oldSelf = self
let attributeEncoder: (T, Key, Box) throws -> () = { value, key, box in
guard let attribute = box as? SimpleBox else {
throw EncodingError.invalidValue(value, EncodingError.Context(
codingPath: [],
debugDescription: "Complex values cannot be encoded as attributes."
))
}
oldSelf.container.withShared { container in
container.attributes.append(attribute, at: oldSelf.converted(key).stringValue)
}
}
let elementEncoder: (T, Key, Box) throws -> () = { _, key, box in
oldSelf.container.withShared { container in
container.elements.append(box, at: oldSelf.converted(key).stringValue)
}
}
defer {
self = oldSelf
}
switch strategy(key) {
case .attribute:
try attributeEncoder(value, key, box)
case .element:
try elementEncoder(value, key, box)
case .both:
try attributeEncoder(value, key, box)
try elementEncoder(value, key, box)
}
}
public mutating func nestedContainer<NestedKey>(
keyedBy _: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> {
if NestedKey.self is XMLChoiceCodingKey.Type {
return nestedChoiceContainer(keyedBy: NestedKey.self, forKey: key)
} else {
return nestedKeyedContainer(keyedBy: NestedKey.self, forKey: key)
}
}
mutating func nestedKeyedContainer<NestedKey>(
keyedBy _: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> {
let sharedKeyed = SharedBox(KeyedBox())
self.container.withShared { container in
container.elements.append(sharedKeyed, at: converted(key).stringValue)
}
codingPath.append(key)
defer { self.codingPath.removeLast() }
let container = XMLKeyedEncodingContainer<NestedKey>(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedKeyed
)
return KeyedEncodingContainer(container)
}
mutating func nestedChoiceContainer<NestedKey>(
keyedBy _: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> {
let sharedChoice = SharedBox(ChoiceBox())
self.container.withShared { container in
container.elements.append(sharedChoice, at: converted(key).stringValue)
}
codingPath.append(key)
defer { self.codingPath.removeLast() }
let container = XMLChoiceEncodingContainer<NestedKey>(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedChoice
)
return KeyedEncodingContainer(container)
}
public mutating func nestedUnkeyedContainer(
forKey key: Key
) -> UnkeyedEncodingContainer {
let sharedUnkeyed = SharedBox(UnkeyedBox())
container.withShared { container in
container.elements.append(sharedUnkeyed, at: converted(key).stringValue)
}
codingPath.append(key)
defer { self.codingPath.removeLast() }
return XMLUnkeyedEncodingContainer(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedUnkeyed
)
}
public mutating func superEncoder() -> Encoder {
return XMLReferencingEncoder(
referencing: encoder,
key: XMLKey.super,
convertedKey: converted(XMLKey.super),
wrapping: container
)
}
public mutating func superEncoder(forKey key: Key) -> Encoder {
return XMLReferencingEncoder(
referencing: encoder,
key: key,
convertedKey: converted(key),
wrapping: container
)
}
}
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/25/17.
//
import Foundation
/// XMLReferencingEncoder is a special subclass of _XMLEncoder which has its
/// own storage, but references the contents of a different encoder.
/// It's used in superEncoder(), which returns a new encoder for encoding a
// superclass -- the lifetime of the encoder should not escape the scope it's
/// created in, but it doesn't necessarily know when it's done being used
/// (to write to the original container).
class XMLReferencingEncoder: XMLEncoderImplementation {
// MARK: Reference types.
/// The type of container we're referencing.
private enum Reference {
/// Referencing a specific index in an unkeyed container.
case unkeyed(SharedBox<UnkeyedBox>, Int)
/// Referencing a specific key in a keyed container.
case keyed(SharedBox<KeyedBox>, String)
/// Referencing a specific key in a keyed container.
case choice(SharedBox<ChoiceBox>, String)
}
// MARK: - Properties
/// The encoder we're referencing.
let encoder: XMLEncoderImplementation
/// The container reference itself.
private let reference: Reference
// MARK: - Initialization
/// Initializes `self` by referencing the given array container in the given encoder.
init(
referencing encoder: XMLEncoderImplementation,
at index: Int,
wrapping sharedUnkeyed: SharedBox<UnkeyedBox>
) {
self.encoder = encoder
reference = .unkeyed(sharedUnkeyed, index)
super.init(
options: encoder.options,
nodeEncodings: encoder.nodeEncodings,
codingPath: encoder.codingPath
)
codingPath.append(XMLKey(index: index))
}
/// Initializes `self` by referencing the given dictionary container in the given encoder.
init(
referencing encoder: XMLEncoderImplementation,
key: CodingKey,
convertedKey: CodingKey,
wrapping sharedKeyed: SharedBox<KeyedBox>
) {
self.encoder = encoder
reference = .keyed(sharedKeyed, convertedKey.stringValue)
super.init(
options: encoder.options,
nodeEncodings: encoder.nodeEncodings,
codingPath: encoder.codingPath
)
codingPath.append(key)
}
init(
referencing encoder: XMLEncoderImplementation,
key: CodingKey,
convertedKey: CodingKey,
wrapping sharedKeyed: SharedBox<ChoiceBox>
) {
self.encoder = encoder
reference = .choice(sharedKeyed, convertedKey.stringValue)
super.init(
options: encoder.options,
nodeEncodings: encoder.nodeEncodings,
codingPath: encoder.codingPath
)
codingPath.append(key)
}
// MARK: - Coding Path Operations
override var canEncodeNewValue: Bool {
// With a regular encoder, the storage and coding path grow together.
// A referencing encoder, however, inherits its parents coding path, as well as the key it was created for.
// We have to take this into account.
return storage.count == codingPath.count - encoder.codingPath.count - 1
}
// MARK: - Deinitialization
// Finalizes `self` by writing the contents of our storage to the referenced encoder's storage.
deinit {
let box: Box
switch self.storage.count {
case 0: box = KeyedBox()
case 1: box = self.storage.popContainer()
default: fatalError("Referencing encoder deallocated with multiple containers on stack.")
}
switch self.reference {
case let .unkeyed(sharedUnkeyedBox, index):
sharedUnkeyedBox.withShared { unkeyedBox in
unkeyedBox.insert(box, at: index)
}
case let .keyed(sharedKeyedBox, key):
sharedKeyedBox.withShared { keyedBox in
keyedBox.elements.append(box, at: key)
}
case let .choice(sharedChoiceBox, key):
sharedChoiceBox.withShared { choiceBox in
choiceBox.element = box
choiceBox.key = key
}
}
}
}
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 11/20/18.
//
import Foundation
struct XMLUnkeyedEncodingContainer: UnkeyedEncodingContainer {
// MARK: Properties
/// A reference to the encoder we're writing to.
private let encoder: XMLEncoderImplementation
/// A reference to the container we're writing to.
private let container: SharedBox<UnkeyedBox>
/// The path of coding keys taken to get to this point in encoding.
public private(set) var codingPath: [CodingKey]
/// The number of elements encoded into the container.
public var count: Int {
return container.withShared { $0.count }
}
// MARK: - Initialization
/// Initializes `self` with the given references.
init(
referencing encoder: XMLEncoderImplementation,
codingPath: [CodingKey],
wrapping container: SharedBox<UnkeyedBox>
) {
self.encoder = encoder
self.codingPath = codingPath
self.container = container
}
// MARK: - UnkeyedEncodingContainer Methods
public mutating func encodeNil() throws {
container.withShared { container in
container.append(encoder.box())
}
}
public mutating func encode<T: Encodable>(_ value: T) throws {
try encode(value) { encoder, value in
try encoder.box(value)
}
}
private mutating func encode<T: Encodable>(
_ value: T,
encode: (XMLEncoderImplementation, T) throws -> Box
) rethrows {
encoder.codingPath.append(XMLKey(index: count))
defer { self.encoder.codingPath.removeLast() }
try container.withShared { container in
container.append(try encode(encoder, value))
}
}
public mutating func nestedContainer<NestedKey>(
keyedBy _: NestedKey.Type
) -> KeyedEncodingContainer<NestedKey> {
if NestedKey.self is XMLChoiceCodingKey.Type {
return nestedChoiceContainer(keyedBy: NestedKey.self)
} else {
return nestedKeyedContainer(keyedBy: NestedKey.self)
}
}
public mutating func nestedKeyedContainer<NestedKey>(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
codingPath.append(XMLKey(index: count))
defer { self.codingPath.removeLast() }
let sharedKeyed = SharedBox(KeyedBox())
self.container.withShared { container in
container.append(sharedKeyed)
}
let container = XMLKeyedEncodingContainer<NestedKey>(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedKeyed
)
return KeyedEncodingContainer(container)
}
public mutating func nestedChoiceContainer<NestedKey>(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
codingPath.append(XMLKey(index: count))
defer { self.codingPath.removeLast() }
let sharedChoice = SharedBox(ChoiceBox())
self.container.withShared { container in
container.append(sharedChoice)
}
let container = XMLChoiceEncodingContainer<NestedKey>(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedChoice
)
return KeyedEncodingContainer(container)
}
public mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
codingPath.append(XMLKey(index: count))
defer { self.codingPath.removeLast() }
let sharedUnkeyed = SharedBox(UnkeyedBox())
container.withShared { container in
container.append(sharedUnkeyed)
}
return XMLUnkeyedEncodingContainer(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedUnkeyed
)
}
public mutating func superEncoder() -> Encoder {
return XMLReferencingEncoder(
referencing: encoder,
at: count,
wrapping: container
)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment