├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example └── yget │ ├── .gitignore │ ├── config.yaml │ └── main.go ├── go.mod └── yaml ├── config.go ├── config_test.go ├── doc.go ├── parser.go ├── parser_test.go ├── types.go └── types_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | .DS_Store 3 | *.[568ao] 4 | ._* 5 | .nfs.* 6 | [568a].out 7 | *~ 8 | *.orig 9 | *.rej 10 | *.exe 11 | .*.swp 12 | core 13 | *.cgo*.go 14 | *.cgo*.c 15 | _cgo_* 16 | _obj 17 | _test 18 | _testmain.go 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | - 1.6 6 | - 1.7 7 | - 1.8 8 | - 1.9 9 | - 1.10 10 | - 1.11 11 | - 1.12 12 | - 1.13 13 | - 1.14 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple YAML-like Configs 2 | ======================== 3 | 4 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/kylelemons/go-gypsy/yaml)](https://pkg.go.dev/github.com/kylelemons/go-gypsy/yaml) 5 | [![GoDoc](https://godoc.org/github.com/kylelemons/go-gypsy/yaml?status.svg)](https://godoc.org/github.com/kylelemons/go-gypsy/yaml) 6 | [![Build Status](https://travis-ci.com/kylelemons/go-gypsy.svg?branch=master)](https://travis-ci.com/kylelemons/go-gypsy) 7 | 8 | This repository contains a very simple parser for a YAML-like config language. 9 | Check out the API and the spec on [GoDoc](https://godoc.org/github.com/kylelemons/go-gypsy). 10 | -------------------------------------------------------------------------------- /example/yget/.gitignore: -------------------------------------------------------------------------------- 1 | yget 2 | -------------------------------------------------------------------------------- /example/yget/config.yaml: -------------------------------------------------------------------------------- 1 | mapping: 2 | key1: value1 3 | key2: value2 4 | list: 5 | - item1 6 | - item2 7 | config: 8 | server: 9 | - www.google.com 10 | - www.cnn.com 11 | - www.example.com 12 | admin: 13 | - username: god 14 | password: z3u5 15 | - username: lowly 16 | password: f!r3m3 17 | -------------------------------------------------------------------------------- /example/yget/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google, Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "log" 21 | "os" 22 | ) 23 | 24 | import "github.com/kylelemons/go-gypsy/yaml" 25 | 26 | var ( 27 | file = flag.String("file", "config.yaml", "(Simple) YAML file to read") 28 | ) 29 | 30 | func main() { 31 | cmd := os.Args[0] 32 | flag.Usage = func() { 33 | fmt.Println(`Usage:`, cmd, `[] [ ...] 34 | 35 | All s given on the commandline are looked up in 36 | the config file "config.yaml" (or whatever is specified for -file). 37 | 38 | Examples: 39 | $`, cmd, `mapping.key1 # = value1 40 | Get the key1 element of the "mapping" mapping 41 | 42 | $`, cmd, `config.server[1] 43 | Get the second (1th) element of the "server" list inside the "config" mapping 44 | 45 | $`, cmd, `mapping mapping.key1 config config.server config.admin[1].password 46 | Retrieve a bunch of options. With the example yaml file, some of these 47 | options are errors, which will print the (text of the) actual Go error from 48 | node.Get 49 | 50 | Options:`) 51 | flag.PrintDefaults() 52 | } 53 | 54 | flag.Parse() 55 | 56 | config, err := yaml.ReadFile(*file) 57 | if err != nil { 58 | log.Fatalf("readfile(%q): %s", *file, err) 59 | } 60 | 61 | params := flag.Args() 62 | 63 | width := 0 64 | for _, param := range params { 65 | if w := len(param); w > width { 66 | width = w 67 | } 68 | } 69 | 70 | for _, param := range params { 71 | val, err := config.Get(param) 72 | if err != nil { 73 | fmt.Printf("%-*s = %s\n", width, param, err) 74 | continue 75 | } 76 | fmt.Printf("%-*s = %q\n", width, param, val) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kylelemons/go-gypsy 2 | 3 | go 1.5 4 | -------------------------------------------------------------------------------- /yaml/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google, Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package yaml 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "os" 21 | "strconv" 22 | "strings" 23 | ) 24 | 25 | // A File represents the top-level YAML node found in a file. It is intended 26 | // for use as a configuration file. 27 | type File struct { 28 | Root Node 29 | 30 | // TODO(kevlar): Add a cache? 31 | } 32 | 33 | // ReadFile reads a YAML configuration file from the given filename. 34 | func ReadFile(filename string) (*File, error) { 35 | fin, err := os.Open(filename) 36 | if err != nil { 37 | return nil, err 38 | } 39 | defer fin.Close() 40 | 41 | f := new(File) 42 | f.Root, err = Parse(fin) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return f, nil 48 | } 49 | 50 | // Config reads a YAML configuration from a static string. If an error is 51 | // found, it will panic. This is a utility function and is intended for use in 52 | // initializers. 53 | func Config(yamlconf string) *File { 54 | var err error 55 | buf := bytes.NewBufferString(yamlconf) 56 | 57 | f := new(File) 58 | f.Root, err = Parse(buf) 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | return f 64 | } 65 | 66 | // ConfigFile reads a YAML configuration file from the given filename and 67 | // panics if an error is found. This is a utility function and is intended for 68 | // use in initializers. 69 | func ConfigFile(filename string) *File { 70 | f, err := ReadFile(filename) 71 | if err != nil { 72 | panic(err) 73 | } 74 | return f 75 | } 76 | 77 | // Get retrieves a scalar from the file specified by a string of the same 78 | // format as that expected by Child. If the final node is not a Scalar, Get 79 | // will return an error. 80 | func (f *File) Get(spec string) (string, error) { 81 | node, err := Child(f.Root, spec) 82 | if err != nil { 83 | return "", err 84 | } 85 | 86 | if node == nil { 87 | return "", &NodeNotFound{ 88 | Full: spec, 89 | Spec: spec, 90 | } 91 | } 92 | 93 | scalar, ok := node.(Scalar) 94 | if !ok { 95 | return "", &NodeTypeMismatch{ 96 | Full: spec, 97 | Spec: spec, 98 | Token: "$", 99 | Expected: "yaml.Scalar", 100 | Node: node, 101 | } 102 | } 103 | return scalar.String(), nil 104 | } 105 | 106 | func (f *File) GetInt(spec string) (int64, error) { 107 | s, err := f.Get(spec) 108 | if err != nil { 109 | return 0, err 110 | } 111 | 112 | i, err := strconv.ParseInt(s, 10, 64) 113 | if err != nil { 114 | return 0, err 115 | } 116 | 117 | return i, nil 118 | } 119 | 120 | func (f *File) GetBool(spec string) (bool, error) { 121 | s, err := f.Get(spec) 122 | if err != nil { 123 | return false, err 124 | } 125 | 126 | b, err := strconv.ParseBool(s) 127 | if err != nil { 128 | return false, err 129 | } 130 | 131 | return b, nil 132 | } 133 | 134 | // Count retrieves a the number of elements in the specified list from the file 135 | // using the same format as that expected by Child. If the final node is not a 136 | // List, Count will return an error. 137 | func (f *File) Count(spec string) (int, error) { 138 | node, err := Child(f.Root, spec) 139 | if err != nil { 140 | return -1, err 141 | } 142 | 143 | if node == nil { 144 | return -1, &NodeNotFound{ 145 | Full: spec, 146 | Spec: spec, 147 | } 148 | } 149 | 150 | lst, ok := node.(List) 151 | if !ok { 152 | return -1, &NodeTypeMismatch{ 153 | Full: spec, 154 | Spec: spec, 155 | Token: "$", 156 | Expected: "yaml.List", 157 | Node: node, 158 | } 159 | } 160 | return lst.Len(), nil 161 | } 162 | 163 | // Require retrieves a scalar from the file specified by a string of the same 164 | // format as that expected by Child. If the final node is not a Scalar, String 165 | // will panic. This is a convenience function for use in initializers. 166 | func (f *File) Require(spec string) string { 167 | str, err := f.Get(spec) 168 | if err != nil { 169 | panic(err) 170 | } 171 | return str 172 | } 173 | 174 | // Child retrieves a child node from the specified node as follows: 175 | // .mapkey - Get the key 'mapkey' of the Node, which must be a Map 176 | // [idx] - Choose the index from the current Node, which must be a List 177 | // 178 | // The above selectors may be applied recursively, and each successive selector 179 | // applies to the result of the previous selector. For convenience, a "." is 180 | // implied as the first character if the first character is not a "." or "[". 181 | // The node tree is walked from the given node, considering each token of the 182 | // above format. If a node along the evaluation path is not found, an error is 183 | // returned. If a node is not the proper type, an error is returned. If the 184 | // final node is not a Scalar, an error is returned. 185 | func Child(root Node, spec string) (Node, error) { 186 | if len(spec) == 0 { 187 | return root, nil 188 | } 189 | 190 | if first := spec[0]; first != '.' && first != '[' { 191 | spec = "." + spec 192 | } 193 | 194 | var recur func(Node, string, string) (Node, error) 195 | recur = func(n Node, last, s string) (Node, error) { 196 | 197 | if len(s) == 0 { 198 | return n, nil 199 | } 200 | 201 | if n == nil { 202 | return nil, &NodeNotFound{ 203 | Full: spec, 204 | Spec: last, 205 | } 206 | } 207 | 208 | // Extract the next token 209 | delim := 1 + strings.IndexAny(s[1:], ".[") 210 | if delim <= 0 { 211 | delim = len(s) 212 | } 213 | tok := s[:delim] 214 | remain := s[delim:] 215 | 216 | switch s[0] { 217 | case '[': 218 | s, ok := n.(List) 219 | if !ok { 220 | return nil, &NodeTypeMismatch{ 221 | Node: n, 222 | Expected: "yaml.List", 223 | Full: spec, 224 | Spec: last, 225 | Token: tok, 226 | } 227 | } 228 | 229 | if tok[0] == '[' && tok[len(tok)-1] == ']' { 230 | if num, err := strconv.Atoi(tok[1 : len(tok)-1]); err == nil { 231 | if num >= 0 && num < len(s) { 232 | return recur(s[num], last+tok, remain) 233 | } 234 | } 235 | } 236 | return nil, &NodeNotFound{ 237 | Full: spec, 238 | Spec: last + tok, 239 | } 240 | default: 241 | m, ok := n.(Map) 242 | if !ok { 243 | return nil, &NodeTypeMismatch{ 244 | Node: n, 245 | Expected: "yaml.Map", 246 | Full: spec, 247 | Spec: last, 248 | Token: tok, 249 | } 250 | } 251 | 252 | n, ok = m[tok[1:]] 253 | if !ok { 254 | return nil, &NodeNotFound{ 255 | Full: spec, 256 | Spec: last + tok, 257 | } 258 | } 259 | return recur(n, last+tok, remain) 260 | } 261 | } 262 | return recur(root, "", spec) 263 | } 264 | 265 | type NodeNotFound struct { 266 | Full string 267 | Spec string 268 | } 269 | 270 | func (e *NodeNotFound) Error() string { 271 | return fmt.Sprintf("yaml: %s: %q not found", e.Full, e.Spec) 272 | } 273 | 274 | type NodeTypeMismatch struct { 275 | Full string 276 | Spec string 277 | Token string 278 | Node Node 279 | Expected string 280 | } 281 | 282 | func (e *NodeTypeMismatch) Error() string { 283 | return fmt.Sprintf("yaml: %s: type mismatch: %q is %T, want %s (at %q)", 284 | e.Full, e.Spec, e.Node, e.Expected, e.Token) 285 | } 286 | -------------------------------------------------------------------------------- /yaml/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google, Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package yaml 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | var dummyConfigFile = ` 22 | mapping: 23 | key1: value1 24 | key2: value2 25 | key3: 5 26 | key4: true 27 | key5: false 28 | list: 29 | - item1 30 | - item2 31 | config: 32 | server: 33 | - www.google.com 34 | - www.cnn.com 35 | - www.example.com 36 | admin: 37 | - username: god 38 | password: z3u5 39 | - username: lowly 40 | password: f!r3m3 41 | ` 42 | 43 | var configGetTests = []struct { 44 | Spec string 45 | Want string 46 | Err string 47 | }{ 48 | {"mapping.key1", "value1", ""}, 49 | {"mapping.key2", "value2", ""}, 50 | {"list[0]", "item1", ""}, 51 | {"list[1]", "item2", ""}, 52 | {"list", "", `yaml: list: type mismatch: "list" is yaml.List, want yaml.Scalar (at "$")`}, 53 | {"list.0", "", `yaml: .list.0: type mismatch: ".list" is yaml.List, want yaml.Map (at ".0")`}, 54 | {"config.server[0]", "www.google.com", ""}, 55 | {"config.server[1]", "www.cnn.com", ""}, 56 | {"config.server[2]", "www.example.com", ""}, 57 | {"config.server[3]", "", `yaml: .config.server[3]: ".config.server[3]" not found`}, 58 | {"config.listen[0]", "", `yaml: .config.listen[0]: ".config.listen" not found`}, 59 | {"config.admin[0].username", "god", ""}, 60 | {"config.admin[1].username", "lowly", ""}, 61 | {"config.admin[2].username", "", `yaml: .config.admin[2].username: ".config.admin[2]" not found`}, 62 | } 63 | 64 | func TestGet(t *testing.T) { 65 | config := Config(dummyConfigFile) 66 | 67 | for _, test := range configGetTests { 68 | got, err := config.Get(test.Spec) 69 | if want := test.Want; got != want { 70 | t.Errorf("Get(%q) = %q, want %q", test.Spec, got, want) 71 | } 72 | 73 | switch err { 74 | case nil: 75 | got = "" 76 | default: 77 | got = err.Error() 78 | } 79 | if want := test.Err; got != want { 80 | t.Errorf("Get(%q) error %#q, want %#q", test.Spec, got, want) 81 | } 82 | } 83 | 84 | i, err := config.GetInt("mapping.key3") 85 | if err != nil || i != 5 { 86 | t.Errorf("GetInt mapping.key3 wrong") 87 | } 88 | 89 | b, err := config.GetBool("mapping.key4") 90 | if err != nil || b != true { 91 | t.Errorf("GetBool mapping.key4 wrong") 92 | } 93 | 94 | b, err = config.GetBool("mapping.key5") 95 | if err != nil || b != false { 96 | t.Errorf("GetBool mapping.key5 wrong") 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /yaml/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google, Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Gypsy is a simplified YAML parser written in Go. It is intended to be used as 16 | // a simple configuration file, and as such does not support a lot of the more 17 | // nuanced syntaxes allowed in full-fledged YAML. YAML does not allow indent with 18 | // tabs, and GYPSY does not ever consider a tab to be a space character. It is 19 | // recommended that your editor be configured to convert tabs to spaces when 20 | // editing Gypsy config files. 21 | // 22 | // Gypsy understands the following to be a list: 23 | // 24 | // - one 25 | // - two 26 | // - three 27 | // 28 | // This is parsed as a `yaml.List`, and can be retrieved from the 29 | // `yaml.Node.List()` method. In this case, each element of the `yaml.List` would 30 | // be a `yaml.Scalar` whose value can be retrieved with the `yaml.Scalar.String()` 31 | // method. 32 | // 33 | // Gypsy understands the following to be a mapping: 34 | // 35 | // key: value 36 | // foo: bar 37 | // running: away 38 | // 39 | // A mapping is an unordered list of `key:value` pairs. All whitespace after the 40 | // colon is stripped from the value and is used for alignment purposes during 41 | // export. If the value is not a list or a map, everything after the first 42 | // non-space character until the end of the line is used as the `yaml.Scalar` 43 | // value. 44 | // 45 | // Gypsy allows arbitrary nesting of maps inside lists, lists inside of maps, and 46 | // maps and/or lists nested inside of themselves. 47 | // 48 | // A map inside of a list: 49 | // 50 | // - name: John Smith 51 | // age: 42 52 | // - name: Jane Smith 53 | // age: 45 54 | // 55 | // A list inside of a map: 56 | // 57 | // schools: 58 | // - Meadow Glen 59 | // - Forest Creek 60 | // - Shady Grove 61 | // libraries: 62 | // - Joseph Hollingsworth Memorial 63 | // - Andrew Keriman Memorial 64 | // 65 | // A list of lists: 66 | // 67 | // - - one 68 | // - two 69 | // - three 70 | // - - un 71 | // - deux 72 | // - trois 73 | // - - ichi 74 | // - ni 75 | // - san 76 | // 77 | // A map of maps: 78 | // 79 | // google: 80 | // company: Google, Inc. 81 | // ticker: GOOG 82 | // url: http://google.com/ 83 | // yahoo: 84 | // company: Yahoo, Inc. 85 | // ticker: YHOO 86 | // url: http://yahoo.com/ 87 | // 88 | // In the case of a map of maps, all sub-keys must be on subsequent lines and 89 | // indented equally. It is allowable for the first key/value to be on the same 90 | // line if there is more than one key/value pair, but this is not recommended. 91 | // 92 | // Values can also be expressed in long form (leading whitespace of the first line 93 | // is removed from it and all subsequent lines). In the normal (baz) case, 94 | // newlines are treated as spaces, all indentation is removed. In the folded case 95 | // (bar), newlines are treated as spaces, except pairs of newlines (e.g. a blank 96 | // line) are treated as a single newline, only the indentation level of the first 97 | // line is removed, and newlines at the end of indented lines are preserved. In 98 | // the verbatim (foo) case, only the indent at the level of the first line is 99 | // stripped. The example: 100 | // 101 | // foo: | 102 | // lorem ipsum dolor 103 | // sit amet 104 | // bar: > 105 | // lorem ipsum 106 | // 107 | // dolor 108 | // 109 | // sit amet 110 | // baz: 111 | // lorem ipsum 112 | // dolor sit amet 113 | // 114 | // The YAML subset understood by Gypsy can be expressed (loosely) in the following 115 | // grammar (not including comments): 116 | // 117 | // OBJECT = MAPPING | SEQUENCE | SCALAR . 118 | // SHORT-OBJECT = SHORT-MAPPING | SHORT-SEQUENCE | SHORT-SCALAR . 119 | // EOL = '\n' 120 | // 121 | // MAPPING = { LONG-MAPPING | SHORT-MAPPING } . 122 | // SEQUENCE = { LONG-SEQUENCE | SHORT-SEQUENCE } . 123 | // SCALAR = { LONG-SCALAR | SHORT-SCALAR } . 124 | // 125 | // LONG-MAPPING = { INDENT KEY ':' OBJECT EOL } . 126 | // SHORT-MAPPING = '{' KEY ':' SHORT-OBJECT { ',' KEY ':' SHORT-OBJECT } '}' EOL . 127 | // 128 | // LONG-SEQUENCE = { INDENT '-' OBJECT EOL } EOL . 129 | // SHORT-SEQUENCE = '[' SHORT-OBJECT { ',' SHORT-OBJECT } ']' EOL . 130 | // 131 | // LONG-SCALAR = ( '|' | '>' | ) EOL { INDENT SHORT-SCALAR EOL } 132 | // SHORT-SCALAR = { alpha | digit | punct | ' ' | '\t' } . 133 | // 134 | // KEY = { alpha | digit } 135 | // INDENT = { ' ' } 136 | // 137 | // Any line where the first non-space character is a sharp sign (#) is a comment. 138 | // It will be ignored. 139 | // Only full-line comments are allowed. 140 | package yaml 141 | 142 | // BUG(kevlar): Multi-line strings are currently not supported. 143 | -------------------------------------------------------------------------------- /yaml/parser.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google, Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package yaml 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "strings" 24 | ) 25 | 26 | // Parse returns a root-level Node parsed from the lines read from r. In 27 | // general, this will be done for you by one of the File constructors. 28 | func Parse(r io.Reader) (node Node, err error) { 29 | lb := &lineBuffer{ 30 | Reader: bufio.NewReader(r), 31 | } 32 | 33 | defer func() { 34 | if r := recover(); r != nil { 35 | switch r := r.(type) { 36 | case error: 37 | err = r 38 | case string: 39 | err = errors.New(r) 40 | default: 41 | err = fmt.Errorf("%v", r) 42 | } 43 | } 44 | }() 45 | 46 | node = parseNode(lb, 0, nil) 47 | return 48 | } 49 | 50 | // Supporting types and constants 51 | 52 | const ( 53 | typUnknown = iota 54 | typSequence 55 | typMapping 56 | typScalar 57 | ) 58 | 59 | var typNames = []string{ 60 | "Unknown", "Sequence", "Mapping", "Scalar", 61 | } 62 | 63 | type lineReader interface { 64 | Next(minIndent int) *indentedLine 65 | } 66 | 67 | type indentedLine struct { 68 | lineno int 69 | indent int 70 | line []byte 71 | } 72 | 73 | func (line *indentedLine) String() string { 74 | return fmt.Sprintf("%2d: %s%s", line.indent, 75 | strings.Repeat(" ", 0*line.indent), string(line.line)) 76 | } 77 | 78 | func parseNode(r lineReader, ind int, initial Node) (node Node) { 79 | first := true 80 | node = initial 81 | 82 | // read lines 83 | for { 84 | line := r.Next(ind) 85 | if line == nil { 86 | break 87 | } 88 | 89 | if len(line.line) == 0 { 90 | continue 91 | } 92 | 93 | if first { 94 | ind = line.indent 95 | first = false 96 | } 97 | 98 | types := []int{} 99 | pieces := []string{} 100 | 101 | var inlineValue func([]byte) 102 | inlineValue = func(partial []byte) { 103 | // TODO(kevlar): This can be a for loop now 104 | vtyp, brk := getType(partial) 105 | begin, end := partial[:brk], partial[brk:] 106 | 107 | if vtyp == typMapping { 108 | end = end[1:] 109 | } 110 | end = bytes.TrimLeft(end, " ") 111 | 112 | switch vtyp { 113 | case typScalar: 114 | types = append(types, typScalar) 115 | pieces = append(pieces, string(end)) 116 | return 117 | case typMapping: 118 | types = append(types, typMapping) 119 | pieces = append(pieces, strings.TrimSpace(string(begin))) 120 | 121 | trimmed := bytes.TrimSpace(end) 122 | if len(trimmed) == 1 && trimmed[0] == '|' { 123 | text := "" 124 | 125 | for { 126 | l := r.Next(1) 127 | if l == nil { 128 | break 129 | } 130 | 131 | s := string(l.line) 132 | s = strings.TrimSpace(s) 133 | if len(s) == 0 { 134 | break 135 | } 136 | text = text + "\n" + s 137 | } 138 | 139 | types = append(types, typScalar) 140 | pieces = append(pieces, string(text)) 141 | return 142 | } 143 | inlineValue(end) 144 | case typSequence: 145 | types = append(types, typSequence) 146 | pieces = append(pieces, "-") 147 | inlineValue(end) 148 | } 149 | } 150 | 151 | inlineValue(line.line) 152 | var prev Node 153 | 154 | // Nest inlines 155 | for len(types) > 0 { 156 | last := len(types) - 1 157 | typ, piece := types[last], pieces[last] 158 | 159 | var current Node 160 | if last == 0 { 161 | current = node 162 | } 163 | //child := parseNode(r, line.indent+1, typUnknown) // TODO allow scalar only 164 | 165 | // Add to current node 166 | switch typ { 167 | case typScalar: // last will be == nil 168 | if _, ok := current.(Scalar); current != nil && !ok { 169 | panic("cannot append scalar to non-scalar node") 170 | } 171 | if current != nil { 172 | current = Scalar(piece) + " " + current.(Scalar) 173 | break 174 | } 175 | current = Scalar(piece) 176 | case typMapping: 177 | var mapNode Map 178 | var ok bool 179 | var child Node 180 | 181 | // Get the current map, if there is one 182 | if mapNode, ok = current.(Map); current != nil && !ok { 183 | _ = current.(Map) // panic 184 | } else if current == nil { 185 | mapNode = make(Map) 186 | } 187 | 188 | if _, inlineMap := prev.(Scalar); inlineMap && last > 0 { 189 | current = Map{ 190 | piece: prev, 191 | } 192 | break 193 | } 194 | 195 | child = parseNode(r, line.indent+1, prev) 196 | mapNode[piece] = child 197 | current = mapNode 198 | 199 | case typSequence: 200 | var listNode List 201 | var ok bool 202 | var child Node 203 | 204 | // Get the current list, if there is one 205 | if listNode, ok = current.(List); current != nil && !ok { 206 | _ = current.(List) // panic 207 | } else if current == nil { 208 | listNode = make(List, 0) 209 | } 210 | 211 | if _, inlineList := prev.(Scalar); inlineList && last > 0 { 212 | current = List{ 213 | prev, 214 | } 215 | break 216 | } 217 | 218 | child = parseNode(r, line.indent+1, prev) 219 | listNode = append(listNode, child) 220 | current = listNode 221 | 222 | } 223 | 224 | if last < 0 { 225 | last = 0 226 | } 227 | types = types[:last] 228 | pieces = pieces[:last] 229 | prev = current 230 | } 231 | 232 | node = prev 233 | } 234 | return 235 | } 236 | 237 | func getType(line []byte) (typ, split int) { 238 | if len(line) == 0 { 239 | return 240 | } 241 | 242 | if line[0] == '-' { 243 | typ = typSequence 244 | split = 1 245 | return 246 | } 247 | 248 | typ = typScalar 249 | 250 | if line[0] == ' ' || line[0] == '"' { 251 | return 252 | } 253 | 254 | // the first character is real 255 | // need to iterate past the first word 256 | // things like "foo:" and "foo :" are mappings 257 | // everything else is a scalar 258 | 259 | idx := bytes.IndexAny(line, " \":") 260 | if idx < 0 { 261 | return 262 | } 263 | 264 | if line[idx] == '"' { 265 | return 266 | } 267 | 268 | if line[idx] == ':' { 269 | typ = typMapping 270 | split = idx 271 | } else if line[idx] == ' ' { 272 | // we have a space 273 | // need to see if its all spaces until a : 274 | for i := idx; i < len(line); i++ { 275 | switch ch := line[i]; ch { 276 | case ' ': 277 | continue 278 | case ':': 279 | // only split on colons followed by a space 280 | if i+1 < len(line) && line[i+1] != ' ' { 281 | continue 282 | } 283 | 284 | typ = typMapping 285 | split = i 286 | break 287 | default: 288 | break 289 | } 290 | } 291 | } 292 | 293 | if typ == typMapping && split+1 < len(line) && line[split+1] != ' ' { 294 | typ = typScalar 295 | split = 0 296 | } 297 | 298 | return 299 | } 300 | 301 | // lineReader implementations 302 | 303 | type lineBuffer struct { 304 | *bufio.Reader 305 | readLines int 306 | pending *indentedLine 307 | } 308 | 309 | func (lb *lineBuffer) Next(min int) (next *indentedLine) { 310 | if lb.pending == nil { 311 | var ( 312 | read []byte 313 | more bool 314 | err error 315 | ) 316 | 317 | l := new(indentedLine) 318 | l.lineno = lb.readLines 319 | more = true 320 | for more { 321 | read, more, err = lb.ReadLine() 322 | if err != nil { 323 | if err == io.EOF { 324 | return nil 325 | } 326 | panic(err) 327 | } 328 | l.line = append(l.line, read...) 329 | } 330 | lb.readLines++ 331 | 332 | for _, ch := range l.line { 333 | switch ch { 334 | case ' ': 335 | l.indent += 1 336 | continue 337 | default: 338 | } 339 | break 340 | } 341 | l.line = l.line[l.indent:] 342 | 343 | // Ignore blank lines and comments. 344 | if len(l.line) == 0 || l.line[0] == '#' { 345 | return lb.Next(min) 346 | } 347 | 348 | lb.pending = l 349 | } 350 | next = lb.pending 351 | if next.indent < min { 352 | return nil 353 | } 354 | lb.pending = nil 355 | return 356 | } 357 | 358 | type lineSlice []*indentedLine 359 | 360 | func (ls *lineSlice) Next(min int) (next *indentedLine) { 361 | if len(*ls) == 0 { 362 | return nil 363 | } 364 | next = (*ls)[0] 365 | if next.indent < min { 366 | return nil 367 | } 368 | *ls = (*ls)[1:] 369 | return 370 | } 371 | 372 | func (ls *lineSlice) Push(line *indentedLine) { 373 | *ls = append(*ls, line) 374 | } 375 | -------------------------------------------------------------------------------- /yaml/parser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google, Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package yaml 16 | 17 | import ( 18 | "bytes" 19 | "strings" 20 | "testing" 21 | ) 22 | 23 | var parseTests = []struct { 24 | Input string 25 | Output string 26 | }{ 27 | { 28 | Input: "key1: val1\n", 29 | Output: "key1: val1\n", 30 | }, 31 | { 32 | Input: "key2 : val1\n", 33 | Output: "key2: val1\n", 34 | }, 35 | { 36 | Input: "key3:val1\n", 37 | Output: "key3:val1\n", 38 | }, 39 | { 40 | Input: "key4 :val1\n", 41 | Output: "key4 :val1\n", 42 | }, 43 | { 44 | Input: "key: nest: val\n", 45 | Output: "key:\n" + 46 | " nest: val\n", 47 | }, 48 | { 49 | Input: "a: b: c: d\n" + 50 | " # comment\n" + 51 | " e: f\n" + 52 | " g: h: i\n" + 53 | "\n" + 54 | " j: k\n" + 55 | "# comment\n" + 56 | " l: m\n" + 57 | "n: o\n" + 58 | "", 59 | Output: "n: o\n" + 60 | "a:\n" + 61 | " l: m\n" + 62 | " b:\n" + 63 | " c: d\n" + 64 | " e: f\n" + 65 | " g:\n" + 66 | " h: i\n" + 67 | " j: k\n" + 68 | "", 69 | }, 70 | { 71 | Input: "- item\n" + 72 | "", 73 | Output: "- item\n" + 74 | "", 75 | }, 76 | { 77 | Input: "- item2\n" + 78 | "- item1\n" + 79 | "", 80 | Output: "- item2\n" + 81 | "- item1\n" + 82 | "", 83 | }, 84 | { 85 | Input: "- - list1a\n" + 86 | " - list1b\n" + 87 | "- - list2a\n" + 88 | " - list2b\n" + 89 | "", 90 | Output: "- - list1a\n" + 91 | " - list1b\n" + 92 | "- - list2a\n" + 93 | " - list2b\n" + 94 | "", 95 | }, 96 | { 97 | Input: "- \n" + 98 | " - - listA1a\n" + 99 | " - listA1b\n" + 100 | " - - listA2a\n" + 101 | " - listA2b\n" + 102 | "-\n" + 103 | " - - listB1a\n" + 104 | " - listB1b\n" + 105 | " - - listB2a\n" + 106 | " - listB2b\n" + 107 | "", 108 | Output: "- - - listA1a\n" + 109 | " - listA1b\n" + 110 | " - - listA2a\n" + 111 | " - listA2b\n" + 112 | "- - - listB1a\n" + 113 | " - listB1b\n" + 114 | " - - listB2a\n" + 115 | " - listB2b\n" + 116 | "", 117 | }, 118 | { 119 | Input: " - keyA1a: aaa\n" + 120 | " keyA1b: bbb\n" + 121 | " - keyA2a: ccc\n" + 122 | " keyA2b: ddd\n" + 123 | " - keyB1a: eee\n" + 124 | " keyB1b: fff\n" + 125 | " - keyB2a: ggg\n" + 126 | " keyB2b: hhh\n" + 127 | "", 128 | Output: "- keyA1a: aaa\n" + 129 | " keyA1b: bbb\n" + 130 | "- keyA2a: ccc\n" + 131 | " keyA2b: ddd\n" + 132 | "- keyB1a: eee\n" + 133 | " keyB1b: fff\n" + 134 | "- keyB2a: ggg\n" + 135 | " keyB2b: hhh\n" + 136 | "", 137 | }, 138 | { 139 | Input: "japanese:\n" + 140 | " - ichi\n" + 141 | " - ni\n" + 142 | " - san\n" + 143 | "french:\n" + 144 | " - un\n" + 145 | " - deux\n" + 146 | " - trois\n" + 147 | "english:\n" + 148 | " - one\n" + 149 | " - two\n" + 150 | " - three\n" + 151 | "", 152 | Output: "english:\n" + 153 | " - one\n" + 154 | " - two\n" + 155 | " - three\n" + 156 | "french:\n" + 157 | " - un\n" + 158 | " - deux\n" + 159 | " - trois\n" + 160 | "japanese:\n" + 161 | " - ichi\n" + 162 | " - ni\n" + 163 | " - san\n" + 164 | "", 165 | }, 166 | { 167 | Input: `test: "localhost:8080"`, 168 | Output: `test: "localhost:8080"` + "\n", 169 | }, 170 | } 171 | 172 | func TestParse(t *testing.T) { 173 | for idx, test := range parseTests { 174 | buf := bytes.NewBufferString(test.Input) 175 | node, err := Parse(buf) 176 | if err != nil { 177 | t.Errorf("parse: %s", err) 178 | } 179 | if got, want := Render(node), test.Output; got != want { 180 | t.Errorf("---%d---", idx) 181 | t.Errorf("got: %q:\n%s", got, got) 182 | t.Errorf("want: %q:\n%s", want, want) 183 | } 184 | } 185 | } 186 | 187 | var getTypeTests = []struct { 188 | Value string 189 | Type int 190 | Split int 191 | }{ 192 | { 193 | Value: "a: b", 194 | Type: typMapping, 195 | Split: 1, 196 | }, 197 | { 198 | Value: "- b", 199 | Type: typSequence, 200 | Split: 1, 201 | }, 202 | } 203 | 204 | func TestGetType(t *testing.T) { 205 | for idx, test := range getTypeTests { 206 | v, s := getType([]byte(test.Value)) 207 | if got, want := v, test.Type; got != want { 208 | t.Errorf("%d. type(%q) = %s, want %s", idx, test.Value, 209 | typNames[got], typNames[want]) 210 | } 211 | if got, want := s, test.Split; got != want { 212 | got0, got1 := test.Value[:got], test.Value[got:] 213 | want0, want1 := test.Value[:want], test.Value[want:] 214 | t.Errorf("%d. split is %s|%s, want %s|%s", idx, 215 | got0, got1, want0, want1) 216 | } 217 | } 218 | } 219 | 220 | func Test_MultiLineString(t *testing.T) { 221 | buf := bytes.NewBufferString("a : |\n a\n b\n\nc : d") 222 | node, err := Parse(buf) 223 | if err != nil { 224 | t.Error(err) 225 | } else { 226 | m := node.(Map) 227 | v := m["a"].(Scalar) 228 | v2 := strings.TrimSpace(string(v)) 229 | if v2 != "a\nb" { 230 | t.Errorf("multi line parsed wrong thing: %v", v) 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /yaml/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google, Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package yaml 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "sort" 22 | "strings" 23 | ) 24 | 25 | // A Node is a YAML Node which can be a Map, List or Scalar. 26 | type Node interface { 27 | write(io.Writer, int, int) 28 | } 29 | 30 | // A Map is a YAML Mapping which maps Strings to Nodes. 31 | type Map map[string]Node 32 | 33 | // Key returns the value associeted with the key in the map. 34 | func (node Map) Key(key string) Node { 35 | return node[key] 36 | } 37 | 38 | func (node Map) write(out io.Writer, firstind, nextind int) { 39 | indent := bytes.Repeat([]byte{' '}, nextind) 40 | ind := firstind 41 | 42 | width := 0 43 | scalarkeys := []string{} 44 | objectkeys := []string{} 45 | for key, value := range node { 46 | if _, ok := value.(Scalar); ok { 47 | if swid := len(key); swid > width { 48 | width = swid 49 | } 50 | scalarkeys = append(scalarkeys, key) 51 | continue 52 | } 53 | objectkeys = append(objectkeys, key) 54 | } 55 | sort.Strings(scalarkeys) 56 | sort.Strings(objectkeys) 57 | 58 | for _, key := range scalarkeys { 59 | value := node[key].(Scalar) 60 | out.Write(indent[:ind]) 61 | fmt.Fprintf(out, "%-*s %s\n", width+1, key+":", string(value)) 62 | ind = nextind 63 | } 64 | for _, key := range objectkeys { 65 | out.Write(indent[:ind]) 66 | if node[key] == nil { 67 | fmt.Fprintf(out, "%s: \n", key) 68 | continue 69 | } 70 | fmt.Fprintf(out, "%s:\n", key) 71 | ind = nextind 72 | node[key].write(out, ind+2, ind+2) 73 | } 74 | } 75 | 76 | // A List is a YAML Sequence of Nodes. 77 | type List []Node 78 | 79 | // Get the number of items in the List. 80 | func (node List) Len() int { 81 | return len(node) 82 | } 83 | 84 | // Get the idx'th item from the List. 85 | func (node List) Item(idx int) Node { 86 | if idx >= 0 && idx < len(node) { 87 | return node[idx] 88 | } 89 | return nil 90 | } 91 | 92 | func (node List) write(out io.Writer, firstind, nextind int) { 93 | indent := bytes.Repeat([]byte{' '}, nextind) 94 | ind := firstind 95 | 96 | for _, value := range node { 97 | out.Write(indent[:ind]) 98 | fmt.Fprint(out, "- ") 99 | ind = nextind 100 | value.write(out, 0, ind+2) 101 | } 102 | } 103 | 104 | // A Scalar is a YAML Scalar. 105 | type Scalar string 106 | 107 | // String returns the string represented by this Scalar. 108 | func (node Scalar) String() string { return string(node) } 109 | 110 | func (node Scalar) write(out io.Writer, ind, _ int) { 111 | fmt.Fprintf(out, "%s%s\n", strings.Repeat(" ", ind), string(node)) 112 | } 113 | 114 | // Render returns a string of the node as a YAML document. Note that 115 | // Scalars will have a newline appended if they are rendered directly. 116 | func Render(node Node) string { 117 | buf := bytes.NewBuffer(nil) 118 | node.write(buf, 0, 0) 119 | return buf.String() 120 | } 121 | -------------------------------------------------------------------------------- /yaml/types_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google, Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package yaml 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | var stringTests = []struct { 22 | Tree Node 23 | Expect string 24 | }{ 25 | { 26 | Tree: Scalar("test"), 27 | Expect: `test 28 | `, 29 | }, 30 | { 31 | Tree: List{ 32 | Scalar("One"), 33 | Scalar("Two"), 34 | Scalar("Three"), 35 | }, 36 | Expect: `- One 37 | - Two 38 | - Three 39 | `, 40 | }, 41 | { 42 | Tree: Map{ 43 | "phonetic": Scalar("true"), 44 | "organization": Scalar("Navy"), 45 | "alphabet": List{ 46 | Scalar("Alpha"), 47 | Scalar("Bravo"), 48 | Scalar("Charlie"), 49 | }, 50 | }, 51 | Expect: `organization: Navy 52 | phonetic: true 53 | alphabet: 54 | - Alpha 55 | - Bravo 56 | - Charlie 57 | `, 58 | }, 59 | { 60 | Tree: Map{ 61 | "answer": Scalar("42"), 62 | "question": List{ 63 | Scalar("What do you get when you multiply six by nine?"), 64 | Scalar("How many roads must a man walk down?"), 65 | }, 66 | }, 67 | Expect: `answer: 42 68 | question: 69 | - What do you get when you multiply six by nine? 70 | - How many roads must a man walk down? 71 | `, 72 | }, 73 | { 74 | Tree: List{ 75 | Map{ 76 | "name": Scalar("John Smith"), 77 | "age": Scalar("42"), 78 | }, 79 | Map{ 80 | "name": Scalar("Jane Smith"), 81 | "age": Scalar("45"), 82 | }, 83 | }, 84 | Expect: `- age: 42 85 | name: John Smith 86 | - age: 45 87 | name: Jane Smith 88 | `, 89 | }, 90 | { 91 | Tree: List{ 92 | List{Scalar("one"), Scalar("two"), Scalar("three")}, 93 | List{Scalar("un"), Scalar("deux"), Scalar("trois")}, 94 | List{Scalar("ichi"), Scalar("ni"), Scalar("san")}, 95 | }, 96 | Expect: `- - one 97 | - two 98 | - three 99 | - - un 100 | - deux 101 | - trois 102 | - - ichi 103 | - ni 104 | - san 105 | `, 106 | }, 107 | { 108 | Tree: Map{ 109 | "yahoo": Map{"url": Scalar("http://yahoo.com/"), "company": Scalar("Yahoo! Inc.")}, 110 | "google": Map{"url": Scalar("http://google.com/"), "company": Scalar("Google, Inc.")}, 111 | }, 112 | Expect: `google: 113 | company: Google, Inc. 114 | url: http://google.com/ 115 | yahoo: 116 | company: Yahoo! Inc. 117 | url: http://yahoo.com/ 118 | `, 119 | }, 120 | } 121 | 122 | func TestRender(t *testing.T) { 123 | for idx, test := range stringTests { 124 | if got, want := Render(test.Tree), test.Expect; got != want { 125 | t.Errorf("%d. got:\n%s\n%d. want:\n%s\n", idx, got, idx, want) 126 | } 127 | } 128 | } 129 | --------------------------------------------------------------------------------