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 }