petitlyrics

This commit is contained in:
eevee
2024-07-17 01:25:40 +03:00
parent 828f8d36bc
commit 4e6bf27c44
63 changed files with 5962 additions and 33 deletions
@@ -0,0 +1,98 @@
//
// XMLAttribute.swift
// XMLCoder
//
// Created by Benjamin Wetherfield on 6/3/20.
//
protocol XMLAttributeProtocol {}
/** Property wrapper specifying that a given property should be encoded and decoded as an XML attribute.
For example, this type
```swift
struct Book: Codable {
@Attribute var id: Int
}
```
will encode value `Book(id: 42)` as `<Book id="42"></Book>`. And vice versa,
it will decode the former into the latter.
*/
@propertyWrapper
public struct Attribute<Value>: XMLAttributeProtocol {
public var wrappedValue: Value
public init(_ wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
}
extension Attribute: Codable where Value: Codable {
public func encode(to encoder: Encoder) throws {
try wrappedValue.encode(to: encoder)
}
public init(from decoder: Decoder) throws {
try wrappedValue = .init(from: decoder)
}
}
extension Attribute: Equatable where Value: Equatable {}
extension Attribute: Hashable where Value: Hashable {}
extension Attribute: ExpressibleByIntegerLiteral where Value: ExpressibleByIntegerLiteral {
public typealias IntegerLiteralType = Value.IntegerLiteralType
public init(integerLiteral value: Value.IntegerLiteralType) {
wrappedValue = Value(integerLiteral: value)
}
}
extension Attribute: ExpressibleByUnicodeScalarLiteral where Value: ExpressibleByUnicodeScalarLiteral {
public init(unicodeScalarLiteral value: Value.UnicodeScalarLiteralType) {
wrappedValue = Value(unicodeScalarLiteral: value)
}
public typealias UnicodeScalarLiteralType = Value.UnicodeScalarLiteralType
}
extension Attribute: ExpressibleByExtendedGraphemeClusterLiteral where Value: ExpressibleByExtendedGraphemeClusterLiteral {
public typealias ExtendedGraphemeClusterLiteralType = Value.ExtendedGraphemeClusterLiteralType
public init(extendedGraphemeClusterLiteral value: Value.ExtendedGraphemeClusterLiteralType) {
wrappedValue = Value(extendedGraphemeClusterLiteral: value)
}
}
extension Attribute: ExpressibleByStringLiteral where Value: ExpressibleByStringLiteral {
public typealias StringLiteralType = Value.StringLiteralType
public init(stringLiteral value: Value.StringLiteralType) {
wrappedValue = Value(stringLiteral: value)
}
}
extension Attribute: ExpressibleByBooleanLiteral where Value: ExpressibleByBooleanLiteral {
public typealias BooleanLiteralType = Value.BooleanLiteralType
public init(booleanLiteral value: Value.BooleanLiteralType) {
wrappedValue = Value(booleanLiteral: value)
}
}
extension Attribute: ExpressibleByNilLiteral where Value: ExpressibleByNilLiteral {
public init(nilLiteral: ()) {
wrappedValue = Value(nilLiteral: ())
}
}
protocol XMLOptionalAttributeProtocol: XMLAttributeProtocol {
init()
}
extension Attribute: XMLOptionalAttributeProtocol where Value: AnyOptional {
init() {
wrappedValue = Value()
}
}
@@ -0,0 +1,53 @@
// 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
}
}
@@ -0,0 +1,32 @@
// 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()
}
}
@@ -0,0 +1,41 @@
// 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)
}
}
@@ -0,0 +1,57 @@
// 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
}
}
@@ -0,0 +1,99 @@
// 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
}
}
@@ -0,0 +1,62 @@
// 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
}
}
@@ -0,0 +1,49 @@
// 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
}
}
@@ -0,0 +1,78 @@
// 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
}
}
@@ -0,0 +1,59 @@
// 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
}
}
@@ -0,0 +1,57 @@
// 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)}"
}
}
@@ -0,0 +1,33 @@
// 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"
}
}
@@ -0,0 +1,35 @@
// 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
}
}
@@ -0,0 +1,25 @@
// 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
}
}
@@ -0,0 +1,39 @@
// 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
}
}
@@ -0,0 +1,62 @@
// 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
}
}
@@ -0,0 +1,44 @@
// 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
}
}
@@ -0,0 +1,19 @@
// 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
}
}
@@ -0,0 +1,13 @@
// 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)
}
@@ -0,0 +1,42 @@
//
// XMLElementNode.swift
// XMLCoder
//
// Created by Benjamin Wetherfield on 6/4/20.
//
protocol XMLElementProtocol {}
/** Property wrapper specifying that a given property should be encoded and decoded as an XML element.
For example, this type
```swift
struct Book: Codable {
@Element var id: Int
}
```
will encode value `Book(id: 42)` as `<Book><id>42</id></Book>`. And vice versa,
it will decode the former into the latter.
*/
@propertyWrapper
public struct Element<Value>: XMLElementProtocol {
public var wrappedValue: Value
public init(_ wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
}
extension Element: Codable where Value: Codable {
public func encode(to encoder: Encoder) throws {
try wrappedValue.encode(to: encoder)
}
public init(from decoder: Decoder) throws {
try wrappedValue = .init(from: decoder)
}
}
extension Element: Equatable where Value: Equatable {}
extension Element: Hashable where Value: Hashable {}
@@ -0,0 +1,43 @@
//
// XMLBothNode.swift
// XMLCoder
//
// Created by Benjamin Wetherfield on 6/7/20.
//
protocol XMLElementAndAttributeProtocol {}
/** Property wrapper specifying that a given property should be decoded from either an XML element
or an XML attribute. When encoding, the value will be present as both an attribute, and an element.
For example, this type
```swift
struct Book: Codable {
@ElementAndAttribute var id: Int
}
```
will encode value `Book(id: 42)` as `<Book id="42"><id>42</id></Book>`. It will decode both
`<Book><id>42</id></Book>` and `<Book id="42"></Book>` as `Book(id: 42)`.
*/
@propertyWrapper
public struct ElementAndAttribute<Value>: XMLElementAndAttributeProtocol {
public var wrappedValue: Value
public init(_ wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
}
extension ElementAndAttribute: Codable where Value: Codable {
public func encode(to encoder: Encoder) throws {
try wrappedValue.encode(to: encoder)
}
public init(from decoder: Decoder) throws {
try wrappedValue = .init(from: decoder)
}
}
extension ElementAndAttribute: Equatable where Value: Equatable {}
extension ElementAndAttribute: Hashable where Value: Hashable {}
@@ -0,0 +1,21 @@
// 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
}()
@@ -0,0 +1,82 @@
// 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)
}
mutating func reserveCapacity(_ capacity: Int) {
buffer.reserveCapacity(capacity)
keyMap.reserveCapacity(capacity)
}
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)]"
}
}
@@ -0,0 +1,31 @@
// 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. If you custom sequence type is not decoded correctly, try
/// making it confirm to `XMLDecodableSequence`. Default conformances for
/// `Array` and `Dictionary` are already provided by the XMLCoder library.
public protocol XMLDecodableSequence {
init()
}
extension Array: XMLDecodableSequence {}
extension Dictionary: XMLDecodableSequence {}
/// Type-erased protocol helper for a metatype check in generic `decode`
/// overload.
protocol AnyOptional {
init()
}
extension Optional: AnyOptional {
init() {
self = nil
}
}
@@ -0,0 +1,52 @@
// 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()
}
}
extension String {
func isAllWhitespace() -> Bool {
return unicodeScalars.allSatisfy(CharacterSet.whitespacesAndNewlines.contains)
}
}
@@ -0,0 +1,13 @@
// Copyright (c) 2018-2023 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Alkenso (Vladimir Vashurkin) on 08.06.2023.
//
import Foundation
extension CodingKey {
internal var isInlined: Bool { stringValue == "" }
}
@@ -0,0 +1,59 @@
// 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-typelike 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 {}
@@ -0,0 +1,448 @@
// 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 XMLCoderElement: Equatable {
struct Attribute: Equatable {
let key: String
let value: String
}
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
}
private var isInlined: Bool {
return key.isEmpty
}
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) {
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
}
mutating func trimTextNodes() {
guard containsTextNodes else { return }
for idx in elements.indices {
elements[idx].stringValue = elements[idx].stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
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)
})
var storage = KeyedStorage<String, Box>()
// storage.reserveCapacity(self.elements)
for element in self.elements {
let hasElements = !element.elements.isEmpty
let hasAttributes = !element.attributes.isEmpty
let hasText = element.stringValue != nil
if hasElements || hasAttributes {
storage.append(element.transformToBoxTree(), at: element.key)
} else if hasText {
storage.append(element.transformToBoxTree(), at: element.key)
} else {
storage.append(SingleKeyedBox(key: element.key, element: NullBox()), at: element.key)
}
}
return KeyedBox(elements: storage, attributes: attributes)
}
func toXMLString(
with header: XMLHeader?,
doctype: XMLDocumentType?,
escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]),
formatting: XMLEncoder.OutputFormatting,
indentation: XMLEncoder.PrettyPrintIndentation
) -> String {
var base = ""
if let header = header, let headerXML = header.toXML() {
base += headerXML
}
if let doctype = doctype {
base += doctype.toXML()
}
return base + _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 = ""
let indentLevel = isInlined ? level : level + 1
string += element._toXMLString(indented: indentLevel, escapedCharacters, formatting, indentation)
string += prettyPrinted && !isInlined ? "\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 attributesBelongingToContainer = self.elements.filter {
$0.key.isEmpty && !$0.attributes.isEmpty
}.flatMap {
$0.attributes
}
let allAttributes = self.attributes + attributesBelongingToContainer
let attributes = formatting.contains(.sortedKeys) ?
allAttributes.sorted(by: { $0.key < $1.key }) :
allAttributes
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 && !isInlined:
prefix = String(repeating: " ", count: level * count)
case let .tabs(count) where prettyPrinted && !isInlined:
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 || formatting.contains(.noEmptyElements) {
let prettyPrintElements = prettyPrinted && !containsTextNodes
if !key.isEmpty {
string += prettyPrintElements ? ">\n" : ">"
}
if !elements.isEmpty {
formatXMLElements(escapedCharacters, formatting, indentation, &string, level, prettyPrintElements)
}
if prettyPrintElements { string += prefix }
if !key.isEmpty {
string += "</\(key)>"
}
} else {
if !key.isEmpty {
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))")
}
}
}
extension XMLCoderElement {
func isWhitespaceWithNoElements() -> Bool {
let stringValueIsWhitespaceOrNil = stringValue?.isAllWhitespace() ?? true
return self.key == "" && stringValueIsWhitespaceOrNil && self.elements.isEmpty
}
}
@@ -0,0 +1,65 @@
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Joannis Orlandos on 8/11/22.
//
import Foundation
public struct XMLDocumentType {
public enum External: String {
case `public` = "PUBLIC"
case system = "SYSTEM"
}
public let rootElement: String
public let external: External
public let dtdName: String?
public let dtdLocation: String
internal init(
rootElement: String,
external: External,
dtdName: String?,
dtdLocation: String
) {
self.rootElement = rootElement
self.external = external
self.dtdName = dtdName
self.dtdLocation = dtdLocation
}
public static func `public`(rootElement: String, dtdName: String, dtdLocation: String) -> XMLDocumentType {
XMLDocumentType(
rootElement: rootElement,
external: .public,
dtdName: dtdName,
dtdLocation: dtdLocation
)
}
public static func system(rootElement: String, dtdLocation: String) -> XMLDocumentType {
XMLDocumentType(
rootElement: rootElement,
external: .system,
dtdName: nil,
dtdLocation: dtdLocation
)
}
func toXML() -> String {
var string = "<!DOCTYPE \(rootElement) \(external.rawValue)"
if let dtdName = dtdName {
string += " \"\(dtdName)\""
}
string += " \"\(dtdLocation)\""
string += ">\n"
return string
}
}
@@ -0,0 +1,57 @@
// 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
/// Type that allows overriding XML header during encoding. Pass a value of this type to the `encode`
/// function of `XMLEncoder` to specify the exact value of the header you'd like to see in the encoded
/// data.
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
}
}
@@ -0,0 +1,38 @@
// 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")!
}
@@ -0,0 +1,200 @@
// 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
private let removeWhitespaceElements: Bool
init(trimValueWhitespaces: Bool = true, removeWhitespaceElements: Bool = false) {
self.trimValueWhitespaces = trimValueWhitespaces
self.removeWhitespaceElements = removeWhitespaceElements
super.init()
}
static func parse(
with data: Data,
errorContextLength length: UInt,
shouldProcessNamespaces: Bool,
trimValueWhitespaces: Bool,
removeWhitespaceElements: Bool
) throws -> Box {
let parser = XMLStackParser(trimValueWhitespaces: trimValueWhitespaces,
removeWhitespaceElements: removeWhitespaceElements)
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
}
let lowerBound = String.Index(utf16Offset: lowerBoundIndex, in: string)
let upperBound = String.Index(utf16Offset: upperBoundIndex, in: string)
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])
}
func trimWhitespacesIfNeeded(_ 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] = [:])
{
let attributes = attributeDict.map { key, value in
XMLCoderElement.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 var element = stack.popLast() else {
return
}
if trimValueWhitespaces && element.containsTextNodes {
element.trimTextNodes()
}
let updatedElement = removeWhitespaceElements ? elementWithFilteredElements(element: element) : element
withCurrentElement { currentElement in
currentElement.append(element: updatedElement)
}
if stack.isEmpty {
root = updatedElement
}
}
func elementWithFilteredElements(element: XMLCoderElement) -> XMLCoderElement {
var hasWhitespaceElements = false
var hasNonWhitespaceElements = false
var filteredElements: [XMLCoderElement] = []
for ele in element.elements {
if ele.isWhitespaceWithNoElements() {
hasWhitespaceElements = true
} else {
hasNonWhitespaceElements = true
filteredElements.append(ele)
}
}
if hasWhitespaceElements && hasNonWhitespaceElements {
return XMLCoderElement(key: element.key, elements: filteredElements, attributes: element.attributes)
}
return element
}
func parser(_: XMLParser, foundCharacters string: String) {
let processedString = trimWhitespacesIfNeeded(string)
guard processedString.count > 0, string.count != 0 else {
return
}
withCurrentElement { currentElement in
currentElement.append(string: string)
}
}
func parser(_: XMLParser, foundCDATA CDATABlock: Data) {
guard let string = String(data: CDATABlock, encoding: .utf8) else {
return
}
withCurrentElement { currentElement in
currentElement.append(cdata: string)
}
}
}
@@ -0,0 +1,54 @@
// 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: - 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))"
}
}
}
@@ -0,0 +1,44 @@
// 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.
//
/** Allows conforming types to specify how its properties will be decoded.
For example:
```swift
struct Book: Codable, Equatable, DynamicNodeDecoding {
let id: UInt
let title: String
let categories: [Category]
enum CodingKeys: String, CodingKey {
case id
case title
case categories = "category"
}
static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
switch key {
case Book.CodingKeys.id: return .attribute
default: return .element
}
}
}
```
allows XML of this form to be decoded into values of type `Book`:
```xml
<book id="123">
<title>Cat in the Hat</title>
<category>Kids</category>
<category>Wildlife</category>
</book>
```
*/
public protocol DynamicNodeDecoding: Decodable {
static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
}
@@ -0,0 +1,57 @@
// 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())
}
}
@@ -0,0 +1,105 @@
// 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> {
guard container.unboxed.key == key.stringValue else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: NestedKey.self,
reality: container
)
}
let value = container.unboxed.element
guard let container = XMLKeyedDecodingContainer<NestedKey>(box: value, decoder: decoder) else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [String: Any].self,
reality: value
)
}
return KeyedDecodingContainer(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
)
}
}
@@ -0,0 +1,405 @@
// 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: - 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
/// Convert from "CODING_KEY" to "codingKey"
case convertFromUppercase
/// 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 _convertFromUppercase(_ stringKey: String) -> String {
_convert(stringKey.lowercased(), usingSeparator: "_")
}
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 {
/// Decodes a node from attributes of form `nodeName="value"`.
case attribute
/// Decodes a node from elements of form `<nodeName>value</nodeName>`.
case element
/// Decodes a node from either elements of form `<nodeName>value</nodeName>` or attributes
/// of form `nodeName="value"`, with elements taking priority.
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 nil }
}
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
/** A boolean value that determines whether to remove pure whitespace elements
that have sibling elements that aren't pure whitespace. The default value
is `false`.
*/
open var removeWhitespaceElements: 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, removeWhitespaceElements: Bool = false) {
self.trimValueWhitespaces = trimValueWhitespaces
self.removeWhitespaceElements = removeWhitespaceElements
}
// 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,
removeWhitespaceElements: removeWhitespaceElements
)
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
@@ -0,0 +1,497 @@
// 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 .convertFromUppercase:
return XMLDecoder.KeyDecodingStrategy._convertFromUppercase
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
}
}
}
}
@@ -0,0 +1,52 @@
// 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()
}
}
@@ -0,0 +1,417 @@
// Copyright (c) 2017-2021 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? XMLDecodableSequence.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)\"
"""
))
}
guard let container = XMLKeyedDecodingContainer<NestedKey>(box: value, decoder: decoder) 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)
}
}
extension XMLKeyedDecodingContainer {
internal init?(box: Box, decoder: XMLDecoderImplementation) {
switch box {
case let keyedContainer as KeyedContainer:
self.init(
referencing: decoder,
wrapping: keyedContainer
)
case let keyedBox as KeyedBox:
self.init(
referencing: decoder,
wrapping: SharedBox(keyedBox)
)
case let singleBox as SingleKeyedBox:
let element = (singleBox.key, singleBox.element)
let keyedContainer = KeyedBox(elements: [element], attributes: [])
self.init(
referencing: decoder,
wrapping: SharedBox(keyedContainer)
)
default:
return nil
}
}
}
/// 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
return (key.isInlined ? keyedBox.elements.values : 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
key.isInlined ? keyedBox.attributes.values : 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()
}
// 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? XMLDecodableSequence.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 CDATA if present otherwise `""`.
if strategy(key) != .attribute, elements.isEmpty, attributes.isEmpty, type == String.self, key.stringValue == "", let emptyString = "" as? T {
let cdata = container.withShared { keyedBox in
keyedBox.elements["#CDATA"].map {
return ($0 as? KeyedBox)?.value ?? $0
}
}.first
return ((cdata as? StringBox)?.unboxed as? T) ?? emptyString
}
let box: Box
if key.isInlined {
box = container.typeErasedUnbox()
} else {
switch strategy(key) {
case .attribute?:
box = try getAttributeBox(for: type, attributes, key)
case .element?:
box = try getElementBox(for: type, elements, key)
case .elementOrAttribute?:
box = try getAttributeOrElementBox(attributes, elements, key)
default:
switch type {
case is XMLAttributeProtocol.Type:
box = try getAttributeBox(for: type, attributes, key)
case is XMLElementProtocol.Type:
box = try getElementBox(for: type, elements, key)
default:
box = try getAttributeOrElementBox(attributes, elements, key)
}
}
}
let value: T?
if !(type is XMLDecodableSequence.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 if box.isNull, let type = type as? XMLOptionalAttributeProtocol.Type, let nullAttribute = type.init() as? T {
value = nullAttribute
} 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 getAttributeBox<T: Decodable>(for type: T.Type, _ attributes: [KeyedBox.Attribute], _ key: Key) throws -> Box {
if let box = attributes.first { return box }
if type is AnyOptional.Type || type is XMLOptionalAttributeProtocol.Type { return NullBox() }
throw DecodingError.keyNotFound(key, DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "No attribute found for key \(_errorDescription(of: key))."
))
}
private func getElementBox<T: Decodable>(for type: T.Type, _ elements: [KeyedBox.Element], _ key: Key) throws -> Box {
guard elements.isEmpty else { return elements }
if type is AnyOptional.Type || type is XMLDecodableSequence.Type { return elements }
throw DecodingError.keyNotFound(key, DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "No element found for key \(_errorDescription(of: key))."
))
}
private func getAttributeOrElementBox(_ attributes: [KeyedBox.Attribute], _ elements: [KeyedBox.Element], _ key: Key) throws -> Box {
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)).
"""
))
}
return anyBox
}
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
)
}
}
@@ -0,0 +1,232 @@
// 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
)
}
}
@@ -0,0 +1,57 @@
// 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.
//
/** Allows conforming types to specify how its properties will be encoded.
For example:
```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
}
}
}
```
produces XML of this form for values of type `Book`:
```xml
<book id="123">
<id>123</id>
<title>Cat in the Hat</title>
<category>Kids</category>
<category>Wildlife</category>
</book>
```
*/
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)
}
}
@@ -0,0 +1,36 @@
// 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))
}
}
@@ -0,0 +1,103 @@
// 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))
}
}
@@ -0,0 +1,208 @@
// 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
)
}
}
@@ -0,0 +1,445 @@
// Copyright © 2017-2021 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
/// `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)
/// Produce XML with no short-hand annotation for empty elements, e.g., use `<p></p>` over `</p>`
public static let noEmptyElements = OutputFormatting(rawValue: 1 << 2)
}
/// The indentation to use when XML is pretty-printed.
public enum PrettyPrintIndentation {
case spaces(Int)
case tabs(Int)
}
/// A node's encoding type. Specifies how a node will be encoded.
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 nil }
}
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
/// - parameter header: the XML header to start the encoded data with.
/// - 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,
doctype: XMLDocumentType? = 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(XMLCoderElement.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,
doctype: doctype,
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)
}
}
}
@@ -0,0 +1,292 @@
// 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()
}
}
@@ -0,0 +1,66 @@
// 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()!
}
}
@@ -0,0 +1,267 @@
// Copyright (c) 2018-2021 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)
default:
switch value {
case is XMLElementProtocol:
encodeElement(forKey: key, box: box)
case is XMLAttributeProtocol:
try encodeAttribute(value, forKey: key, box: box)
case is XMLElementAndAttributeProtocol:
try encodeAttribute(value, forKey: key, box: box)
encodeElement(forKey: key, box: box)
default:
encodeElement(forKey: key, box: box)
}
}
}
private mutating func encodeAttribute<T: Encodable>(
_ value: T,
forKey key: Key,
box: Box
) throws {
guard let attribute = box as? SimpleBox else {
throw EncodingError.invalidValue(value, EncodingError.Context(
codingPath: [],
debugDescription: "Complex values cannot be encoded as attributes."
))
}
container.withShared { container in
container.attributes.append(attribute, at: self.converted(key).stringValue)
}
}
private mutating func encodeElement(
forKey key: Key,
box: Box
) {
container.withShared { container in
container.elements.append(box, at: self.converted(key).stringValue)
}
}
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
)
}
}
@@ -0,0 +1,130 @@
// 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
}
}
}
}
@@ -0,0 +1,134 @@
// 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
)
}
}
@@ -147,6 +147,7 @@ func getCurrentTrackLyricsData(originalLyrics: Lyrics? = nil) throws -> Data {
case .genius: GeniusLyricsRepository()
case .lrclib: LrcLibLyricsRepository()
case .musixmatch: MusixmatchLyricsRepository.shared
case .petit: PetitLyricsRepository()
}
let lyricsDto: LyricsDto
@@ -158,40 +159,44 @@ func getCurrentTrackLyricsData(originalLyrics: Lyrics? = nil) throws -> Data {
lastLyricsError = nil
}
catch let error as LyricsError {
lastLyricsError = error
switch error {
catch let error {
if let error = error as? LyricsError {
lastLyricsError = error
case .InvalidMusixmatchToken:
switch error {
case .InvalidMusixmatchToken:
if !hasShownUnauthorizedPopUp {
PopUpHelper.showPopUp(
delayed: false,
message: "The tweak is unable to load lyrics from Musixmatch due to Unauthorized error. Please check or update your Musixmatch token. If you use an iPad, you should get the token from the Musixmatch app for iPad.",
buttonText: "OK"
)
hasShownUnauthorizedPopUp.toggle()
}
if !hasShownUnauthorizedPopUp {
case .MusixmatchRestricted:
PopUpHelper.showPopUp(
delayed: false,
message: "The tweak is unable to load lyrics from Musixmatch due to Unauthorized error. Please check or update your Musixmatch token. If you use an iPad, you should get the token from the Musixmatch app for iPad.",
buttonText: "OK"
)
if !hasShownRestrictedPopUp {
PopUpHelper.showPopUp(
delayed: false,
message: "The tweak is unable to load lyrics from Musixmatch because they are restricted. It's likely a copyright issue due to the US IP address, so you should change it if you're in the US or use a VPN.",
buttonText: "OK"
)
hasShownRestrictedPopUp.toggle()
}
hasShownUnauthorizedPopUp.toggle()
default:
break
}
case .MusixmatchRestricted:
if !hasShownRestrictedPopUp {
PopUpHelper.showPopUp(
delayed: false,
message: "The tweak is unable to load lyrics from Musixmatch because they are restricted. It's likely a copyright issue due to the US IP address, so you should change it if you're in the US or use a VPN.",
buttonText: "OK"
)
hasShownRestrictedPopUp.toggle()
}
default:
break
}
else {
lastLyricsError = .UnknownError
}
if source == .genius || !UserDefaults.geniusFallback {
@@ -6,6 +6,7 @@ enum LyricsError: Error, CustomStringConvertible {
case InvalidMusixmatchToken
case DecodingError
case NoSuchSong
case UnknownError
var description: String {
switch self {
@@ -14,6 +15,7 @@ enum LyricsError: Error, CustomStringConvertible {
case .InvalidMusixmatchToken: "Unauthorized"
case .DecodingError: "Decoding Error"
case .NoCurrentTrack: "No Track Instance"
case .UnknownError: "Unknown Error"
}
}
}
@@ -0,0 +1,9 @@
import Foundation
struct PetitLyricsData: Codable {
var lines: [PetitLyricsLine]
enum CodingKeys: String, CodingKey {
case lines = "line"
}
}
@@ -0,0 +1,11 @@
import Foundation
struct PetitLyricsLine: Codable {
var linestring: String
var words: [PetitLyricsWord]
enum CodingKeys: String, CodingKey {
case linestring
case words = "word"
}
}
@@ -0,0 +1,8 @@
import Foundation
enum PetitLyricsType: Int, Codable {
case notDetermined = 0
case plain = 1
case linesSynced = 2
case wordsSynced = 3
}
@@ -0,0 +1,5 @@
import Foundation
struct PetitLyricsWord: Codable {
var starttime: Int
}
@@ -0,0 +1,18 @@
import Foundation
struct PetitResponse: Codable {
var songs: [PetitSong]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
songs = try container.decode(PetitSongs.self, forKey: .songs).songs
}
struct PetitSongs: Decodable {
var songs: [PetitSong]
enum CodingKeys: String, CodingKey {
case songs = "song"
}
}
}
@@ -0,0 +1,9 @@
import Foundation
struct PetitSong: Codable {
var lyricsId: Int
var title: String
var availableLyricsType: PetitLyricsType
var lyricsType: PetitLyricsType
var lyricsData: String
}
@@ -4,12 +4,14 @@ enum LyricsSource : Int, CustomStringConvertible {
case genius
case lrclib
case musixmatch
case petit
var description : String {
switch self {
case .genius: "Genius"
case .lrclib: "LRCLIB"
case .musixmatch: "Musixmatch"
case .petit: "PetitLyrics"
}
}
}
}
@@ -0,0 +1,128 @@
import Foundation
struct PetitLyricsRepository: LyricsRepository {
private let url = "https://p1.petitlyrics.com/api/GetPetitLyricsData.php"
private let session: URLSession
init() {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"Content-Type": "application/x-www-form-urlencoded"
]
session = URLSession(configuration: configuration)
}
private func perform(_ data: [String: Any]) throws -> PetitResponse {
var finalData = data
finalData["clientAppId"] = "p1110417"
finalData["terminalType"] = 10
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "POST"
request.httpBody = finalData.queryString.addingPercentEncoding(
withAllowedCharacters: .urlHostAllowed
)!.data(using: .utf8)
let semaphore = DispatchSemaphore(value: 0)
var data: Data?
var error: Error?
let task = session.dataTask(with: request) { response, _, err in
error = err
data = response
semaphore.signal()
}
task.resume()
semaphore.wait()
if let error = error {
throw error
}
return try XMLDecoder().decode(PetitResponse.self, from: data!)
}
//
private func searchSong(_ title: String, artist: String) throws -> PetitSong {
let response = try perform(
["key_title": title, "key_artist": artist, "max_count": 1]
)
guard let song = response.songs.first else {
throw LyricsError.NoSuchSong
}
return song
}
//
private func getSong(_ lyricsId: Int, availableLyricsType: PetitLyricsType) throws -> PetitSong {
var lyricsType: PetitLyricsType
if availableLyricsType == .linesSynced {
lyricsType = .plain
}
else {
lyricsType = availableLyricsType
}
let response = try perform(
["key_lyricsId": lyricsId, "lyricsType": lyricsType.rawValue]
)
guard let song = response.songs.first else {
throw LyricsError.DecodingError
}
return song
}
//
func getLyrics(_ query: LyricsSearchQuery, options: LyricsOptions) throws -> LyricsDto {
let searchResult = try searchSong(query.title, artist: query.primaryArtist)
let song = try getSong(
searchResult.lyricsId,
availableLyricsType: searchResult.availableLyricsType
)
guard let lyricsData = Data(base64Encoded: song.lyricsData) else {
throw LyricsError.DecodingError
}
switch song.lyricsType {
case .wordsSynced:
let lyrics = try XMLDecoder().decode(PetitLyricsData.self, from: lyricsData)
return LyricsDto(
lines: lyrics.lines.map {
LyricsLineDto(
content: $0.linestring,
offsetMs: $0.words.first!.starttime
)
},
timeSynced: true
)
case .plain:
let stringLyrics = String(data: lyricsData, encoding: .utf8)!
return LyricsDto(
lines: stringLyrics
.components(separatedBy: "\n")
.map { LyricsLineDto(content: $0) },
timeSynced: false
)
default:
throw LyricsError.DecodingError
}
}
}
@@ -0,0 +1,7 @@
import Foundation
extension Locale {
static func isInRegion(_ regionCode: String, orHasLanguage languageCode: String) -> Bool {
self.current.regionCode == regionCode || self.preferredLanguages.contains(languageCode)
}
}
@@ -2,8 +2,8 @@ import SwiftUI
extension EeveeLyricsSettingsView {
@ViewBuilder func LyricsSourceSection() -> some View {
Section(footer: Text("""
func lyricsSourceFooter() -> some View {
var text = """
You can select the lyrics source you prefer.
Genius: Offers the best quality lyrics, provides the most songs, and updates lyrics the fastest. Does not and will never be time-synced.
@@ -11,9 +11,24 @@ Genius: Offers the best quality lyrics, provides the most songs, and updates lyr
LRCLIB: The most open service, offering time-synced lyrics. However, it lacks lyrics for many songs.
Musixmatch: The service Spotify uses. Provides time-synced lyrics for many songs, but you'll need a user token to use this source.
"""
if Locale.isInRegion("JP", orHasLanguage: "ja") {
text.append("\n\n")
text.append("PetitLyrics: Offers plenty of time-synced Japanese and some international lyrics.")
}
text.append("\n\n")
text.append("""
If the tweak is unable to find a song or process the lyrics, you'll see a "Couldn't load the lyrics for this song" message. The lyrics might be wrong for some songs when using Genius due to how the tweak searches songs. I've made it work in most cases.
""")) {
""")
return Text(text)
}
@ViewBuilder func LyricsSourceSection() -> some View {
Section(footer: lyricsSourceFooter()) {
Picker(
"Lyrics Source",
selection: $lyricsSource
@@ -21,6 +36,9 @@ If the tweak is unable to find a song or process the lyrics, you'll see a "Could
Text("Genius").tag(LyricsSource.genius)
Text("LRCLIB").tag(LyricsSource.lrclib)
Text("Musixmatch").tag(LyricsSource.musixmatch)
if Locale.isInRegion("JP", orHasLanguage: "ja") {
Text("PetitLyrics").tag(LyricsSource.petit)
}
}
if lyricsSource == .musixmatch {