XMLCoder

使用 Swift 的 Codable 协议进行 XML 编码和解码。

Version License Platform Coverage

此软件包是原始 ShawnMoore/XMLParsing 的一个分支,具有更多功能和改进的测试覆盖率。 自动生成的文档可在 我们的 GitHub Pages 上找到。

加入我们的 Discord,提出任何问题和进行友好交流。

示例

import XMLCoder
import Foundation

let sourceXML = """
<note>
    <to>Bob</to>
    <from>Jane</from>
    <heading>Reminder</heading>
    <body>Don't forget to use XMLCoder!</body>
</note>
"""

struct Note: Codable {
    let to: String
    let from: String
    let heading: String
    let body: String
}

let note = try! XMLDecoder().decode(Note.self, from: Data(sourceXML.utf8))

let encodedXML = try! XMLEncoder().encode(note, withRootKey: "note")

高级功能

以下功能在 0.4.0 版本或更高版本中可用(除非另有说明)

去除命名空间前缀

有时您需要处理 XML 命名空间前缀,例如在下面的 XML 中

<h:table xmlns:h="http://www.w3.org/TR/html4/">
  <h:tr>
    <h:td>Apples</h:td>
    <h:td>Bananas</h:td>
  </h:tr>
</h:table>

使用 shouldProcessNamespaces 属性启用从元素名称中去除前缀

struct Table: Codable, Equatable {
    struct TR: Codable, Equatable {
        let td: [String]
    }

    let tr: [TR]
}


let decoder = XMLDecoder()

// Setting this property to `true` for the namespace prefix to be stripped
// during decoding so that key names could match.
decoder.shouldProcessNamespaces = true

let decoded = try decoder.decode(Table.self, from: xmlData)

动态节点编码

XMLCoder 提供了两个辅助协议,允许您自定义节点是否作为属性或元素进行编码和解码:DynamicNodeEncodingDynamicNodeDecoding

协议的声明非常简单

protocol DynamicNodeEncoding: Encodable {
    static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding
}

protocol DynamicNodeDecoding: Decodable {
    static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
}

相应 static 函数返回的值如下所示

enum NodeDecoding {
    // decodes a value from an attribute
    case attribute

    // decodes a value from an element
    case element

    // the default, attempts to decode as an element first,
    // otherwise reads from an attribute
    case elementOrAttribute
}

enum NodeEncoding {
    // encodes a value in an attribute
    case attribute

    // the default, encodes a value in an element
    case element

    // encodes a value in both attribute and element
    case both
}

为您想要自定义的类型添加对相应协议的遵循。 相应地,以下示例代码

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
        }
    }
}

适用于此 XML

<book id="123">
    <id>123</id>
    <title>Cat in the Hat</title>
    <category>Kids</category>
    <category>Wildlife</category>
</book>

请参阅 #70@JoeMatt 提供的 PR 获取更多详细信息。

编码键值内在

假设您需要解码一个类似于以下的 XML

<?xml version="1.0" encoding="UTF-8"?>
<foo id="123">456</foo>

默认情况下,您可以将 foo 解码为一个元素,但随后无法解码 id 属性。 XMLCoder 以特殊方式处理某些 CodingKey 值,以允许为此 XML 进行适当的编码。 只需添加一个 stringValue 等于 ""(空字符串)的编码键。 下面是一个编码上述 XML 的类型声明示例,但对具有这些值的编码键的特殊处理既适用于编码也适用于解码。

struct Foo: Codable, DynamicNodeEncoding {
    let id: String
    let value: String

    enum CodingKeys: String, CodingKey {
        case id
        case value = ""
    }

    static func nodeEncoding(forKey key: CodingKey)
    -> XMLEncoder.NodeEncoding {
        switch key {
        case CodingKeys.id:
            return .attribute
        default:
            return .element
        }
    }
}

感谢 @JoeMatt 在 PR #73 中实现此功能。

保留元素内容中的空格

默认情况下,在解码期间会修剪元素内容中的空格。 这包括使用 值内在键 解码的字符串值。 从 0.5 版本 开始,您现在可以在 XMLDecoder 实例上设置属性 trimValueWhitespacesfalse(默认值为 true),以保留解码字符串中的所有空格。

删除空白元素

当解码漂亮的 XML 时,如果 trimValueWhitespaces 设置为 false,则可能会将空白元素作为子元素添加到 XMLCoderElement 的实例上。 这些空白元素使得解码需要自定义 Decodable 逻辑的数据结构成为不可能。 从 0.13.0 版本 开始,您可以将 removeWhitespaceElements 属性设置为 true(默认值为 false)在 XMLDecoder 上以删除这些空白元素。

选择元素编码

0.8 版本开始,您可以通过将您的 CodingKey 类型额外遵循 XMLChoiceCodingKey 来编码和解码具有关联值的 enum。 这允许编码和解码结构类似于以下示例的 XML 元素

<container>
    <int>1</int>
    <string>two</string>
    <string>three</string>
    <int>4</int>
    <int>5</int>
</container>

要解码这些元素,您可以使用此类型

enum IntOrString: Codable {
    case int(Int)
    case string(String)
    
    enum CodingKeys: String, XMLChoiceCodingKey {
        case int
        case string
    }
    
    enum IntCodingKeys: String, CodingKey { case _0 = "" }
    enum StringCodingKeys: String, CodingKey { case _0 = "" }
}

这在 PR #119@jsbean@bwetherfield 中有更详细的描述。

具有(内联)复杂关联值的选择元素

让我们扩展之前的示例,用关联值中的复杂类型替换简单类型。 此示例将涵盖类似于以下的 XML

<container>
    <nested attr="n1_a1">
        <val>n1_v1</val>
        <labeled>
            <val>n2_val</val>
        </labeled>
    </nested>
    <simple attr="n1_a1">
        <val>n1_v1</val>
    </simple>
</container>
enum InlineChoice: Equatable, Codable {
    case simple(Nested1)
    case nested(Nested1, labeled: Nested2)
    
    enum CodingKeys: String, CodingKey, XMLChoiceCodingKey {
        case simple, nested
    }
    
    enum SimpleCodingKeys: String, CodingKey { case _0 = "" }
    
    enum NestedCodingKeys: String, CodingKey {
        case _0 = ""
        case labeled
    }
    
    struct Nested1: Equatable, Codable, DynamicNodeEncoding {
        var attr = "n1_a1"
        var val = "n1_v1"
        
        public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
            switch key {
            case CodingKeys.attr: return .attribute
            default: return .element
            }
        }
    }

    struct Nested2: Equatable, Codable {
        var val = "n2_val"
    }
}

Combine 集成

从 XMLCoder 0.9 版本 开始,当 Apple 的 Combine 框架可用时,XMLDecoder 遵循 TopLevelDecoder 协议,这允许它与 decode(type:decoder:) 运算符一起使用

import Combine
import Foundation
import XMLCoder

func fetchBook(from url: URL) -> AnyPublisher<Book, Error> {
    return URLSession.shared.dataTaskPublisher(for: url)
        .map(\.data)
        .decode(type: Book.self, decoder: XMLDecoder())
        .eraseToAnyPublisher()
}

这是在 PR #132@sharplet 实现的。

此外,从 XMLCoder 0.11 开始,XMLEncoder 遵循 TopLevelEncoder 协议

import Combine
import XMLCoder

func encode(book: Book) -> AnyPublisher<Data, Error> {
    return Just(book)
        .encode(encoder: XMLEncoder())
        .eraseToAnyPublisher()
}

上面示例中的生成的 XML 将以 <book 开头,要自定义根元素的大小写(例如 <Book),您需要在编码器上设置适当的 keyEncoding 策略。 要完全更改元素名称,您必须更改类型名称,这是 TopLevelEncoder API 的一个不幸的限制。

根元素属性

有时您需要在根元素上设置属性,这些属性与您的模型类型没有直接关系。 从 XMLCoder 0.11 开始,XMLEncoder 上的 encode 函数接受一个新的 rootAttributes 参数来帮助实现此目的

struct Policy: Encodable {
    var name: String
}

let encoder = XMLEncoder()
let data = try encoder.encode(Policy(name: "test"), rootAttributes: [
    "xmlns": "http://www.nrf-arts.org/IXRetail/namespace",
    "xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
    "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
])

生成的 XML 将如下所示

<policy xmlns="http://www.nrf-arts.org/IXRetail/namespace"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <name>test</name>
</policy>

这是在 PR #160@portellaa 实现的。

属性包装器

如果您的 Swift 版本允许使用属性包装器,您可能更喜欢此 API 而不是更详细的 动态节点编码

例如,此类型

struct Book: Codable {
    @Element var id: Int
}

会将值 Book(id: 42) 编码为 <Book><id>42</id></Book>。 反之亦然,它会将后者解码为前者。

同样,

struct Book: Codable {
    @Attribute var id: Int
}

会将值 Book(id: 42) 编码为 <Book id="42"></Book>,反之亦然进行解码。

如果您事先不知道属性在解码期间将作为元素还是属性存在,请使用 @ElementAndAttribute

struct Book: Codable {
    @ElementAndAttribute var id: Int
}

这会将值 Book(id: 42) 编码为 <Book id="42"><id>42</id></Book>。 它会将 <Book><id>42</id></Book><Book id="42"></Book> 都解码为 Book(id: 42)

此功能从 XMLCoder 0.13.0 开始可用,由 @bwetherfield 实现。

XML 标头

您可以通过将 XML 标头和/或 doctype 提供给 encode 函数,在编码对象时添加它们。 这些参数都是可选的,并且仅在显式提供时才会呈现。

struct User: Codable {
    @Element var username: String
}

let data = try encoder.encode(
    User(username: "Joanis"),
    withRootKey: "user",
    header: XMLHeader(version: 1.0, encoding: "UTF-8"),
    doctype: .system(
        rootElement: "user",
        dtdLocation: "http://example.com/myUser_v1.dtd"
    )
)

安装

要求

Apple 平台

Linux

Windows

Swift 包管理器

Swift Package Manager 是一种用于管理 Swift 代码分发的工具。 它与 Swift 构建系统集成,以自动执行下载、编译和链接所有平台上依赖项的过程。

设置好 Swift 包后,将 XMLCoder 添加为依赖项就像将其添加到 Package.swiftdependencies 值一样容易。

dependencies: [
    .package(url: "https://github.com/CoreOffice/XMLCoder.git", from: "0.15.0")
]

如果您在 Xcode 构建的应用程序中使用 XMLCoder,您也可以 使用 Xcode 的 GUI 将其添加为直接依赖项。

CocoaPods

CocoaPods 是 Apple 平台上 Swift 和 Objective-C Cocoa 项目的依赖管理器。 您可以使用以下命令安装它

$ gem install cocoapods

导航到项目目录并使用以下命令创建 Podfile

$ pod install

在您的 Podfile 中,指定 XMLCoder pod

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'YourApp' do
  # Comment the next line if you're not using Swift or don't want
  # to use dynamic frameworks
  use_frameworks!

  # Pods for YourApp
  pod 'XMLCoder', '~> 0.14.0'
end

然后,运行以下命令

$ pod install

打开创建的 YourApp.xcworkspace 文件。 这应该是您每天用来创建应用程序的文件,而不是 YourApp.xcodeproj 文件。

Carthage

Carthage 是 Apple 平台的依赖管理器,它构建您的依赖项并为您提供二进制框架。

可以使用 Homebrew 使用以下命令安装 Carthage

$ brew update
$ brew install carthage

在您的 Cartfile 中,添加 GitHub 路径到 XMLCoder

github "CoreOffice/XMLCoder" ~> 0.15.0

然后,运行以下命令来构建框架

$ carthage update

将构建的框架拖到您的 Xcode 项目中。

与 Vapor 一起使用

extension XMLEncoder: ContentEncoder {
    public func encode<E: Encodable>(
        _ encodable: E,
        to body: inout ByteBuffer,
        headers: inout HTTPHeaders
    ) throws {
        headers.contentType = .xml
        
        // Note: You can provide an XMLHeader or DocType if necessary
        let data = try self.encode(encodable)
        body.writeData(data)
    }
}

extension XMLDecoder: ContentDecoder {
    public func decode<D: Decodable>(
        _ decodable: D.Type,
        from body: ByteBuffer,
        headers: HTTPHeaders
    ) throws -> D {
        // Force wrap is acceptable, as we're guaranteed these bytes exist through `readableBytes`
        let body = body.readData(length: body.readableBytes)!
        return try self.decode(D.self, from: body)
    }
}

贡献

本项目遵守 贡献者盟约行为准则。 通过参与,您需要遵守此代码。 请向 coreoffice@desiatov.com 报告不可接受的行为。

编码风格

本项目使用 SwiftFormatSwiftLint 来强制执行格式和编码风格。 我们鼓励您以最适合您的任何方式在存储库的本地克隆中运行 SwiftFormat,无论是手动还是通过 Xcode 源代码编辑器扩展构建阶段git pre-commit 钩子 等自动运行。

为了确保这些工具能在您提交 macOS 上的更改之前运行,我们建议您运行一次此命令来设置 pre-commit 钩子。

brew bundle # installs SwiftLint, SwiftFormat and pre-commit
pre-commit install # installs pre-commit hook to run checks before you commit

有关更多详细信息以及其他平台的安装说明,请参阅 pre-commit 文档页面

SwiftFormat 和 SwiftLint 也会在 CI 上为每个 PR 运行,因此 CI 构建可能会因格式或样式不一致而失败。我们要求所有 PR 在合并之前都必须通过 CI 构建。

测试覆盖率

我们的目标是保持 XMLCoder 的稳定性,并根据 XML 1.0 标准 正确地序列化任何 XML。 所有这些都可以很容易地自动测试,我们正在逐步提高 XMLCoder 的测试覆盖率,并且不希望它降低。 降低测试覆盖率的 PR 被合并的可能性要低得多。 如果您添加任何新功能,请务必添加测试,对于现有代码中的更改和任何重构也是如此。