github.com/solo-io/cue@v0.4.7/internal/encoding/yaml/encode.go (about)

     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  
    15  package yaml
    16  
    17  import (
    18  	"bytes"
    19  	"math/big"
    20  	"strings"
    21  
    22  	"gopkg.in/yaml.v3"
    23  
    24  	"github.com/solo-io/cue/cue/ast"
    25  	"github.com/solo-io/cue/cue/errors"
    26  	"github.com/solo-io/cue/cue/literal"
    27  	"github.com/solo-io/cue/cue/token"
    28  	"github.com/solo-io/cue/internal/astinternal"
    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.
    44  func Encode(n ast.Node) (b []byte, err error) {
    45  	y, err := encode(n)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  	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
    57  }
    58  
    59  func 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)", astinternal.DebugStr(x), x)
    91  	default:
    92  		return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", astinternal.DebugStr(x), x)
    93  	}
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	addDocs(n, y, y)
    98  	return y, nil
    99  }
   100  
   101  func 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  
   133  func 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  
   147  func 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.
   164  func 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)", astinternal.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.Attribute:
   187  			continue
   188  
   189  		case *ast.Field:
   190  			if x.Token == token.ISA {
   191  				return nil, errors.Newf(x.TokenPos, "yaml: definition not allowed")
   192  			}
   193  			if x.Optional != token.NoPos {
   194  				return nil, errors.Newf(x.Optional, "yaml: optional fields not allowed")
   195  			}
   196  			if hasEmbed {
   197  				return nil, errors.Newf(x.TokenPos, "yaml: embedding mixed with fields")
   198  			}
   199  			name, _, err := ast.LabelName(x.Label)
   200  			if err != nil {
   201  				return nil, errors.Newf(x.Label.Pos(), "yaml: only literal labels allowed")
   202  			}
   203  
   204  			label := &yaml.Node{}
   205  			addDocs(x.Label, label, label)
   206  			label.SetString(name)
   207  
   208  			value, err := encode(x.Value)
   209  			if err != nil {
   210  				return nil, err
   211  			}
   212  			lastHead = label
   213  			lastFoot = value
   214  			addDocs(x, label, value)
   215  			n.Content = append(n.Content, label)
   216  			n.Content = append(n.Content, value)
   217  
   218  		case *ast.EmbedDecl:
   219  			if hasEmbed {
   220  				return nil, errors.Newf(x.Pos(), "yaml: multiple embedded values")
   221  			}
   222  			hasEmbed = true
   223  			e, err := encode(x.Expr)
   224  			if err != nil {
   225  				return nil, err
   226  			}
   227  			addDocs(x, e, e)
   228  			lastHead = e
   229  			lastFoot = e
   230  			n.Content = append(n.Content, e)
   231  		}
   232  		if docForNext.Len() > 0 {
   233  			docForNext.WriteString(lastHead.HeadComment)
   234  			lastHead.HeadComment = docForNext.String()
   235  			docForNext.Reset()
   236  		}
   237  	}
   238  
   239  	if docForNext.Len() > 0 && lastFoot != nil {
   240  		if !strings.HasSuffix(lastFoot.FootComment, "\n") {
   241  			lastFoot.FootComment += "\n"
   242  		}
   243  		n := docForNext.Len()
   244  		lastFoot.FootComment += docForNext.String()[:n-1]
   245  	}
   246  
   247  	if hasEmbed {
   248  		return n.Content[0], nil
   249  	}
   250  
   251  	return n, nil
   252  }
   253  
   254  // addDocs prefixes head, replaces line and appends foot comments.
   255  func addDocs(n ast.Node, h, f *yaml.Node) {
   256  	head := ""
   257  	isDoc := false
   258  	for _, c := range ast.Comments(n) {
   259  		switch {
   260  		case c.Line:
   261  			f.LineComment = docToYAML(c)
   262  
   263  		case c.Position > 0:
   264  			if f.FootComment != "" {
   265  				f.FootComment += "\n\n"
   266  			} else if relPos := c.Pos().RelPos(); relPos == token.NewSection {
   267  				f.FootComment += "\n"
   268  			}
   269  			f.FootComment += docToYAML(c)
   270  
   271  		default:
   272  			if head != "" {
   273  				head += "\n\n"
   274  			}
   275  			head += docToYAML(c)
   276  			isDoc = isDoc || c.Doc
   277  		}
   278  	}
   279  
   280  	if head != "" {
   281  		if h.HeadComment != "" || !isDoc {
   282  			head += "\n\n"
   283  		}
   284  		h.HeadComment = head + h.HeadComment
   285  	}
   286  }
   287  
   288  // docToYAML converts a CUE CommentGroup to a YAML comment string. This ensures
   289  // that comments with empty lines get properly converted.
   290  func docToYAML(c *ast.CommentGroup) string {
   291  	s := c.Text()
   292  	if strings.HasSuffix(s, "\n") { // always true
   293  		s = s[:len(s)-1]
   294  	}
   295  	lines := strings.Split(s, "\n")
   296  	for i, l := range lines {
   297  		if l == "" {
   298  			lines[i] = "#"
   299  		} else {
   300  			lines[i] = "# " + l
   301  		}
   302  	}
   303  	return strings.Join(lines, "\n")
   304  }