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 }