diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Attribute.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Attribute.swift
new file mode 100644
index 0000000..6f6dcdc
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Attribute.swift
@@ -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 ``. And vice versa,
+ it will decode the former into the latter.
+ */
+@propertyWrapper
+public struct Attribute: 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()
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/BoolBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/BoolBox.swift
new file mode 100644
index 0000000..5997804
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/BoolBox.swift
@@ -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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/Box.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/Box.swift
new file mode 100644
index 0000000..71ecf82
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/Box.swift
@@ -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()
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/ChoiceBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/ChoiceBox.swift
new file mode 100644
index 0000000..4d2f9b0
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/ChoiceBox.swift
@@ -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)
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/DataBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/DataBox.swift
new file mode 100644
index 0000000..dfa7f94
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/DataBox.swift
@@ -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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/DateBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/DateBox.swift
new file mode 100644
index 0000000..a9d18d4
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/DateBox.swift
@@ -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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/DecimalBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/DecimalBox.swift
new file mode 100644
index 0000000..35e0944
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/DecimalBox.swift
@@ -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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/DoubleBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/DoubleBox.swift
new file mode 100644
index 0000000..4230260
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/DoubleBox.swift
@@ -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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/FloatBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/FloatBox.swift
new file mode 100644
index 0000000..3f0e283
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/FloatBox.swift
@@ -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(_ 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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/IntBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/IntBox.swift
new file mode 100644
index 0000000..35380a6
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/IntBox.swift
@@ -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(_ unboxed: Integer) {
+ self.unboxed = Unboxed(unboxed)
+ }
+
+ init?(xmlString: String) {
+ guard let unboxed = Unboxed(xmlString) else {
+ return nil
+ }
+ self.init(unboxed)
+ }
+
+ func unbox() -> 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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/KeyedBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/KeyedBox.swift
new file mode 100644
index 0000000..271f665
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/KeyedBox.swift
@@ -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
+ typealias Elements = KeyedStorage
+
+ 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(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)}"
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/NullBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/NullBox.swift
new file mode 100644
index 0000000..e5dd66b
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/NullBox.swift
@@ -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"
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/SharedBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/SharedBox.swift
new file mode 100644
index 0000000..d05af76
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/SharedBox.swift
@@ -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 {
+ private(set) var unboxed: Unboxed
+
+ init(_ wrapped: Unboxed) {
+ unboxed = wrapped
+ }
+
+ func withShared(_ 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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/SingleKeyedBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/SingleKeyedBox.swift
new file mode 100644
index 0000000..47c6370
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/SingleKeyedBox.swift
@@ -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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/StringBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/StringBox.swift
new file mode 100644
index 0000000..c33629c
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/StringBox.swift
@@ -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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/UIntBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/UIntBox.swift
new file mode 100644
index 0000000..2fd70ac
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/UIntBox.swift
@@ -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(_ unboxed: Integer) {
+ self.unboxed = Unboxed(unboxed)
+ }
+
+ init?(xmlString: String) {
+ guard let unboxed = Unboxed(xmlString) else {
+ return nil
+ }
+ self.init(unboxed)
+ }
+
+ func unbox() -> 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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/URLBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/URLBox.swift
new file mode 100644
index 0000000..6c12061
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/URLBox.swift
@@ -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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/UnkeyedBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/UnkeyedBox.swift
new file mode 100644
index 0000000..203a216
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/UnkeyedBox.swift
@@ -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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/ValueBox.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/ValueBox.swift
new file mode 100644
index 0000000..a6739c4
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Box/ValueBox.swift
@@ -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)
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Element.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Element.swift
new file mode 100644
index 0000000..57a2b7d
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Element.swift
@@ -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 `42`. And vice versa,
+ it will decode the former into the latter.
+ */
+@propertyWrapper
+public struct Element: 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 {}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/ElementAndAttribute.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/ElementAndAttribute.swift
new file mode 100644
index 0000000..69d6c02
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/ElementAndAttribute.swift
@@ -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 `42`. It will decode both
+ `42` and `` as `Book(id: 42)`.
+ */
+@propertyWrapper
+public struct ElementAndAttribute: 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 {}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/ISO8601DateFormatter.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/ISO8601DateFormatter.swift
new file mode 100644
index 0000000..08c248b
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/ISO8601DateFormatter.swift
@@ -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
+}()
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/KeyedStorage.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/KeyedStorage.swift
new file mode 100644
index 0000000..808a38d
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/KeyedStorage.swift
@@ -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 {
+ 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(_ 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(_ transform: (Key, Value) throws -> T) rethrows -> [T] {
+ return try buffer.map(transform)
+ }
+
+ func compactMap(
+ _ 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)]"
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Metatypes.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Metatypes.swift
new file mode 100644
index 0000000..40be7f3
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Metatypes.swift
@@ -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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/String+Extensions.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/String+Extensions.swift
new file mode 100644
index 0000000..59ce223
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/String+Extensions.swift
@@ -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)
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Utils.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Utils.swift
new file mode 100644
index 0000000..747f327
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/Utils.swift
@@ -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 == "" }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLChoiceCodingKey.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLChoiceCodingKey.swift
new file mode 100644
index 0000000..aa3dbff
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLChoiceCodingKey.swift
@@ -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-type–like enums with associated values to and from `XML`
+/// choice elements.
+///
+/// - Important: In order for your `XML`-destined `Codable` type to be encoded and/or decoded
+/// properly, you must conform your custom `CodingKey` type additionally to `XMLChoiceCodingKey`.
+///
+/// For example, say you have defined a type which can hold _either_ an `Int` _or_ a `String`:
+///
+/// enum IntOrString {
+/// case int(Int)
+/// case string(String)
+/// }
+///
+/// Implementing the requirements for the `Codable` protocol like this:
+///
+/// extension IntOrString: Codable {
+/// enum CodingKeys: String, XMLChoiceCodingKey {
+/// case int
+/// case string
+/// }
+///
+/// func encode(to encoder: Encoder) throws {
+/// var container = encoder.container(keyedBy: CodingKeys.self)
+/// switch self {
+/// case let .int(value):
+/// try container.encode(value, forKey: .int)
+/// case let .string(value):
+/// try container.encode(value, forKey: .string)
+/// }
+/// }
+///
+/// init(from decoder: Decoder) throws {
+/// let container = try decoder.container(keyedBy: CodingKeys.self)
+/// do {
+/// self = .int(try container.decode(Int.self, forKey: .int))
+/// } catch {
+/// self = .string(try container.decode(String.self, forKey: .string))
+/// }
+/// }
+/// }
+///
+/// Retroactively conform the `CodingKeys` enum to `XMLChoiceCodingKey` when targeting `XML` as your
+/// encoded format.
+///
+/// extension IntOrString.CodingKeys: XMLChoiceCodingKey {}
+///
+/// - Note: The `XMLChoiceCodingKey` marker protocol allows the `XMLEncoder` / `XMLDecoder` to
+/// resolve ambiguities particular to the `XML` format between nested unkeyed container elements and
+/// choice elements.
+public protocol XMLChoiceCodingKey: CodingKey {}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLCoderElement.swift
new file mode 100644
index 0000000..293f612
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLCoderElement.swift
@@ -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()
+ // 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 ""
+ } 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:
+ 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:
+ 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:
+ self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedUnkeyedBox.unboxed, attributes: attributes)
+ case let sharedKeyedBox as SharedBox:
+ self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedKeyedBox.unboxed, attributes: attributes)
+ case let sharedChoiceBox as SharedBox:
+ 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
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLDocumentType.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLDocumentType.swift
new file mode 100644
index 0000000..ec257eb
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLDocumentType.swift
@@ -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 = "\n"
+
+ return string
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLHeader.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLHeader.swift
new file mode 100644
index 0000000..dd0e591
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLHeader.swift
@@ -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 = "\n"
+
+ return string
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLKey.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLKey.swift
new file mode 100644
index 0000000..0761f19
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLKey.swift
@@ -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")!
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLStackParser.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLStackParser.swift
new file mode 100644
index 0000000..c18a4fd
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Auxiliaries/XMLStackParser.swift
@@ -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.. 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.. ()) 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)
+ }
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/DecodingErrorExtension.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/DecodingErrorExtension.swift
new file mode 100644
index 0000000..011477e
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/DecodingErrorExtension.swift
@@ -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))"
+ }
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/DynamicNodeDecoding.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/DynamicNodeDecoding.swift
new file mode 100644
index 0000000..9c87ba4
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/DynamicNodeDecoding.swift
@@ -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
+
+ Cat in the Hat
+ Kids
+ Wildlife
+
+ ```
+ */
+public protocol DynamicNodeDecoding: Decodable {
+ static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/SingleValueDecodingContainer.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/SingleValueDecodingContainer.swift
new file mode 100644
index 0000000..0f08b22
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/SingleValueDecodingContainer.swift
@@ -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.Type) throws -> T {
+ return try unbox(try topContainer())
+ }
+
+ public func decode(_: 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.Type) throws -> T {
+ return try unbox(try topContainer())
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLChoiceDecodingContainer.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLChoiceDecodingContainer.swift
new file mode 100644
index 0000000..7d4c102
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLChoiceDecodingContainer.swift
@@ -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: 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
+
+ /// 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) {
+ 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(_ 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(
+ keyedBy _: NestedKey.Type, forKey key: Key
+ ) throws -> KeyedDecodingContainer {
+ 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(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
+ )
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLDecoder.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLDecoder.swift
new file mode 100644
index 0000000..1f67b26
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLDecoder.swift
@@ -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..value`.
+ case element
+ /// Decodes a node from either elements of form `value` 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(
+ _ 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
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLDecoderImplementation.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLDecoderImplementation.swift
new file mode 100644
index 0000000..961322f
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLDecoderImplementation.swift
@@ -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(keyedBy keyType: Key.Type) throws -> KeyedDecodingContainer {
+ if let keyed = try topContainer() as? SharedBox {
+ return KeyedDecodingContainer(XMLKeyedDecodingContainer(
+ 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:
+ return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed)
+ case let keyed as SharedBox:
+ 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(keyedBy _: Key.Type) throws -> KeyedDecodingContainer {
+ let topContainer = try self.topContainer()
+ let keyedBox: KeyedBox
+ switch topContainer {
+ case _ where topContainer.isNull:
+ throw DecodingError.valueNotFound(
+ KeyedDecodingContainer.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:
+ 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(
+ referencing: self,
+ wrapping: SharedBox(keyedBox)
+ )
+ return KeyedDecodingContainer(container)
+ }
+
+ /// - Returns: A `KeyedDecodingContainer` for an XML choice element.
+ private func choiceContainer(keyedBy _: Key.Type) throws -> KeyedDecodingContainer {
+ 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(
+ 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(_ 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:
+ guard let value = unkeyedBox.withShared({
+ $0.first as? B
+ }) else { throw error }
+ return value
+ case let keyedBox as SharedBox:
+ 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(_ 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(_ 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(_ 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
+ }
+ }
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLDecodingStorage.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLDecodingStorage.swift
new file mode 100644
index 0000000..18d3bdd
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLDecodingStorage.swift
@@ -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()
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift
new file mode 100644
index 0000000..826b92b
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift
@@ -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: KeyedDecodingContainerProtocol {
+ typealias Key = K
+ typealias KeyedContainer = SharedBox
+ typealias UnkeyedContainer = SharedBox
+
+ // 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(
+ _ 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(
+ keyedBy _: NestedKey.Type, forKey key: Key
+ ) throws -> KeyedDecodingContainer {
+ 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.self) -- \
+ no value found for key \"\(key.stringValue)\"
+ """
+ ))
+ }
+
+ guard let container = XMLKeyedDecodingContainer(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(_ type: T.Type,
+ forKey key: Key) throws -> T
+ where T: BinaryInteger & SignedInteger & Decodable
+ {
+ return try decodeConcrete(type, forKey: key)
+ }
+
+ private func decodeUnsignedInteger(_ type: T.Type,
+ forKey key: Key) throws -> T
+ where T: BinaryInteger & UnsignedInteger & Decodable
+ {
+ return try decodeConcrete(type, forKey: key)
+ }
+
+ private func decodeFloatingPoint(_ type: T.Type,
+ forKey key: Key) throws -> T
+ where T: BinaryFloatingPoint & Decodable
+ {
+ return try decodeConcrete(type, forKey: key)
+ }
+
+ private func decodeConcrete(
+ _ 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(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(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
+ )
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift
new file mode 100644
index 0000000..0ba8b3a
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift
@@ -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
+
+ /// 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) {
+ 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(_ type: T.Type) throws -> T {
+ return try decode(type) { decoder, box in
+ try decoder.unbox(box)
+ }
+ }
+
+ private mutating func decode(
+ _ 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(
+ keyedBy _: NestedKey.Type
+ ) throws -> KeyedDecodingContainer {
+ decoder.codingPath.append(XMLKey(index: currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard !isAtEnd else {
+ throw DecodingError.valueNotFound(
+ KeyedDecodingContainer.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.self, DecodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Cannot get keyed decoding container -- found null value instead."
+ ))
+ }
+
+ guard let keyedContainer = value as? SharedBox else {
+ throw DecodingError.typeMismatch(at: codingPath,
+ expectation: [String: Any].self,
+ reality: value)
+ }
+
+ currentIndex += 1
+ let container = XMLKeyedDecodingContainer(
+ 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 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
+ )
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/DynamicNodeEncoding.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/DynamicNodeEncoding.swift
new file mode 100644
index 0000000..79cbec6
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/DynamicNodeEncoding.swift
@@ -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
+
+ 123
+ Cat in the Hat
+ Kids
+ Wildlife
+
+ ```
+ */
+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)
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/EncodingErrorExtension.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/EncodingErrorExtension.swift
new file mode 100644
index 0000000..a364159
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/EncodingErrorExtension.swift
@@ -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(_ 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))
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/SingleValueEncodingContainer.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/SingleValueEncodingContainer.swift
new file mode 100644
index 0000000..2d96fdc
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/SingleValueEncodingContainer.swift
@@ -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(_ value: T) throws {
+ assertCanEncodeNewValue()
+ try storage.push(container: box(value))
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLChoiceEncodingContainer.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLChoiceEncodingContainer.swift
new file mode 100644
index 0000000..5a07e4d
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLChoiceEncodingContainer.swift
@@ -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: 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
+
+ /// 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
+ ) {
+ 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(
+ _ value: T,
+ forKey key: Key
+ ) throws {
+ return try encode(value, forKey: key) { encoder, value in
+ try encoder.box(value)
+ }
+ }
+
+ private mutating func encode(
+ _ 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(
+ keyedBy _: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ if NestedKey.self is XMLChoiceCodingKey.Type {
+ return nestedChoiceContainer(keyedBy: NestedKey.self, forKey: key)
+ } else {
+ return nestedKeyedContainer(keyedBy: NestedKey.self, forKey: key)
+ }
+ }
+
+ mutating func nestedKeyedContainer(
+ keyedBy _: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ 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(
+ referencing: encoder,
+ codingPath: codingPath,
+ wrapping: sharedKeyed
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ mutating func nestedChoiceContainer(
+ keyedBy _: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ 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(
+ 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
+ )
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLEncoder.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLEncoder.swift
new file mode 100644
index 0000000..23efd3d
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLEncoder.swift
@@ -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 `` over `
`
+ 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] = []
+ // 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)..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.. 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 = [
+ ("&", "&"),
+ ("<", "<"),
+ (">", ">"),
+ ("'", "'"),
+ ("\"", """),
+ ]
+
+ /// Characters and their escaped representations to be escaped in elements
+ open var charactersEscapedInElements = [
+ ("&", "&"),
+ ("<", "<"),
+ (">", ">"),
+ ("'", "'"),
+ ("\"", """),
+ ]
+
+ /// 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(_ 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(_ 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)
+ }
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLEncoderImplementation.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLEncoderImplementation.swift
new file mode 100644
index 0000000..94e880c
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLEncoderImplementation.swift
@@ -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(keyedBy _: Key.Type) -> KeyedEncodingContainer {
+ 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
+ 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 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(keyedBy _: Key.Type) -> KeyedEncodingContainer {
+ let container = XMLKeyedEncodingContainer(
+ referencing: self,
+ codingPath: codingPath,
+ wrapping: storage.pushKeyedContainer()
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ private func choiceContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer {
+ let container = XMLChoiceEncodingContainer(
+ referencing: self,
+ codingPath: codingPath,
+ wrapping: storage.pushChoiceContainer()
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ private func mergeWithExistingKeyedContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer {
+ switch storage.lastContainer {
+ case let keyed as SharedBox:
+ let container = XMLKeyedEncodingContainer(
+ referencing: self,
+ codingPath: codingPath,
+ wrapping: keyed
+ )
+ return KeyedEncodingContainer(container)
+ case let choice as SharedBox:
+ _ = storage.popContainer()
+ let keyed = KeyedBox(
+ elements: KeyedBox.Elements([choice.withShared { ($0.key, $0.element) }]),
+ attributes: []
+ )
+ let container = XMLKeyedEncodingContainer(
+ 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(_ value: T) -> SimpleBox {
+ return IntBox(value)
+ }
+
+ func box(_ 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(
+ _ 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(_ 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()
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLEncodingStorage.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLEncodingStorage.swift
new file mode 100644
index 0000000..9a33a9a
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLEncodingStorage.swift
@@ -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 {
+ let container = SharedBox(keyedBox)
+ containers.append(container)
+ return container
+ }
+
+ mutating func pushChoiceContainer() -> SharedBox {
+ let container = SharedBox(ChoiceBox())
+ containers.append(container)
+ return container
+ }
+
+ mutating func pushUnkeyedContainer() -> SharedBox {
+ 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()!
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift
new file mode 100644
index 0000000..441d7ff
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift
@@ -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: 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
+
+ /// 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
+ ) {
+ 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(
+ _ value: T,
+ forKey key: Key
+ ) throws {
+ return try encode(value, forKey: key) { encoder, value in
+ try encoder.box(value)
+ }
+ }
+
+ private mutating func encode(
+ _ 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(
+ _ 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(
+ keyedBy _: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ if NestedKey.self is XMLChoiceCodingKey.Type {
+ return nestedChoiceContainer(keyedBy: NestedKey.self, forKey: key)
+ } else {
+ return nestedKeyedContainer(keyedBy: NestedKey.self, forKey: key)
+ }
+ }
+
+ mutating func nestedKeyedContainer(
+ keyedBy _: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ 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(
+ referencing: encoder,
+ codingPath: codingPath,
+ wrapping: sharedKeyed
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ mutating func nestedChoiceContainer(
+ keyedBy _: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ 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(
+ 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
+ )
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLReferencingEncoder.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLReferencingEncoder.swift
new file mode 100644
index 0000000..ff88334
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLReferencingEncoder.swift
@@ -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, Int)
+
+ /// Referencing a specific key in a keyed container.
+ case keyed(SharedBox, String)
+
+ /// Referencing a specific key in a keyed container.
+ case choice(SharedBox, 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
+ ) {
+ 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
+ ) {
+ 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
+ ) {
+ 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
+ }
+ }
+ }
+}
diff --git a/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLUnkeyedEncodingContainer.swift b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLUnkeyedEncodingContainer.swift
new file mode 100644
index 0000000..87f72dc
--- /dev/null
+++ b/Sources/EeveeSpotify/Dependencies/XMLCoder/Encoder/XMLUnkeyedEncodingContainer.swift
@@ -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
+
+ /// 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
+ ) {
+ 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(_ value: T) throws {
+ try encode(value) { encoder, value in
+ try encoder.box(value)
+ }
+ }
+
+ private mutating func encode(
+ _ 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(
+ keyedBy _: NestedKey.Type
+ ) -> KeyedEncodingContainer {
+ if NestedKey.self is XMLChoiceCodingKey.Type {
+ return nestedChoiceContainer(keyedBy: NestedKey.self)
+ } else {
+ return nestedKeyedContainer(keyedBy: NestedKey.self)
+ }
+ }
+
+ public mutating func nestedKeyedContainer(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer {
+ codingPath.append(XMLKey(index: count))
+ defer { self.codingPath.removeLast() }
+
+ let sharedKeyed = SharedBox(KeyedBox())
+ self.container.withShared { container in
+ container.append(sharedKeyed)
+ }
+
+ let container = XMLKeyedEncodingContainer(
+ referencing: encoder,
+ codingPath: codingPath,
+ wrapping: sharedKeyed
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ public mutating func nestedChoiceContainer(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer {
+ codingPath.append(XMLKey(index: count))
+ defer { self.codingPath.removeLast() }
+
+ let sharedChoice = SharedBox(ChoiceBox())
+ self.container.withShared { container in
+ container.append(sharedChoice)
+ }
+
+ let container = XMLChoiceEncodingContainer(
+ 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
+ )
+ }
+}
diff --git a/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift b/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift
index 89fa168..74b4300 100644
--- a/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift
+++ b/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift
@@ -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 {
diff --git a/Sources/EeveeSpotify/Lyrics/Models/LyricsError.swift b/Sources/EeveeSpotify/Lyrics/Models/LyricsError.swift
index a16ba37..75973ad 100644
--- a/Sources/EeveeSpotify/Lyrics/Models/LyricsError.swift
+++ b/Sources/EeveeSpotify/Lyrics/Models/LyricsError.swift
@@ -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"
}
}
}
diff --git a/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitLyricsData.swift b/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitLyricsData.swift
new file mode 100644
index 0000000..8adfdf9
--- /dev/null
+++ b/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitLyricsData.swift
@@ -0,0 +1,9 @@
+import Foundation
+
+struct PetitLyricsData: Codable {
+ var lines: [PetitLyricsLine]
+
+ enum CodingKeys: String, CodingKey {
+ case lines = "line"
+ }
+}
diff --git a/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitLyricsLine.swift b/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitLyricsLine.swift
new file mode 100644
index 0000000..ce18b4e
--- /dev/null
+++ b/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitLyricsLine.swift
@@ -0,0 +1,11 @@
+import Foundation
+
+struct PetitLyricsLine: Codable {
+ var linestring: String
+ var words: [PetitLyricsWord]
+
+ enum CodingKeys: String, CodingKey {
+ case linestring
+ case words = "word"
+ }
+}
diff --git a/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitLyricsType.swift b/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitLyricsType.swift
new file mode 100644
index 0000000..b3fce69
--- /dev/null
+++ b/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitLyricsType.swift
@@ -0,0 +1,8 @@
+import Foundation
+
+enum PetitLyricsType: Int, Codable {
+ case notDetermined = 0
+ case plain = 1
+ case linesSynced = 2
+ case wordsSynced = 3
+}
diff --git a/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitLyricsWord.swift b/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitLyricsWord.swift
new file mode 100644
index 0000000..df68dcb
--- /dev/null
+++ b/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitLyricsWord.swift
@@ -0,0 +1,5 @@
+import Foundation
+
+struct PetitLyricsWord: Codable {
+ var starttime: Int
+}
diff --git a/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitResponse.swift b/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitResponse.swift
new file mode 100644
index 0000000..497019c
--- /dev/null
+++ b/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitResponse.swift
@@ -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"
+ }
+ }
+}
diff --git a/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitSong.swift b/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitSong.swift
new file mode 100644
index 0000000..1eb604c
--- /dev/null
+++ b/Sources/EeveeSpotify/Lyrics/Models/Petit/PetitSong.swift
@@ -0,0 +1,9 @@
+import Foundation
+
+struct PetitSong: Codable {
+ var lyricsId: Int
+ var title: String
+ var availableLyricsType: PetitLyricsType
+ var lyricsType: PetitLyricsType
+ var lyricsData: String
+}
diff --git a/Sources/EeveeSpotify/Lyrics/Models/Settings/LyricsSource.swift b/Sources/EeveeSpotify/Lyrics/Models/Settings/LyricsSource.swift
index 3b3c515..17d21f7 100644
--- a/Sources/EeveeSpotify/Lyrics/Models/Settings/LyricsSource.swift
+++ b/Sources/EeveeSpotify/Lyrics/Models/Settings/LyricsSource.swift
@@ -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"
}
}
-}
\ No newline at end of file
+}
diff --git a/Sources/EeveeSpotify/Lyrics/Repositories/PetitLyricsRepository.swift b/Sources/EeveeSpotify/Lyrics/Repositories/PetitLyricsRepository.swift
new file mode 100644
index 0000000..8267c26
--- /dev/null
+++ b/Sources/EeveeSpotify/Lyrics/Repositories/PetitLyricsRepository.swift
@@ -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
+ }
+ }
+}
diff --git a/Sources/EeveeSpotify/Models/Extensions/Locale+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/Locale+Extension.swift
new file mode 100644
index 0000000..3ee9b22
--- /dev/null
+++ b/Sources/EeveeSpotify/Models/Extensions/Locale+Extension.swift
@@ -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)
+ }
+}
diff --git a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+LyricsSourceSection.swift b/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+LyricsSourceSection.swift
index 652242f..c762c57 100644
--- a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+LyricsSourceSection.swift
+++ b/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+LyricsSourceSection.swift
@@ -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 {