v / vlib / yaml / yaml.v
410 lines · 373 sloc · 7.81 KB · d96fe54c0a96fd6d651e9cb3c61b137ccc5c7dbe
Raw
1module yaml
2
3import json
4import os
5import strings
6import x.json2
7
8// Null is a simple representation of the YAML `null` value.
9pub struct Null {}
10
11// null is an instance of `Null`, to ease comparisons with it.
12pub const null = Any(Null{})
13
14// Any is the tree representation used by the YAML module.
15pub type Any = []Any | Null | bool | f64 | i64 | int | map[string]Any | string | u64
16
17// Doc is a parsed YAML document.
18pub struct Doc {
19pub:
20 root Any
21}
22
23// parse_file parses the YAML file at `path`.
24pub fn parse_file(path string) !Doc {
25 return parse_text(os.read_file(path)!)
26}
27
28// parse_text parses the YAML document provided in `text`.
29pub fn parse_text(text string) !Doc {
30 mut normalized := text
31 if normalized.contains_u8(`\r`) {
32 normalized = normalized.replace('\r\n', '\n').replace('\r', '\n')
33 }
34 if normalized.len >= 3 && normalized[0] == 0xef && normalized[1] == 0xbb
35 && normalized[2] == 0xbf {
36 normalized = normalized[3..]
37 }
38 // `split('\n')` would otherwise turn the canonical trailing line break into
39 // a phantom empty last line, which the block-scalar reader then treats as a
40 // genuine blank line and over-counts during chomping.
41 if normalized.ends_with('\n') {
42 normalized = normalized[..normalized.len - 1]
43 }
44 trimmed := normalized.trim_space()
45 if trimmed == '' {
46 return Doc{
47 root: null
48 }
49 }
50 if trimmed.starts_with('{') || trimmed.starts_with('[') {
51 // JSON-superset fast path. `parse_flow_value` already consumes the
52 // flow-style grammar that YAML borrows from JSON, so it builds the
53 // `yaml.Any` tree directly — no second-pass `from_json2` rebuild.
54 // Falls through to the block parser if the body is anything other
55 // than a clean flow document.
56 if val := parse_flow_value(trimmed) {
57 return Doc{
58 root: val
59 }
60 }
61 }
62 mut parser := Parser{
63 lines: normalized.split('\n')
64 }
65 return Doc{
66 root: parser.parse()!
67 }
68}
69
70// decode decodes YAML text into the target type `T`.
71// The generic encode/decode path uses the main `json` module for field parity.
72pub fn decode[T](yaml_text string) !T {
73 doc := parse_text(yaml_text)!
74 return doc.decode[T]()
75}
76
77// decode_file decodes the YAML file at `path` into the target type `T`.
78pub fn decode_file[T](path string) !T {
79 return decode[T](os.read_file(path)!)
80}
81
82// encode encodes the value `value` into a YAML string.
83// The generic encode/decode path uses the main `json` module for field parity.
84pub fn encode[T](value T) string {
85 json_text := json.encode(value)
86 raw := json2.decode[json2.Any](json_text) or { return '' }
87 return from_json2(raw).to_yaml()
88}
89
90// encode_file encodes `value` as YAML and writes it to `path`.
91pub fn encode_file[T](path string, value T) ! {
92 os.write_file(path, encode(value))!
93}
94
95// decode decodes the YAML document into the target type `T`. An empty
96// document (parsed as the YAML 1.2 null node) decodes to a default-initialized
97// `T`, matching the common "empty config file = use defaults" idiom.
98pub fn (d Doc) decode[T]() !T {
99 if d.root is Null {
100 return json.decode(T, '{}')!
101 }
102 return json.decode(T, d.to_json())!
103}
104
105// to_any converts the YAML document to `yaml.Any`.
106pub fn (d Doc) to_any() Any {
107 return d.root
108}
109
110// to_json converts the YAML document to JSON.
111pub fn (d Doc) to_json() string {
112 return d.root.to_json()
113}
114
115// to_yaml converts the YAML document back to YAML text.
116pub fn (d Doc) to_yaml() string {
117 return d.root.to_yaml()
118}
119
120// value queries a value from the YAML document.
121// `key` supports dotted keys and array indexing like `servers[0].host`.
122pub fn (d Doc) value(key string) Any {
123 return d.root.value(key)
124}
125
126// value_opt queries a value from the YAML document and returns an error when missing.
127pub fn (d Doc) value_opt(key string) !Any {
128 return d.root.value_opt(key)
129}
130
131// str returns a display-friendly string form of `Any`.
132pub fn (a Any) str() string {
133 return a.string()
134}
135
136// string returns `Any` as a string when possible, or a YAML representation otherwise.
137pub fn (a Any) string() string {
138 return match a {
139 string { a }
140 bool, f64, i64, int, u64 { a.str() }
141 Null { 'null' }
142 []Any, map[string]Any { a.to_yaml() }
143 }
144}
145
146// int returns `Any` as an `int`.
147pub fn (a Any) int() int {
148 return match a {
149 int {
150 a
151 }
152 i64 {
153 int(a)
154 }
155 u64 {
156 int(a)
157 }
158 f64 {
159 int(a)
160 }
161 bool {
162 if a {
163 1
164 } else {
165 0
166 }
167 }
168 string {
169 a.int()
170 }
171 else {
172 0
173 }
174 }
175}
176
177// i64 returns `Any` as an `i64`.
178pub fn (a Any) i64() i64 {
179 return match a {
180 i64 {
181 a
182 }
183 int {
184 i64(a)
185 }
186 u64 {
187 i64(a)
188 }
189 f64 {
190 i64(a)
191 }
192 bool {
193 if a {
194 i64(1)
195 } else {
196 i64(0)
197 }
198 }
199 string {
200 a.i64()
201 }
202 else {
203 i64(0)
204 }
205 }
206}
207
208// u64 returns `Any` as a `u64`.
209pub fn (a Any) u64() u64 {
210 return match a {
211 u64 {
212 a
213 }
214 int {
215 u64(a)
216 }
217 i64 {
218 u64(a)
219 }
220 f64 {
221 u64(a)
222 }
223 bool {
224 if a {
225 u64(1)
226 } else {
227 u64(0)
228 }
229 }
230 string {
231 a.u64()
232 }
233 else {
234 u64(0)
235 }
236 }
237}
238
239// f64 returns `Any` as an `f64`.
240pub fn (a Any) f64() f64 {
241 return match a {
242 f64 {
243 a
244 }
245 int {
246 f64(a)
247 }
248 i64 {
249 f64(a)
250 }
251 u64 {
252 f64(a)
253 }
254 bool {
255 if a {
256 1.0
257 } else {
258 0.0
259 }
260 }
261 string {
262 a.f64()
263 }
264 else {
265 0.0
266 }
267 }
268}
269
270// bool returns `Any` as a `bool`.
271pub fn (a Any) bool() bool {
272 return match a {
273 bool {
274 a
275 }
276 int {
277 a != 0
278 }
279 i64 {
280 a != 0
281 }
282 u64 {
283 a != 0
284 }
285 f64 {
286 a != 0.0
287 }
288 string {
289 lower := a.to_lower()
290 lower in ['true', 'yes', 'on', '1']
291 }
292 else {
293 false
294 }
295 }
296}
297
298// array returns `Any` as an array.
299pub fn (a Any) array() []Any {
300 return match a {
301 []Any {
302 a
303 }
304 map[string]Any {
305 mut arr := []Any{cap: a.len}
306 for _, value in a {
307 arr << value
308 }
309 arr
310 }
311 else {
312 [a]
313 }
314 }
315}
316
317// as_map returns `Any` as a map.
318pub fn (a Any) as_map() map[string]Any {
319 return match a {
320 map[string]Any {
321 a
322 }
323 []Any {
324 mut out := map[string]Any{}
325 for i, value in a {
326 out['${i}'] = value
327 }
328 out
329 }
330 else {
331 {
332 '0': a
333 }
334 }
335 }
336}
337
338// default_to returns `value` when `a` is `Null`.
339pub fn (a Any) default_to(value Any) Any {
340 return match a {
341 Null { value }
342 else { a }
343 }
344}
345
346// value queries a value from the current node using dotted keys and array indices.
347pub fn (a Any) value(key string) Any {
348 return a.value_opt(key) or { null }
349}
350
351// value_opt queries a value from the current node and returns an error when missing.
352// A YAML key whose value is the explicit `null` literal returns that `Null`
353// (it is not treated as missing); only an absent key or a non-traversable
354// path raises an error.
355pub fn (a Any) value_opt(key string) !Any {
356 key_split := parse_dotted_key(key) or { return error('yaml: invalid dotted key `${key}`') }
357 return a.value_(a, key_split) or { error('yaml: no value for key `${key}`') }
358}
359
360// value queries a value from the map.
361pub fn (m map[string]Any) value(key string) Any {
362 return Any(m).value(key)
363}
364
365// value queries a value from the array.
366pub fn (a []Any) value(key string) Any {
367 return Any(a).value(key)
368}
369
370// as_strings returns the contents of the array as `[]string`.
371pub fn (a []Any) as_strings() []string {
372 mut out := []string{cap: a.len}
373 for value in a {
374 out << value.string()
375 }
376 return out
377}
378
379// as_strings returns the contents of the map as `map[string]string`.
380pub fn (m map[string]Any) as_strings() map[string]string {
381 mut out := map[string]string{}
382 for key, value in m {
383 out[key] = value.string()
384 }
385 return out
386}
387
388// to_json converts `Any` to JSON.
389pub fn (a Any) to_json() string {
390 mut sb := strings.new_builder(256)
391 emit_any_as_json(mut sb, a)
392 return sb.str()
393}
394
395// to_yaml converts `Any` to YAML.
396pub fn (a Any) to_yaml() string {
397 mut sb := strings.new_builder(256)
398 emit_yaml_any(mut sb, a, 0)
399 return sb.str()
400}
401
402// to_yaml converts a YAML array to YAML text.
403pub fn (a []Any) to_yaml() string {
404 return Any(a).to_yaml()
405}
406
407// to_yaml converts a YAML map to YAML text.
408pub fn (m map[string]Any) to_yaml() string {
409 return Any(m).to_yaml()
410}
411