├── .gitattributes ├── .gitignore ├── Description.toml ├── Howto_map_TOML.md ├── LICENSE ├── README.md ├── README_CN.md ├── builder_test.go ├── item.go ├── item_test.go ├── parser.go ├── parser_test.go ├── scanner.go ├── stage.go ├── tests ├── example.toml └── hard_example.toml ├── toml.go ├── toml_test.go └── wercker.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | *.go text eol=lf 3 | *.html text eol=lf 4 | *.tmpl text eol=lf 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /Description.toml: -------------------------------------------------------------------------------- 1 | # 这是一个用 TOML 格式书写的 readme, 用于理解 tom-toml 的实现方法. 2 | # 开头的这几行注释被命名为多行注释, MultiComments, 保存在 Value struct 中 3 | 4 | keyName = "string value" # 这是行尾注释, eolComment, 保存在 Value struct 中 5 | 6 | # 对于上面的内容在 tom-toml 中会生成一个 Value, 下面用伪代码进行描述 7 | # Value { 8 | # MultiComments: "第1行和第2行的内容,包括行首的 # 字符", 9 | # eolComment: "第4行的行尾注释,包括 # 字符", 10 | # kind: String, 11 | # v: interface{}{"string value"}, 12 | # } 13 | # 可以看到 v 中保存了值部分. 虽然 Value struct 中也 key 的定义, 14 | # 但它的目是为格式化输出做准备的, 不是用来保存最原始的 keyName. 15 | # 因为 Toml 的定义是 map[string]*Item, 实际操作中使用者可能会: 16 | # delete(m, "keyName") 17 | # m["kenName"] = GenItem 18 | # 也就是说 keyName 是作为 key 保存在 map 中. 19 | # 因此使用者需要理解 tom-toml 对 key/value 的管理方式. 20 | # 另外, MultiComments 总是附加于紧随其后的 TOML 定义 21 | 22 | [tableName] # 第6至20行的注释会当作 tableName 的 MultiComments. 23 | 24 | Int = 123456789 25 | Float = 0.12 26 | IntArray = [ # 行尾注释1 27 | # 多行注释1 28 | 1, 29 | 2, # 行尾注释2 30 | ] # 行尾注释3 31 | 32 | # 用伪代码描述上面的内容得到的 Toml 对象: 33 | # map[string]*Item{ // Item 扩展自 Value 34 | # "keyName": &Item{..这里省略, 见前面的描述..}, 35 | # "tableName": &Item{ 36 | # MultiComments: "第6至20行的内容,包括行首的 # 字符", 37 | # eolComment: "第22行的行尾注释,包括 # 字符", 38 | # kind: Table, 39 | # v: interface{}{nil}, // 注意这里 40 | # }, 41 | # "tableName.Int": &Item{ 42 | # MultiComments: "", 43 | # eolComment: "", 44 | # kind: Interger, 45 | # v: interface{}{123456789}, 46 | # }, 47 | # "tableName.Float": &Item{ 48 | # MultiComments: "", 49 | # eolComment: "", 50 | # kind: Float, 51 | # v: interface{}{0.12}, 52 | # }, 53 | # "tableName.IntArray": &Item{ 54 | # MultiComments: "", 55 | # eolComment: "# 行尾注释3", // 注意这里,"# 行尾注释1" 被覆盖掉了 56 | # kind: IntegerArray, 57 | # v: []*Value{ 58 | # &Value{ 59 | # MultiComments: "# 多行注释1", 60 | # eolComment: "", 61 | # kind: Integer, 62 | # v: interface{}{1}, 63 | # }, 64 | # &Value{ 65 | # MultiComments: "", 66 | # eolComment: "# 行尾注释2", 67 | # kind: Integer, 68 | # v: interface{}{2}, 69 | # }, 70 | # }, 71 | # }, 72 | # } 73 | # 可以看到 key/value 是分开保存的. 注意行尾注释有可能有多种方法. 74 | # 因此访问 toml["tableName"] 只会得到一个 key. 75 | # 如果要得到整个 tableName 段集合, 需要使用 76 | # toml.Fetch("tableName") 77 | # 从第32行开始的这些注释,被称作 LastComments, 作为特例使用 "" 作为key, 保存到 Toml 的map中. -------------------------------------------------------------------------------- /Howto_map_TOML.md: -------------------------------------------------------------------------------- 1 | # 问题 2 | 3 | 这里讨论基于用 map 实现 TOML 的问题. 4 | 5 | 当然, 我们把访问路径用 "." 连接. 这样的 map 访问数据非常直接方便. 你可以用 6 | 7 | ```go 8 | tm["k.e.y"] // 一下就访问到最终的目标 9 | // 而不用像这样 10 | tm.Get("k").Get("e").Get("y") 11 | tm.Get("k.e.y") 12 | ``` 13 | 14 | TOML v0.2.0 的定义中, Table/ArrayOfTables 是可以深层嵌套的. 15 | 16 | 参见 https://github.com/mojombo/toml/pull/153 17 | 18 | 用 map 实现 TOML, 最简单的方法是: 19 | 20 | ```go 21 | map[string]interface{} 22 | ``` 23 | 24 | 但是这种万能接口, 使用者还需要 type assertion, 并不方便, 所以采用 struct 是需要的. 25 | 26 | # 分析 27 | 28 | 先看看 TOML 都定义了什么? 29 | 30 | - Comment 注释. 前置(多行)注释和行尾注释还有 TOML 文本最后的注释 31 | - Value String, Integer, Float, Boolean, Datetime 以及数组形式 32 | - Key 给 Value 命名, 以便访问 33 | - Table 一组 Key = Value 的集合 34 | - TableName 给 Table 命名 35 | - ArrayOfTables 正如其名, 可以看作是 嵌套TOML 36 | - TablesName 给 ArrayOfTables 命名, 它表示了一组 Table 的名字 37 | 38 | TOML 文本最后的注释比较特别, 只有一个. 可以特殊处理, 比如用 key="", 下文用 TomlComments 表示. 39 | 40 | 显而易见的规则: 41 | 42 | 非 ArrayOfTables 中的 Value 是可以直接 key 访问. 43 | 44 | TableName 也是可以直接 "k.e.y" 访问. TableName 成了 Value 的一种类型. 45 | 46 | tm["k.e.y"] 里面的 key 其实是个访问路径, 由 "TableName[.Key]" 组成. 这是个完整的全路径. 47 | 48 | Table 中可以嵌套 Table, 同样用 "k.e.y" 访问. 49 | 50 | ArrayOfTables 中可以嵌套 ArrayOfTables, 下文分析访问方式 51 | 52 | Table, ArrayOfTables 可以互相嵌套. 53 | 54 | 在 map 中无法直接用 tm["k.e.y"] 访问到 ArrayOfTables 中的元素, 因为 key 中没法让数组下标生效, 如果加入下标,维护将会很麻烦. 55 | 正如笔者前面说的, 如果把 ArraOfTables 当作 `Array Of TOML` 这就很容易理解了. 56 | 还不如直接命名为 TOMLArray 来的简单明了. 57 | 58 | 注释. ArrayOfTables 中的每一个下标 [[TablesName]] 也允许有注释. 59 | 60 | 格式化输出 TOML文本要求数据必须能被有序访问. 61 | 62 | 对 Toml 定义的影响: 63 | 64 | ```go 65 | type Toml map[string]Item 66 | ``` 67 | 68 | Item 有可能是 69 | 70 | - Value 71 | - TableName 72 | - ArrayOfTables 经过前述分析就是 嵌套TOML. 73 | - 内部实现需要的 "." 开头的数据 74 | 75 | Value 的 Kind 包括 76 | 77 | String 78 | Integer 79 | Float 80 | Boolean 81 | Datetime 82 | StringArray 83 | IntegerArray 84 | FloatArray 85 | BooleanArray 86 | DatetimeArray 87 | Array 元素是 xxxxArray 类型, 规范没有明确是否可以 Array 嵌套 Array. 88 | 89 | TableName 和 ArrayOfTables 是独立的, 就是他们自己. 90 | 91 | 实现的时候, 当然所有的数据都用 interface{} 保存在 Value 结构中. 只不过在接口上 Value和Item有所区别. 实际上 92 | 93 | Value 的接口囊括了 TableName 的支持. 94 | ArrayOfTables 只有在 Item 接口中才能访问到. 95 | 96 | 97 | # 结果 98 | 99 | 定义 100 | 101 | ```go 102 | type Value struct{} // 省略细节, 包含 TableName 类型 103 | 104 | type Toml map[string]Item 105 | 106 | func (t Toml) Fetch(prefix string) Toml // 返回 "prefix." 开头的子集, 并省略它 107 | 108 | type Tables []Toml // 官方没有给出具体名字, 只有造一个 109 | 110 | type Item struct{ 111 | *Value 112 | } 113 | 114 | func (i Item) TomlArray() Tables // 如果 kind 是ArrayOfTables 的话 115 | 116 | ``` 117 | 118 | Item 导出 Value 可以方便一些操作, 目前 Item 只是多支持了 ArrayOfTables. 119 | 尽管有些不同, Kind 的命名尽量采用 TOML 定义的字面值. 120 | 121 | # 访问 122 | 123 | 前面分析过"k.e.y"是个完全路径, 总是写完全路径有时候不是很方便. tom-toml 提供便捷方法 `Fetch(prefix)` 简化访问. 所以, 访问路径有两种情况. 124 | 125 | ## 完全路径 126 | 127 | 形式为 128 | - "Key" 访问顶层 Key-Value, 它不在 Table 中 129 | - "TableName" 访问 TableName 本身, 不包含 "Key = Value" 部分 130 | - "TableName.Key" 访问 Table 中的 Key-Value 131 | - "ArrayOfTables" 得到 Toml 数组, 访问某个 Toml 中的元素仍然用完全路径 132 | 133 | 即: 用 "." 连接的 TableName 和 Key 路径. 134 | 135 | *吐槽:事儿本来很容易理解, 用文字表达清晰却不容易, 还容易发生歧义. 只是 map 的 key而已, 地球人都知道.* 136 | 137 | 举例: 注释中直接写上对应访问代码, tm 代表对应的 Toml 对象. 138 | 139 | ```toml 140 | id = 1 # tm["id"] 141 | [user] # tm["user"] // returns TableName only 142 | name = "Jack" # tm["user.name"] 143 | [user.profile] # tm["user.profile"] // TableName only 144 | email = "jack@exampl.com" # tm["user.profile.email"] 145 | 146 | [[users]] # t := tm["users"][0] 147 | id = 1 # t["id"] 148 | [users.jack] # t["jack"] 149 | email = "jack@exampl.com" # t["jack.email"] 150 | 151 | [[users]] # t := tm["users"][1] 152 | id = 2 # t["id"] 153 | [users.tom] # t["tom"] 154 | email = "tom@exampl.com" # t["tom.email"] 155 | [[users.tom.follows]] # ft := t["tom.follows"][0] 156 | name = "john" # ft["name"] 157 | ``` 158 | 159 | 访问 ArrayOfTables 中的Key-Value, 看上去更像是用了相对路径, 解释见下文. 160 | 161 | *吐槽:不必太在意把 ArrayOfTables 也算做全路径访问, 这样做对简化代码实现和操作很有效* 162 | 163 | ## 相对路径 164 | 165 | 相对路径由方法 Fetch 方法支持. 对应上面的例子, 使用 166 | ```go 167 | user := tm.Fetch("user") 168 | ``` 169 | 170 | 那么得到的 user 和访问是这样的 171 | 172 | ```toml 173 | name = "Jack" # user["name"] 174 | [profile] # user["profile"] // TableName only 175 | email = "jack@exampl.com" # user["profile.email"] 176 | ``` 177 | 178 | 如果使用 179 | ```go 180 | profile := tm.Fetch("user.profile") 181 | ``` 182 | 183 | 那么得到的 user 和访问是这样的 184 | 185 | ```toml 186 | email = "jack@exampl.com" # profile["email"] 187 | ``` 188 | 189 | 当然, 这个 "prefix" 必须是个 TableName. 否则无法工作, 只能得到空集. 190 | 191 | ```go 192 | bad := Fetch("user.name") // 无法工作 193 | ``` 194 | 195 | Fetch 也无法作用于 ArrayTables , 下面的代码也无法工作, 只能得到空集. 196 | 197 | ```go 198 | users := tm.Fetch("users") 199 | ``` 200 | 201 | 下面的代码解释无法工作的原因 202 | 203 | ```toml 204 | [[users]] # 如果可以, users 将被减省 205 | id = 1 # id = 1 206 | [users.jack] # [jack] 207 | email = "jack@exampl.com" # email = "jack@exampl.com" 208 | 209 | [[users]] # 下面的数据就无法安排了 210 | id = 2 # id = 2 # Invalid, 和上面的 id 重复了 211 | [users.tom] # 212 | email = "tom@exampl.com" # 213 | [[users.tom.follows]] # 214 | name = "john" # 215 | ``` 216 | 217 | 原因很简单, ArrayOfTables 是数组, Key 会发生重复, 合并数组会产生数据覆盖. 218 | 219 | ## 嵌套TOML 220 | 221 | 通过上述分析, ArrayOfTables 也可以看作向 dst TOML 中嵌入了多个 Src1,Src2,Src3 ... TOML, 也就是 嵌套TOML. 222 | 可以看作为了保障 key 不重复加了个不重复的前缀. 这给合并多个 TOML 文档提供了可能. 223 | 224 | # 限制 225 | 226 | - 保留元素 保留 key 以"."开头的元素. 修改这些元素会产生不可预计的结果. 227 | - 禁止循环嵌套 因可以用函数生成Toml, 循环嵌套有可能发生. 228 | 229 | 限制的原因和内部实现有关, 不细述. 230 | 231 | # 疑问 232 | 233 | 有一些未确定的问题 234 | 235 | ## ArrayOfTables 236 | 237 | 官方申明下面 Table 的文档是合法的. 238 | 239 | ```toml 240 | # [x] you 241 | # [x.y] don't 242 | # [x.y.z] need these 243 | [x.y.z.w] # for this to work 244 | ``` 245 | 246 | 官方申明下面 ArrayOfTables的文档是非法的. 247 | 248 | ```toml 249 | # INVALID TOML DOC 250 | [[fruit]] 251 | name = "apple" 252 | 253 | [[fruit.variety]] 254 | name = "red delicious" 255 | 256 | # This table conflicts with the previous table 257 | [fruit.variety] 258 | name = "granny smith" 259 | ``` 260 | 261 | 官方文档未明确下面的文档是否合法. 262 | 263 | 没有声明 `[foo]` 或者 `[[foo]]`, 直接 264 | 265 | ```toml 266 | [[foo.bar]] 267 | ``` 268 | 269 | 这应该是非法的, 因为如果补全这种写法的话, 可能是 270 | 271 | ```toml 272 | [foo] 273 | [[foo.bar]] 274 | ``` 275 | 276 | 也有可能是 277 | 278 | ```toml 279 | [[foo]] 280 | [[foo.bar]] 281 | ``` 282 | 283 | 会产生歧义. 284 | 285 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, achun 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | tom-toml 2 | ======== 3 | 4 | [TOML](https://github.com/mojombo/toml) format parser for Golang 5 | 6 | This library supports TOML version 7 | [v0.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md) 8 | 9 | [![wercker status](https://app.wercker.com/status/28e2ac15ba6930f928b10187ad4043c3 "wercker status")](https://app.wercker.com/project/bykey/28e2ac15ba6930f928b10187ad4043c3) 10 | 11 | [中文 README](README_CN.md) 更详尽. 12 | 13 | ## Import 14 | 15 | import "github.com/achun/tom-toml" 16 | 17 | ## Usage 18 | 19 | Say you have a TOML file that looks like this: 20 | 21 | ```toml 22 | [servers.alpha] 23 | ip = "10.0.0.1" # IP 24 | dc = "eqdc10" 25 | 26 | [smtpAuth] 27 | Identity = "" 28 | Username = "Do_Not_Reply" 29 | Password = "password" 30 | Host = "example.com" 31 | Subject = "message" 32 | To = ["me@example.com","you@example.com"] 33 | ``` 34 | 35 | Read the ip and dc like this: 36 | 37 | ```go 38 | package main 39 | 40 | import ( 41 | "fmt" 42 | "github.com/achun/tom-toml" 43 | ) 44 | 45 | type smtpAuth struct { 46 | Identity string 47 | Username string 48 | Password string 49 | Host string 50 | Subject string 51 | To []string 52 | } 53 | 54 | func main() { 55 | conf, err := toml.LoadFile("good.toml") 56 | 57 | if err != nil { 58 | fmt.Println(err) 59 | return 60 | } 61 | 62 | fmt.Println(conf["servers.alpha.ip"].String()) 63 | fmt.Println(conf["servers.alpha.dc"].String()) 64 | 65 | sa := smtpAuth{} 66 | auth := conf.Fetch("smtpAuth") // case sensitive 67 | 68 | sa.To = make([]string, auth["To"].Len()) 69 | auth.Apply(&sa) 70 | 71 | fmt.Println(sa) 72 | } 73 | ``` 74 | 75 | outputs: 76 | 77 | 10.0.0.1 78 | eqdc10 79 | { Do_Not_Reply password example.com message [me@example.com you@example.com]} 80 | 81 | 82 | ## Documentation 83 | 84 | The documentation is available at 85 | [gowalker.org](http://gowalker.org/github.com/achun/tom-toml). 86 | 87 | ## Contribute 88 | 89 | Feel free to report bugs and patches using GitHub's pull requests system on 90 | [achun/tom-toml](https://github.com/achun/tom-toml). Any feedback would be 91 | much appreciated! 92 | 93 | 94 | ## License 95 | 96 | Copyright (c) 2014, achun 97 | All rights reserved. 98 | 99 | Redistribution and use in source and binary forms, with or without modification, 100 | are permitted provided that the following conditions are met: 101 | 102 | * Redistributions of source code must retain the above copyright notice, this 103 | list of conditions and the following disclaimer. 104 | 105 | * Redistributions in binary form must reproduce the above copyright notice, this 106 | list of conditions and the following disclaimer in the documentation and/or 107 | other materials provided with the distribution. 108 | 109 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 110 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 111 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 112 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 113 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 114 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 115 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 116 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 117 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 118 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 119 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | tom-toml 2 | ======== 3 | 4 | [TOML](https://github.com/mojombo/toml) 格式 Go 语言支持包. 5 | 6 | 本包支持 TOML 版本 7 | [v0.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md) 8 | 9 | [![wercker status](https://app.wercker.com/status/28e2ac15ba6930f928b10187ad4043c3 "wercker status")](https://app.wercker.com/project/bykey/28e2ac15ba6930f928b10187ad4043c3) 10 | 11 | ## 文档 12 | 13 | Go DOC 文档请访问 14 | [gowalker.org](http://gowalker.org/github.com/achun/tom-toml). 15 | 16 | [readme.toml](readme.toml) 是用 toml 格式对 tom-toml 的深入介绍. 17 | 18 | ## Import 19 | 20 | import "github.com/achun/tom-toml" 21 | 22 | 23 | ## 使用 24 | 25 | 假设你有一个TOML文件 `example.toml` 看起来像这样: 26 | 27 | ```toml 28 | # 注释以"#"开头, 这是多行注释, 可以分多行 29 | # tom-toml 把这两行注释绑定到紧随其后的 key, 也就是 title 30 | 31 | title = "TOML Example" # 这是行尾注释, tom-toml 把此注释绑定给 title 32 | 33 | # 虽然只有一行, 这也属于多行注释, tom-toml 把此注释绑定给 owner 34 | [owner] # 这是行尾注释, tom-toml 把这一行注释绑定到 owner 35 | 36 | name = "om Preston-Werner" # 这是行尾注释, tom-toml 把这一行注释绑定到 owner.name 37 | 38 | # 下面列举 TOML 所支持的类型与格式要求 39 | organization = "GitHub" # 字符串 40 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." # 字符串可以包含转义字符 41 | dob = 1979-05-27T07:32:00Z # 日期, 使用 ISO 8601 Zulu 时区(最后的 Z 表示时区为 +00:00). 对 Go 来说兼容 RFC3339 layout. 42 | 43 | [database] 44 | server = "192.168.1.1" 45 | ports = [ 8001, 8001, 8002 ] # 数组, 其元素类型也必须是TOML所支持的. Go 语言下类型是 slice 46 | connection_max = 5000 # 整型, tom-toml 使用 int64 类型 47 | enabled = true # 布尔型 48 | 49 | [servers] 50 | 51 | # 可以使用缩进, tabs 或者 spaces 都可以, 毫无问题. 52 | [servers.alpha] 53 | ip = "10.0.0.1" # IP 格式只能用字符串了 54 | dc = "eqdc10" 55 | 56 | [servers.beta] 57 | ip = "10.0.0.2" 58 | dc = "eqdc10" 59 | 60 | [clients] 61 | data = [ ["gamma", "delta"], [1, 2] ] # 又一个数组 62 | donate = 49.90 # 浮点, tom-toml 使用 float64 类型 63 | 64 | # 通过 smtp 发电子邮件所需要的的参数 65 | [smtpAuth] 66 | Identity = "" 67 | Username = "Do_Not_Reply" 68 | Password = "password" 69 | Host = "example.com" 70 | Subject = "message" 71 | To = ["me@example.com","you@example.com"] 72 | ``` 73 | 74 | 读取 `servers.alpha` 中的 ip 和 dc: 75 | 76 | ```go 77 | package main 78 | 79 | import ( 80 | "fmt" 81 | "github.com/achun/tom-toml" 82 | ) 83 | 84 | type smtpAuth struct { 85 | Identity string 86 | Username string 87 | Password string 88 | Host string 89 | Subject string 90 | To []string 91 | } 92 | 93 | func main() { 94 | conf, err := toml.LoadFile("good.toml") 95 | 96 | if err != nil { 97 | fmt.Println(err) 98 | return 99 | } 100 | 101 | fmt.Println(conf["servers.alpha.ip"].String()) 102 | fmt.Println(conf["servers.alpha.dc"].String()) 103 | 104 | sa := smtpAuth{} 105 | auth := conf.Fetch("smtpAuth") // 严格区分大小写 106 | 107 | sa.To = make([]string, auth["To"].Len()) 108 | auth.Apply(&sa) 109 | 110 | fmt.Println(sa) 111 | } 112 | ``` 113 | 114 | 输出是这样的: 115 | 116 | 10.0.0.1 117 | eqdc10 118 | { Do_Not_Reply password example.com message [me@example.com you@example.com]} 119 | 120 | 您应该注意到了注释的表现形式, tom-toml 提供了注释支持. 121 | 122 | ## 注意 123 | 124 | 先写下解释用的 TOML 文本 125 | ```toml 126 | [nameOftable] # Kind() 为 TableName, String() 同此行 127 | key1 = "v1" # Kind() 为 String, String() 是 "v1" 128 | key2 = "v2" # Kind() 为 String, String() 是 "v2" 129 | [[arrayOftables]] # Kind() 为 ArrayOfTables, String() 是此行及以下行 130 | key3 = "v3" # Kind() 为 String, String() 是 "v3" 131 | ``` 132 | 133 | 因为采用 `map` 和支持注释的原因, 使用上有些特别. Toml 对象中存储的 134 | 135 | - TableName 仅是 TOML 规范中的 `[nameOftable]` 的字面值. 136 | - Table 仅是 TOML 规范中的 `[arrayOftables]` 的一个 Table. 137 | 138 | 因此用 `tm` 表示上述 Toml 对象的话 139 | 140 | tm["nameOftable"] 仅仅是 `[nameOftable]`, 不包含 Key/Value 部分 141 | tm["arrayOftables"] 是全部的 `arrayOftables`, 因为它是数组 142 | tm.Fetch("nameOftable") 是`[nameOftable]`的 Key/Value 部分, 类型是 Toml 143 | tm["nameOftable.key1"] 直接访问到了值为 "v1" 的数据 144 | t:=tm["arrayOftables"].Table(0) 是第一个 Table, Kind() 是 TableBody 145 | t["key3"] Key3 只能这样访问到 146 | 147 | 148 | 可以看出 149 | 150 | - 只有通过 `Fetch()` 方法才能得到一个 TOML 规范中定义的 Table 的主体. 151 | - 只有通过 `Table()` 方法才能得到 `Table` 类型. 152 | - `arrayOftables.key3` 这种写法是错误的, 不满足 TOML 规范的定义 153 | 154 | map 带来 “nameOftable.key1” 这种点字符串方便的同时也产生了一些不便. 155 | map 进行存储的话只能是这样, 就算不支持注释, 也逃不过 ArrayOfTables 的古怪. 156 | 157 | 158 | ## 贡献 159 | 160 | 请使用 GitHub 系统提出 issues 或者 pull 补丁到 161 | [achun/tom-toml](https://github.com/achun/tom-toml). 欢迎任何反馈! 162 | 163 | 164 | ## License 165 | Copyright (c) 2014, achun 166 | All rights reserved. 167 | 168 | Redistribution and use in source and binary forms, with or without modification, 169 | are permitted provided that the following conditions are met: 170 | 171 | * Redistributions of source code must retain the above copyright notice, this 172 | list of conditions and the following disclaimer. 173 | 174 | * Redistributions in binary form must reproduce the above copyright notice, this 175 | list of conditions and the following disclaimer in the documentation and/or 176 | other materials provided with the distribution. 177 | 178 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 179 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 180 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 181 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 182 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 183 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 184 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 185 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 186 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 187 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 188 | -------------------------------------------------------------------------------- /builder_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "github.com/achun/testing-want" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | var skipTest bool = false 10 | 11 | func TestComments(t *testing.T) { 12 | if skipTest { 13 | return 14 | } 15 | 16 | wt := want.T(t) 17 | testBuilder(wt, []byte(` 18 | [[name]] # 世界, 19 | id = 1 20 | [[name]] # Template, 尝试独立的 _layouts 21 | id =2 22 | `), 23 | `TestComments`) 24 | } 25 | 26 | func TestReadFile(t *testing.T) { 27 | if skipTest { 28 | return 29 | } 30 | source, err := ioutil.ReadFile("tests/example.toml") 31 | wt := want.T(t) 32 | wt.Nil(err) 33 | testBuilder(wt, source, "TestReadFile") 34 | } 35 | 36 | func testBuilder(wt want.Want, source []byte, from string) { 37 | 38 | p := &parse{Scanner: NewScanner(source)} 39 | 40 | tb := newBuilder(nil) 41 | p.Handler( 42 | func(token Token, str string) (err error) { 43 | tb, err = tb.Token(token, str) 44 | return 45 | }) 46 | 47 | p.Run() 48 | if p.err != nil { 49 | line, col, str := p.Scanner.LastLine() 50 | str = want.String("line:", line, " col:", col, "\n", str) 51 | wt.Nil(p.err, "from: ", from, str, FetchBuilderInfo(tb)) 52 | } 53 | } 54 | 55 | func FetchBuilderInfo(tb tomlBuilder) string { 56 | str := "\nToken: " + tb.root.token.String() + " ," + tb.token.String() 57 | if tb.it != nil { 58 | str += want.String("Item: ", tb.it.kind.String(), " TableName: ", tb.tableName, " Len: ", tb.it.Len()) 59 | } 60 | 61 | if tb.iv != nil { 62 | str += want.String("Value: ", tb.iv.kind.String(), " TableName: ", tb.tableName, " Len: ", tb.iv.Len()) 63 | } 64 | 65 | for key, it := range tb.Toml() { 66 | if it.kind != InvalidKind || key == iD { 67 | continue 68 | } 69 | str += want.String("Id: ", it.idx, " ,Path: ", key, " ,IsNil: ", it.v == nil) 70 | } 71 | return str 72 | } 73 | -------------------------------------------------------------------------------- /item.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | // Kind 用来标识 TOML 所有的规格. 13 | // 对于用于配置的 TOML 定义, Kind 其实是分段定义和值定义的. 14 | // 由于 TOML 官方没有使用 段/值 这样的词汇, tom-toml 选用规格这个词. 15 | type Kind uint 16 | 17 | // don't change order 18 | const ( 19 | InvalidKind Kind = iota 20 | String 21 | Integer 22 | Float 23 | Boolean 24 | Datetime 25 | StringArray 26 | IntegerArray 27 | FloatArray 28 | BooleanArray 29 | DatetimeArray 30 | Array 31 | // TableName 因为 tom-toml 支持注释的原因, 不存储具体值数据, 只存储规格本身信息. 32 | // 又因为 Toml 是个 map, 本身就具有 key/value 的存储功能, 所以无需另外定义 Table 33 | TableName 34 | ArrayOfTables 35 | ) 36 | 37 | // 内部使用的 id 38 | var iD = time.Now().UTC().Format(".ID20060102150405.000000000") 39 | 40 | func (k Kind) String() string { 41 | return kindsName[k] 42 | } 43 | 44 | var kindsName = [...]string{ 45 | "InvalidKind", 46 | "String", 47 | "Integer", 48 | "Float", 49 | "Boolean", 50 | "Datetime", 51 | "StringArray", 52 | "IntegerArray", 53 | "FloatArray", 54 | "BooleanArray", 55 | "DatetimeArray", 56 | "Array", 57 | "TableName", 58 | "ArrayOfTables", 59 | } 60 | 61 | var ( 62 | NotSupported = errors.New("not supported") 63 | OutOfRange = errors.New("out of range") 64 | InternalError = errors.New("internal error") 65 | InvalidItem = errors.New("invalid Item") 66 | ) 67 | 68 | // 计数器为保持格式化输出次序准备. 69 | var _counter = 0 70 | var _counterLocker sync.Mutex 71 | 72 | func counter(idx int) int { 73 | if idx > 0 { 74 | return idx 75 | } 76 | _counterLocker.Lock() 77 | defer _counterLocker.Unlock() 78 | _counter++ 79 | return _counter 80 | } 81 | 82 | // GenItem 函数返回一个新 Item. 83 | // 为保持格式化输出次序, GenItem 内部使用了一个计数器. 84 | // 使用者应该使用该函数来得到新的 Item. 而不是用 new(Item) 获得. 85 | // 那样的话就无法保持格式化输出次序. 86 | func GenItem(kind Kind) Item { 87 | if kind < 0 || kind > ArrayOfTables { 88 | panic(NotSupported) 89 | } 90 | 91 | it := Item{} 92 | it.Value = &Value{} 93 | it.multiComments = []string{} 94 | it.kind = kind 95 | 96 | it.idx = counter(0) 97 | 98 | if kind == ArrayOfTables { 99 | it.v = TomlArray{} 100 | } 101 | 102 | return it 103 | } 104 | 105 | type aString []string 106 | 107 | func (s aString) String() string { 108 | return strings.Join(s, "\n") 109 | } 110 | 111 | // Value 用来存储 String 至 Array 的值. 112 | type Value struct { 113 | kind Kind 114 | idx int 115 | v interface{} 116 | eolComment string // end of line comment 117 | multiComments aString // Multi-line comments 118 | //key string // cached key name for TOML formatter 119 | } 120 | 121 | // NewValue 函数返回一个新 *Value. 122 | // 为保持格式化输出次序, NewValue 内部使用了一个计数器. 123 | // 使用者应该使用该函数来得到新的 *Value. 而不是用 new(Value) 获得. 124 | // 那样的话就无法保持格式化输出次序. 125 | func NewValue(kind Kind) *Value { 126 | if kind > TableName { 127 | return nil 128 | } 129 | 130 | it := &Value{} 131 | it.kind = kind 132 | it.multiComments = []string{} 133 | 134 | it.idx = counter(0) 135 | 136 | return it 137 | } 138 | 139 | // returns multiline comments 140 | func (p *Value) Comments() []string { 141 | return append([]string{}, p.multiComments...) 142 | } 143 | 144 | // returns end-of-line comment 145 | func (p *Value) Comment() string { 146 | return p.eolComment 147 | } 148 | 149 | // set multiline comments 150 | func (p *Value) SetComments(as []string) { 151 | c := []string{} 152 | for _, s := range as { 153 | if strings.IndexAny(s, "\n\r\x1E") >= 0 { 154 | return 155 | } 156 | 157 | s = strings.TrimSpace(s) 158 | if s != "" && s[0] != '#' { 159 | s = "# " + s 160 | } 161 | c = append(c, s) 162 | } 163 | p.multiComments = c 164 | return 165 | } 166 | 167 | // set end-of-line comment 168 | func (p *Value) SetComment(s string) { 169 | if strings.IndexAny(s, "\n\r\x1E") >= 0 { 170 | return 171 | } 172 | 173 | s = strings.TrimSpace(s) 174 | if s != "" && s[0] != '#' { 175 | s = "# " + s 176 | } 177 | 178 | p.eolComment = s 179 | } 180 | 181 | // Kind 返回数据的风格. 182 | func (p *Value) Kind() Kind { 183 | if !p.IsValid() { 184 | return InvalidKind 185 | } 186 | return p.kind 187 | } 188 | 189 | /** 190 | Id 返回 int 值, 此值表示 Value 在运行期中生成次序的唯一序号. 191 | 返回 0 表示该 Value 无效. 192 | */ 193 | func (p *Value) Id() int { 194 | if !p.IsValid() { 195 | return 0 196 | } 197 | return p.idx 198 | } 199 | 200 | func (p *Value) KindIs(kind ...Kind) bool { 201 | if p == nil { 202 | return false 203 | } 204 | for _, k := range kind { 205 | if p.kind == k { 206 | return true 207 | } 208 | } 209 | return false 210 | } 211 | 212 | // IsValid 返回 p 是否有效. 213 | func (p *Value) IsValid() bool { 214 | return p != nil && p.kind != InvalidKind && (p.v != nil || p.kind == TableName) 215 | } 216 | 217 | // IsValue 返回 p 是否存储了值数据 218 | func (p *Value) IsValue() bool { 219 | return p != nil && p.kind != InvalidKind && p.v != nil && p.kind < TableName 220 | } 221 | 222 | func (p *Value) canNotSet(k Kind) bool { 223 | return p == nil || p.kind != InvalidKind && p.kind != k 224 | } 225 | 226 | // Set 用来设置 *Value 要存储的具体值. 参数 x 的类型范围可以是 227 | // String,Integer,Float,Boolean,Datetime 之一 228 | // 如果 *Value 的 Kind 是 InvalidKind(也就是没有明确值类型), 229 | // 调用 Set 后, *Value 的 kind 会相应的更改, 否则要求 x 的类型必须符合 *Value 的 kind 230 | // Set 失败会返回 NotSupported 错误. 231 | func (p *Value) Set(x interface{}) error { 232 | if p == nil { 233 | return NotSupported 234 | } 235 | switch v := x.(type) { 236 | case string: 237 | if p.canNotSet(String) { 238 | return NotSupported 239 | } 240 | p.v = v 241 | p.kind = String 242 | case bool: 243 | if p.canNotSet(Boolean) { 244 | return NotSupported 245 | } 246 | p.v = v 247 | p.kind = Boolean 248 | case float64: 249 | if p.canNotSet(Float) { 250 | return NotSupported 251 | } 252 | p.v = v 253 | p.kind = Float 254 | case time.Time: 255 | if p.canNotSet(Datetime) { 256 | return NotSupported 257 | } 258 | p.v = v.UTC() 259 | p.kind = Datetime 260 | case int64: 261 | if p.canNotSet(Integer) { 262 | return NotSupported 263 | } 264 | p.v = int64(v) 265 | p.kind = Integer 266 | case int: 267 | if p.canNotSet(Integer) { 268 | return NotSupported 269 | } 270 | p.v = int64(v) 271 | p.kind = Integer 272 | case uint: 273 | if p.canNotSet(Integer) { 274 | return NotSupported 275 | } 276 | p.v = int64(v) 277 | p.kind = Integer 278 | case int8: 279 | if p.canNotSet(Integer) { 280 | return NotSupported 281 | } 282 | p.v = int64(v) 283 | p.kind = Integer 284 | case int16: 285 | if p.canNotSet(Integer) { 286 | return NotSupported 287 | } 288 | p.v = int64(v) 289 | p.kind = Integer 290 | case int32: 291 | if p.canNotSet(Integer) { 292 | return NotSupported 293 | } 294 | p.v = int64(v) 295 | p.kind = Integer 296 | case uint8: 297 | if p.canNotSet(Integer) { 298 | return NotSupported 299 | } 300 | p.v = int64(v) 301 | p.kind = Integer 302 | case uint16: 303 | if p.canNotSet(Integer) { 304 | return NotSupported 305 | } 306 | p.v = int64(v) 307 | p.kind = Integer 308 | case uint32: 309 | if p.canNotSet(Integer) { 310 | return NotSupported 311 | } 312 | p.v = int64(v) 313 | p.kind = Integer 314 | case uint64: 315 | if p.canNotSet(Integer) { 316 | return NotSupported 317 | } 318 | if v > 9223372036854775807 { 319 | return OutOfRange 320 | } 321 | p.kind = Integer 322 | p.v = int64(v) 323 | default: 324 | return NotSupported 325 | } 326 | p.idx = counter(p.idx) 327 | return nil 328 | } 329 | 330 | func conv(s string, kind Kind) (v interface{}, err error) { 331 | switch kind { 332 | case String: 333 | v = s 334 | case Integer: 335 | v, err = strconv.ParseInt(s, 10, 64) 336 | case Float: 337 | v, err = strconv.ParseFloat(s, 64) 338 | case Boolean: 339 | v, err = strconv.ParseBool(s) 340 | case Datetime: 341 | v, err = time.Parse(time.RFC3339, s) // time zone +00:00 342 | if err == nil { 343 | v = v.(time.Time).UTC() 344 | } 345 | default: 346 | err = NotSupported 347 | } 348 | return 349 | } 350 | 351 | // SetAs是个便捷方法, 通过参数 kind 对 string 参数进行转换并执行 Set. 352 | func (p *Value) SetAs(s string, kind Kind) (err error) { 353 | if p.canNotSet(kind) { 354 | return NotSupported 355 | } 356 | switch kind { 357 | case String: 358 | p.v = s 359 | case Integer: 360 | var v int64 361 | v, err = strconv.ParseInt(s, 10, 64) 362 | if err == nil { 363 | p.v = v 364 | } 365 | case Float: 366 | var v float64 367 | v, err = strconv.ParseFloat(s, 64) 368 | if err == nil { 369 | p.v = v 370 | } 371 | case Boolean: 372 | var v bool 373 | v, err = strconv.ParseBool(s) 374 | if err == nil { 375 | p.v = v 376 | } 377 | case Datetime: 378 | var v time.Time 379 | v, err = time.Parse(time.RFC3339, s) 380 | if err == nil { 381 | p.v = v.UTC() 382 | } 383 | default: 384 | return NotSupported 385 | } 386 | if err == nil { 387 | p.kind = kind 388 | p.idx = counter(p.idx) 389 | } 390 | return 391 | } 392 | 393 | func asValue(i interface{}) (v *Value, ok bool) { 394 | it, ok := i.(Item) 395 | if ok { 396 | v = it.Value 397 | } else { 398 | v, ok = i.(*Value) 399 | } 400 | return 401 | } 402 | 403 | // Add element for Array or typeArray. 404 | /** 405 | Add 方法为数组添加元素, 支持空数组元素. 406 | p 本身的 Kind 决定是否支持参数元素 Kind. 407 | */ 408 | func (p *Value) Add(ai ...interface{}) error { 409 | if p == nil || p.kind < StringArray || p.kind > Array { 410 | return NotSupported 411 | } 412 | if len(ai) == 0 { 413 | return nil 414 | } 415 | 416 | vs := make([]*Value, len(ai)) 417 | 418 | k := 0 419 | mix := false 420 | 421 | // 全部检查一遍 422 | for i, s := range ai { 423 | v, ok := asValue(s) 424 | 425 | if !ok { 426 | v = &Value{} 427 | err := v.Set(s) 428 | if err != nil { 429 | return err 430 | } 431 | } 432 | 433 | if v == nil || v.kind > Array { 434 | return NotSupported 435 | } 436 | 437 | if p.kind != Array && v.kind != p.kind+String-StringArray { 438 | return NotSupported 439 | } 440 | 441 | // 用于分析 kind 的情况 442 | k = k | 1<> (StringArray - 1) << (StringArray - 1) 453 | if p.kind != Array || nk != k { 454 | return NotSupported 455 | } 456 | } else { 457 | if p.kind == Array { 458 | if p.Len() > 0 && vs[0].kind < StringArray { 459 | return NotSupported 460 | } 461 | // plain 462 | if vs[0].kind < StringArray { 463 | p.kind = vs[0].kind + StringArray - String 464 | } 465 | } 466 | } 467 | 468 | if p.v == nil { 469 | p.v = make([]*Value, 0) 470 | } 471 | o, ok := p.v.([]*Value) 472 | 473 | if !ok { 474 | return NotSupported 475 | } 476 | 477 | p.v = append(o, vs...) 478 | return nil 479 | } 480 | 481 | // String 返回 *Value 存储数据的字符串表示. 482 | // 注意所有的规格定义都是可以字符串化的. 483 | func (p *Value) String() string { 484 | return p.string("", 0) 485 | } 486 | 487 | /** 488 | 如果值是 Integer 可以使用 Int 返回其 int64 值. 489 | 否则返回 0 490 | */ 491 | func (p *Value) Int() int64 { 492 | if !p.IsValid() || p.kind != Integer { 493 | return 0 494 | } 495 | return p.v.(int64) 496 | } 497 | 498 | /** 499 | 如果值是 Integer 可以使用 Integer 返回其 int 值. 500 | 否则返回 0 501 | */ 502 | func (p *Value) Integer() int { 503 | if !p.IsValid() || p.kind != Integer { 504 | return 0 505 | } 506 | return int(p.v.(int64)) 507 | } 508 | 509 | /** 510 | 如果值是 Integer 可以使用 UInteger 返回其 uint 值. 511 | 否则返回 0. 注意此方法不检查值是否为负数. 512 | */ 513 | func (p *Value) UInteger() uint { 514 | if !p.IsValid() || p.kind != Integer { 515 | return 0 516 | } 517 | return uint(p.v.(int64)) 518 | } 519 | 520 | /** 521 | 如果值是 Integer 可以使用 UInt 返回其 uint64 值. 522 | 否则返回 0 523 | */ 524 | func (p *Value) UInt() uint64 { 525 | if !p.IsValid() || p.kind != Integer { 526 | return 0 527 | } 528 | return uint64(p.v.(int64)) 529 | } 530 | 531 | /** 532 | 如果值是 Float 可以使用 Float 返回其 float64 值. 533 | 否则返回 0 534 | */ 535 | func (p *Value) Float() float64 { 536 | if !p.IsValid() || p.kind != Float { 537 | return 0 538 | } 539 | return p.v.(float64) 540 | } 541 | 542 | /** 543 | 如果值是 Boolean 可以使用 Boolean 返回其 bool 值. 544 | 否则返回 false 545 | */ 546 | func (p *Value) Boolean() bool { 547 | if !p.IsValid() || p.kind != Boolean { 548 | return false 549 | } 550 | return p.v.(bool) 551 | } 552 | 553 | /** 554 | 如果值是 Datetime 可以使用 Datetime 返回其 time.Time 值. 555 | 否则返回UTC时间公元元年1月1日 00:00:00. 可以用 IsZero() 进行判断. 556 | */ 557 | func (p *Value) Datetime() time.Time { 558 | if !p.IsValid() || p.kind != Datetime { 559 | return time.Time{} 560 | } 561 | return p.v.(time.Time) 562 | } 563 | 564 | func (p *Value) StringArray() []string { 565 | if !p.IsValid() { 566 | return nil 567 | } 568 | a, ok := p.v.([]*Value) 569 | if !ok || p.kind != StringArray { 570 | return nil 571 | } 572 | re := make([]string, len(a)) 573 | for i, v := range a { 574 | re[i] = v.String() 575 | } 576 | return re 577 | } 578 | 579 | func (p *Value) IntArray() []int64 { 580 | if !p.IsValid() { 581 | return nil 582 | } 583 | a, ok := p.v.([]*Value) 584 | if !ok || p.kind != IntegerArray { 585 | return nil 586 | } 587 | re := make([]int64, len(a)) 588 | for i, v := range a { 589 | re[i] = v.Int() 590 | } 591 | return re 592 | } 593 | 594 | func (p *Value) UIntArray() []uint64 { 595 | if !p.IsValid() { 596 | return nil 597 | } 598 | a, ok := p.v.([]*Value) 599 | if !ok || p.kind != IntegerArray { 600 | return nil 601 | } 602 | re := make([]uint64, len(a)) 603 | for i, v := range a { 604 | re[i] = v.UInt() 605 | } 606 | return re 607 | } 608 | 609 | func (p *Value) IntegerArray() []int { 610 | if !p.IsValid() { 611 | return nil 612 | } 613 | a, ok := p.v.([]*Value) 614 | if !ok || p.kind != IntegerArray { 615 | return nil 616 | } 617 | re := make([]int, len(a)) 618 | for i, v := range a { 619 | re[i] = int(v.Int()) 620 | } 621 | return re 622 | } 623 | 624 | func (p *Value) UIntegerArray() []uint { 625 | if !p.IsValid() { 626 | return nil 627 | } 628 | a, ok := p.v.([]*Value) 629 | if !ok || p.kind != IntegerArray { 630 | return nil 631 | } 632 | re := make([]uint, len(a)) 633 | for i, v := range a { 634 | re[i] = uint(v.UInt()) 635 | } 636 | return re 637 | } 638 | 639 | func (p *Value) FloatArray() []float64 { 640 | if !p.IsValid() { 641 | return nil 642 | } 643 | a, ok := p.v.([]*Value) 644 | if !ok || p.kind != FloatArray { 645 | return nil 646 | } 647 | re := make([]float64, len(a)) 648 | for i, v := range a { 649 | re[i] = v.Float() 650 | } 651 | return re 652 | } 653 | 654 | func (p *Value) BooleanArray() []bool { 655 | if !p.IsValid() { 656 | return nil 657 | } 658 | a, ok := p.v.([]*Value) 659 | if !ok || p.kind != BooleanArray { 660 | return nil 661 | } 662 | re := make([]bool, len(a)) 663 | for i, v := range a { 664 | re[i] = v.Boolean() 665 | } 666 | return re 667 | } 668 | 669 | func (p *Value) DatetimeArray() []time.Time { 670 | if !p.IsValid() { 671 | return nil 672 | } 673 | a, ok := p.v.([]*Value) 674 | if !ok || p.kind != DatetimeArray { 675 | return nil 676 | } 677 | re := make([]time.Time, len(a)) 678 | for i, v := range a { 679 | re[i] = v.Datetime() 680 | } 681 | return re 682 | } 683 | 684 | // Len returns length for Array , typeArray. 685 | // Otherwise Kind return -1. 686 | // +dl 687 | 688 | /** 689 | Len 返回数组类型元素个数. 否则返回 -1. 690 | */ 691 | func (p *Value) Len() int { 692 | if p.IsValid() && p.kind >= StringArray && p.kind <= Array { 693 | a, ok := p.v.([]*Value) 694 | if ok { 695 | return len(a) 696 | } 697 | } 698 | return -1 699 | } 700 | 701 | // Index returns *Value for Array , typeArray. 702 | // idx negative available. 703 | // Otherwise Kind return nil. 704 | // +dl 705 | 706 | /** 707 | Index 根据 idx 下标返回类型数组或二维数组对应的元素. 708 | idx 可以用负数作为下标. 709 | 如果非数组或者下标超出范围返回 nil. 710 | */ 711 | func (p *Value) Index(idx int) *Value { 712 | if !p.IsValid() || p.kind < StringArray && p.kind > Array { 713 | return nil 714 | } 715 | a, ok := p.v.([]*Value) 716 | if !ok { 717 | return nil 718 | } 719 | size := len(a) 720 | if idx < 0 { 721 | idx = size + idx 722 | } 723 | if idx < 0 || idx >= size { 724 | return nil 725 | } 726 | return a[idx] 727 | } 728 | 729 | // for ArrayOfTables 730 | type TomlArray []Toml 731 | 732 | func (t TomlArray) Index(idx int) Toml { 733 | if idx < len(t) { 734 | return t[idx] 735 | } 736 | return nil 737 | } 738 | 739 | func (t TomlArray) Len() int { 740 | return len(t) 741 | } 742 | 743 | // Item 扩展自 *Value,支持 ArrayOfTables. 744 | type Item struct { 745 | *Value 746 | } 747 | 748 | /** 749 | 如果是 ArrayOfTables 返回 TomlArray, 否则返回 nil. 750 | 使用返回的 TomlArray 时, 注意其数组特性. 751 | */ 752 | func (i Item) TomlArray() TomlArray { 753 | if i.Value == nil || i.Value.v == nil || i.kind != ArrayOfTables { 754 | return nil 755 | } 756 | t, ok := i.v.(TomlArray) 757 | if ok { 758 | return t 759 | } 760 | return nil 761 | } 762 | 763 | /** 764 | 如果是 ArrayOfTables 追加 toml, 返回发生的错误. 765 | */ 766 | func (i Item) AddTable(tm Toml) error { 767 | if i.Value == nil || i.Value.v == nil || i.kind != ArrayOfTables { 768 | return NotSupported 769 | } 770 | 771 | if len(tm) == 0 { 772 | return nil 773 | } 774 | 775 | if tm.Id().idx <= i.idx { 776 | return NotSupported 777 | } 778 | 779 | aot, ok := i.v.(TomlArray) 780 | if !ok { 781 | return InternalError 782 | } 783 | 784 | i.v = append(aot, tm) 785 | return nil 786 | } 787 | 788 | // Index returns Toml for ArrayOfTables[idx]. 789 | // Otherwise Kind return nil. 790 | // +dl 791 | 792 | /** 793 | 如果是 ArrayOfTables 返回下标为 idx 的 Toml, 否则返回 nil. 794 | 支持倒序下标. 795 | */ 796 | func (i Item) Table(idx int) Toml { 797 | if i.Value == nil || i.Value.v == nil || i.kind != ArrayOfTables { 798 | return nil 799 | } 800 | aot, ok := i.v.(TomlArray) 801 | if !ok { 802 | return nil 803 | } 804 | 805 | size := len(aot) 806 | if idx < 0 { 807 | idx = size + idx 808 | } 809 | if idx < 0 || idx >= size { 810 | return nil 811 | } 812 | return aot[idx] 813 | } 814 | 815 | // Len returns length for Array , typeArray , ArrayOfTables. 816 | // Otherwise Kind return -1. 817 | // +dl 818 | 819 | /** 820 | Len 返回数组类型的元素个数. 821 | 否则返回 -1. 822 | */ 823 | func (i Item) Len() int { 824 | if i.Value != nil && i.kind == ArrayOfTables { 825 | a, ok := i.v.(TomlArray) 826 | if ok { 827 | return len(a) 828 | } 829 | return -1 830 | } 831 | return i.Value.Len() 832 | } 833 | 834 | // Value 的 string, 只考虑 layout, 不考虑上层的缩进关系. 835 | // indent 是专门为 typeArrayString 留的. 这样做可以简化代码. 836 | func (p *Value) string(indent string, layout int) string { 837 | if !p.IsValid() { 838 | return "" 839 | } 840 | 841 | switch p.kind { 842 | case String: 843 | if layout == 0 { 844 | return p.v.(string) 845 | } else { 846 | return strconv.Quote(p.v.(string)) 847 | } 848 | case Integer: 849 | return strconv.FormatInt(p.v.(int64), 10) 850 | case Float: 851 | return strconv.FormatFloat(p.v.(float64), 'f', -1, 64) 852 | case Boolean: 853 | return strconv.FormatBool(p.v.(bool)) 854 | case Datetime: 855 | return p.v.(time.Time).Format("2006-01-02T15:04:05Z") // ISO 8601 856 | 857 | case StringArray, IntegerArray, FloatArray, BooleanArray, DatetimeArray: 858 | return p.typeArrayString(indent, 1) 859 | case Array: 860 | return p.typeArrayString(indent, 1) 861 | /* 862 | case TableName: 863 | return "[]" 864 | case ArrayOfTables: 865 | return "[[" + p.key + "]]" 866 | default: 867 | return InvalidItem.Error() 868 | */ 869 | } 870 | return "" 871 | } 872 | 873 | // typeArray 比较特殊, 单独处理缩进问题 874 | func (p *Value) typeArrayString(indent string, layout int) string { 875 | a := p.v.([]*Value) 876 | fmt := "" 877 | max := len(a) - 1 878 | for i, it := range a { 879 | 880 | if !it.IsValid() { 881 | return InvalidItem.Error() 882 | } 883 | 884 | if layout == 1 && len(it.multiComments) != 0 { 885 | fmt += "\n" 886 | for _, s := range it.multiComments { 887 | fmt += indent + s + "\n" 888 | } 889 | } 890 | 891 | if i != max { 892 | fmt += it.string(indent, layout) + ", " 893 | } else { 894 | fmt += it.string(indent, layout) 895 | } 896 | 897 | if layout == 1 && it.eolComment != "" { 898 | fmt += it.eolComment + "\n" + indent 899 | } 900 | } 901 | 902 | return "[" + fmt + "]" 903 | } 904 | 905 | func (it Item) Apply(dst interface{}) (count int) { 906 | if !it.IsValid() { 907 | return 908 | } 909 | return it.Value.Apply(dst) 910 | } 911 | 912 | func (it *Value) Apply(dst interface{}) (count int) { 913 | if !it.IsValid() { 914 | return 915 | } 916 | 917 | var ( 918 | vv reflect.Value 919 | ok bool 920 | ) 921 | 922 | vv, ok = dst.(reflect.Value) 923 | if ok { 924 | vv = reflect.Indirect(vv) 925 | } else { 926 | vv = reflect.Indirect(reflect.ValueOf(dst)) 927 | } 928 | 929 | if !vv.IsValid() || !vv.CanSet() { 930 | return 931 | } 932 | 933 | return it.apply(vv) 934 | } 935 | 936 | func (it *Value) apply(vv reflect.Value) (count int) { 937 | 938 | vt := vv.Type() 939 | 940 | switch vt.Kind() { 941 | case reflect.Bool: 942 | if it.kind == Boolean { 943 | vv.SetBool(it.Boolean()) 944 | count++ 945 | } 946 | case reflect.String: 947 | if it.kind >= String && it.kind < StringArray { 948 | vv.SetString(it.String()) 949 | count++ 950 | } 951 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 952 | if it.kind == Integer { 953 | vv.SetInt(it.Int()) 954 | count++ 955 | } 956 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 957 | if it.kind == Integer { 958 | vv.SetUint(it.UInt()) 959 | count++ 960 | } 961 | case reflect.Float32, reflect.Float64: 962 | if it.kind == Float { 963 | vv.SetFloat(it.Float()) 964 | count++ 965 | } 966 | case reflect.Interface: 967 | if vt.String() == "interface {}" && it.IsValid() { 968 | vv.Set(reflect.ValueOf(it.v)) 969 | count++ 970 | } 971 | case reflect.Struct: 972 | if it.IsValid() && it.kind == Datetime && vt.String() == "time.Time" { 973 | vv.Set(reflect.ValueOf(it.Datetime())) 974 | count++ 975 | } 976 | case reflect.Array, reflect.Slice: 977 | 978 | l := it.Len() 979 | if l <= 0 { 980 | break 981 | } 982 | 983 | if vt.Kind() == reflect.Slice && vv.Len() < l { 984 | 985 | // ? How to reflect.NewAt(typ, p) 986 | 987 | if vv.Cap() < l { 988 | l = vv.Cap() 989 | } 990 | vv.SetLen(l) 991 | } 992 | 993 | for i := 0; i < l && i < vv.Len(); i++ { 994 | count += it.Index(i).apply(vv.Index(i)) 995 | } 996 | } 997 | return 998 | } 999 | -------------------------------------------------------------------------------- /item_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "github.com/achun/testing-want" 5 | "testing" 6 | ) 7 | 8 | func TestItemAdd(t *testing.T) { 9 | wt := want.T(t) 10 | 11 | wt.Error(GenItem(InvalidKind).Add(1)) 12 | wt.Error(GenItem(String).Add(1)) 13 | wt.Error(GenItem(Integer).Add(1)) 14 | wt.Error(GenItem(Float).Add(1)) 15 | wt.Error(GenItem(Boolean).Add(1)) 16 | wt.Error(GenItem(Datetime).Add(1)) 17 | 18 | a := GenItem(Array) 19 | 20 | wt.Nil(a.Add(1), "emptyArray.Add(int)") 21 | wt.Nil(a.Add(2, 3), "IntegerArray.Add(int,int)") 22 | 23 | wt.Equal(a.kind, IntegerArray) 24 | wt.Equal(a.Len(), 3) 25 | wt.Equal(a.String(), "[1, 2, 3]") 26 | wt.Equal(a.Index(0).Int(), int64(1)) 27 | wt.Equal(a.Index(1).Int(), int64(2)) 28 | wt.Equal(a.Index(2).Int(), int64(3)) 29 | // 负数下标 30 | wt.Equal(a.Index(-1).Int(), int64(3)) 31 | // 超出下标 32 | wt.Nil(a.Index(3)) 33 | wt.Equal(a.Index(3).Int(), int64(0)) 34 | 35 | wt.Error(a.Add("string")) 36 | 37 | aa := GenItem(Array) 38 | wt.Nil(aa.Add(a), "Array.Add(IntegerArray)") 39 | wt.Equal(aa.kind, Array) 40 | 41 | b := GenItem(Array) 42 | wt.Nil(b.Add("hello")) 43 | wt.Nil(b.Add("world")) 44 | wt.Equal(b.kind, StringArray) 45 | wt.Equal(b.Len(), 2) 46 | 47 | wt.Nil(aa.Add(b), "Array.Add(StringArray)") 48 | wt.Equal(aa.kind, Array) 49 | wt.Equal(aa.Len(), 2) 50 | 51 | wt.Nil(aa.Add(a, b), "Array.Add(IntegerArray,StringArray)") 52 | wt.Equal(aa.kind, Array) 53 | 54 | wt.Equal(b.Add(1), NotSupported, "StringArray.Add(int)") 55 | 56 | wt.Equal(aa.kind, Array) 57 | wt.Equal(aa.Add(1), NotSupported, aa.kind.String(), ".Add(int)") 58 | 59 | } 60 | 61 | func TestItemPlain(t *testing.T) { 62 | wt := want.T(t) 63 | a := GenItem(Datetime) 64 | 65 | wt.Nil(a.SetAs("2012-01-02T13:11:14Z", Datetime)) 66 | 67 | wt.Equal(a.String(), "2012-01-02T13:11:14Z") 68 | wt.Equal(a.String(), "2012-01-02T13:11:14Z") 69 | } 70 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type Status int 10 | 11 | const ( 12 | SNot Status = iota 13 | SInvalid 14 | SUnexpected 15 | SMaybe 16 | SYes 17 | SYesKeep // SYes 并且多读了一个字符, 保持当前的字符给后续解析 18 | ) 19 | 20 | var statusName = [...]string{ 21 | "Not", 22 | "Invalid", 23 | "Unexpected", 24 | "Maybe", 25 | "Yes", 26 | "YesKeep", 27 | } 28 | 29 | func (t Status) String() string { 30 | return statusName[t] 31 | } 32 | 33 | const ( 34 | BOM = 0xFEFF // UTF-8 encoded byte order mark 35 | ) 36 | 37 | type Token uint 38 | 39 | // don't change order 40 | const ( 41 | tokenEOF Token = iota 42 | tokenString 43 | tokenInteger 44 | tokenFloat 45 | tokenBoolean 46 | tokenDatetime 47 | tokenWhitespace 48 | tokenComment 49 | tokenTableName 50 | tokenArrayOfTables 51 | tokenNewLine 52 | tokenKey 53 | tokenEqual 54 | tokenArrayLeftBrack 55 | tokenArrayRightBrack 56 | tokenComma 57 | tokenError 58 | tokenRuneError 59 | tokenNothing 60 | ) 61 | 62 | func (t Token) String() string { 63 | i := int(t) 64 | if i < 0 || i >= len(tokensName) { 65 | return "User defined error: " + fmt.Sprint(i) 66 | } 67 | return tokensName[i] 68 | } 69 | 70 | var tokensName = [...]string{ 71 | "EOF", 72 | "String", 73 | "Integer", 74 | "Float", 75 | "Boolean", 76 | "Datetime", 77 | "Whitespace", 78 | "Comment", 79 | "TableName", 80 | "ArrayOfTables", 81 | "NewLine", 82 | "Key", 83 | "Equal", 84 | "ArrayLeftBrack", 85 | "ArrayRightBrack", 86 | "Comma", 87 | "Error", 88 | "EncodingError", 89 | "Nothing", 90 | } 91 | 92 | type TokenHandler func(Token, string) error 93 | 94 | type parser interface { 95 | Scanner 96 | Handler(TokenHandler) 97 | 98 | Run() 99 | Keep() 100 | IsTestMode() bool 101 | 102 | Err(msg string) 103 | Token(token Token) error 104 | Invalid(token Token) 105 | NotMatch(token ...Token) 106 | Unexpected(token Token) 107 | } 108 | 109 | type parse struct { 110 | Scanner 111 | err error 112 | handler TokenHandler 113 | next bool 114 | testMode bool // 测试模式允许不完整的 stage 115 | } 116 | 117 | func (p *parse) Close() {} 118 | 119 | func (p *parse) Run() { 120 | stagePlay(p, openStage()) 121 | } 122 | 123 | func (p *parse) IsTestMode() bool { 124 | return p.testMode 125 | } 126 | 127 | func (p *parse) Next() rune { 128 | if p.next { 129 | return p.Scanner.Next() 130 | } 131 | p.next = true 132 | return p.Scanner.Rune() 133 | } 134 | 135 | func (p *parse) Keep() { 136 | p.next = false 137 | } 138 | 139 | func (p *parse) Err(msg string) { 140 | p.err = errors.New(msg) 141 | p.Token(tokenError) 142 | } 143 | 144 | func (p *parse) NotMatch(token ...Token) { 145 | var msg string 146 | if len(token) == 1 { 147 | msg = "incomplete" 148 | } else { 149 | msg = "no one can match for" 150 | } 151 | 152 | for _, t := range token { 153 | msg += " " + t.String() 154 | } 155 | 156 | p.err = errors.New(msg) 157 | p.Token(tokenError) 158 | } 159 | 160 | func (p *parse) Invalid(token Token) { 161 | p.err = errors.New("invalid " + tokensName[token]) 162 | p.Token(tokenError) 163 | } 164 | 165 | func (p *parse) Unexpected(token Token) { 166 | p.err = errors.New("unexpected token " + tokensName[token]) 167 | p.Token(tokenError) 168 | } 169 | 170 | func (p *parse) Token(token Token) (err error) { 171 | var str string 172 | if token == tokenError { 173 | if p.err == nil { 174 | p.err = errors.New("invalid format.") 175 | } 176 | err = p.err 177 | str = err.Error() 178 | } else { 179 | str = p.Scanner.Fetch(p.next) 180 | if token != tokenEOF && token != tokenWhitespace { 181 | str = strings.TrimSpace(str) 182 | } 183 | } 184 | 185 | if p.handler == nil { 186 | fmt.Println(token.String(), str) 187 | } else { 188 | if token == tokenError { 189 | p.handler(token, str) 190 | if err == nil { 191 | err = errors.New("tokenError") 192 | } 193 | } else { 194 | err = p.handler(token, str) 195 | } 196 | } 197 | return 198 | } 199 | 200 | // Handler to set TokenHandler 201 | func (p *parse) Handler(h TokenHandler) { 202 | p.handler = h 203 | } 204 | 205 | // tokens 206 | func itsWhitespace(r rune, flag int, maybe bool) (Status, Token) { 207 | if maybe && flag == 0 { 208 | return SNot, tokenWhitespace 209 | } 210 | if isWhitespace(r) { 211 | return SMaybe, 1 212 | } 213 | if flag == 1 { 214 | return SYesKeep, tokenWhitespace 215 | } 216 | return SNot, tokenWhitespace 217 | } 218 | func itsComment(r rune, flag int, maybe bool) (Status, Token) { 219 | if maybe && flag == 0 { 220 | return SNot, tokenComment 221 | } 222 | 223 | switch flag { 224 | case 0: 225 | if r == '#' { 226 | return SMaybe, 1 227 | } 228 | case 1: 229 | if isEOF(r) { 230 | return SYes, tokenComment 231 | } 232 | if isNewLine(r) { 233 | return SYesKeep, tokenComment 234 | } 235 | return SMaybe, 1 236 | } 237 | return SNot, tokenComment 238 | } 239 | func itsString(r rune, flag int, maybe bool) (Status, Token) { 240 | if maybe && flag == 0 { 241 | return SNot, tokenString 242 | } 243 | 244 | switch flag { 245 | case 0: 246 | if r == '"' { 247 | return SMaybe, 2 248 | } 249 | return SNot, tokenString 250 | case 1: // skip 251 | if !isNewLine(r) { 252 | return SMaybe, 2 253 | } 254 | case 2: 255 | if r == '"' { 256 | return SYes, tokenString 257 | } 258 | if r == '\\' { 259 | return SMaybe, 1 260 | } 261 | if !isNewLine(r) { 262 | return SMaybe, 2 263 | } 264 | } 265 | return SInvalid, tokenString 266 | } 267 | 268 | // 要求在 itsFlaot 的前面 269 | func itsInteger(r rune, flag int, maybe bool) (Status, Token) { 270 | if maybe && flag == 0 { 271 | return SNot, tokenInteger 272 | } 273 | 274 | switch flag { 275 | case 0: 276 | if r == '-' { 277 | return SMaybe, 1 278 | } 279 | if is09(r) { 280 | return SMaybe, 2 281 | } 282 | case 1: 283 | if is09(r) { 284 | return SMaybe, 2 285 | } 286 | case 2: 287 | if is09(r) { 288 | return SMaybe, 2 289 | } 290 | if isSuffixOfValue(r) { 291 | return SYesKeep, tokenInteger 292 | } 293 | } 294 | return SNot, tokenInteger 295 | } 296 | 297 | func itsFloat(r rune, flag int, maybe bool) (Status, Token) { 298 | 299 | // 还是有 bug ??? 注释中可能有这些值 300 | switch flag { 301 | case 0: 302 | if r == '-' { 303 | return SMaybe, 1 304 | } 305 | if is09(r) { 306 | return SMaybe, 2 307 | } 308 | case 1: 309 | if is09(r) { 310 | return SMaybe, 2 311 | } 312 | case 2: 313 | if is09(r) { 314 | return SMaybe, 2 315 | } 316 | if r == '.' { 317 | return SMaybe, 3 318 | } 319 | case 3: 320 | if is09(r) { 321 | return SMaybe, 4 322 | } 323 | return SInvalid, tokenFloat 324 | case 4: 325 | if is09(r) { 326 | return SMaybe, 4 327 | } 328 | if isSuffixOfValue(r) { 329 | return SYesKeep, tokenFloat 330 | } 331 | } 332 | return SNot, tokenFloat 333 | } 334 | 335 | func itsBoolean(r rune, flag int, maybe bool) (Status, Token) { 336 | const layout = "truefalse" 337 | switch flag { 338 | case 0: 339 | if maybe { 340 | return SNot, tokenBoolean 341 | } 342 | if r == 't' { 343 | return SMaybe, 1 344 | } 345 | if r == 'f' { 346 | return SMaybe, 5 347 | } 348 | case 1, 2, 3, 5, 6, 7, 8: 349 | if rune(layout[flag]) == r { 350 | return SMaybe, Token(flag + 1) 351 | } 352 | case 4, 9: 353 | if isSuffixOfValue(r) { 354 | return SYesKeep, tokenBoolean 355 | } 356 | } 357 | return SNot, tokenBoolean 358 | } 359 | func itsDatetime(r rune, flag int, maybe bool) (Status, Token) { 360 | const layout = "0000-00-00T00:00:00Z" 361 | if flag >= 0 && flag < 20 { 362 | if layout[flag] == '0' && is09(r) || r == rune(layout[flag]) { 363 | return SMaybe, Token(flag + 1) 364 | } 365 | if flag <= 4 { 366 | return SNot, tokenDatetime 367 | } 368 | } 369 | if flag == 20 && isSuffixOfValue(r) { 370 | return SYesKeep, tokenDatetime 371 | } 372 | return SInvalid, tokenDatetime 373 | } 374 | 375 | // [ASCII] http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters 376 | func itsTableName(r rune, flag int, maybe bool) (Status, Token) { 377 | switch flag { 378 | case 0: 379 | if !maybe && r == '[' { 380 | return SMaybe, 1 381 | } 382 | case 1: 383 | if r == '[' { 384 | return SNot, tokenTableName 385 | } 386 | if isNewLine(r) || isWhitespace(r) || r == ']' || r == '.' { 387 | return SInvalid, tokenTableName 388 | } 389 | return SMaybe, 2 390 | case 2: 391 | if isNewLine(r) || isWhitespace(r) { 392 | return SInvalid, tokenTableName 393 | } 394 | if r != ']' { 395 | return SMaybe, 2 396 | } 397 | return SYes, tokenTableName 398 | } 399 | return SNot, tokenTableName 400 | } 401 | 402 | func itsArrayOfTables(r rune, flag int, maybe bool) (Status, Token) { 403 | switch flag { 404 | case 0, 1: 405 | if r == '[' { 406 | return SMaybe, Token(flag + 1) 407 | } 408 | case 2: 409 | if isNewLine(r) || isWhitespace(r) || r == ']' || r == '.' { 410 | return SInvalid, tokenArrayOfTables 411 | } 412 | return SMaybe, 3 413 | case 3: 414 | if isNewLine(r) || isWhitespace(r) { 415 | return SInvalid, tokenArrayOfTables 416 | } 417 | if r != ']' { 418 | return SMaybe, 3 419 | } 420 | return SMaybe, 4 421 | case 4: 422 | if r != ']' { 423 | return SInvalid, tokenArrayOfTables 424 | } 425 | return SYes, tokenArrayOfTables 426 | } 427 | return SNot, tokenArrayOfTables 428 | } 429 | func itsNewLine(r rune, flag int, maybe bool) (Status, Token) { 430 | if flag == 0 && isNewLine(r) { 431 | return SYes, tokenNewLine 432 | } 433 | return SNot, tokenNewLine 434 | } 435 | 436 | func itsKey(r rune, flag int, maybe bool) (Status, Token) { 437 | if maybe && flag == 0 { 438 | return SNot, tokenKey 439 | } 440 | switch flag { 441 | case 0: 442 | return SMaybe, 1 443 | case 1: 444 | if isNewLine(r) || isEOF(r) { 445 | return SInvalid, tokenKey 446 | } 447 | if r == '=' || isWhitespace(r) { 448 | return SYesKeep, tokenKey 449 | } 450 | return SMaybe, 1 451 | } 452 | return SNot, tokenKey 453 | } 454 | func itsEqual(r rune, flag int, maybe bool) (Status, Token) { 455 | if maybe && flag == 0 { 456 | return SNot, tokenEqual 457 | } 458 | if r == '=' { 459 | return SYes, tokenEqual 460 | } 461 | return SNot, tokenEqual 462 | } 463 | 464 | func itsComma(r rune, flag int, maybe bool) (Status, Token) { 465 | if !maybe && r == ',' { 466 | return SYes, tokenComma 467 | } 468 | return SNot, tokenComma 469 | } 470 | 471 | func itsArrayLeftBrack(r rune, flag int, maybe bool) (Status, Token) { 472 | if !maybe && r == '[' { 473 | return SYes, tokenArrayLeftBrack 474 | } 475 | return SNot, tokenArrayLeftBrack 476 | } 477 | func itsArrayRightBrack(r rune, flag int, maybe bool) (Status, Token) { 478 | if !maybe && r == ']' { 479 | return SYes, tokenArrayRightBrack 480 | } 481 | return SNot, tokenArrayRightBrack 482 | } 483 | 484 | func itsEOF(r rune, flag int, maybe bool) (Status, Token) { 485 | if r == EOF { 486 | return SYes, tokenEOF 487 | } 488 | return SNot, tokenEOF 489 | } 490 | 491 | func itsSNot(r rune, flag int, maybe bool) (Status, Token) { 492 | return SNot, tokenEOF 493 | } 494 | 495 | func isEOF(r rune) bool { 496 | return r == EOF 497 | } 498 | func is09(r rune) bool { 499 | 500 | return r >= '0' && r <= '9' 501 | } 502 | 503 | // Spec: Whitespace means tab (0x09) or space (0x20). 504 | func isWhitespace(r rune) bool { 505 | return r == ' ' || r == '\t' // || r == '\v' || r == '\f' // 0x85, 0xA0 ? 506 | } 507 | 508 | // [NewLine](http://en.wikipedia.org/wiki/Newline) 509 | // LF = 0x0A // Line feed, \n 510 | // CR = 0x0D // Carriage return, \r 511 | // LFCR = 0x0A0D // \n\r 512 | // CRLF = 0x0D0A // \r\n 513 | // RS = 0x1E // QNX pre-POSIX implementation. 514 | func isNewLine(r rune) bool { 515 | return r == '\n' || r == '\r' || r == 0x1E 516 | } 517 | func isSuffixOfValue(r rune) bool { 518 | return isWhitespace(r) || isNewLine(r) || isEOF(r) || r == '#' || r == ',' || r == ']' 519 | } 520 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "github.com/achun/testing-want" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func assertBadParse(wt want.Want, src string, msg string) { 10 | scan := NewScanner([]byte(src)) 11 | p := &parse{Scanner: scan, testMode: true} 12 | p.Handler(func(Token, string) error { return nil }) 13 | 14 | stagePlay(p, openStage()) 15 | nwt := wt 16 | nwt.Skip = 3 17 | if msg == "" { 18 | nwt.Error(p.err, "TOML want an error: ", src) 19 | } else { 20 | nwt.Equal(p.err.Error(), msg, func() string { 21 | l, c, s := p.LastLine() 22 | return want.String("Line ", l, ", Column ", c, "\n"+s, "\n"+arrowCol(s, c)) 23 | }) 24 | } 25 | } 26 | func arrowCol(line string, col int) string { 27 | ret := "" 28 | for i, r := range line { 29 | if i+1 == col { 30 | break 31 | } 32 | if r < 256 { 33 | ret += " " 34 | } else { 35 | ret += " " 36 | } 37 | } 38 | return ret + "^" 39 | } 40 | 41 | // outs =["token","value"] 42 | func assertParse(wt want.Want, str string, outs ...string) { 43 | scan := NewScanner([]byte(str)) 44 | p := &parse{Scanner: scan, testMode: true} 45 | i := 0 46 | l := len(outs) 47 | last := "" 48 | if l > 0 { 49 | last = outs[l-1] 50 | } 51 | p.Handler(func(token Token, str string) (err error) { 52 | if tokenWhitespace == token || token == tokenNewLine || token == tokenEOF { 53 | return 54 | } 55 | 56 | if i == l { 57 | wt.False(true, "outputs more: ", i, " > ", l-1, "\nlast: ", last, "\ngot token: ", token.String()+"\nFetch: "+str, func() string { 58 | l, c, s := p.LastLine() 59 | return want.String("Line ", l, ", Column ", c, "\n"+s, "\n"+arrowCol(s, c)) 60 | }) 61 | 62 | } 63 | 64 | wt.Equal(token.String()+" "+str, outs[i], i, func() string { 65 | l, c, s := p.LastLine() 66 | return want.String("Line ", l, ", Column ", c, "\n"+s, "\n"+arrowCol(s, c)) 67 | }) 68 | 69 | i++ 70 | return 71 | }) 72 | stagePlay(p, openStage()) 73 | wt = wt 74 | wt.Skip = 3 75 | wt.Nil(p.err, func() string { 76 | l, c, s := p.LastLine() 77 | return want.String("Line ", l, ", Column ", c, "\n"+s, "\n"+arrowCol(s, c)) 78 | }) 79 | 80 | if l > 0 { 81 | j := i 82 | if j >= l { 83 | j = l - 1 84 | } 85 | wt.Equal(i, l, "loss: ", outs[j]) 86 | } 87 | } 88 | 89 | func TestScanner(t *testing.T) { 90 | if skipTest { 91 | return 92 | } 93 | wt := want.T(t) 94 | s := NewScanner([]byte("0123456789")) 95 | wt.Equal(s.Fetch(true), "0") 96 | 97 | r := s.Rune() 98 | for i := 0; i < 10; i++ { 99 | wt.Equal(r, rune('0'+i)) 100 | r = s.Next() 101 | } 102 | 103 | wt.Equal(s.Next(), rune(EOF)) 104 | wt.True(s.Eof()) 105 | wt.Equal(s.Fetch(true), "123456789") 106 | wt.Equal(s.Next(), rune(EOF)) 107 | wt.Equal(s.Next(), rune(EOF)) 108 | wt.Equal(s.Fetch(true), "") 109 | } 110 | 111 | func TestEmpty(tt *testing.T) { 112 | t := want.Want{tt, 7} 113 | if skipTest { 114 | return 115 | } 116 | assertParse(t, ``) 117 | assertParse(t, ` `) 118 | assertParse(t, ` `) 119 | assertParse(t, ` 120 | 121 | 122 | `) 123 | } 124 | 125 | func TestToken(tt *testing.T) { 126 | const ( 127 | al = `ArrayLeftBrack [` 128 | ar = `ArrayRightBrack ]` 129 | eq = `Equal =` 130 | i = `Integer` 131 | ca = `Comma ,` 132 | ) 133 | t := want.Want{tt, 7} 134 | 135 | if skipTest { 136 | return 137 | } 138 | assertParse(t, `string = "is string \n newline"`, `Key string`, eq, `String "is string \n newline"`) 139 | assertParse(t, `#`, `Comment #`) 140 | assertParse(t, `# 141 | 142 | # 1 143 | `, `Comment #`, `Comment # 1`) 144 | assertParse(t, `k = true # 1.1`, `Key k`, eq, `Boolean true`, `Comment # 1.1`) 145 | assertParse(t, `k = false# ""`, `Key k`, eq, `Boolean false`, `Comment # ""`) 146 | 147 | assertParse(t, `key = 1`, `Key key`, eq, `Integer 1`) 148 | assertParse(t, `key = []`, `Key key`, eq, al, ar) 149 | assertParse(t, `key = [1]`, `Key key`, eq, al, `Integer 1`, ar) 150 | assertParse(t, `ia = [1 , 2]`, `Key ia`, eq, al, `Integer 1`, ca, `Integer 2`, ar) 151 | assertParse(t, `ia = [[1],[2,3],["A","B"]]`, `Key ia`, eq, 152 | al, 153 | al, `Integer 1`, ar, ca, 154 | al, `Integer 2`, ca, `Integer 3`, ar, ca, 155 | al, `String "A"`, ca, `String "B"`, ar, 156 | ar) 157 | assertParse(t, `ia = [[[ 0,1 ],["A","B"],[["D"],["E"]]],[ 2,3]]`, `Key ia`, eq, 158 | al, 159 | 160 | al, 161 | al, `Integer 0`, ca, `Integer 1`, ar, ca, 162 | al, `String "A"`, ca, `String "B"`, ar, ca, 163 | al, 164 | al, `String "D"`, ar, ca, 165 | al, `String "E"`, ar, ar, 166 | ar, ca, 167 | 168 | al, `Integer 2`, ca, `Integer 3`, ar, 169 | ar) 170 | 171 | assertParse(t, `str = ""`, `Key str`, eq, `String ""`) 172 | 173 | const noEqual = `roles does not match one of stageEqual` 174 | assertBadParse(t, `key`, "invalid Key") 175 | assertBadParse(t, `key 1`, noEqual) 176 | assertBadParse(t, `ke y = name`, noEqual) 177 | assertBadParse(t, `key # comment`, noEqual) 178 | 179 | const noValues = `roles does not match one of stageValues` 180 | assertBadParse(t, `key = name`, noValues) 181 | assertBadParse(t, `key = # comment`, noValues) 182 | 183 | const noInteger = `roles does not match one of stageIntegerArray` 184 | assertBadParse(t, `key = [1,ent]`, noInteger) 185 | assertBadParse(t, `key = [1,"ent"]`, noInteger) 186 | 187 | const noArrayVlaues = `roles does not match one of stageArray` 188 | assertBadParse(t, `key = [`, noArrayVlaues) 189 | 190 | assertBadParse(t, `[]`, "invalid TableName") 191 | assertBadParse(t, `[table ]`, "invalid TableName") 192 | assertBadParse(t, `[tab le]`, "invalid TableName") 193 | assertBadParse(t, `[ table]`, "invalid TableName") 194 | assertBadParse(t, `[ table]`, "invalid TableName") 195 | assertBadParse(t, `[[tables]`, "invalid ArrayOfTables") 196 | assertBadParse(t, `[[ tables]]`, "invalid ArrayOfTables") 197 | assertBadParse(t, `[[tab les]]`, "invalid ArrayOfTables") 198 | assertBadParse(t, `[[tables ]]`, "invalid ArrayOfTables") 199 | assertBadParse(t, `[[ tables]]`, "invalid ArrayOfTables") 200 | assertBadParse(t, `[[tab les]]`, "invalid ArrayOfTables") 201 | 202 | assertParse(t, `[name]`, "TableName [name]") 203 | assertParse(t, `[name] #`, "TableName [name]", `Comment #`) 204 | assertParse(t, `[[name]]`, "ArrayOfTables [[name]]") 205 | assertParse(t, `[[name]]#`, "ArrayOfTables [[name]]", `Comment #`) 206 | assertParse(t, ` 207 | [[name]] # 世界, 208 | id = 1 209 | [[name]] # Template, 尝试独立的 _layouts 210 | id =2 211 | `, 212 | `ArrayOfTables [[name]]`, `Comment # 世界,`, 213 | `Key id`, eq, `Integer 1`, 214 | `ArrayOfTables [[name]]`, `Comment # Template, 尝试独立的 _layouts`, 215 | `Key id`, eq, `Integer 2`, 216 | ) 217 | 218 | } 219 | 220 | func TestParserOK(t *testing.T) { 221 | if skipTest { 222 | return 223 | } 224 | assertParse(want.Want{t, 7}, 225 | ` 226 | # comment 1 227 | # comment 2 228 | [table#] # comment 3 229 | [[arrayoftable]] 230 | # comment 4 231 | key = -111# comment 5 232 | float= 123.22 233 | int = 11234 234 | datetime = 2012-01-02T13:11:14Z 235 | string = "is string \n newline" 236 | intger = [ 237 | 1,# comment 6 238 | 2# comment 7 239 | ] 240 | strings=[# comment 8 241 | "a", 242 | "b", 243 | ]# comment 9 244 | # comment 10`, 245 | `Comment # comment 1`, 246 | `Comment # comment 2`, 247 | `TableName [table#]`, `Comment # comment 3`, 248 | `ArrayOfTables [[arrayoftable]]`, 249 | `Comment # comment 4`, 250 | `Key key`, `Equal =`, `Integer -111`, `Comment # comment 5`, 251 | `Key float`, `Equal =`, `Float 123.22`, 252 | `Key int`, `Equal =`, `Integer 11234`, 253 | `Key datetime`, `Equal =`, `Datetime 2012-01-02T13:11:14Z`, 254 | `Key string`, `Equal =`, `String "is string \n newline"`, 255 | `Key intger`, `Equal =`, 256 | `ArrayLeftBrack [`, 257 | `Integer 1`, `Comma ,`, `Comment # comment 6`, `Integer 2`, `Comment # comment 7`, 258 | `ArrayRightBrack ]`, 259 | `Key strings`, `Equal =`, 260 | `ArrayLeftBrack [`, `Comment # comment 8`, 261 | `String "a"`, `Comma ,`, `String "b"`, `Comma ,`, 262 | `ArrayRightBrack ]`, `Comment # comment 9`, `Comment # comment 10`, 263 | ) 264 | } 265 | 266 | func TestFile(t *testing.T) { 267 | if skipTest { 268 | return 269 | } 270 | buf, err := ioutil.ReadFile("tests/example.toml") 271 | if err != nil { 272 | t.Fatal(err) 273 | } 274 | assertParse(want.Want{t, 7}, string(buf), 275 | `Comment # This is a TOML document. Boom.`, 276 | `Key title`, `Equal =`, `String "TOML Example"`, 277 | `TableName [owner]`, 278 | `Comment # owner information`, 279 | `Key name`, `Equal =`, `String "Tom Preston-Werner"`, 280 | `Key organization`, `Equal =`, `String "GitHub"`, 281 | `Key bio`, `Equal =`, `String "GitHub Cofounder & CEO\nLikes tater tots and beer."`, 282 | `Key dob`, `Equal =`, `Datetime 1979-05-27T07:32:00Z`, `Comment # First class dates? Why not?`, 283 | `TableName [database]`, 284 | `Key server`, `Equal =`, `String "192.168.1.1"`, 285 | `Key ports`, `Equal =`, 286 | 287 | `ArrayLeftBrack [`, 288 | `Integer 8001`, `Comma ,`, `Integer 8001`, `Comma ,`, `Integer 8002`, 289 | `ArrayRightBrack ]`, 290 | 291 | `Key connection_max`, `Equal =`, `Integer 5000`, 292 | `Key enabled`, `Equal =`, `Boolean true`, 293 | `TableName [servers]`, 294 | `Comment # You can indent as you please. Tabs or spaces. TOML don't care.`, 295 | `TableName [servers.alpha]`, 296 | `Key ip`, `Equal =`, `String "10.0.0.1"`, 297 | `Key dc`, `Equal =`, `String "eqdc10"`, 298 | `TableName [servers.beta]`, 299 | `Key ip`, `Equal =`, `String "10.0.0.2"`, 300 | `Key dc`, `Equal =`, `String "eqdc10"`, 301 | `Key country`, `Equal =`, `String "中国"`, `Comment # This should be parsed as UTF-8`, 302 | `TableName [clients]`, 303 | `Key data`, `Equal =`, 304 | 305 | `ArrayLeftBrack [`, 306 | 307 | `ArrayLeftBrack [`, 308 | `String "gamma"`, `Comma ,`, `String "delta"`, 309 | `ArrayRightBrack ]`, 310 | `Comma ,`, 311 | `ArrayLeftBrack [`, 312 | `Integer 1`, `Comma ,`, `Integer 2`, 313 | `ArrayRightBrack ]`, 314 | 315 | `ArrayRightBrack ]`, 316 | 317 | `Comment # just an update to make sure parsers support it`, 318 | `Comment # Line breaks are OK when inside arrays`, 319 | `Key hosts`, `Equal =`, 320 | 321 | `ArrayLeftBrack [`, 322 | `String "alpha"`, `Comma ,`, `String "omega"`, 323 | `ArrayRightBrack ]`, 324 | 325 | `Comment # Products`, 326 | `ArrayOfTables [[products]]`, 327 | `Key name`, `Equal =`, `String "Hammer"`, 328 | `Key sku`, `Equal =`, `Integer 738594937`, 329 | `ArrayOfTables [[products]]`, 330 | `Key name`, `Equal =`, `String "Nail"`, 331 | `Key sku`, `Equal =`, `Integer 284758393`, 332 | `Key color`, `Equal =`, `String "gray"`, 333 | 334 | `Comment # nested`, 335 | `ArrayOfTables [[fruit]]`, 336 | `Key name`, `Equal =`, `String "apple"`, 337 | 338 | `TableName [fruit.physical]`, 339 | `Key color`, `Equal =`, `String "red"`, 340 | `Key shape`, `Equal =`, `String "round"`, 341 | 342 | `ArrayOfTables [[fruit.variety]]`, 343 | `Key name`, `Equal =`, `String "red delicious"`, 344 | 345 | `ArrayOfTables [[fruit.variety]]`, 346 | `Key name`, `Equal =`, `String "granny smith"`, 347 | 348 | `ArrayOfTables [[fruit]]`, 349 | `Key name`, `Equal =`, `String "banana"`, 350 | 351 | `ArrayOfTables [[fruit.variety]]`, 352 | `Key name`, `Equal =`, `String "plantain"`, 353 | `Comment # last comments for`, 354 | `Comment # TOML document`, 355 | ) 356 | } 357 | -------------------------------------------------------------------------------- /scanner.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "unicode/utf8" 5 | ) 6 | 7 | const ( 8 | EOF = 0xF8 9 | RuneError = 0xFFFD 10 | ) 11 | 12 | type Scanner interface { 13 | Fetch(skip bool) string 14 | Rune() rune 15 | Next() rune 16 | Eof() bool 17 | LastLine() (int, int, string) 18 | } 19 | 20 | type scanner struct { 21 | buf []byte 22 | pos int 23 | offset int // for Get() 24 | size int 25 | line int 26 | first int // offset to first char of line 27 | col int 28 | r rune 29 | } 30 | 31 | // first scanner 32 | func NewScanner(source []byte) Scanner { 33 | p := scanner{} 34 | p.buf = source 35 | r := p.Next() 36 | 37 | if r == BOM { 38 | p.Fetch(true) 39 | p.Next() 40 | } 41 | 42 | p.line = 1 43 | p.first = 0 44 | 45 | if r == RuneError { 46 | p.buf = nil 47 | } 48 | return &p 49 | } 50 | 51 | func (p *scanner) Eof() bool { 52 | return p.r == EOF 53 | } 54 | 55 | func (p *scanner) Rune() rune { 56 | return p.r 57 | } 58 | 59 | // Fetch returns last char, at string(buffer[:Scan.offset - sizeOfLastChar]). 60 | 61 | // Fetch 返回未被取出的字符串. skip 表示是否包含 pos 处的字符 62 | func (p *scanner) Fetch(last bool) (str string) { 63 | e := p.pos 64 | if !last && p.r != EOF { 65 | e -= p.size 66 | } 67 | 68 | str = string(p.buf[p.offset:e]) 69 | p.offset = e 70 | return 71 | } 72 | 73 | func (p *scanner) LastLine() (int, int, string) { 74 | s := p.first 75 | e := p.pos - p.size 76 | if p.col == 1 && s > 0 { 77 | s-- 78 | } 79 | 80 | r := p.buf[s] 81 | 82 | for s > 0 && r != '\n' && r != '\r' && r != 0x1E { 83 | s-- 84 | r = p.buf[s] 85 | } 86 | 87 | r = p.buf[e] 88 | for e < len(p.buf) && r != '\n' && r != '\r' && r != 0x1E { 89 | r = p.buf[e] 90 | e++ 91 | } 92 | 93 | return p.line, p.col, string(p.buf[s:e]) 94 | 95 | } 96 | 97 | // Next returns read char frome the buffer. 98 | // b is byte(char) when size of char equal 1, otherwise it is const MultiBytes. 99 | // r is rune value of char,If the encoding is invalid, it is RuneError. 100 | // if end of buffer or encoding is invalid, char is error string, b equal const EOF, r equal const RuneError. 101 | func (p *scanner) Next() rune { 102 | if p.r == EOF { 103 | return p.r 104 | } 105 | 106 | if p.pos >= len(p.buf) { 107 | p.r = EOF 108 | return p.r 109 | } 110 | 111 | r := p.r 112 | 113 | p.r, p.size = utf8.DecodeRune(p.buf[p.pos:]) 114 | 115 | n := p.r 116 | p.col++ 117 | if n == '\n' || n == '\r' || n == 0x1E { 118 | if n == r || n != r && r != '\n' && r != '\r' { 119 | p.col = 1 120 | p.line++ 121 | p.first = p.pos 122 | } 123 | } 124 | p.pos += p.size 125 | 126 | return p.r 127 | } 128 | -------------------------------------------------------------------------------- /stage.go: -------------------------------------------------------------------------------- 1 | /** 2 | 硬编码实现 PEG. 3 | */ 4 | package toml 5 | 6 | import ( 7 | "strconv" 8 | ) 9 | 10 | const ( 11 | // 发给 itsToken 的指令 12 | cTokenReset = -1 - iota 13 | cTokenName // 返回 token 用于判断名称 14 | ) 15 | 16 | type itsToken func(rune, int, bool) (Status, Token) 17 | 18 | func (i itsToken) Token() Token { 19 | if i == nil { 20 | return tokenNothing 21 | } 22 | _, token := i(0, cTokenName, true) 23 | return token 24 | } 25 | 26 | func (i itsToken) Reset() { 27 | if i != nil { 28 | i(0, cTokenReset, true) 29 | } 30 | } 31 | 32 | type stager interface { 33 | // 场景名 34 | Name() string 35 | // 场景全名 36 | String() string 37 | // 返回场景角色 38 | Roles() []role 39 | // 通过 当前的 Token 和 当前的 stager 返回下一个 stager 40 | Next(Token, stager) stager 41 | // 没有匹配的情况下, 执行 Must, 返回非 nil stager . 42 | Must(rune) (Status, Token, stager) 43 | } 44 | 45 | const ( 46 | stageEnd constStage = iota 47 | stageInvalid 48 | stageError 49 | ) 50 | 51 | var constStagesName = [...]string{ 52 | "stageEnd", 53 | "stageInvalid", 54 | "stageError", 55 | } 56 | 57 | // 常量场景, 用于标记特殊状态 58 | type constStage int 59 | 60 | func (s constStage) String() string { 61 | if int(s) < len(constStagesName) { 62 | return constStagesName[s] 63 | } 64 | return "stage." + strconv.Itoa(int(s)) 65 | } 66 | func (s constStage) Name() string { 67 | return s.String() 68 | } 69 | func (s constStage) Roles() []role { return nil } 70 | func (s constStage) Next(Token, stager) stager { return s } 71 | func (s constStage) Must(rune) (Status, Token, stager) { 72 | return SNot, tokenNothing, stageError 73 | } 74 | 75 | // 角色(rules?) 76 | type role struct { 77 | /** 78 | Is 判断角色是否能识别传入的字符. 79 | 返回值: 80 | Status 识别状态 81 | Token 此值被复用, 如果 Status 是 SMaybe, 会作为 flag 的值. 82 | 参数: 83 | char 是待判定字符 84 | flag 是上一次 SMaybe 状态的 uint(Token), 默认为 0. 85 | race 表示是否有其他 role 竞争 86 | */ 87 | Is itsToken 88 | // 与角色绑定的预期 stager 89 | // nil 值表示保持 当前的 stager 不变 90 | Stager stager 91 | } 92 | 93 | // 基本场景, 固定的, 不变的 94 | type stage struct { 95 | name string 96 | roles []role 97 | } 98 | 99 | func (s stage) String() string { 100 | if s.name != "" { 101 | return s.name 102 | } 103 | return "UnnamedStage" 104 | } 105 | 106 | func (s stage) Name() string { 107 | return s.String() 108 | } 109 | 110 | func (s stage) Roles() []role { 111 | roles := make([]role, len(s.roles)) 112 | copy(roles, s.roles) 113 | return roles 114 | for _, role := range roles { 115 | role.Is.Reset() 116 | } 117 | return roles 118 | } 119 | 120 | func (s stage) Next(token Token, stage stager) stager { 121 | return s 122 | } 123 | 124 | func (s stage) Must(rune) (Status, Token, stager) { 125 | return SNot, tokenNothing, stageInvalid 126 | } 127 | 128 | /** 129 | firstStage 可用于第一个场景, NotMatch 方法中对 Roles 进行自循环 130 | */ 131 | type firstStage struct { 132 | stage 133 | } 134 | 135 | func (s firstStage) Must(char rune) (Status, Token, stager) { 136 | roles := s.Roles() 137 | for _, role := range roles { 138 | role.Is.Reset() 139 | status, token := role.Is(char, 0, false) 140 | if status == SYes || status == SYesKeep { 141 | return status, token, role.Stager 142 | } 143 | } 144 | return SNot, tokenNothing, stageInvalid 145 | } 146 | 147 | /** 148 | 升降场景, 升上去总要降下来, 用于嵌套情况, 比如数组. 149 | 成员 stager 提供 roles. 150 | 当 level 为 0 回退到 back 场景. 151 | Next 被调用时 level+1. 152 | Must 被调用时决定是否 level-1, 153 | 因只有数组这一种情况, 判断函数就是 itsArrayRightBrack. 154 | 当然如果 itsArrayRightBrack 不返回 SYes, 那一定是出错了. 155 | */ 156 | type liftStage struct { 157 | stager // stageArray 158 | //back stager 159 | closed bool 160 | must itsToken 161 | } 162 | 163 | func (s liftStage) String() string { 164 | return "liftStage." + s.stager.String() 165 | } 166 | 167 | func (s liftStage) Next(token Token, current stager) stager { 168 | s.stager = current 169 | return s 170 | } 171 | 172 | func (s liftStage) Must(r rune) (Status, Token, stager) { 173 | status, token := s.must(r, 0, false) 174 | if status == SYes || status == SYesKeep { 175 | if token == tokenArrayRightBrack { 176 | if s.closed { 177 | return status, token, stageError 178 | } 179 | s.closed = true 180 | return status, token, s 181 | } 182 | return status, token, s.stager 183 | } 184 | if s.closed { 185 | s.must.Reset() 186 | return s.stager.Must(r) 187 | } 188 | return SInvalid, tokenNothing, stageInvalid 189 | } 190 | 191 | // 回退场景, stager 提供 roles, 192 | // back 开始应该为 nil, 由 Next 进行设置, 下一次 Next 返回 back 193 | // Token close 描述 toles 中要替换的 stager, TOML 中只有 tokenArrayRightBrack 194 | type backStage struct { 195 | stager 196 | back stager 197 | must itsToken 198 | } 199 | 200 | func (s backStage) String() string { 201 | return "backStage." + s.stager.String() 202 | } 203 | func (s backStage) Next(token Token, current stager) stager { 204 | if s.back == nil { 205 | s.back = current 206 | } 207 | return s 208 | } 209 | 210 | func (s backStage) Must(char rune) (Status, Token, stager) { 211 | if s.must == nil { 212 | return s.back.Must(char) 213 | } 214 | status, token := s.must(char, 0, false) 215 | if status == SYes || status == SYesKeep { 216 | return status, token, s.back 217 | } 218 | return SInvalid, tokenNothing, stageInvalid 219 | } 220 | 221 | // 角色环(token 环) 222 | func rolesCircle(fns ...itsToken) itsToken { 223 | max := len(fns) 224 | if max == 0 { 225 | return func(char rune, flag int, race bool) (Status, Token) { return SNot, tokenNothing } 226 | } 227 | i := 0 228 | return func(char rune, flag int, race bool) (Status, Token) { 229 | var ( 230 | s Status 231 | t Token 232 | ) 233 | if flag == cTokenReset { 234 | i = 0 // 清零, 新循环开始了 235 | return SNot, tokenNothing 236 | } 237 | if i == max { 238 | i = 0 239 | } 240 | s, t = fns[i](char, flag, race) 241 | if flag == cTokenName { 242 | return s, t 243 | } 244 | if s == SYes || s == SYesKeep { 245 | i++ 246 | } 247 | 248 | return s, t 249 | } 250 | } 251 | 252 | /** 253 | 跟屁虫 token, f1 要先通过一次之后, f1, f2 顺序尝试 254 | */ 255 | func rolesYesman(f1, f2 itsToken) itsToken { 256 | yes := false 257 | return func(char rune, flag int, race bool) (Status, Token) { 258 | if flag == cTokenReset { 259 | yes = false // 清零, 新循环开始了 260 | return SNot, tokenNothing 261 | } 262 | s, t := f1(char, flag, race) 263 | if flag == cTokenName { 264 | return s, t 265 | } 266 | if s == SYes || s == SYesKeep { 267 | yes = true 268 | return s, t 269 | } 270 | if !yes { 271 | return s, t 272 | } 273 | 274 | return f2(char, flag, race) 275 | } 276 | } 277 | 278 | /** 279 | 角色粉, f1, f2 顺序尝试, 如果 f1 没有要先通过一次, f2 被匹配, 返回 SInvalid 280 | 用例: 多维数组 [[...],[...]] 总是先有 "]", 如果先出现 , 那就 非法了 281 | */ 282 | func rolesFans(f1, f2 itsToken) itsToken { 283 | yes := false 284 | return func(char rune, flag int, race bool) (Status, Token) { 285 | if flag == cTokenReset { 286 | //yes = false // 清零, 新循环开始了 287 | return SNot, tokenNothing 288 | } 289 | s, t := f1(char, flag, race) 290 | if flag == cTokenName { 291 | return s, t 292 | } 293 | if s == SYes || s == SYesKeep { 294 | yes = true 295 | return s, t 296 | } 297 | 298 | s, t = f2(char, flag, race) 299 | 300 | if !yes && (s == SYes || s == SYesKeep) { 301 | return SUnexpected, t 302 | } 303 | return s, t 304 | } 305 | } 306 | 307 | // 开启新舞台, 返回第一个场景 308 | func openStage() stager { 309 | stageEmpty := &firstStage{stage{name: "stageEmpty"}} 310 | stageEqual := &stage{name: "stageEqual"} 311 | stageValues := &stage{name: "stageValues"} 312 | stageArray := &stage{name: "stageArray"} 313 | stageStringArray := &stage{name: "stageStringArray"} 314 | stageBooleanArray := &stage{name: "stageBooleanArray"} 315 | stageIntegerArray := &stage{name: "stageIntegerArray"} 316 | stageFloatArray := &stage{name: "stageFloatArray"} 317 | stageDatetimeArray := &stage{name: "stageDatetimeArray"} 318 | 319 | stageEmpty.roles = []role{ 320 | {itsEOF, stageEnd}, 321 | {itsWhitespace, nil}, 322 | {itsNewLine, nil}, 323 | {itsComment, nil}, 324 | {itsTableName, nil}, 325 | {itsArrayOfTables, nil}, 326 | {itsKey, stageEqual}, 327 | } 328 | // Key = 其实是完全匹配 token 序列. 329 | stageEqual.roles = []role{ 330 | {itsWhitespace, nil}, 331 | {itsEqual, stageValues}, 332 | } 333 | 334 | stageValues.roles = []role{ 335 | {itsWhitespace, nil}, 336 | {itsArrayLeftBrack, 337 | backStage{stageArray, stageEmpty, itsArrayRightBrack}}, 338 | {itsString, stageEmpty}, 339 | {itsBoolean, stageEmpty}, 340 | {itsInteger, stageEmpty}, 341 | {itsFloat, stageEmpty}, 342 | {itsDatetime, stageEmpty}, 343 | } 344 | 345 | stageArray.roles = []role{ 346 | {itsWhitespace, nil}, 347 | {itsComment, nil}, 348 | {itsNewLine, nil}, 349 | {itsArrayLeftBrack, 350 | liftStage{nil, false, 351 | rolesCircle(itsArrayRightBrack, itsComma)}}, 352 | {itsString, 353 | backStage{stageStringArray, nil, nil}}, 354 | {itsBoolean, 355 | backStage{stageBooleanArray, nil, nil}}, 356 | {itsInteger, 357 | backStage{stageIntegerArray, nil, nil}}, 358 | {itsFloat, 359 | backStage{stageFloatArray, nil, nil}}, 360 | {itsDatetime, 361 | backStage{stageDatetimeArray, nil, nil}}, 362 | } 363 | 364 | stageStringArray.roles = []role{ 365 | {itsWhitespace, nil}, 366 | {itsNewLine, nil}, 367 | {itsComment, nil}, 368 | {rolesCircle(itsComma, itsString), nil}, 369 | } 370 | 371 | stageBooleanArray.roles = []role{ 372 | {itsWhitespace, nil}, 373 | {itsNewLine, nil}, 374 | {itsComment, nil}, 375 | {rolesCircle(itsComma, itsBoolean), nil}, 376 | } 377 | 378 | stageIntegerArray.roles = []role{ 379 | {itsWhitespace, nil}, 380 | {itsNewLine, nil}, 381 | {itsComment, nil}, 382 | {rolesCircle(itsComma, itsInteger), nil}, 383 | } 384 | 385 | stageFloatArray.roles = []role{ 386 | {itsWhitespace, nil}, 387 | {itsNewLine, nil}, 388 | {itsComment, nil}, 389 | {rolesCircle(itsComma, itsFloat), nil}, 390 | } 391 | 392 | stageDatetimeArray.roles = []role{ 393 | {itsWhitespace, nil}, 394 | {itsNewLine, nil}, 395 | {itsComment, nil}, 396 | {rolesCircle(itsComma, itsDatetime), nil}, 397 | } 398 | 399 | return stageEmpty 400 | } 401 | 402 | func stagePlay(p parser, stage stager) { 403 | Loop: 404 | for stage != nil { 405 | if stage == stageEnd { 406 | break 407 | } 408 | if stage == stageInvalid { 409 | p.Invalid(tokenError) 410 | break 411 | } 412 | if stage == stageError { 413 | p.Err(stage.String()) 414 | break 415 | } 416 | 417 | roles := stage.Roles() 418 | skip := make([]bool, len(roles)) 419 | flag := make([]int, len(roles)) 420 | 421 | if len(roles) == 0 { 422 | p.Invalid(tokenError) 423 | break 424 | } 425 | 426 | var ( 427 | st Status 428 | token Token // flag 是 uint 和 Token 复用 429 | maybe int 430 | r rune 431 | ) 432 | 433 | for { 434 | 435 | r = p.Next() 436 | if r == RuneError { 437 | p.Invalid(tokenRuneError) 438 | return 439 | } 440 | for i, role := range roles { 441 | if skip[i] { 442 | continue 443 | } 444 | st, token = role.Is(r, flag[i], maybe != 0) 445 | switch st { 446 | case SMaybe: 447 | if flag[i] == 0 { 448 | maybe++ 449 | } 450 | flag[i] = int(token) 451 | 452 | case SYes, SYesKeep: 453 | if st == SYesKeep { 454 | p.Keep() 455 | } 456 | if p.Token(token) != nil { 457 | return 458 | } 459 | 460 | if role.Stager != nil { 461 | stage = role.Stager.Next(token, stage) 462 | } 463 | continue Loop 464 | 465 | case SNot: 466 | 467 | if flag[i] != 0 { 468 | maybe-- 469 | } 470 | skip[i] = true 471 | 472 | case SInvalid: 473 | p.Invalid(token) 474 | return 475 | } 476 | } 477 | if maybe != 0 && r != EOF { 478 | continue 479 | } 480 | 481 | stageName := stage.Name() 482 | st, token, stage = stage.Must(r) 483 | 484 | if stage == nil || st != SYes && st != SYesKeep { 485 | if st == SUnexpected { 486 | p.Err("unexpercted " + token.String() + " of " + stageName) 487 | } else { 488 | p.Err("roles does not match one of " + stageName) 489 | } 490 | return 491 | } 492 | if st == SYesKeep { 493 | p.Keep() 494 | } 495 | if p.Token(token) != nil { 496 | return 497 | } 498 | break 499 | } 500 | } 501 | 502 | } 503 | -------------------------------------------------------------------------------- /tests/example.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom. 2 | 3 | title = "TOML Example" 4 | 5 | [owner] # owner information 6 | name = "Tom Preston-Werner" 7 | organization = "GitHub" 8 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 9 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 10 | 11 | [database] 12 | server = "192.168.1.1" 13 | ports = [ 8001, 8001, 8002 ] 14 | connection_max = 5000 15 | enabled = true 16 | 17 | [servers] 18 | 19 | # You can indent as you please. Tabs or spaces. TOML don't care. 20 | [servers.alpha] 21 | ip = "10.0.0.1" 22 | dc = "eqdc10" 23 | 24 | [servers.beta] 25 | ip = "10.0.0.2" 26 | dc = "eqdc10" 27 | country = "中国" # This should be parsed as UTF-8 28 | 29 | [clients] 30 | data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it 31 | 32 | # Line breaks are OK when inside arrays 33 | hosts = [ 34 | "alpha", 35 | "omega" 36 | ] 37 | 38 | # Products 39 | 40 | [[products]] 41 | name = "Hammer" 42 | sku = 738594937 43 | 44 | [[products]] 45 | name = "Nail" 46 | sku = 284758393 47 | color = "gray" 48 | 49 | # nested 50 | [[fruit]] 51 | name = "apple" 52 | 53 | [fruit.physical] 54 | color = "red" 55 | shape = "round" 56 | 57 | [[fruit.variety]] 58 | name = "red delicious" 59 | 60 | [[fruit.variety]] 61 | name = "granny smith" 62 | 63 | [[fruit]] 64 | name = "banana" 65 | 66 | [[fruit.variety]] 67 | name = "plantain" 68 | 69 | # last comments for 70 | # TOML document -------------------------------------------------------------------------------- /tests/hard_example.toml: -------------------------------------------------------------------------------- 1 | # Test file for TOML 2 | # Only this one tries to emulate a TOML file written by a user of the kind of parser writers probably hate 3 | # This part you'll really hate 4 | 5 | [the] 6 | test_string = "You'll hate me after this - #" # " Annoying, isn't it? 7 | 8 | [the.hard] 9 | test_array = [ "] ", " # "] # ] There you go, parse this! 10 | test_array2 = [ "Test #11 ]proved that", "Experiment #9 was a success" ] 11 | # You didn't think it'd as easy as chucking out the last #, did you? 12 | another_test_string = " Same thing, but with a string #" 13 | harder_test_string = " And when \"'s are in the string, along with # \"" # "and comments are there too" 14 | # Things will get harder 15 | 16 | [the.hard.bit#] 17 | what? = "You don't think some user won't do that?" 18 | multi_line_array = [ 19 | "]", 20 | # ] Oh yes I did 21 | ] 22 | 23 | # Each of the following keygroups/key value pairs should produce an error. Uncomment to them to test 24 | 25 | #[error] if you didn't catch this, your parser is broken 26 | #string = "Anything other than tabs, spaces and newline after a keygroup or key value pair has ended should produce an error unless it is a comment" like this 27 | #array = [ 28 | # "This might most likely happen in multiline arrays", 29 | # Like here, 30 | # "or here, 31 | # and here" 32 | # ] End of array comment, forgot the # 33 | #number = 3.14 pi <--again forgot the # -------------------------------------------------------------------------------- /toml.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "reflect" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // Toml 是一个 maps, 不是 tree 实现. 13 | type Toml map[string]Item 14 | 15 | // Must be use New() to got Toml, do not use Toml{}. 16 | // 新建一个 Toml, 必须使用 New() 函数, 不要用 Toml{}. 17 | // 因为 Toml 的实现需要有一个用于管理的 Id Value, New() 可以做到. 18 | func New() Toml { 19 | tm := Toml{} 20 | tm[iD] = GenItem(0) 21 | return tm 22 | } 23 | 24 | /** 25 | Id 返回用于管理的 ".ID..." 对象副本. 26 | 如果 Id 不存在, 会自动建立一个, 但这不能保证顺序的可靠性. 27 | */ 28 | func (tm Toml) Id() Value { 29 | id, ok := tm[iD] 30 | if !ok || id.idx <= 0 { 31 | id = GenItem(0) 32 | tm[iD] = id 33 | } 34 | return *id.Value 35 | } 36 | 37 | // String returns TOML layout string. 38 | // 格式化输出带缩进的 TOML 格式. 39 | func (p Toml) String() string { 40 | return p.string("", 0) 41 | } 42 | 43 | type kkId struct { 44 | kind Kind 45 | key string // it is key of map 46 | id int 47 | } 48 | 49 | type sortIdx []kkId 50 | 51 | func (p sortIdx) Len() int { return len(p) } 52 | func (p sortIdx) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 53 | 54 | func (p sortIdx) Less(i, j int) bool { 55 | return p[i].id < p[j].id 56 | } 57 | 58 | type sortKey []kkId 59 | 60 | func (p sortKey) Len() int { return len(p) } 61 | func (p sortKey) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 62 | 63 | func (p sortKey) Less(i, j int) bool { 64 | return p[i].key < p[j].key 65 | } 66 | 67 | // prefix for nested TOML 68 | func (p Toml) string(prefix string, indent int) (fmt string) { 69 | l := len(p) 70 | if l == 1 { 71 | return 72 | } 73 | 74 | // outputs end-of-line comments for ArrayOfTables 75 | // 输出嵌套TOML的行尾注释, 前一个 Toml 负责输出 ArrayOfTabes 的 Key 76 | id := p.Id() 77 | if id.idx <= 0 { 78 | return 79 | } 80 | 81 | if prefix != "" { 82 | prefix = prefix + "." 83 | } 84 | 85 | indentstr := strings.Repeat("\t", indent) 86 | 87 | // 如果有 prefix 那一定是嵌套的. 88 | if prefix == "" && id.eolComment != "" { 89 | fmt += " " + id.eolComment + "\n" 90 | } 91 | 92 | // 收集整理 kind,Key,idx 信息, 以便有序输出. 93 | var tops sortIdx 94 | var tabs sortIdx 95 | var vals sortKey 96 | 97 | for rawkey, it := range p { 98 | 99 | // ???需要更严格的检查 100 | key := strings.TrimSpace(rawkey) 101 | if key == "" || key != rawkey || !it.IsValid() { 102 | continue 103 | } 104 | 105 | ki := kkId{ 106 | it.kind, 107 | key, 108 | it.idx, 109 | } 110 | 111 | pos := strings.LastIndex(key, ".") 112 | 113 | if ki.kind < TableName && pos == -1 { 114 | // Top level Key-Vlaue 115 | tops = append(tops, ki) 116 | continue 117 | } 118 | 119 | if ki.kind < TableName { 120 | // Key-Value 121 | vals = append(vals, ki) 122 | } else { 123 | tabs = append(tabs, ki) 124 | // TableName and ArrayOfTables 125 | } 126 | } 127 | 128 | sort.Sort(tops) 129 | sort.Sort(tabs) 130 | sort.Sort(vals) 131 | 132 | // Top level Key-Vlaue 133 | for _, kv := range tops { 134 | it := p[kv.key] 135 | 136 | for _, s := range it.multiComments { 137 | fmt += indentstr + s + "\n" 138 | } 139 | fmt += indentstr + kv.key + " = " + it.string(indentstr, 1) 140 | 141 | if it.eolComment != "" { 142 | fmt += " " + it.eolComment + "\n" 143 | } 144 | 145 | } 146 | 147 | if len(tops) != 0 { 148 | fmt += "\n" 149 | } 150 | 151 | // TableName and ArrayOfTables 152 | kvindent := indentstr + "\t" 153 | for _, kv := range tabs { 154 | 155 | it := p[kv.key] 156 | 157 | // ArrayOfTables 158 | if it.kind == ArrayOfTables { 159 | fmt += "\n" 160 | nested := kv.key 161 | if prefix != "" { 162 | nested = prefix + nested 163 | } 164 | for _, tm := range it.TomlArray() { 165 | id := tm.Id() 166 | 167 | for _, s := range id.multiComments { 168 | fmt += indentstr + s + "\n" 169 | } 170 | 171 | if id.eolComment == "" { 172 | fmt += indentstr + "[[" + prefix + kv.key + "]]\n" 173 | } else { 174 | fmt += indentstr + "[[" + prefix + kv.key + "]] " + id.eolComment + "\n" 175 | } 176 | 177 | fmt += tm.string(nested, indent+1) 178 | } 179 | continue 180 | } 181 | 182 | // TableName 183 | if fmt != "" { 184 | fmt += "\n" 185 | } 186 | 187 | for _, s := range it.multiComments { 188 | fmt += indentstr + s + "\n" 189 | } 190 | 191 | if it.eolComment == "" { 192 | fmt += indentstr + "[" + prefix + kv.key + "]\n" 193 | } else { 194 | fmt += indentstr + "[" + prefix + kv.key + "] " + it.eolComment + "\n" 195 | } 196 | 197 | // Key-Value 198 | tableName := kv.key + "." 199 | 200 | for _, kv := range vals { 201 | if !strings.HasPrefix(kv.key, tableName) { 202 | continue 203 | } 204 | 205 | key := kv.key[len(tableName):] 206 | 207 | // key 有 ".", 那么后续再输出 208 | if strings.Index(key, ".") != -1 { 209 | continue 210 | } 211 | 212 | it := p[kv.key] 213 | 214 | if len(it.multiComments) != 0 { 215 | fmt += "\n" 216 | for _, s := range it.multiComments { 217 | fmt += kvindent + s + "\n" 218 | } 219 | } 220 | 221 | if it.eolComment == "" { 222 | fmt += kvindent + key + " = " + it.string(kvindent, 1) + "\n" 223 | } else { 224 | fmt += kvindent + key + " = " + it.string(kvindent, 1) + " " + 225 | it.eolComment + "\n" 226 | } 227 | } 228 | } 229 | 230 | // TOML 最后的多行注释 231 | if prefix == "" { 232 | for _, s := range id.multiComments { 233 | fmt += indentstr + s + "\n" 234 | } 235 | } 236 | return 237 | } 238 | 239 | // Fetch returns Sub Toml of p, and reset name. not clone. 240 | 241 | /** 242 | such as: 243 | p.Fetch("") // returns all valid elements in p 244 | p.Fetch("prefix") // same as p.Fetch("prefix.") 245 | 从 Toml 中提取出 prefix 开头的所有 Table 元素, 返回值也是一个 Toml. 246 | 注意: 247 | 返回值是原 Toml 的子集. 248 | 返回子集中不包括 [prefix] TableName. 249 | 对返回子集添加 *Item 不会增加到原 Toml 中. 250 | 对返回子集中的 *Item 进行更新, 原 Toml 也会更新. 251 | 子集中不会含有 ArrayOfTables 类型数据. 252 | */ 253 | func (p Toml) Fetch(prefix string) Toml { 254 | nt := Toml{} 255 | ln := len(prefix) 256 | if ln != 0 { 257 | if prefix[ln-1] != '.' { 258 | prefix += "." 259 | ln++ 260 | } 261 | } 262 | 263 | for key, it := range p { 264 | if !it.IsValid() || strings.Index(key, prefix) != 0 { 265 | continue 266 | } 267 | newkey := key[ln:] 268 | if newkey == "" { 269 | continue 270 | } 271 | nt[newkey] = it 272 | } 273 | return nt 274 | } 275 | 276 | // TableNames returns all name of TableName and ArrayOfTables. 277 | // 返回所有 TableName 的名字和 ArrayOfTables 的名字. 278 | func (p Toml) TableNames() (tableNames []string, arrayOfTablesNames []string) { 279 | for key, it := range p { 280 | if it.IsValid() { 281 | if it.kind == TableName { 282 | tableNames = append(tableNames, key) 283 | } else if it.kind == ArrayOfTables { 284 | arrayOfTablesNames = append(arrayOfTablesNames, key) 285 | } 286 | } 287 | } 288 | return 289 | } 290 | 291 | // Apply to each field in the struct, case sensitive. 292 | /** 293 | Apply 把 p 存储的值赋给 dst , TypeOf(dst).Kind() 为 reflect.Struct, 返回赋值成功的次数. 294 | */ 295 | func (p Toml) Apply(dst interface{}) (count int) { 296 | var ( 297 | vv reflect.Value 298 | ok bool 299 | ) 300 | 301 | vv, ok = dst.(reflect.Value) 302 | if ok { 303 | vv = reflect.Indirect(vv) 304 | } else { 305 | vv = reflect.Indirect(reflect.ValueOf(dst)) 306 | } 307 | return p.apply(vv) 308 | } 309 | 310 | func (p Toml) apply(vv reflect.Value) (count int) { 311 | 312 | var it Item 313 | vt := vv.Type() 314 | if !vv.IsValid() || !vv.CanSet() || vt.Kind() != reflect.Struct || vt.String() == "time.Time" { 315 | return 316 | } 317 | 318 | for i := 0; i < vv.NumField(); i++ { 319 | name := vt.Field(i).Name 320 | it = p[name] 321 | 322 | if !it.IsValid() { 323 | continue 324 | } 325 | 326 | if it.kind == TableName { 327 | count += p.Fetch(name).Apply(vv.Field(i)) 328 | } else { 329 | count += it.apply(vv.Field(i)) 330 | } 331 | } 332 | return 333 | } 334 | 335 | var ( 336 | InValidFormat = errors.New("invalid TOML format") 337 | Redeclared = errors.New("duplicate definitionin") 338 | ) 339 | 340 | // 从 TOML 格式 source 解析出 Toml 对象. 341 | func Parse(source []byte) (tm Toml, err error) { 342 | p := &parse{Scanner: NewScanner(source)} 343 | 344 | tb := newBuilder(nil) 345 | 346 | p.Handler( 347 | func(token Token, str string) error { 348 | tb, err = tb.Token(token, str) 349 | return err 350 | }) 351 | 352 | p.Run() 353 | tm = tb.root.Toml() 354 | tm[iD].multiComments = tb.comments 355 | return 356 | } 357 | 358 | // 如果 p!=nil 表示是子集模式, tablename 必须有相同的 prefix 359 | type tomlBuilder struct { 360 | tm Toml 361 | root *tomlBuilder 362 | p *tomlBuilder 363 | it *Item 364 | iv *Value 365 | comments aString // comment or comments 366 | tableName string // cache tableName 367 | prefix string // with "." for nested TOML 368 | token Token // 有些时候需要知道上一个 token, 比如尾注释 369 | } 370 | 371 | func newBuilder(root *tomlBuilder) tomlBuilder { 372 | tb := tomlBuilder{} 373 | 374 | tb.tm = New() 375 | 376 | if root == nil { 377 | tb.root = &tb 378 | } else { 379 | tb.root = root 380 | tb.token = tb.root.token 381 | } 382 | return tb 383 | } 384 | 385 | func (t tomlBuilder) Toml() Toml { 386 | return t.tm 387 | } 388 | 389 | func (t tomlBuilder) Token(token Token, str string) (tomlBuilder, error) { 390 | defer func() { 391 | // 缓存上一个 token, eolComment 等需要用 392 | if token == tokenWhitespace { 393 | return 394 | } 395 | 396 | t.root.token = token 397 | 398 | if token != tokenComment && token != tokenNewLine { 399 | t.token = token 400 | } 401 | }() 402 | switch token { 403 | case tokenError: 404 | return t.Error(str) 405 | case tokenRuneError: 406 | return t.RuneError(str) 407 | case tokenEOF: 408 | return t.EOF(str) 409 | case tokenWhitespace: 410 | return t.Whitespace(str) 411 | case tokenEqual: 412 | return t.Equal(str) 413 | case tokenNewLine: 414 | return t.NewLine(str) 415 | case tokenComment: 416 | return t.Comment(str) 417 | case tokenString: 418 | return t.String(str) 419 | case tokenInteger: 420 | return t.Integer(str) 421 | case tokenFloat: 422 | return t.Float(str) 423 | case tokenBoolean: 424 | return t.Boolean(str) 425 | case tokenDatetime: 426 | return t.Datetime(str) 427 | case tokenTableName: 428 | return t.TableName(str) 429 | case tokenArrayOfTables: 430 | return t.ArrayOfTables(str) 431 | case tokenKey: 432 | return t.Key(str) 433 | case tokenArrayLeftBrack: // [ 434 | return t.ArrayLeftBrack(str) 435 | case tokenArrayRightBrack: // ] 436 | return t.ArrayRightBrack(str) 437 | case tokenComma: 438 | return t.Comma(str) 439 | } 440 | return t, NotSupported 441 | } 442 | 443 | func (t tomlBuilder) Error(str string) (tomlBuilder, error) { 444 | return t, errors.New(str) 445 | } 446 | 447 | func (t tomlBuilder) RuneError(str string) (tomlBuilder, error) { 448 | return t, errors.New(str) 449 | } 450 | 451 | func (t tomlBuilder) EOF(str string) (tomlBuilder, error) { 452 | return t, nil 453 | } 454 | 455 | func (t tomlBuilder) Whitespace(str string) (tomlBuilder, error) { 456 | return t, nil 457 | } 458 | 459 | func (t tomlBuilder) NewLine(str string) (tomlBuilder, error) { 460 | return t, nil 461 | } 462 | 463 | func (t tomlBuilder) Comment(str string) (tomlBuilder, error) { 464 | 465 | // eolComment 466 | if t.root.token != tokenEOF && t.root.token != tokenNewLine { 467 | 468 | if len(t.comments) != 0 { 469 | return t, InternalError 470 | } 471 | 472 | // [[aot]] #comment, save to iD.eolComment 473 | if t.root.token == tokenArrayOfTables { 474 | id, ok := t.tm[iD] 475 | if !ok || id.eolComment != "" { 476 | return t, InternalError 477 | } 478 | id.Value.eolComment = str 479 | return t, nil 480 | } 481 | 482 | if t.iv == nil && t.it == nil { 483 | return t, InternalError 484 | } 485 | 486 | if t.iv == nil { 487 | if t.it.eolComment == "" { 488 | t.it.eolComment, t.comments = str, aString{} 489 | } 490 | } else if t.iv.eolComment == "" { 491 | t.iv.eolComment, t.comments = str, aString{} 492 | } else { 493 | return t, InternalError 494 | } 495 | 496 | return t, nil 497 | } 498 | 499 | // multiComments 500 | t.comments = append(t.comments, str) 501 | return t, nil 502 | } 503 | 504 | func (t tomlBuilder) String(str string) (tomlBuilder, error) { 505 | if t.iv == nil { 506 | return t, InternalError 507 | } 508 | 509 | str, err := strconv.Unquote(str) 510 | if err != nil { 511 | return t, err 512 | } 513 | 514 | if t.iv.kind != Array && t.iv.kind != StringArray { 515 | return t, t.iv.SetAs(str, String) 516 | } 517 | return t, t.iv.Add(str) 518 | } 519 | 520 | func (t tomlBuilder) Integer(str string) (tomlBuilder, error) { 521 | if t.iv == nil { 522 | return t, InternalError 523 | } 524 | 525 | if t.iv.kind != Array && t.iv.kind != IntegerArray { 526 | return t, t.iv.SetAs(str, Integer) 527 | } 528 | v, err := conv(str, Integer) 529 | if err != nil { 530 | return t, err 531 | } 532 | return t, t.iv.Add(v) 533 | } 534 | func (t tomlBuilder) Float(str string) (tomlBuilder, error) { 535 | if t.iv == nil { 536 | return t, InternalError 537 | } 538 | 539 | if t.iv.kind != Array && t.iv.kind != FloatArray { 540 | return t, t.iv.SetAs(str, Float) 541 | } 542 | v, err := conv(str, Float) 543 | if err != nil { 544 | return t, err 545 | } 546 | return t, t.iv.Add(v) 547 | } 548 | func (t tomlBuilder) Boolean(str string) (tomlBuilder, error) { 549 | if t.iv == nil { 550 | return t, InternalError 551 | } 552 | 553 | if t.iv.kind != Array && t.iv.kind != BooleanArray { 554 | return t, t.iv.SetAs(str, Boolean) 555 | } 556 | v, err := conv(str, Boolean) 557 | if err != nil { 558 | return t, err 559 | } 560 | return t, t.iv.Add(v) 561 | } 562 | func (t tomlBuilder) Datetime(str string) (tomlBuilder, error) { 563 | if t.iv == nil { 564 | return t, InternalError 565 | } 566 | 567 | if t.iv.kind != Array && t.iv.kind != DatetimeArray { 568 | return t, t.iv.SetAs(str, Datetime) 569 | } 570 | v, err := conv(str, Datetime) 571 | if err != nil { 572 | return t, err 573 | } 574 | return t, t.iv.Add(v) 575 | } 576 | 577 | func (t tomlBuilder) TableName(str string) (tomlBuilder, error) { 578 | path := str[1 : len(str)-1] 579 | 580 | it, ok := t.tm[path] 581 | if ok { 582 | return t, Redeclared 583 | } 584 | 585 | comments := t.comments 586 | t.comments = aString{} 587 | 588 | if t.prefix != "" { 589 | if t.p == nil { 590 | return t, InternalError 591 | } 592 | 593 | if path == t.prefix { 594 | return t, Redeclared 595 | } 596 | 597 | if !strings.HasPrefix(path, t.prefix+".") { 598 | t = *t.p 599 | t.comments = comments 600 | return t.TableName(str) 601 | } 602 | path = path[len(t.prefix)+1:] 603 | } 604 | 605 | // cached tableName for Key 606 | t.tableName = path 607 | 608 | it = GenItem(TableName) 609 | 610 | it.multiComments = append(it.multiComments, comments...) 611 | 612 | t.tm[path] = it 613 | t.it = &it 614 | t.iv = nil 615 | return t, nil 616 | } 617 | 618 | func (t tomlBuilder) Key(str string) (tomlBuilder, error) { 619 | 620 | it := GenItem(0) 621 | 622 | it.multiComments, t.comments = t.comments, aString{} 623 | 624 | if t.tableName != "" { 625 | str = t.tableName + "." + str 626 | } 627 | 628 | t.tm[str] = it 629 | t.iv = it.Value 630 | 631 | return t, nil 632 | } 633 | 634 | func (t tomlBuilder) Equal(str string) (tomlBuilder, error) { 635 | 636 | if t.root.token != tokenKey { 637 | return t, InValidFormat 638 | } 639 | 640 | if t.iv == nil { 641 | return t, InternalError 642 | } 643 | return t, nil 644 | } 645 | 646 | func (t tomlBuilder) ArrayOfTables(str string) (nt tomlBuilder, err error) { 647 | path := str[2 : len(str)-2] 648 | 649 | if t.prefix != "" { 650 | if t.p == nil { 651 | return t, InternalError 652 | } 653 | 654 | comments := t.comments 655 | 656 | // 增加兄弟 table 657 | if t.prefix == path { 658 | t = *t.p 659 | t.comments = comments 660 | return t.ArrayOfTables(str) 661 | } else if !strings.HasPrefix(path, t.prefix+".") { 662 | // 递归向上 663 | t = *t.p 664 | t.comments = comments 665 | return t.ArrayOfTables(str) 666 | } 667 | path = path[len(t.prefix)+1:] 668 | } 669 | 670 | return t.nestToml(path) 671 | } 672 | 673 | // 嵌套 TOML , prefix 就是 [[arrayOftablesName]] 674 | func (t tomlBuilder) nestToml(prefix string) (tomlBuilder, error) { 675 | 676 | it, ok := t.tm[prefix] 677 | 678 | // [[foo.bar]] 合法性检查不够完全?????? 679 | if ok && it.kind != ArrayOfTables { 680 | return t, Redeclared 681 | } 682 | 683 | tb := newBuilder(t.root) 684 | 685 | tb.p = &t 686 | tb.tm = New() 687 | 688 | id := tb.tm[iD] 689 | 690 | // Comments 691 | id.multiComments, t.comments = t.comments, aString{} 692 | 693 | // first [[...]] 694 | if !ok { 695 | it = GenItem(ArrayOfTables) 696 | it.v = TomlArray{tb.tm} 697 | t.tm[prefix] = it 698 | 699 | } else { 700 | // again [[...]] 701 | ts := it.v.(TomlArray) 702 | it.v = append(ts, tb.tm) 703 | } 704 | tb.prefix = prefix 705 | return tb, nil 706 | } 707 | 708 | func (t tomlBuilder) ArrayLeftBrack(str string) (tomlBuilder, error) { 709 | if t.iv == nil { 710 | return t, NotSupported 711 | } 712 | 713 | if t.iv.kind == InvalidKind { 714 | t.iv.kind = Array 715 | return t, nil 716 | } 717 | if t.iv.kind != Array { 718 | return t, NotSupported 719 | } 720 | 721 | nt := t 722 | nt.iv = NewValue(Array) 723 | nt.p = &t 724 | t.iv.Add(nt.iv) 725 | return nt, nil 726 | } 727 | 728 | func (t tomlBuilder) ArrayRightBrack(str string) (tomlBuilder, error) { 729 | 730 | if t.iv == nil || t.iv.kind < StringArray || t.iv.kind > Array { 731 | return t, InValidFormat 732 | } 733 | 734 | if t.p == nil { 735 | return t, nil 736 | } 737 | return *t.p, nil 738 | } 739 | 740 | func (t tomlBuilder) Comma(str string) (tomlBuilder, error) { 741 | if t.iv == nil || t.iv.kind < StringArray || t.iv.kind > Array { 742 | return t, InValidFormat 743 | } 744 | 745 | return t, nil 746 | } 747 | 748 | // Create a Toml from a file. 749 | // 便捷方法, 从 TOML 文件解析出 Toml 对象. 750 | func LoadFile(path string) (toml Toml, err error) { 751 | source, err := ioutil.ReadFile(path) 752 | if err != nil { 753 | return 754 | } 755 | toml, err = Parse(source) 756 | return 757 | } 758 | -------------------------------------------------------------------------------- /toml_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "github.com/achun/testing-want" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func init() { 10 | want.LocalFileLine = true 11 | } 12 | 13 | func TestTomlFile(t *testing.T) { 14 | if skipTest { 15 | return 16 | } 17 | wt := want.T(t) 18 | tm, err := LoadFile("tests/example.toml") 19 | wt.Nil(err) 20 | wantExample(wt, tm) 21 | source := tm.String() 22 | //println(source) 23 | ok := false 24 | defer func() { 25 | if !ok { 26 | println(source) 27 | } 28 | }() 29 | testBuilder(wt, []byte(source), "toml_test.go TestTomlFile()") 30 | tm, err = Parse([]byte(source)) 31 | wt.Nil(err) 32 | wantExample(wt, tm) 33 | ok = true 34 | } 35 | 36 | func wantExample(wt want.Want, tm Toml) { 37 | id := tm.Id() 38 | wt.Equal(id.Kind(), InvalidKind) 39 | wt.Equal(id.multiComments, aString{"# last comments for", "# TOML document"}) 40 | wt.Equal(id.eolComment, "") 41 | 42 | it := tm["title"] 43 | wt.Equal(it.Kind(), String) 44 | wt.Equal(it.multiComments, "# This is a TOML document. Boom.") 45 | wt.Equal(it.eolComment, "") 46 | wt.Equal(it.String(), "TOML Example") 47 | 48 | it = tm["owner"] 49 | wt.Equal(it.Kind(), TableName) 50 | wt.Equal(it.multiComments, "") 51 | wt.Equal(it.eolComment, "# owner information") 52 | 53 | it = tm["owner.name"] 54 | wt.Equal(it.Kind(), String) 55 | wt.Equal(it.multiComments, "") 56 | wt.Equal(it.eolComment, "") 57 | wt.Equal(it.String(), "Tom Preston-Werner") 58 | 59 | it = tm["owner.organization"] 60 | wt.Equal(it.Kind(), String) 61 | wt.Equal(it.multiComments, "") 62 | wt.Equal(it.eolComment, "") 63 | wt.Equal(it.String(), "GitHub") 64 | 65 | it = tm["owner.bio"] 66 | wt.Equal(it.Kind(), String) 67 | wt.Equal(it.multiComments, "") 68 | wt.Equal(it.eolComment, "") 69 | wt.Equal(it.String(), "GitHub Cofounder & CEO\nLikes tater tots and beer.") 70 | 71 | it = tm["owner.dob"] 72 | wt.Equal(it.Kind(), Datetime) 73 | wt.Equal(it.multiComments, "") 74 | wt.Equal(it.eolComment, "# First class dates? Why not?") 75 | wt.Equal(it.String(), "1979-05-27T07:32:00Z") 76 | wt.Equal(it.Datetime().Format("2006-01-02T15:04:05Z00:00"), "1979-05-27T07:32:00Z00:00") 77 | 78 | it = tm["database"] 79 | wt.Equal(it.Kind(), TableName) 80 | wt.Equal(it.multiComments, "") 81 | wt.Equal(it.eolComment, "") 82 | 83 | it = tm["database.server"] 84 | wt.Equal(it.Kind(), String) 85 | wt.Equal(it.multiComments, "") 86 | wt.Equal(it.eolComment, "") 87 | wt.Equal(it.String(), "192.168.1.1") 88 | 89 | it = tm["database.ports"] 90 | wt.Equal(it.Kind(), IntegerArray, it.kind) 91 | wt.Equal(it.multiComments, "") 92 | wt.Equal(it.eolComment, "") 93 | wt.Equal(it.Len(), 3) 94 | wt.Equal(it.Index(0).Integer(), 8001) 95 | wt.Equal(it.Index(1).Integer(), 8001) 96 | wt.Equal(it.Index(2).Integer(), 8002) 97 | 98 | it = tm["database.connection_max"] 99 | wt.Equal(it.Kind(), Integer) 100 | wt.Equal(it.multiComments, "") 101 | wt.Equal(it.eolComment, "") 102 | wt.Equal(it.Integer(), 5000) 103 | 104 | it = tm["database.enabled"] 105 | wt.Equal(it.Kind(), Boolean, it.kind) 106 | wt.Equal(it.multiComments, "") 107 | wt.Equal(it.eolComment, "") 108 | wt.Equal(it.Boolean(), true) 109 | 110 | it = tm["servers"] 111 | wt.Equal(it.Kind(), TableName, it.kind) 112 | wt.Equal(it.multiComments, "") 113 | wt.Equal(it.eolComment, "") 114 | 115 | it = tm["servers.alpha"] 116 | wt.Equal(it.Kind(), TableName, it.kind) 117 | wt.Equal(it.multiComments, "# You can indent as you please. Tabs or spaces. TOML don't care.") 118 | wt.Equal(it.eolComment, "") 119 | 120 | it = tm["servers.alpha.ip"] 121 | wt.Equal(it.Kind(), String, it.kind) 122 | wt.Equal(it.multiComments, "") 123 | wt.Equal(it.eolComment, "") 124 | wt.Equal(it.String(), "10.0.0.1") 125 | 126 | it = tm["servers.alpha.dc"] 127 | wt.Equal(it.Kind(), String) 128 | wt.Equal(it.multiComments, "") 129 | wt.Equal(it.eolComment, "") 130 | wt.Equal(it.String(), "eqdc10") 131 | 132 | it = tm["servers.beta"] 133 | wt.Equal(it.Kind(), TableName) 134 | wt.Equal(it.multiComments, "") 135 | wt.Equal(it.eolComment, "") 136 | 137 | it = tm["servers.beta.ip"] 138 | wt.Equal(it.Kind(), String) 139 | wt.Equal(it.multiComments, "") 140 | wt.Equal(it.eolComment, "") 141 | wt.Equal(it.String(), "10.0.0.2") 142 | 143 | it = tm["servers.beta.dc"] 144 | wt.Equal(it.Kind(), String) 145 | wt.Equal(it.multiComments, "") 146 | wt.Equal(it.eolComment, "") 147 | wt.Equal(it.String(), "eqdc10") 148 | 149 | it = tm["servers.beta.country"] 150 | wt.Equal(it.Kind(), String) 151 | wt.Equal(it.multiComments, "") 152 | wt.Equal(it.eolComment, "# This should be parsed as UTF-8") 153 | wt.Equal(it.String(), "中国") 154 | 155 | it = tm["clients"] 156 | wt.Equal(it.Kind(), TableName) 157 | wt.Equal(it.multiComments, "") 158 | wt.Equal(it.eolComment, "") 159 | 160 | it = tm["clients.data"] 161 | wt.Equal(it.Kind(), Array) 162 | wt.Equal(it.multiComments, "") 163 | wt.Equal(it.eolComment, "# just an update to make sure parsers support it") 164 | wt.Equal(it.Len(), 2) 165 | 166 | iv := it.Index(0) 167 | wt.Equal(iv.Kind(), StringArray) 168 | wt.Equal(iv.Len(), 2) 169 | wt.Equal(iv.Index(0).Kind(), String) 170 | wt.Equal(iv.Index(1).Kind(), String) 171 | wt.Equal(iv.Index(0).String(), "gamma") 172 | wt.Equal(iv.Index(1).String(), "delta") 173 | 174 | iv = it.Index(1) 175 | wt.Equal(iv.Kind(), IntegerArray, iv.kind) 176 | wt.Equal(iv.Len(), 2) 177 | wt.Equal(iv.Index(0).Kind(), Integer) 178 | wt.Equal(iv.Index(1).Kind(), Integer) 179 | wt.Equal(iv.Index(0).Integer(), 1) 180 | wt.Equal(iv.Index(1).Integer(), 2) 181 | 182 | it = tm["clients.hosts"] 183 | wt.Equal(it.Kind(), StringArray) 184 | wt.Equal(it.multiComments, "# Line breaks are OK when inside arrays") 185 | wt.Equal(it.eolComment, "") 186 | wt.Equal(it.Len(), 2) 187 | wt.Equal(it.Index(0).Kind(), String) 188 | wt.Equal(it.Index(1).Kind(), String) 189 | wt.Equal(it.Index(0).String(), "alpha") 190 | wt.Equal(it.Index(1).String(), "omega") 191 | 192 | /** ArrayOfTables **/ 193 | 194 | // products ============== 195 | it = tm["products"] 196 | wt.Equal(it.Kind(), ArrayOfTables) 197 | wt.Equal(it.Len(), 2) 198 | 199 | ts := it.TomlArray() 200 | wt.Equal(ts.Len(), 2) 201 | 202 | wt.Equal(ts[0].Id().multiComments, "# Products") 203 | wt.Equal(ts[0].Id().eolComment, "") 204 | wt.Equal(ts[1].Id().multiComments, "") 205 | wt.Equal(ts[1].Id().eolComment, "") 206 | 207 | it = ts[0]["name"] 208 | wt.Equal(it.Kind(), String) 209 | wt.Equal(it.String(), "Hammer") 210 | wt.Equal(it.multiComments, "") 211 | wt.Equal(it.eolComment, "") 212 | 213 | it = ts[0]["sku"] 214 | wt.Equal(it.Kind(), Integer) 215 | wt.Equal(it.Integer(), 738594937) 216 | wt.Equal(it.multiComments, "") 217 | wt.Equal(it.eolComment, "") 218 | 219 | it = ts[1]["name"] 220 | wt.Equal(it.Kind(), String) 221 | wt.Equal(it.String(), "Nail") 222 | wt.Equal(it.multiComments, "") 223 | wt.Equal(it.eolComment, "") 224 | 225 | it = ts[1]["sku"] 226 | wt.Equal(it.Kind(), Integer) 227 | wt.Equal(it.Integer(), 284758393) 228 | wt.Equal(it.multiComments, "") 229 | wt.Equal(it.eolComment, "") 230 | 231 | it = ts[1]["color"] 232 | wt.Equal(it.Kind(), String) 233 | wt.Equal(it.String(), "gray") 234 | wt.Equal(it.multiComments, "") 235 | wt.Equal(it.eolComment, "") 236 | 237 | // fruit =============== 238 | it = tm["fruit"] 239 | wt.Equal(it.Kind(), ArrayOfTables) 240 | wt.Equal(it.Len(), 2) 241 | 242 | ts = it.TomlArray() 243 | wt.Equal(ts.Len(), 2) 244 | 245 | tns, aots := ts[0].TableNames() 246 | wt.Equal(tns, []string{"physical"}) 247 | wt.Equal(aots, []string{"variety"}) 248 | 249 | id = ts[0].Id() 250 | wt.Equal(id.Kind(), InvalidKind) 251 | wt.Equal(id.multiComments, "# nested") 252 | wt.Equal(id.eolComment, "") 253 | 254 | id = ts[1].Id() 255 | wt.Equal(id.Kind(), InvalidKind) 256 | wt.Equal(id.multiComments, "") 257 | wt.Equal(id.eolComment, "") 258 | 259 | // fruit[0] 260 | it = ts[0]["name"] 261 | wt.Equal(it.Kind(), String) 262 | wt.Equal(it.String(), "apple") 263 | wt.Equal(it.multiComments, "") 264 | wt.Equal(it.eolComment, "") 265 | 266 | it = ts[0]["physical"] 267 | wt.Equal(it.Kind(), TableName) 268 | wt.Equal(it.multiComments, "") 269 | wt.Equal(it.eolComment, "") 270 | 271 | it = ts[0]["physical.color"] 272 | wt.Equal(it.Kind(), String) 273 | wt.Equal(it.String(), "red") 274 | wt.Equal(it.multiComments, "") 275 | wt.Equal(it.eolComment, "") 276 | 277 | it = ts[0]["physical.shape"] 278 | wt.Equal(it.Kind(), String) 279 | wt.Equal(it.String(), "round") 280 | wt.Equal(it.multiComments, "") 281 | wt.Equal(it.eolComment, "") 282 | 283 | it = ts[0]["variety"] 284 | wt.Equal(it.Kind(), ArrayOfTables) 285 | wt.Equal(it.Len(), 2) 286 | 287 | // nested again, fruit[0]variety[0] 288 | tm = it.TomlArray()[0] 289 | wt.Equal(len(tm), 2) // with iD 290 | 291 | it = tm["name"] 292 | wt.Equal(it.Kind(), String) 293 | wt.Equal(it.String(), "red delicious") 294 | wt.Equal(it.multiComments, "") 295 | wt.Equal(it.eolComment, "") 296 | 297 | // nested nested, fruit[0]variety[1] 298 | tm = ts[0]["variety"].TomlArray()[1] 299 | wt.Equal(len(tm), 2) // with iD 300 | 301 | it = tm["name"] 302 | wt.Equal(it.Kind(), String) 303 | wt.Equal(it.String(), "granny smith") 304 | wt.Equal(it.multiComments, "") 305 | wt.Equal(it.eolComment, "") 306 | 307 | // fruit[1] 308 | it = ts[1]["name"] 309 | wt.Equal(it.Kind(), String) 310 | wt.Equal(it.String(), "banana") 311 | wt.Equal(it.multiComments, "") 312 | wt.Equal(it.eolComment, "") 313 | 314 | it = ts[1]["variety"] 315 | wt.Equal(it.Kind(), ArrayOfTables) 316 | wt.Equal(it.Len(), 1) 317 | 318 | tm = it.TomlArray()[0] 319 | wt.Equal(len(tm), 2) // with iD 320 | 321 | it = tm["name"] 322 | wt.Equal(it.Kind(), String) 323 | wt.Equal(it.String(), "plantain") 324 | wt.Equal(it.multiComments, "") 325 | wt.Equal(it.eolComment, "") 326 | } 327 | 328 | func TestTomlEmpty(t *testing.T) { 329 | if skipTest { 330 | return 331 | } 332 | wt := want.T(t) 333 | tm, err := Parse([]byte(``)) 334 | 335 | wt.Nil(err) 336 | wt.Equal(len(tm), 1) // was iD 337 | } 338 | 339 | var testDat = ` 340 | Key = "string" 341 | Int = 123456 342 | Time = 2014-01-02T15:04:05Z 343 | [Table] 344 | IntArray = [1,2,3] 345 | ` 346 | 347 | type testStruct struct { 348 | Key string 349 | Int int 350 | Time time.Time 351 | Table testTable 352 | } 353 | type testTable struct { 354 | IntArray []int 355 | } 356 | 357 | func TestTomlApply(t *testing.T) { 358 | if skipTest { 359 | return 360 | } 361 | wt := want.T(t) 362 | tm, err := Parse([]byte(testDat)) 363 | 364 | wt.Nil(err) 365 | 366 | ts := testTable{[]int{0, 0, 0}} 367 | v := testStruct{ 368 | "", 0, time.Time{}, 369 | ts, 370 | } 371 | 372 | wt.Equal(tm.Apply(&v), 6) 373 | wt.Equal(v.Key, "string") 374 | wt.Equal(v.Int, 123456) 375 | wt.Equal(v.Time.String(), "2014-01-02 15:04:05 +0000 UTC") 376 | 377 | wt.Equal(len(v.Table.IntArray), 3) 378 | wt.Equal(v.Table.IntArray[0], 1) 379 | wt.Equal(v.Table.IntArray[1], 2) 380 | wt.Equal(v.Table.IntArray[2], 3) 381 | 382 | s := "" 383 | wt.Equal(tm["key"].Apply(&s), 0) // case sensitive 384 | wt.Equal(s, "") 385 | wt.Equal(tm["Key"].Apply(&s), 1) 386 | wt.Equal(s, "string") 387 | 388 | wt.Equal(tm["Table"].Apply(&ts), 0) // Table kind is TableName 389 | wt.Equal(tm.Fetch("Table").Apply(&ts), 3) 390 | 391 | // not support maps 392 | m := map[string]interface{}{ 393 | "Key": "", 394 | "Int": 0, 395 | "Time": time.Time{}, 396 | "Table": &testTable{}, 397 | } 398 | wt.Equal(tm.Apply(m), 0) 399 | } 400 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: wercker/golang@1.1.1 --------------------------------------------------------------------------------