Plist 是 Apple 家平台上一种很常见的配置文件,常见的存储格式是常见的 XML 格式(还有 Binary 格式),不同于 HTML 的复杂,Plist 只包含了比较少的几种标签(tag),所以实现使用 functional 的 parser combinator 来实现一个简单的 plist parser 也是一件很有意思的事情。
Plist
一个 Plist 文件内容长这个样子:
1 | <dict> |
wikipedia 上列出了一个详细的 XML
标签和 macOS/iOS 中的类型关系以及存储格式。
Foundation 类 | Core Foundation 类型 | XML 标签 | 储存格式 |
---|---|---|---|
NSString | CFString | <string> | UTF-8 编码的字符串 |
NSNumber | CFNumber | <real>, <integer> | 十进制数字符串 |
NSNumber | CFBoolean | <true/>, or <false/> | 无数据(只有标签) |
NSDate | CFDate | <date> | ISO 8601 格式的日期字符串 |
NSData | CFData | <data> | Base64 编码的数据 |
NSArray | CFArray | <array> | 可以包含任意数量的子元素 |
NSDictionary | CFDictionary | <dict> | 交替包含 <key> 标签和 plist 元素标签 |
根据这个表格,我们可以定义出 Plist 的数据结构。
Model
1 | /// The plist data model |
Parser
根据 Plist data model,想要解析一个 Plist 字符串 得到 PLIST
类型,只需要一个 parser
。
没错,只需要一个 parser,这个 parser 大概长这样:
1 | let parser: Parser<PLIST> |
这个 let parser: Parser<PLIST>
的实现才是最关键的。一个 PLIST
是由 Bool
Date
Data
Number
String
5 种简单的类型和 Array<PLIST>
Dictionary<PLIST>
2 种容器(nested)类型组成,所以一个 Parser<PLIST>
也是由对应的 Parser<Bool>
Parser<Date>
Parser<Data>
Parser<Number>
Parser<String>
5 中简单的 parser 和 Parser<Array>
Parser<Dictionary>
2 种容器类型 parser 组成。
Bool Parser
在 Plist 中,Bool 类型由两种形式 <true/>
和 <false/>
,所以一个 Bool 类型的 parser 也就是能够解析字符串 <true/>
和 <false/>
。
1 | let _true = string("<true/>") <&> const(PLIST.bool(true)) |
Date Parser
Plist 中的 Date 类型存储的是 UTC 字符串,如 <date>2017-08-05T14:25:14Z</date>
。字符串中的开始标签 <date>
和结束标签 </date>
对于解析的结果来说是没有用的,所以一个 Date 类型的 parser 是要将这个字符串解析成 PLIST.date(date)
, date 为 2017-08-05T14:25:14Z 通过 format 得到。
1 | let _date = string("<date>") *> manyTill(_any, string("</date>")) <&> { PLIST.date(String($0).date!) } |
Data Parser
Plist 中的 Data 类型存储的是 Base64 编码后的数据,所以实现一个 Data Parser 和 Date Parser 差不多,区别是 tag 和 Data 类型初始化。
1 | let _data = string("<data>") *> manyTill(_any, string("</data>")) <&> { PLIST.data(Data(base64Encoded: String($0))!) } |
Number Parser
Plist 中的 Number 的存储实际上分两种。一种是整型,一种是浮点型。整型的 tag
是 integer
,浮点型是 real
。
先看 Integer Parser:
1 | let _integer = string("<integer>") *> manyTill(_digit, string("</integer>")) <&> { PLIST.number(Int(String($0))!) } |
String Parser
String Parser 和 Date Parser 以及 Data Parser 对比起来更简单,实际上就是去掉了最后转换的那一步。
1 | let _string = string("<string>") *> manyTill(_any, string("</string>")) <&> { PLIST.string(String($0)) } |
Tag Parser
通过对比上面几种除了 Bool Parser 之外不同类型的 Parser,可以发现实现的方式很相似。
- closed tag,成对存在。
- 中间存储的都是字符串,最后把字符串转为具体类型。
把这些相似的 Parser 进行抽象,将相同部分封装成一个函数,不同的部分用传参的形式来实现。
1 | func tag<A>(_ tag: String, _ p: Parser<A>) -> Parser<[A]> { |
Array Parser
Array Parser 和 Dictionary Parser 相对比较复杂,因为它们是容器类型,里面可以是任意的 PLIST 类型,包括它们本身。对于 Enum PLIST 来说,可以使用 indirect
关键字来表示这种情况,但是在定义 parser 的时候,确没有这些魔法。
但是通过利用 Swift 的一些特性,还是很容易解决这个递归的问题。先忽略 Dictionary 类型。
1 | let _plist = plist() |
Dictionary Parser
Dictionary Parser 的递归问题和 Array Parser 一样。
1 | let _plist = plist() |
但 Dictionary 和 Array 不一样的地方在于,Array 里面是多个 Plist 的元素,而 Dictionary 是 key-value 对,且必须是 key-value 对,也就是 tag("dict", _keyValue)
。
先实现一个 Key-Value Parser:
1 | let _key = string("<key>") *> manyTill(_any, string("</key>")) <&> { String($0) } |
然后就可以得到 Dictionary Parser:
1 | let _dict = tag("dict", _keyValue) <&> { PLIST.dict(atod($0)) } |
或者换一种写法:
1 | let _kv = _keyValue <&> { ttod($0) } |
Plist Parser
最后
1 | let _plist = _bool <|> _string <|> _integer <|> _date <|> _data <|> _array <|> _dict |