blob: 50128428e74aed5665552fca73ad274b02b38f2f [file] [log] [blame]
// Copyright 2020 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package yaml
import (
"bytes"
"math/big"
"strings"
"gopkg.in/yaml.v3"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/literal"
"cuelang.org/go/cue/token"
"cuelang.org/go/internal/astinternal"
)
// Encode converts a CUE AST to YAML.
//
// The given file must only contain values that can be directly supported by
// YAML:
// Type Restrictions
// BasicLit
// File no imports, aliases, or definitions
// StructLit no embeddings, aliases, or definitions
// List
// Field must be regular; label must be a BasicLit or Ident
// CommentGroup
//
// TODO: support anchors through Ident.
func Encode(n ast.Node) (b []byte, err error) {
y, err := encode(n)
if err != nil {
return nil, err
}
w := &bytes.Buffer{}
enc := yaml.NewEncoder(w)
// Use idiomatic indentation.
enc.SetIndent(2)
if err = enc.Encode(y); err != nil {
return nil, err
}
return w.Bytes(), nil
}
func encode(n ast.Node) (y *yaml.Node, err error) {
switch x := n.(type) {
case *ast.BasicLit:
y, err = encodeScalar(x)
case *ast.ListLit:
y, err = encodeExprs(x.Elts)
line := x.Lbrack.Line()
if err == nil && line > 0 && line == x.Rbrack.Line() {
y.Style = yaml.FlowStyle
}
case *ast.StructLit:
y, err = encodeDecls(x.Elts)
line := x.Lbrace.Line()
if err == nil && line > 0 && line == x.Rbrace.Line() {
y.Style = yaml.FlowStyle
}
case *ast.File:
y, err = encodeDecls(x.Decls)
case *ast.UnaryExpr:
b, ok := x.X.(*ast.BasicLit)
if ok && x.Op == token.SUB && (b.Kind == token.INT || b.Kind == token.FLOAT) {
y, err = encodeScalar(b)
if !strings.HasPrefix(y.Value, "-") {
y.Value = "-" + y.Value
break
}
}
return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", astinternal.DebugStr(x), x)
default:
return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", astinternal.DebugStr(x), x)
}
if err != nil {
return nil, err
}
addDocs(n, y, y)
return y, nil
}
func encodeScalar(b *ast.BasicLit) (n *yaml.Node, err error) {
n = &yaml.Node{Kind: yaml.ScalarNode}
switch b.Kind {
case token.INT:
var x big.Int
if err := setNum(n, b.Value, &x); err != nil {
return nil, err
}
case token.FLOAT:
var x big.Float
if err := setNum(n, b.Value, &x); err != nil {
return nil, err
}
case token.TRUE, token.FALSE, token.NULL:
n.Value = b.Value
case token.STRING:
str, err := literal.Unquote(b.Value)
if err != nil {
return nil, err
}
n.SetString(str)
default:
return nil, errors.Newf(b.Pos(), "unknown literal type %v", b.Kind)
}
return n, nil
}
func setNum(n *yaml.Node, s string, x interface{}) error {
if yaml.Unmarshal([]byte(s), x) == nil {
n.Value = s
return nil
}
var ni literal.NumInfo
if err := literal.ParseNum(s, &ni); err != nil {
return err
}
n.Value = ni.String()
return nil
}
func encodeExprs(exprs []ast.Expr) (n *yaml.Node, err error) {
n = &yaml.Node{Kind: yaml.SequenceNode}
for _, elem := range exprs {
e, err := encode(elem)
if err != nil {
return nil, err
}
n.Content = append(n.Content, e)
}
return n, nil
}
// encodeDecls converts a sequence of declarations to a value. If it encounters
// an embedded value, it will return this expression. This is more relaxed for
// structs than is currently allowed for CUE, but the expectation is that this
// will be allowed at some point. The input would still be illegal CUE.
func encodeDecls(decls []ast.Decl) (n *yaml.Node, err error) {
n = &yaml.Node{Kind: yaml.MappingNode}
docForNext := strings.Builder{}
var lastHead, lastFoot *yaml.Node
hasEmbed := false
for _, d := range decls {
switch x := d.(type) {
default:
return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", astinternal.DebugStr(x), x)
case *ast.Package:
if len(n.Content) > 0 {
return nil, errors.Newf(x.Pos(), "invalid package clause")
}
continue
case *ast.CommentGroup:
docForNext.WriteString(docToYAML(x))
docForNext.WriteString("\n\n")
continue
case *ast.Attribute:
continue
case *ast.Field:
if x.Token == token.ISA {
return nil, errors.Newf(x.TokenPos, "yaml: definition not allowed")
}
if x.Optional != token.NoPos {
return nil, errors.Newf(x.Optional, "yaml: optional fields not allowed")
}
if hasEmbed {
return nil, errors.Newf(x.TokenPos, "yaml: embedding mixed with fields")
}
name, _, err := ast.LabelName(x.Label)
if err != nil {
return nil, errors.Newf(x.Label.Pos(), "yaml: only literal labels allowed")
}
label := &yaml.Node{}
addDocs(x.Label, label, label)
label.SetString(name)
value, err := encode(x.Value)
if err != nil {
return nil, err
}
lastHead = label
lastFoot = value
addDocs(x, label, value)
n.Content = append(n.Content, label)
n.Content = append(n.Content, value)
case *ast.EmbedDecl:
if hasEmbed {
return nil, errors.Newf(x.Pos(), "yaml: multiple embedded values")
}
hasEmbed = true
e, err := encode(x.Expr)
if err != nil {
return nil, err
}
addDocs(x, e, e)
lastHead = e
lastFoot = e
n.Content = append(n.Content, e)
}
if docForNext.Len() > 0 {
docForNext.WriteString(lastHead.HeadComment)
lastHead.HeadComment = docForNext.String()
docForNext.Reset()
}
}
if docForNext.Len() > 0 && lastFoot != nil {
if !strings.HasSuffix(lastFoot.FootComment, "\n") {
lastFoot.FootComment += "\n"
}
n := docForNext.Len()
lastFoot.FootComment += docForNext.String()[:n-1]
}
if hasEmbed {
return n.Content[0], nil
}
return n, nil
}
// addDocs prefixes head, replaces line and appends foot comments.
func addDocs(n ast.Node, h, f *yaml.Node) {
head := ""
isDoc := false
for _, c := range ast.Comments(n) {
switch {
case c.Line:
f.LineComment = docToYAML(c)
case c.Position > 0:
if f.FootComment != "" {
f.FootComment += "\n\n"
} else if relPos := c.Pos().RelPos(); relPos == token.NewSection {
f.FootComment += "\n"
}
f.FootComment += docToYAML(c)
default:
if head != "" {
head += "\n\n"
}
head += docToYAML(c)
isDoc = isDoc || c.Doc
}
}
if head != "" {
if h.HeadComment != "" || !isDoc {
head += "\n\n"
}
h.HeadComment = head + h.HeadComment
}
}
// docToYAML converts a CUE CommentGroup to a YAML comment string. This ensures
// that comments with empty lines get properly converted.
func docToYAML(c *ast.CommentGroup) string {
s := c.Text()
if strings.HasSuffix(s, "\n") { // always true
s = s[:len(s)-1]
}
lines := strings.Split(s, "\n")
for i, l := range lines {
if l == "" {
lines[i] = "#"
} else {
lines[i] = "# " + l
}
}
return strings.Join(lines, "\n")
}