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 }