mirror of
https://github.com/whoeevee/EeveeSpotifyReborn.git
synced 2026-01-08 23:23:20 +00:00
498 lines
17 KiB
Swift
498 lines
17 KiB
Swift
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
|
|
//
|
|
// This software is released under the MIT License.
|
|
// https://opensource.org/licenses/MIT
|
|
//
|
|
// Created by Shawn Moore on 11/20/17.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
class XMLDecoderImplementation: Decoder {
|
|
// MARK: Properties
|
|
|
|
/// The decoder's storage.
|
|
var storage = XMLDecodingStorage()
|
|
|
|
/// Options set on the top-level decoder.
|
|
let options: XMLDecoder.Options
|
|
|
|
/// The path to the current point in encoding.
|
|
public internal(set) var codingPath: [CodingKey]
|
|
|
|
public var nodeDecodings: [(CodingKey) -> XMLDecoder.NodeDecoding?]
|
|
|
|
/// Contextual user-provided information for use during encoding.
|
|
public var userInfo: [CodingUserInfoKey: Any] {
|
|
return options.userInfo
|
|
}
|
|
|
|
// The error context length
|
|
open var errorContextLength: UInt = 0
|
|
|
|
// MARK: - Initialization
|
|
|
|
/// Initializes `self` with the given top-level container and options.
|
|
init(
|
|
referencing container: Box,
|
|
options: XMLDecoder.Options,
|
|
nodeDecodings: [(CodingKey) -> XMLDecoder.NodeDecoding?],
|
|
codingPath: [CodingKey] = []
|
|
) {
|
|
storage.push(container: container)
|
|
self.codingPath = codingPath
|
|
self.nodeDecodings = nodeDecodings
|
|
self.options = options
|
|
}
|
|
|
|
// MARK: - Decoder Methods
|
|
|
|
internal func topContainer() throws -> Box {
|
|
guard let topContainer = storage.topContainer() else {
|
|
throw DecodingError.valueNotFound(Box.self, DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription: "Cannot get decoding container -- empty container stack."
|
|
))
|
|
}
|
|
return topContainer
|
|
}
|
|
|
|
private func popContainer() throws -> Box {
|
|
guard let topContainer = storage.popContainer() else {
|
|
throw DecodingError.valueNotFound(Box.self, DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription:
|
|
"""
|
|
Cannot get decoding container -- empty container stack.
|
|
"""
|
|
))
|
|
}
|
|
return topContainer
|
|
}
|
|
|
|
public func container<Key>(keyedBy keyType: Key.Type) throws -> KeyedDecodingContainer<Key> {
|
|
if let keyed = try topContainer() as? SharedBox<KeyedBox> {
|
|
return KeyedDecodingContainer(XMLKeyedDecodingContainer<Key>(
|
|
referencing: self,
|
|
wrapping: keyed
|
|
))
|
|
}
|
|
if Key.self is XMLChoiceCodingKey.Type {
|
|
return try choiceContainer(keyedBy: keyType)
|
|
} else {
|
|
return try keyedContainer(keyedBy: keyType)
|
|
}
|
|
}
|
|
|
|
public func unkeyedContainer() throws -> UnkeyedDecodingContainer {
|
|
let topContainer = try self.topContainer()
|
|
|
|
guard !topContainer.isNull else {
|
|
throw DecodingError.valueNotFound(
|
|
UnkeyedDecodingContainer.self,
|
|
DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription:
|
|
"""
|
|
Cannot get unkeyed decoding container -- found null box instead.
|
|
"""
|
|
)
|
|
)
|
|
}
|
|
|
|
switch topContainer {
|
|
case let unkeyed as SharedBox<UnkeyedBox>:
|
|
return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed)
|
|
case let keyed as SharedBox<KeyedBox>:
|
|
return XMLUnkeyedDecodingContainer(
|
|
referencing: self,
|
|
wrapping: SharedBox(keyed.withShared { $0.elements.map(SingleKeyedBox.init) })
|
|
)
|
|
default:
|
|
throw DecodingError.typeMismatch(
|
|
at: codingPath,
|
|
expectation: [Any].self,
|
|
reality: topContainer
|
|
)
|
|
}
|
|
}
|
|
|
|
public func singleValueContainer() throws -> SingleValueDecodingContainer {
|
|
return self
|
|
}
|
|
|
|
private func keyedContainer<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
|
|
let topContainer = try self.topContainer()
|
|
let keyedBox: KeyedBox
|
|
switch topContainer {
|
|
case _ where topContainer.isNull:
|
|
throw DecodingError.valueNotFound(
|
|
KeyedDecodingContainer<Key>.self,
|
|
DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription:
|
|
"""
|
|
Cannot get keyed decoding container -- found null box instead.
|
|
"""
|
|
)
|
|
)
|
|
case let string as StringBox:
|
|
keyedBox = KeyedBox(
|
|
elements: KeyedStorage([("", string)]),
|
|
attributes: KeyedStorage()
|
|
)
|
|
case let containsEmpty as SingleKeyedBox where containsEmpty.element is NullBox:
|
|
keyedBox = KeyedBox(
|
|
elements: KeyedStorage([("", StringBox(""))]),
|
|
attributes: KeyedStorage()
|
|
)
|
|
case let unkeyed as SharedBox<UnkeyedBox>:
|
|
guard let keyed = unkeyed.withShared({ $0.first }) as? KeyedBox else {
|
|
fallthrough
|
|
}
|
|
keyedBox = keyed
|
|
default:
|
|
throw DecodingError.typeMismatch(
|
|
at: codingPath,
|
|
expectation: [String: Any].self,
|
|
reality: topContainer
|
|
)
|
|
}
|
|
let container = XMLKeyedDecodingContainer<Key>(
|
|
referencing: self,
|
|
wrapping: SharedBox(keyedBox)
|
|
)
|
|
return KeyedDecodingContainer(container)
|
|
}
|
|
|
|
/// - Returns: A `KeyedDecodingContainer` for an XML choice element.
|
|
private func choiceContainer<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
|
|
let topContainer = try self.topContainer()
|
|
let choiceBox: ChoiceBox
|
|
switch topContainer {
|
|
case let choice as ChoiceBox:
|
|
choiceBox = choice
|
|
case let singleKeyed as SingleKeyedBox:
|
|
choiceBox = ChoiceBox(singleKeyed)
|
|
default:
|
|
throw DecodingError.typeMismatch(
|
|
at: codingPath,
|
|
expectation: [String: Any].self,
|
|
reality: topContainer
|
|
)
|
|
}
|
|
let container = XMLChoiceDecodingContainer<Key>(
|
|
referencing: self,
|
|
wrapping: SharedBox(choiceBox)
|
|
)
|
|
return KeyedDecodingContainer(container)
|
|
}
|
|
}
|
|
|
|
// MARK: - Concrete Value Representations
|
|
|
|
extension XMLDecoderImplementation {
|
|
/// Returns the given box unboxed from a container.
|
|
private func typedBox<T, B: Box>(_ box: Box, for valueType: T.Type) throws -> B {
|
|
let error = DecodingError.valueNotFound(valueType, DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription: "Expected \(valueType) but found null instead."
|
|
))
|
|
switch box {
|
|
case let typedBox as B:
|
|
return typedBox
|
|
case let unkeyedBox as SharedBox<UnkeyedBox>:
|
|
guard let value = unkeyedBox.withShared({
|
|
$0.first as? B
|
|
}) else { throw error }
|
|
return value
|
|
case let keyedBox as SharedBox<KeyedBox>:
|
|
guard
|
|
let value = keyedBox.withShared({ $0.value as? B })
|
|
else { throw error }
|
|
return value
|
|
case let singleKeyedBox as SingleKeyedBox:
|
|
if let value = singleKeyedBox.element as? B {
|
|
return value
|
|
} else if let box = singleKeyedBox.element as? KeyedBox, let value = box.elements[""].first as? B {
|
|
return value
|
|
} else {
|
|
throw error
|
|
}
|
|
case is NullBox:
|
|
throw error
|
|
case let keyedBox as KeyedBox:
|
|
guard
|
|
let value = keyedBox.value as? B
|
|
else { fallthrough }
|
|
return value
|
|
default:
|
|
throw DecodingError.typeMismatch(
|
|
at: codingPath,
|
|
expectation: valueType,
|
|
reality: box
|
|
)
|
|
}
|
|
}
|
|
|
|
func unbox(_ box: Box) throws -> Bool {
|
|
let stringBox: StringBox = try typedBox(box, for: Bool.self)
|
|
let string = stringBox.unboxed
|
|
|
|
guard let boolBox = BoolBox(xmlString: string) else {
|
|
throw DecodingError.typeMismatch(at: codingPath, expectation: Bool.self, reality: box)
|
|
}
|
|
|
|
return boolBox.unboxed
|
|
}
|
|
|
|
func unbox(_ box: Box) throws -> Decimal {
|
|
let stringBox: StringBox = try typedBox(box, for: Decimal.self)
|
|
let string = stringBox.unboxed
|
|
|
|
guard let decimalBox = DecimalBox(xmlString: string) else {
|
|
throw DecodingError.typeMismatch(at: codingPath, expectation: Decimal.self, reality: box)
|
|
}
|
|
|
|
return decimalBox.unboxed
|
|
}
|
|
|
|
func unbox<T: BinaryInteger & SignedInteger & Decodable>(_ box: Box) throws -> T {
|
|
let stringBox: StringBox = try typedBox(box, for: T.self)
|
|
let string = stringBox.unboxed
|
|
|
|
guard let intBox = IntBox(xmlString: string) else {
|
|
throw DecodingError.typeMismatch(at: codingPath, expectation: T.self, reality: box)
|
|
}
|
|
|
|
guard let int: T = intBox.unbox() else {
|
|
throw DecodingError.dataCorrupted(DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)."
|
|
))
|
|
}
|
|
|
|
return int
|
|
}
|
|
|
|
func unbox<T: BinaryInteger & UnsignedInteger & Decodable>(_ box: Box) throws -> T {
|
|
let stringBox: StringBox = try typedBox(box, for: T.self)
|
|
let string = stringBox.unboxed
|
|
|
|
guard let uintBox = UIntBox(xmlString: string) else {
|
|
throw DecodingError.typeMismatch(at: codingPath, expectation: T.self, reality: box)
|
|
}
|
|
|
|
guard let uint: T = uintBox.unbox() else {
|
|
throw DecodingError.dataCorrupted(DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)."
|
|
))
|
|
}
|
|
|
|
return uint
|
|
}
|
|
|
|
func unbox(_ box: Box) throws -> Float {
|
|
let stringBox: StringBox = try typedBox(box, for: Float.self)
|
|
let string = stringBox.unboxed
|
|
|
|
guard let floatBox = FloatBox(xmlString: string) else {
|
|
throw DecodingError.typeMismatch(at: codingPath, expectation: Float.self, reality: box)
|
|
}
|
|
|
|
return floatBox.unboxed
|
|
}
|
|
|
|
func unbox(_ box: Box) throws -> Double {
|
|
let stringBox: StringBox = try typedBox(box, for: Double.self)
|
|
let string = stringBox.unboxed
|
|
|
|
guard let doubleBox = DoubleBox(xmlString: string) else {
|
|
throw DecodingError.typeMismatch(at: codingPath, expectation: Double.self, reality: box)
|
|
}
|
|
|
|
return doubleBox.unboxed
|
|
}
|
|
|
|
func unbox(_ box: Box) throws -> String {
|
|
do {
|
|
let stringBox: StringBox = try typedBox(box, for: String.self)
|
|
return stringBox.unboxed
|
|
} catch {
|
|
if box is NullBox {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func unbox(_ box: Box) throws -> Date {
|
|
switch options.dateDecodingStrategy {
|
|
case .deferredToDate:
|
|
storage.push(container: box)
|
|
defer { storage.popContainer() }
|
|
return try Date(from: self)
|
|
|
|
case .secondsSince1970:
|
|
let stringBox: StringBox = try typedBox(box, for: Date.self)
|
|
let string = stringBox.unboxed
|
|
|
|
guard let dateBox = DateBox(secondsSince1970: string) else {
|
|
throw DecodingError.dataCorrupted(DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription: "Expected date string to be formatted in seconds since 1970."
|
|
))
|
|
}
|
|
return dateBox.unboxed
|
|
case .millisecondsSince1970:
|
|
let stringBox: StringBox = try typedBox(box, for: Date.self)
|
|
let string = stringBox.unboxed
|
|
|
|
guard let dateBox = DateBox(millisecondsSince1970: string) else {
|
|
throw DecodingError.dataCorrupted(DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription: "Expected date string to be formatted in milliseconds since 1970."
|
|
))
|
|
}
|
|
return dateBox.unboxed
|
|
case .iso8601:
|
|
let stringBox: StringBox = try typedBox(box, for: Date.self)
|
|
let string = stringBox.unboxed
|
|
|
|
guard let dateBox = DateBox(iso8601: string) else {
|
|
throw DecodingError.dataCorrupted(DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription: "Expected date string to be ISO8601-formatted."
|
|
))
|
|
}
|
|
return dateBox.unboxed
|
|
case let .formatted(formatter):
|
|
let stringBox: StringBox = try typedBox(box, for: Date.self)
|
|
let string = stringBox.unboxed
|
|
|
|
guard let dateBox = DateBox(xmlString: string, formatter: formatter) else {
|
|
throw DecodingError.dataCorrupted(DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription: "Date string does not match format expected by formatter."
|
|
))
|
|
}
|
|
return dateBox.unboxed
|
|
case let .custom(closure):
|
|
storage.push(container: box)
|
|
defer { storage.popContainer() }
|
|
return try closure(self)
|
|
}
|
|
}
|
|
|
|
func unbox(_ box: Box) throws -> Data {
|
|
switch options.dataDecodingStrategy {
|
|
case .deferredToData:
|
|
storage.push(container: box)
|
|
defer { storage.popContainer() }
|
|
return try Data(from: self)
|
|
case .base64:
|
|
let stringBox: StringBox = try typedBox(box, for: Data.self)
|
|
let string = stringBox.unboxed
|
|
|
|
guard let dataBox = DataBox(base64: string) else {
|
|
throw DecodingError.dataCorrupted(DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription: "Encountered Data is not valid Base64"
|
|
))
|
|
}
|
|
return dataBox.unboxed
|
|
case let .custom(closure):
|
|
storage.push(container: box)
|
|
defer { storage.popContainer() }
|
|
return try closure(self)
|
|
}
|
|
}
|
|
|
|
func unbox(_ box: Box) throws -> URL {
|
|
let stringBox: StringBox = try typedBox(box, for: URL.self)
|
|
let string = stringBox.unboxed
|
|
|
|
guard let urlBox = URLBox(xmlString: string) else {
|
|
throw DecodingError.dataCorrupted(DecodingError.Context(
|
|
codingPath: codingPath,
|
|
debugDescription: "Encountered Data is not valid Base64"
|
|
))
|
|
}
|
|
|
|
return urlBox.unboxed
|
|
}
|
|
|
|
func unbox<T: Decodable>(_ box: Box) throws -> T {
|
|
let decoded: T?
|
|
let type = T.self
|
|
|
|
if type == Date.self || type == NSDate.self {
|
|
let date: Date = try unbox(box)
|
|
decoded = date as? T
|
|
} else if type == Data.self || type == NSData.self {
|
|
let data: Data = try unbox(box)
|
|
decoded = data as? T
|
|
} else if type == URL.self || type == NSURL.self {
|
|
let data: URL = try unbox(box)
|
|
decoded = data as? T
|
|
} else if type == Decimal.self || type == NSDecimalNumber.self {
|
|
let decimal: Decimal = try unbox(box)
|
|
decoded = decimal as? T
|
|
} else if
|
|
type == String.self || type == NSString.self,
|
|
let value = (try unbox(box) as String) as? T
|
|
{
|
|
decoded = value
|
|
} else {
|
|
storage.push(container: box)
|
|
defer {
|
|
storage.popContainer()
|
|
}
|
|
|
|
do {
|
|
decoded = try type.init(from: self)
|
|
} catch {
|
|
guard case DecodingError.valueNotFound = error,
|
|
let type = type as? AnyOptional.Type,
|
|
let result = type.init() as? T
|
|
else {
|
|
throw error
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
guard let result = decoded else {
|
|
throw DecodingError.typeMismatch(
|
|
at: codingPath, expectation: type, reality: box
|
|
)
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
extension XMLDecoderImplementation {
|
|
var keyTransform: (String) -> String {
|
|
switch options.keyDecodingStrategy {
|
|
case .convertFromSnakeCase:
|
|
return XMLDecoder.KeyDecodingStrategy._convertFromSnakeCase
|
|
case .convertFromCapitalized:
|
|
return XMLDecoder.KeyDecodingStrategy._convertFromCapitalized
|
|
case .convertFromUppercase:
|
|
return XMLDecoder.KeyDecodingStrategy._convertFromUppercase
|
|
case .convertFromKebabCase:
|
|
return XMLDecoder.KeyDecodingStrategy._convertFromKebabCase
|
|
case .useDefaultKeys:
|
|
return { key in key }
|
|
case let .custom(converter):
|
|
return { key in
|
|
converter(self.codingPath + [XMLKey(stringValue: key, intValue: nil)]).stringValue
|
|
}
|
|
}
|
|
}
|
|
}
|