blob: d9742e7ab232750f19fafece2a6cdd8d1387c493 [file] [log] [blame]
Marcel van Lohuizencf727962020-02-26 13:01:52 +01001// 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 (
Marcel van Lohuizen39705552020-04-15 15:53:51 +020018 "bytes"
Marcel van Lohuizencf727962020-02-26 13:01:52 +010019 "math/big"
20 "strings"
21
22 "gopkg.in/yaml.v3"
23
24 "cuelang.org/go/cue/ast"
25 "cuelang.org/go/cue/errors"
26 "cuelang.org/go/cue/literal"
27 "cuelang.org/go/cue/token"
28 "cuelang.org/go/internal"
29)
30
31// Encode converts a CUE AST to YAML.
32//
33// The given file must only contain values that can be directly supported by
34// YAML:
35// Type Restrictions
36// BasicLit
37// File no imports, aliases, or definitions
38// StructLit no embeddings, aliases, or definitions
39// List
40// Field must be regular; label must be a BasicLit or Ident
41// CommentGroup
42//
43// TODO: support anchors through Ident.
44func Encode(n ast.Node) (b []byte, err error) {
45 y, err := encode(n)
46 if err != nil {
47 return nil, err
48 }
Marcel van Lohuizen39705552020-04-15 15:53:51 +020049 w := &bytes.Buffer{}
50 enc := yaml.NewEncoder(w)
51 // Use idiomatic indentation.
52 enc.SetIndent(2)
53 if err = enc.Encode(y); err != nil {
54 return nil, err
55 }
56 return w.Bytes(), nil
Marcel van Lohuizencf727962020-02-26 13:01:52 +010057}
58
59func encode(n ast.Node) (y *yaml.Node, err error) {
60 switch x := n.(type) {
61 case *ast.BasicLit:
62 y, err = encodeScalar(x)
63
64 case *ast.ListLit:
65 y, err = encodeExprs(x.Elts)
66 line := x.Lbrack.Line()
67 if err == nil && line > 0 && line == x.Rbrack.Line() {
68 y.Style = yaml.FlowStyle
69 }
70
71 case *ast.StructLit:
72 y, err = encodeDecls(x.Elts)
73 line := x.Lbrace.Line()
74 if err == nil && line > 0 && line == x.Rbrace.Line() {
75 y.Style = yaml.FlowStyle
76 }
77
78 case *ast.File:
79 y, err = encodeDecls(x.Decls)
80
81 case *ast.UnaryExpr:
82 b, ok := x.X.(*ast.BasicLit)
83 if ok && x.Op == token.SUB && (b.Kind == token.INT || b.Kind == token.FLOAT) {
84 y, err = encodeScalar(b)
85 if !strings.HasPrefix(y.Value, "-") {
86 y.Value = "-" + y.Value
87 break
88 }
89 }
90 return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", internal.DebugStr(x), x)
91 default:
92 return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", internal.DebugStr(x), x)
93 }
94 if err != nil {
95 return nil, err
96 }
97 addDocs(n, y, y)
98 return y, nil
99}
100
101func encodeScalar(b *ast.BasicLit) (n *yaml.Node, err error) {
102 n = &yaml.Node{Kind: yaml.ScalarNode}
103
104 switch b.Kind {
105 case token.INT:
106 var x big.Int
107 if err := setNum(n, b.Value, &x); err != nil {
108 return nil, err
109 }
110
111 case token.FLOAT:
112 var x big.Float
113 if err := setNum(n, b.Value, &x); err != nil {
114 return nil, err
115 }
116
117 case token.TRUE, token.FALSE, token.NULL:
118 n.Value = b.Value
119
120 case token.STRING:
121 str, err := literal.Unquote(b.Value)
122 if err != nil {
123 return nil, err
124 }
125 n.SetString(str)
126
127 default:
128 return nil, errors.Newf(b.Pos(), "unknown literal type %v", b.Kind)
129 }
130 return n, nil
131}
132
133func setNum(n *yaml.Node, s string, x interface{}) error {
134 if yaml.Unmarshal([]byte(s), x) == nil {
135 n.Value = s
136 return nil
137 }
138
139 var ni literal.NumInfo
140 if err := literal.ParseNum(s, &ni); err != nil {
141 return err
142 }
143 n.Value = ni.String()
144 return nil
145}
146
147func encodeExprs(exprs []ast.Expr) (n *yaml.Node, err error) {
148 n = &yaml.Node{Kind: yaml.SequenceNode}
149
150 for _, elem := range exprs {
151 e, err := encode(elem)
152 if err != nil {
153 return nil, err
154 }
155 n.Content = append(n.Content, e)
156 }
157 return n, nil
158}
159
160// encodeDecls converts a sequence of declarations to a value. If it encounters
161// an embedded value, it will return this expression. This is more relaxed for
162// structs than is currently allowed for CUE, but the expectation is that this
163// will be allowed at some point. The input would still be illegal CUE.
164func encodeDecls(decls []ast.Decl) (n *yaml.Node, err error) {
165 n = &yaml.Node{Kind: yaml.MappingNode}
166
167 docForNext := strings.Builder{}
168 var lastHead, lastFoot *yaml.Node
169 hasEmbed := false
170 for _, d := range decls {
171 switch x := d.(type) {
172 default:
173 return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", internal.DebugStr(x), x)
174
175 case *ast.Package:
176 if len(n.Content) > 0 {
177 return nil, errors.Newf(x.Pos(), "invalid package clause")
178 }
179 continue
180
181 case *ast.CommentGroup:
182 docForNext.WriteString(docToYAML(x))
183 docForNext.WriteString("\n\n")
184 continue
185
186 case *ast.Field:
187 if x.Token == token.ISA {
188 return nil, errors.Newf(x.TokenPos, "yaml: definition not allowed")
189 }
190 if x.Optional != token.NoPos {
191 return nil, errors.Newf(x.Optional, "yaml: optional fields not allowed")
192 }
193 if hasEmbed {
194 return nil, errors.Newf(x.TokenPos, "yaml: embedding mixed with fields")
195 }
196 name, _, err := ast.LabelName(x.Label)
197 if err != nil {
198 return nil, errors.Newf(x.Label.Pos(), "yaml: only literal labels allowed")
199 }
200
201 label := &yaml.Node{}
202 addDocs(x.Label, label, label)
203 label.SetString(name)
204
205 value, err := encode(x.Value)
206 if err != nil {
207 return nil, err
208 }
209 lastHead = label
210 lastFoot = value
211 addDocs(x, label, value)
212 n.Content = append(n.Content, label)
213 n.Content = append(n.Content, value)
214
215 case *ast.EmbedDecl:
216 if hasEmbed {
217 return nil, errors.Newf(x.Pos(), "yaml: multiple embedded values")
218 }
219 hasEmbed = true
220 e, err := encode(x.Expr)
221 if err != nil {
222 return nil, err
223 }
224 addDocs(x, e, e)
225 lastHead = e
226 lastFoot = e
227 n.Content = append(n.Content, e)
228 }
229 if docForNext.Len() > 0 {
230 docForNext.WriteString(lastHead.HeadComment)
231 lastHead.HeadComment = docForNext.String()
232 docForNext.Reset()
233 }
234 }
235
236 if docForNext.Len() > 0 && lastFoot != nil {
237 if !strings.HasSuffix(lastFoot.FootComment, "\n") {
238 lastFoot.FootComment += "\n"
239 }
240 n := docForNext.Len()
241 lastFoot.FootComment += docForNext.String()[:n-1]
242 }
243
244 if hasEmbed {
245 return n.Content[0], nil
246 }
247
248 return n, nil
249}
250
251// addDocs prefixes head, replaces line and appends foot comments.
252func addDocs(n ast.Node, h, f *yaml.Node) {
253 head := ""
254 isDoc := false
255 for _, c := range ast.Comments(n) {
256 switch {
257 case c.Line:
258 f.LineComment = docToYAML(c)
259
260 case c.Position > 0:
261 if f.FootComment != "" {
262 f.FootComment += "\n\n"
263 } else if relPos := c.Pos().RelPos(); relPos == token.NewSection {
264 f.FootComment += "\n"
265 }
266 f.FootComment += docToYAML(c)
267
268 default:
269 if head != "" {
270 head += "\n\n"
271 }
272 head += docToYAML(c)
273 isDoc = isDoc || c.Doc
274 }
275 }
276
277 if head != "" {
278 if h.HeadComment != "" || !isDoc {
279 head += "\n\n"
280 }
281 h.HeadComment = head + h.HeadComment
282 }
283}
284
285// docToYAML converts a CUE CommentGroup to a YAML comment string. This ensures
286// that comments with empty lines get properly converted.
287func docToYAML(c *ast.CommentGroup) string {
288 s := c.Text()
289 if strings.HasSuffix(s, "\n") { // always true
290 s = s[:len(s)-1]
291 }
292 lines := strings.Split(s, "\n")
293 for i, l := range lines {
294 if l == "" {
295 lines[i] = "#"
296 } else {
297 lines[i] = "# " + l
298 }
299 }
300 return strings.Join(lines, "\n")
301}