1// Copyright 2020 CUE Authors
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
15package yaml
16
17import (
18 "bytes"
19 "encoding/base64"
20 "fmt"
21 "math/big"
22 "regexp"
23 "strings"
24 "sync"
25
26 "go.yaml.in/yaml/v3"
27
28 "cuelang.org/go/cue/ast"
29 "cuelang.org/go/cue/errors"
30 "cuelang.org/go/cue/literal"
31 "cuelang.org/go/cue/token"
32 "cuelang.org/go/internal"
33 "cuelang.org/go/internal/astinternal"
34)
35
36// Encode converts a CUE AST to YAML.
37//
38// The given file must only contain values that can be directly supported by
39// YAML:
40//
41// Type Restrictions
42// BasicLit
43// File no imports, aliases, or definitions
44// StructLit no embeddings, aliases, or definitions
45// List
46// Field must be regular; label must be a BasicLit or Ident
47// CommentGroup
48//
49// TODO: support anchors through Ident.
50func Encode(n ast.Node) (b []byte, err error) {
51 y, err := encode(n)
52 if err != nil {
53 return nil, err
54 }
55 w := &bytes.Buffer{}
56 enc := yaml.NewEncoder(w)
57 // Use idiomatic indentation.
58 enc.SetIndent(2)
59 if err = enc.Encode(y); err != nil {
60 return nil, err
61 }
62 return w.Bytes(), nil
63}
64
65func encode(n ast.Node) (y *yaml.Node, err error) {
66 switch x := n.(type) {
67 case *ast.BasicLit:
68 y, err = encodeScalar(x)
69
70 case *ast.ListLit:
71 y, err = encodeExprs(x.Elts)
72 line := x.Lbrack.Line()
73 if err == nil && line > 0 && line == x.Rbrack.Line() {
74 y.Style = yaml.FlowStyle
75 }
76
77 case *ast.StructLit:
78 y, err = encodeDecls(x.Elts)
79 line := x.Lbrace.Line()
80 if err == nil && line > 0 && line == x.Rbrace.Line() {
81 y.Style = yaml.FlowStyle
82 }
83
84 case *ast.File:
85 y, err = encodeDecls(x.Decls)
86
87 case *ast.UnaryExpr:
88 b, ok := x.X.(*ast.BasicLit)
89 if ok && x.Op == token.SUB && (b.Kind == token.INT || b.Kind == token.FLOAT) {
90 y, err = encodeScalar(b)
91 if !strings.HasPrefix(y.Value, "-") {
92 y.Value = "-" + y.Value
93 break
94 }
95 }
96 return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", astinternal.DebugStr(x), x)
97 default:
98 return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", astinternal.DebugStr(x), x)
99 }
100 if err != nil {
101 return nil, err
102 }
103 addDocs(n, y, y)
104 return y, nil
105}
106
107func encodeScalar(b *ast.BasicLit) (n *yaml.Node, err error) {
108 n = &yaml.Node{Kind: yaml.ScalarNode}
109
110 // TODO: use cue.Value and support attributes for setting YAML tags.
111
112 switch b.Kind {
113 case token.INT:
114 var x big.Int
115 if err := setNum(n, b.Value, &x); err != nil {
116 return nil, err
117 }
118
119 case token.FLOAT:
120 var x big.Float
121 if err := setNum(n, b.Value, &x); err != nil {
122 return nil, err
123 }
124
125 case token.TRUE, token.FALSE, token.NULL:
126 n.Value = b.Value
127
128 case token.STRING:
129 info, nStart, _, err := literal.ParseQuotes(b.Value, b.Value)
130 if err != nil {
131 return nil, err
132 }
133 str, err := info.Unquote(b.Value[nStart:])
134 if err != nil {
135 panic(fmt.Sprintf("invalid string: %v", err))
136 }
137 n.SetString(str)
138
139 switch {
140 case !info.IsDouble():
141 n.Tag = "!!binary"
142 n.Value = base64.StdEncoding.EncodeToString([]byte(str))
143
144 case info.IsMulti():
145 // Preserve multi-line format.
146 n.Style = yaml.LiteralStyle
147
148 default:
149 if shouldQuote(str) {
150 n.Style = yaml.DoubleQuotedStyle
151 }
152 }
153
154 default:
155 return nil, errors.Newf(b.Pos(), "unknown literal type %v", b.Kind)
156 }
157 return n, nil
158}
159
160// shouldQuote indicates that a string may be a YAML 1.1. legacy value and that
161// the string should be quoted.
162func shouldQuote(str string) bool {
163 return legacyStrings[str] || useQuote().MatchString(str)
164}
165
166// This regular expression conservatively matches any date, time string,
167// or base60 float.
168var useQuote = sync.OnceValue(func() *regexp.Regexp {
169 return regexp.MustCompile(`^[\-+0-9:\. \t]+([-:]|[tT])[\-+0-9:\. \t]+[zZ]?$|^0x[a-fA-F0-9]+$`)
170})
171
172// legacyStrings contains a map of fixed strings with special meaning for any
173// type in the YAML Tag registry (https://yaml.org/type/index.html) as used
174// in YAML 1.1.
175//
176// These strings are always quoted upon export to allow for backward
177// compatibility with YAML 1.1 parsers.
178var legacyStrings = map[string]bool{
179 "y": true,
180 "Y": true,
181 "yes": true,
182 "Yes": true,
183 "YES": true,
184 "n": true,
185 "N": true,
186 "t": true,
187 "T": true,
188 "f": true,
189 "F": true,
190 "no": true,
191 "No": true,
192 "NO": true,
193 "true": true,
194 "True": true,
195 "TRUE": true,
196 "false": true,
197 "False": true,
198 "FALSE": true,
199 "on": true,
200 "On": true,
201 "ON": true,
202 "off": true,
203 "Off": true,
204 "OFF": true,
205
206 // Non-standard.
207 ".Nan": true,
208}
209
210func setNum(n *yaml.Node, s string, x interface{}) error {
211 if yaml.Unmarshal([]byte(s), x) == nil {
212 n.Value = s
213 return nil
214 }
215
216 var ni literal.NumInfo
217 if err := literal.ParseNum(s, &ni); err != nil {
218 return err
219 }
220 n.Value = ni.String()
221 return nil
222}
223
224func encodeExprs(exprs []ast.Expr) (n *yaml.Node, err error) {
225 n = &yaml.Node{Kind: yaml.SequenceNode}
226
227 for _, elem := range exprs {
228 e, err := encode(elem)
229 if err != nil {
230 return nil, err
231 }
232 n.Content = append(n.Content, e)
233 }
234 return n, nil
235}
236
237// encodeDecls converts a sequence of declarations to a value. If it encounters
238// an embedded value, it will return this expression. This is more relaxed for
239// structs than is currently allowed for CUE, but the expectation is that this
240// will be allowed at some point. The input would still be illegal CUE.
241func encodeDecls(decls []ast.Decl) (n *yaml.Node, err error) {
242 n = &yaml.Node{Kind: yaml.MappingNode}
243
244 docForNext := strings.Builder{}
245 var lastHead, lastFoot *yaml.Node
246 hasEmbed := false
247 for _, d := range decls {
248 switch x := d.(type) {
249 default:
250 return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", astinternal.DebugStr(x), x)
251
252 case *ast.Package:
253 if len(n.Content) > 0 {
254 return nil, errors.Newf(x.Pos(), "invalid package clause")
255 }
256 continue
257
258 case *ast.CommentGroup:
259 docForNext.WriteString(docToYAML(x))
260 docForNext.WriteString("\n\n")
261 continue
262
263 case *ast.Attribute:
264 continue
265
266 case *ast.Field:
267 if !internal.IsRegularField(x) {
268 return nil, errors.Newf(x.TokenPos, "yaml: definition or hidden fields not allowed")
269 }
270 if x.Constraint != token.ILLEGAL {
271 return nil, errors.Newf(x.TokenPos, "yaml: optional fields not allowed")
272 }
273 if hasEmbed {
274 return nil, errors.Newf(x.TokenPos, "yaml: embedding mixed with fields")
275 }
276 name, _, err := ast.LabelName(x.Label)
277 if err != nil {
278 return nil, errors.Newf(x.Label.Pos(), "yaml: only literal labels allowed")
279 }
280
281 label := &yaml.Node{}
282 addDocs(x.Label, label, label)
283 label.SetString(name)
284 if shouldQuote(name) {
285 label.Style = yaml.DoubleQuotedStyle
286 }
287
288 value, err := encode(x.Value)
289 if err != nil {
290 return nil, err
291 }
292 lastHead = label
293 lastFoot = value
294 addDocs(x, label, value)
295 n.Content = append(n.Content, label)
296 n.Content = append(n.Content, value)
297
298 case *ast.EmbedDecl:
299 if hasEmbed {
300 return nil, errors.Newf(x.Pos(), "yaml: multiple embedded values")
301 }
302 hasEmbed = true
303 e, err := encode(x.Expr)
304 if err != nil {
305 return nil, err
306 }
307 addDocs(x, e, e)
308 lastHead = e
309 lastFoot = e
310 n.Content = append(n.Content, e)
311 }
312 if docForNext.Len() > 0 {
313 docForNext.WriteString(lastHead.HeadComment)
314 lastHead.HeadComment = docForNext.String()
315 docForNext.Reset()
316 }
317 }
318
319 if docForNext.Len() > 0 && lastFoot != nil {
320 if !strings.HasSuffix(lastFoot.FootComment, "\n") {
321 lastFoot.FootComment += "\n"
322 }
323 n := docForNext.Len()
324 lastFoot.FootComment += docForNext.String()[:n-1]
325 }
326
327 if hasEmbed {
328 return n.Content[0], nil
329 }
330
331 return n, nil
332}
333
334// addDocs prefixes head, replaces line and appends foot comments.
335func addDocs(n ast.Node, h, f *yaml.Node) {
336 head := ""
337 isDoc := false
338 for _, c := range ast.Comments(n) {
339 switch {
340 case c.Line:
341 f.LineComment = docToYAML(c)
342
343 case c.Position > 0:
344 if f.FootComment != "" {
345 f.FootComment += "\n\n"
346 } else if relPos := c.Pos().RelPos(); relPos == token.NewSection {
347 f.FootComment += "\n"
348 }
349 f.FootComment += docToYAML(c)
350
351 default:
352 if head != "" {
353 head += "\n\n"
354 }
355 head += docToYAML(c)
356 isDoc = isDoc || c.Doc
357 }
358 }
359
360 if head != "" {
361 if h.HeadComment != "" || !isDoc {
362 head += "\n\n"
363 }
364 h.HeadComment = head + h.HeadComment
365 }
366}
367
368// docToYAML converts a CUE CommentGroup to a YAML comment string. This ensures
369// that comments with empty lines get properly converted.
370func docToYAML(c *ast.CommentGroup) string {
371 s := c.Text()
372 s = strings.TrimSuffix(s, "\n") // always trims
373 lines := strings.Split(s, "\n")
374 for i, l := range lines {
375 if l == "" {
376 lines[i] = "#"
377 } else {
378 lines[i] = "# " + l
379 }
380 }
381 return strings.Join(lines, "\n")
382}