服务器端Swift—构建自定义XML解码器

我们实现了一个自定义XML解码器,允许我们使用Decodable解码来自XML API的响应。

对于Swift Talk后端,我们使用订阅服务Recurly连接,该服务使用自定义XML格式。我们必须解析来自此服务的各种资源,例如订阅和帐户,并且所有这些资源都使用相同的格式。

点击此处进交流群 有技术的来闲聊 没技术的来学习

通常,每个使用XML的人都以自己的方式实现它,因此格式在构造某些值类型或它们如何表示的方式上看起来会略有不同nil。在今天的示例中,我们看到Recurly将URL表示为具有href属性的元素:

<?xml version="1.0" encoding="UTF-8"?>
<account href="https://domain.recurly.com/v2/accounts/06a5313b-7972-48a9-a0a9-3d7d741afe44">
  <adjustments href="https://domain.recurly.com/v2/accounts/06a5313b-7972-48a9-a0a9-3d7d741afe44/adjustments"/>
  <account_balance href="https://domain.recurly.com/v2/accounts/06a5313b-7972-48a9-a0a9-3d7d741afe44/balance"/>
  <billing_info href="https://domain.recurly.com/v2/accounts/06a5313b-7972-48a9-a0a9-3d7d741afe44/billing_info"/>
  <!-- ... -->
</account>
复制代码

如果我们进一步向下滚动,我们会看到一个用户名可以是nil,这由一个名为nilwith nil的值作为其值表示:

<username nil="nil"></username>
复制代码

通过将这种XML格式的细节包装在解码器中,我们可以使用单一实现来解码来自Recurly的资源 - 不仅在服务器端,而且在客户端 - 如果我们的应用程序需要与之交谈API。

如果我们只需要处理这个响应,也许手动解析它会更有效。但从长远来看,编写解码器将为我们节省大量时间,因为它将允许我们从Recurly的响应中解码任何结构。

创建XML解码器

我们从基于示例XML的结构开始。为了节省一些时间,我们只使用与XML匹配的蛇形属性名称:

struct Account: Codable {
    var state: String
    var email: String
    var company_name: String
}
复制代码

我们通过编写符合Decoder协议的类来创建解码器,然后让编译器添加协议属性和方法。就像我们上周做的那样,我们将忽略codingPathuserInfo属性,我们将它们设置为空值:

finalclass RecurlyXMLDecoder: Decoder {
    var codingPath: [CodingKey] = []
    var userInfo: [CodingUserInfoKey:Any] = [:]

    // ...
}
复制代码

我们必须实现的第一件事是提供密钥容器的方法。我们通过返回一个KeyedDecodingContainer包含符合的任何类型的值来完成此操作KeyedDecodingContainerProtocol。包装容器类型必须是CodingKey类型的通用。我们创建一个被称为KDC容器类型的结构:

final class RecurlyXMLDecoder: Decoder {
    // ...

    func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
        return KeyedDecodingContainer(KDC())
    }

    struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
        // ...
    }

    // ...
}
复制代码

我们让编译器为keyed解码容器生成所需的协议存根KDC,然后我们再次忽略前两个属性:

struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
    var codingPath: [CodingKey] = []
    var allKeys: [Key] = []

    // ...
}
复制代码

在这个带键的解码容器中,我们必须从XML开始阅读。我们将使用Foundation的一个内置类型XMLElement,它是XMLNode代表单个元素的子类。我们存储一个元素作为解码器的属性,我们还将它传递给键控解码容器,KDC

final class RecurlyXMLDecoder: Decoder {
    // ...
    let element: XMLElement
    init(_ element: XMLElement) {
        self.element = element
    }

    func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
        return KeyedDecodingContainer(KDC(element))
    }

    struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
        var codingPath: [CodingKey] = []
        var allKeys: [Key] = []

        let element: XMLElement
        init(_ element: XMLElement) {
            self.element = element
        }

        // ...
    }

    // ...
}
复制代码

我们在所有其他方法中抛出致命错误,KDC以便查看我们必须首先实现哪种方法。最后,我们可能需要实现大多数方法,但这样我们就不必一次完成所有操作。我们还在其余的方法中抛出致命的错误RecurlyXMLDecoder

我们必须确保在生产代码中不会出现任何致命错误。这对于服务器端代码尤为重要,因为致命错误会使进程停止。所以最后,我们必须以其他方式处理错误,不会导致服务器崩溃。

为了开始我们的实现,我们尝试解码示例XML字符串并查看我们首先崩溃的位置。我们XMLDocument从XML字符串创建一个,然后将其根元素传递给我们的解码器。然后我们尝试解码一个Account结构:

struct Account: Codable {
    var state: String
    var email: String
    var company_name: String
}

let document = try XMLDocument(xmlString: xml, options: [])
let root = document.rootElement()!
let decoder = RecurlyXMLDecoder(root)
let account = try Account(from: decoder)
print(account)
复制代码

正如预期的那样,没有帐户值打印到控制台,因为我们遇到了第一个致命错误:键控解码容器的字符串解码方法中的错误。这是我们需要实现的第一件事:

struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
    // ...

    func decode(_ type: String.Type, forKey key: Key) throws -> String {
        fatalError()
    }

    // ...
}
复制代码

解码字符串

为了解码字符串,我们首先尝试找到具有与给定键匹配的名称的元素的子节点。元素的子元素具有公共类型XMLNode,因此我们必须尝试将找到的子元素转换为子XMLElement类:

struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
    // ...

    func decode(_ type: String.Type, forKey key: Key) throws -> String {
        let child = (element.children ?? []).first(where: { $0.name == key.stringValue }).flatMap { $0 as? XMLElement }
        // ...
    }

    // ...
}
复制代码

如果我们找不到具有正确名称和类型的孩子,我们必须抛出一个DecodingError.keyNotFound错误:

struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
    // ...

    func decode(_ type: String.Type, forKey key: Key) throws -> String {
        guard let child = (element.children ?? []).first(where: { $0.name == key.stringValue }).flatMap({ $0 as? XMLElement }) else {
            throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: codingPath, debugDescription: "TODO"))
        }
        // ...
    }

    // ...
}
复制代码

除了密钥之外,错误还采用上下文参数来提供调试信息,例如编码密钥路径。但是,由于我们没有在此演示中填充此路径,因此我们无法为抛出的错误添加任何更多信息。

如果我们找到了child元素,我们返回它的字符串值。XMLNode的财产stringValue是可选的,但我们相信它永远是nilXMLElement的子类XMLNode。所以我们强制解包它,如果我们的假设是错误的话会导致崩溃:

struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
    // ...

    func decode(_ type: String.Type, forKey key: Key) throws -> String {
        guard let child = // ...
        return child.stringValue! // todo verify that it's never nil
    }

    // ...
}
复制代码

现在运行代码,我们已经成功解码Account,因为它的所有属性都是字符串:

let account = try Account(from: decoder)
print(account)

// Account(state: "active", email: "mail@floriankugler.com", company_name: "")
复制代码

解码无

作为下一步,我们要解码nil以便有可选字段Account。目前,我们将公司名称解码为空字符串,但XML将此值定义为nil

<company_name nil="nil"></company_name>
复制代码

许将值解码为nil,我们将属性转换为可选属性:

struct Account: Codable {
    var state: String
    var email: String
    var company_name: String?
}
复制代码

现在,如果我们运行应用程序,我们会崩溃另一种我们尚未实现的方法。在我们修复此崩溃之前,我们可以通过编码密钥来提取逻辑以查找XML元素的子元素,因为我们的解码器在很多地方都需要它:

extension XMLElement {
    func child(for key: CodingKey) -> XMLElement? {
        return (children ?? []).first(where: { $0.name == key.stringValue }).flatMap({ $0 as? XMLElement })
    }
}
复制代码

这使得字符串解码方法更具可读性:

struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
    // ...

    func decode(_ type: String.Type, forKey key: Key) throws -> String {
        guard let child = element.child(for: key) else {
            // ...
        }
        return child.stringValue! // todo verify that it's never nil
    }

    // ...
}
复制代码

我们使用相同的帮助器来实现下一个方法,如果可以找到给定键的子代contains,则该方法应该返回true

struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
    // ...

    func contains(_ key: Key) -> Bool {
        return element.child(for: key) != nil
    }

    // ...
}
复制代码

接下来,我们遇到致命错误decodeNiltrue如果必须将给定键的子元素解释为,则应返回该错误nil

如果我们根本找不到子元素,我们应该像以前一样抛出一个错误,并且因为这个逻辑与字符串解码方法中的逻辑相同,所以我们可以拉出另一个帮助我们的错误:

struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
    // ...

    func child(for key: CodingKey) throws -> XMLElement {
        guard let child = element.child(for: key) else {
            throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: codingPath, debugDescription: "TODO"))
        }
        return child
    }

    // ...

    func decode(_ type: String.Type, forKey key: Key) throws -> String {
        let child = try self.child(for: key)
        return child.stringValue! // todo verify that it's never nil
    }

    // ...
}
复制代码

decodeNil,我们现在只需要检查找到的子元素是否具有nil属性。如果是,我们返回true,这意味着我们正在解码一个nil值:

struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
    // ...

    func decodeNil(forKey key: Key) throws -> Bool {
        let child = try self.child(for: key)
        return child.attribute(forName: "nil") != nil
    }

    // ...
}
复制代码

让这个逻辑正确起来可能会让人很困惑,所以我们真的喜欢这样一个事实:我们将它包装在一个解码器中,我们只需要一次就把它弄好。当我们实际使用解码器时,我们不再需要担心格式的细节。

现在当我们运行代码而不是空字符串时,我们正确地获取nil了公司名称属性:

let account = try Account(from: decoder)
print(account)

// Account(state: "active", email: "mail@floriankugler.com", company_name: nil)
复制代码

解码嵌套值

对于下一步,我们可以将帐户状态转换为枚举,这本身就是Codable

struct Account: Codable {
    enum State: String, Codable {
        case active, canceled
    }
    var state: State
    var email: String
    var company_name: String?
}
复制代码

这会在另一个方法中崩溃我们的代码,即解码嵌套类型的方法:

struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
    // ...

    func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
        fatalError()
    }

    // ...
}
复制代码

调用此方法是因为我们必须解码一个State值,但我们在方法中知道的是我们必须解码一个通用的可解码类型T。这意味着我们可以调用初始化器T(from: decoder),我们需要一个解码器。

许多Decoder实现在此时使用根解码器以获得最大效率,但我们将创建一个新的实现,因为它更容易,并且性能足以满足我们的用例。我们搜索名称与给定键匹配的子进程并将其传递给新的解码器:

struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
    // ...

    func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
        let el = try child(for: key)
        let decoder = RecurlyXMLDecoder(el)
        return try T(from: decoder)
    }

    // ...
}
复制代码

这一次,我们遇到了解码器singleValueContainer方法中的致命错误。调用此方法是因为我们正在尝试解码a State,并且Decodable编译器为此枚举生成的实现要求解码器提供此类型的容器。

为了实现这个方法,我们需要返回一个SingleValueDecodingContainer我们为其创建结构的方法SVDC。就像我们使用键控解码容器一样,我们将解码器的XML元素传递给结构体,并通过在所有协议方法中抛出致命错误来满足编译器:

final class RecurlyXMLDecoder: Decoder {
    // ...

    func singleValueContainer() throws -> SingleValueDecodingContainer {
        return SVDC(element)
    }

    struct SVDC: SingleValueDecodingContainer {
        var codingPath: [CodingKey] = []
        let element: XMLElement

        init(_ element: XMLElement) {
            self.element = element
        }

        // ...
    }
}
复制代码

这构建并且我们在单值容器的字符串解码方法中崩溃。与键控容器不同,我们不必通过键查找子项,因为我们现在正在解码单个值。所以我们只返回我们给出的根元素的字符串值:

struct SVDC: SingleValueDecodingContainer {
    var codingPath: [CodingKey] = []
    let element: XMLElement

    init(_ element: XMLElement) {
        self.element = element
    }

    // ...

    func decode(_ type: String.Type) throws -> String {
        return element.stringValue! // todo check "never nil" assumption
    }

    // ...
}
复制代码

当我们运行代码时,我们得到一个成功解码的State值:

let account = try Account(from: decoder)
print(account)

// Account(state: XMLDecoder.Account.State.active, email: "mail@floriankugler.com", company_name: nil)
复制代码

接下来

今天我们将把它留在今天,但仍然存在一些挑战。首先,我们必须将XML中表示的日期解码为ISO 8601格式的字符串,而解码器Double默认尝试解码a 。

另一个挑战是数组的解码。XML实际上没有像JSON那样的数组类型 - 可以直接映射到我们的解码器。因此,我们必须找出从XML文档构造数组的另一种方法。

所以我们可以继续处理我们的解码器,直到我们不再遇到致命错误并且我们需要的每个方法都得到实现


小编这里有大量的书籍和面试资料哦(点击下载

原文转载地址:talk.objc.io/episodes/S0…

转载于:https://juejin.im/post/5d255cf05188255d6f33de31

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值