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 15package cmd 16 17import ( 18 "fmt" 19 "path/filepath" 20 "strconv" 21 22 "cuelang.org/go/cue" 23 "cuelang.org/go/cue/ast" 24 "cuelang.org/go/cue/ast/astutil" 25 "cuelang.org/go/cue/build" 26 "cuelang.org/go/cue/errors" 27 "cuelang.org/go/cue/parser" 28 "cuelang.org/go/cue/token" 29 "cuelang.org/go/encoding/protobuf/jsonpb" 30 "cuelang.org/go/internal" 31 "cuelang.org/go/internal/astinternal" 32 "cuelang.org/go/internal/encoding" 33) 34 35// This file contains logic for placing orphan files within a CUE namespace. 36 37func (b *buildPlan) usePlacement() bool { 38 return b.perFile || b.useList || len(b.path) > 0 39} 40 41func (b *buildPlan) parsePlacementFlags() error { 42 cmd := b.cmd 43 b.perFile = flagFiles.Bool(cmd) 44 b.useList = flagList.Bool(cmd) 45 b.useContext = flagWithContext.Bool(cmd) 46 47 for _, str := range flagPath.StringArray(cmd) { 48 l, err := parser.ParseExpr("--path", str) 49 if err != nil { 50 labels, err := parseFullPath(str) 51 if err != nil { 52 return fmt.Errorf( 53 `labels must be expressions (-l foo -l 'strings.ToLower(bar)') or full paths (-l '"foo": "\(strings.ToLower(bar))":) : %v`, err) 54 } 55 b.path = append(b.path, labels...) 56 continue 57 } 58 59 b.path = append(b.path, &ast.ParenExpr{X: l}) 60 } 61 62 if !b.importing && !b.perFile && !b.useList && len(b.path) == 0 { 63 if b.useContext { 64 return fmt.Errorf( 65 "flag %q must be used with at least one of flag %q, %q, or %q", 66 flagWithContext, flagPath, flagList, flagFiles, 67 ) 68 } 69 } else if b.schema != nil { 70 return fmt.Errorf( 71 "cannot combine --%s flag with flag %q, %q, or %q", 72 flagSchema, flagPath, flagList, flagFiles, 73 ) 74 } 75 return nil 76} 77 78func (b *buildPlan) placeOrphans(i *build.Instance, a []*decoderInfo) error { 79 pkg := b.encConfig.PkgName 80 if pkg == "" { 81 pkg = i.PkgName 82 } else if pkg != "" && i.PkgName != "" && i.PkgName != pkg && !flagForce.Bool(b.cmd) { 83 return fmt.Errorf( 84 "%q flag clashes with existing package name (%s vs %s)", 85 flagPackage, pkg, i.PkgName, 86 ) 87 } 88 89 var files []*ast.File 90 91 for _, di := range a { 92 if !i.User && !b.matchFile(filepath.Base(di.file.Filename)) { 93 continue 94 } 95 96 d := di.dec(b) 97 98 var objs []*ast.File 99 100 // Filter only need to filter files that can stream: 101 for ; !d.Done(); d.Next() { 102 if f := d.File(); f != nil { 103 f.Filename = newName(d.Filename(), 0) 104 objs = append(objs, f) 105 } 106 } 107 if err := d.Err(); err != nil { 108 return err 109 } 110 111 if b.perFile { 112 for i, obj := range objs { 113 f, err := placeOrphans(b, d, pkg, obj) 114 if err != nil { 115 return err 116 } 117 f.Filename = newName(d.Filename(), i) 118 files = append(files, f) 119 } 120 continue 121 } 122 // TODO: consider getting rid of this requirement. It is important that 123 // import will catch conflicts ahead of time then, though, and report 124 // this messages as a possible solution if there are conflicts. 125 if b.importing && len(objs) > 1 && len(b.path) == 0 && !b.useList { 126 return fmt.Errorf( 127 "%s, %s, or %s flag needed to handle multiple objects in file %s", 128 flagPath, flagList, flagFiles, shortFile(i.Root, di.file)) 129 } 130 131 if !b.useList && len(b.path) == 0 && !b.useContext { 132 for _, f := range objs { 133 if pkg := b.encConfig.PkgName; pkg != "" { 134 internal.SetPackage(f, pkg, false) 135 } 136 files = append(files, f) 137 } 138 } else { 139 // TODO: handle imports correctly, i.e. for proto. 140 f, err := placeOrphans(b, d, pkg, objs...) 141 if err != nil { 142 return err 143 } 144 f.Filename = newName(d.Filename(), 0) 145 files = append(files, f) 146 } 147 } 148 149 b.imported = append(b.imported, files...) 150 for _, f := range files { 151 if err := i.AddSyntax(f); err != nil { 152 return err 153 } 154 } 155 return nil 156} 157 158func placeOrphans(b *buildPlan, d *encoding.Decoder, pkg string, objs ...*ast.File) (*ast.File, error) { 159 f := &ast.File{} 160 filename := d.Filename() 161 162 index := newIndex() 163 for i, file := range objs { 164 if i == 0 { 165 astutil.CopyMeta(f, file) 166 } 167 expr := internal.ToExpr(file) 168 p, _, _ := internal.PackageInfo(file) 169 170 var path cue.Path 171 var labels []ast.Label 172 173 switch { 174 case len(b.path) > 0: 175 expr := expr 176 if b.useContext { 177 expr = ast.NewStruct( 178 "data", expr, 179 "filename", ast.NewString(filename), 180 "index", ast.NewLit(token.INT, strconv.Itoa(i)), 181 "recordCount", ast.NewLit(token.INT, strconv.Itoa(len(objs))), 182 ) 183 } 184 var f *ast.File 185 if s, ok := expr.(*ast.StructLit); ok { 186 f = &ast.File{Decls: s.Elts} 187 } else { 188 f = &ast.File{Decls: []ast.Decl{&ast.EmbedDecl{Expr: expr}}} 189 } 190 err := astutil.Sanitize(f) 191 if err != nil { 192 return nil, errors.Wrapf(err, token.NoPos, 193 "invalid combination of input files") 194 } 195 inst, err := runtime.CompileFile(f) 196 if err != nil { 197 return nil, err 198 } 199 200 var a []cue.Selector 201 202 for _, label := range b.path { 203 switch x := label.(type) { 204 case *ast.Ident, *ast.BasicLit: 205 case ast.Expr: 206 if p, ok := x.(*ast.ParenExpr); ok { 207 x = p.X // unwrap for better error messages 208 } 209 switch l := inst.Eval(x); l.Kind() { 210 case cue.StringKind, cue.IntKind: 211 label = l.Syntax().(ast.Label) 212 213 default: 214 var arg interface{} = l 215 if err := l.Err(); err != nil { 216 arg = err 217 } 218 return nil, fmt.Errorf( 219 `error evaluating label %v: %v`, 220 astinternal.DebugStr(x), arg) 221 } 222 } 223 a = append(a, cue.Label(label)) 224 labels = append(labels, label) 225 } 226 227 path = cue.MakePath(a...) 228 } 229 230 switch d.Interpretation() { 231 case build.ProtobufJSON: 232 v := b.instance.Value().LookupPath(path) 233 if b.useList { 234 v, _ = v.Elem() 235 } 236 if !v.Exists() { 237 break 238 } 239 if err := jsonpb.NewDecoder(v).RewriteFile(file); err != nil { 240 return nil, err 241 } 242 } 243 244 if b.useList { 245 idx := index 246 for _, e := range labels { 247 idx = idx.label(e) 248 } 249 if idx.field.Value == nil { 250 idx.field.Value = &ast.ListLit{ 251 Lbrack: token.NoSpace.Pos(), 252 Rbrack: token.NoSpace.Pos(), 253 } 254 } 255 list := idx.field.Value.(*ast.ListLit) 256 list.Elts = append(list.Elts, expr) 257 } else if len(labels) == 0 { 258 obj, ok := expr.(*ast.StructLit) 259 if !ok { 260 if _, ok := expr.(*ast.ListLit); ok { 261 return nil, fmt.Errorf("expected struct as object root, did you mean to use the --list flag?") 262 } 263 return nil, fmt.Errorf("cannot map non-struct to object root") 264 } 265 f.Decls = append(f.Decls, obj.Elts...) 266 } else { 267 field := &ast.Field{Label: labels[0]} 268 f.Decls = append(f.Decls, field) 269 if p != nil { 270 astutil.CopyComments(field, p) 271 } 272 for _, e := range labels[1:] { 273 newField := &ast.Field{Label: e} 274 newVal := ast.NewStruct(newField) 275 field.Value = newVal 276 field = newField 277 } 278 field.Value = expr 279 } 280 } 281 282 if pkg != "" { 283 internal.SetPackage(f, pkg, false) 284 } 285 286 if b.useList { 287 switch x := index.field.Value.(type) { 288 case *ast.StructLit: 289 f.Decls = append(f.Decls, x.Elts...) 290 case *ast.ListLit: 291 f.Decls = append(f.Decls, &ast.EmbedDecl{Expr: x}) 292 default: 293 panic("unreachable") 294 } 295 } 296 297 return f, astutil.Sanitize(f) 298} 299 300func parseFullPath(exprs string) (p []ast.Label, err error) { 301 f, err := parser.ParseFile("--path", exprs+"_") 302 if err != nil { 303 return p, fmt.Errorf("parser error in path %q: %v", exprs, err) 304 } 305 306 if len(f.Decls) != 1 { 307 return p, errors.New("path flag must be a space-separated sequence of labels") 308 } 309 310 for d := f.Decls[0]; ; { 311 field, ok := d.(*ast.Field) 312 if !ok { 313 // This should never happen 314 return p, errors.New("%q not a sequence of labels") 315 } 316 317 switch x := field.Label.(type) { 318 case *ast.Ident, *ast.BasicLit: 319 p = append(p, x) 320 321 case ast.Expr: 322 p = append(p, &ast.ParenExpr{X: x}) 323 324 default: 325 return p, fmt.Errorf("unsupported label type %T", x) 326 } 327 328 v, ok := field.Value.(*ast.StructLit) 329 if !ok { 330 break 331 } 332 333 if len(v.Elts) != 1 { 334 return p, errors.New("path value may not contain a struct") 335 } 336 337 d = v.Elts[0] 338 } 339 return p, nil 340} 341 342type listIndex struct { 343 index map[string]*listIndex 344 field *ast.Field 345} 346 347func newIndex() *listIndex { 348 return &listIndex{ 349 index: map[string]*listIndex{}, 350 field: &ast.Field{}, 351 } 352} 353 354func (x *listIndex) label(label ast.Label) *listIndex { 355 key := astinternal.DebugStr(label) 356 idx := x.index[key] 357 if idx == nil { 358 if x.field.Value == nil { 359 x.field.Value = &ast.StructLit{} 360 } 361 obj := x.field.Value.(*ast.StructLit) 362 newField := &ast.Field{Label: label} 363 obj.Elts = append(obj.Elts, newField) 364 idx = &listIndex{ 365 index: map[string]*listIndex{}, 366 field: newField, 367 } 368 x.index[key] = idx 369 } 370 return idx 371} 372 373func newName(filename string, i int) string { 374 if filename == "-" { 375 return filename 376 } 377 ext := filepath.Ext(filename) 378 filename = filename[:len(filename)-len(ext)] 379 if i > 0 { 380 filename += fmt.Sprintf("-%d", i) 381 } 382 filename += ".cue" 383 return filename 384} 385