cuelang.org/go@v0.10.1/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  	"encoding/base64"
    20  	"fmt"
    21  	"math/big"
    22  	"regexp"
    23  	"strings"
    24  	"sync"
    25  
    26  	"gopkg.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.
    50  func 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  
    65  func 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  
   107  func 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.
   162  func 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.
   168  var 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.
   178  var 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  
   210  func 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  
   224  func 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.
   241  func 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.Optional != token.NoPos {
   271  				return nil, errors.Newf(x.Optional, "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.
   335  func 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.
   370  func 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  }