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