| // Copyright 2020 CUE Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package cmd |
| |
| import ( |
| "fmt" |
| "path/filepath" |
| "strconv" |
| |
| "cuelang.org/go/cue" |
| "cuelang.org/go/cue/ast" |
| "cuelang.org/go/cue/ast/astutil" |
| "cuelang.org/go/cue/build" |
| "cuelang.org/go/cue/errors" |
| "cuelang.org/go/cue/parser" |
| "cuelang.org/go/cue/token" |
| "cuelang.org/go/encoding/protobuf/jsonpb" |
| "cuelang.org/go/internal" |
| "cuelang.org/go/internal/astinternal" |
| "cuelang.org/go/internal/encoding" |
| ) |
| |
| // This file contains logic for placing orphan files within a CUE namespace. |
| |
| func (b *buildPlan) usePlacement() bool { |
| return b.perFile || b.useList || len(b.path) > 0 |
| } |
| |
| func (b *buildPlan) parsePlacementFlags() error { |
| cmd := b.cmd |
| b.perFile = flagFiles.Bool(cmd) |
| b.useList = flagList.Bool(cmd) |
| b.useContext = flagWithContext.Bool(cmd) |
| |
| for _, str := range flagPath.StringArray(cmd) { |
| l, err := parser.ParseExpr("--path", str) |
| if err != nil { |
| labels, err := parseFullPath(str) |
| if err != nil { |
| return fmt.Errorf( |
| `labels must be expressions (-l foo -l 'strings.ToLower(bar)') or full paths (-l '"foo": "\(strings.ToLower(bar))":) : %v`, err) |
| } |
| b.path = append(b.path, labels...) |
| continue |
| } |
| |
| b.path = append(b.path, &ast.ParenExpr{X: l}) |
| } |
| |
| if !b.importing && !b.perFile && !b.useList && len(b.path) == 0 { |
| if b.useContext { |
| return fmt.Errorf( |
| "flag %q must be used with at least one of flag %q, %q, or %q", |
| flagWithContext, flagPath, flagList, flagFiles, |
| ) |
| } |
| } else if b.schema != nil { |
| return fmt.Errorf( |
| "cannot combine --%s flag with flag %q, %q, or %q", |
| flagSchema, flagPath, flagList, flagFiles, |
| ) |
| } |
| return nil |
| } |
| |
| func (b *buildPlan) placeOrphans(i *build.Instance, a []*decoderInfo) error { |
| pkg := b.encConfig.PkgName |
| if pkg == "" { |
| pkg = i.PkgName |
| } else if pkg != "" && i.PkgName != "" && i.PkgName != pkg && !flagForce.Bool(b.cmd) { |
| return fmt.Errorf( |
| "%q flag clashes with existing package name (%s vs %s)", |
| flagPackage, pkg, i.PkgName, |
| ) |
| } |
| |
| var files []*ast.File |
| |
| for _, di := range a { |
| if !i.User && !b.matchFile(filepath.Base(di.file.Filename)) { |
| continue |
| } |
| |
| d := di.dec(b) |
| |
| var objs []*ast.File |
| |
| // Filter only need to filter files that can stream: |
| for ; !d.Done(); d.Next() { |
| if f := d.File(); f != nil { |
| f.Filename = newName(d.Filename(), 0) |
| objs = append(objs, f) |
| } |
| } |
| if err := d.Err(); err != nil { |
| return err |
| } |
| |
| if b.perFile { |
| for i, obj := range objs { |
| f, err := placeOrphans(b, d, pkg, obj) |
| if err != nil { |
| return err |
| } |
| f.Filename = newName(d.Filename(), i) |
| files = append(files, f) |
| } |
| continue |
| } |
| // TODO: consider getting rid of this requirement. It is important that |
| // import will catch conflicts ahead of time then, though, and report |
| // this messages as a possible solution if there are conflicts. |
| if b.importing && len(objs) > 1 && len(b.path) == 0 && !b.useList { |
| return fmt.Errorf( |
| "%s, %s, or %s flag needed to handle multiple objects in file %s", |
| flagPath, flagList, flagFiles, shortFile(i.Root, di.file)) |
| } |
| |
| if !b.useList && len(b.path) == 0 && !b.useContext { |
| for _, f := range objs { |
| if pkg := b.encConfig.PkgName; pkg != "" { |
| internal.SetPackage(f, pkg, false) |
| } |
| files = append(files, f) |
| } |
| } else { |
| // TODO: handle imports correctly, i.e. for proto. |
| f, err := placeOrphans(b, d, pkg, objs...) |
| if err != nil { |
| return err |
| } |
| f.Filename = newName(d.Filename(), 0) |
| files = append(files, f) |
| } |
| } |
| |
| b.imported = append(b.imported, files...) |
| for _, f := range files { |
| if err := i.AddSyntax(f); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func placeOrphans(b *buildPlan, d *encoding.Decoder, pkg string, objs ...*ast.File) (*ast.File, error) { |
| f := &ast.File{} |
| filename := d.Filename() |
| |
| index := newIndex() |
| for i, file := range objs { |
| if i == 0 { |
| astutil.CopyMeta(f, file) |
| } |
| expr := internal.ToExpr(file) |
| p, _, _ := internal.PackageInfo(file) |
| |
| var path cue.Path |
| var labels []ast.Label |
| |
| switch { |
| case len(b.path) > 0: |
| expr := expr |
| if b.useContext { |
| expr = ast.NewStruct( |
| "data", expr, |
| "filename", ast.NewString(filename), |
| "index", ast.NewLit(token.INT, strconv.Itoa(i)), |
| "recordCount", ast.NewLit(token.INT, strconv.Itoa(len(objs))), |
| ) |
| } |
| var f *ast.File |
| if s, ok := expr.(*ast.StructLit); ok { |
| f = &ast.File{Decls: s.Elts} |
| } else { |
| f = &ast.File{Decls: []ast.Decl{&ast.EmbedDecl{Expr: expr}}} |
| } |
| err := astutil.Sanitize(f) |
| if err != nil { |
| return nil, errors.Wrapf(err, token.NoPos, |
| "invalid combination of input files") |
| } |
| inst, err := runtime.CompileFile(f) |
| if err != nil { |
| return nil, err |
| } |
| |
| var a []cue.Selector |
| |
| for _, label := range b.path { |
| switch x := label.(type) { |
| case *ast.Ident, *ast.BasicLit: |
| case ast.Expr: |
| if p, ok := x.(*ast.ParenExpr); ok { |
| x = p.X // unwrap for better error messages |
| } |
| switch l := inst.Eval(x); l.Kind() { |
| case cue.StringKind, cue.IntKind: |
| label = l.Syntax().(ast.Label) |
| |
| default: |
| var arg interface{} = l |
| if err := l.Err(); err != nil { |
| arg = err |
| } |
| return nil, fmt.Errorf( |
| `error evaluating label %v: %v`, |
| astinternal.DebugStr(x), arg) |
| } |
| } |
| a = append(a, cue.Label(label)) |
| labels = append(labels, label) |
| } |
| |
| path = cue.MakePath(a...) |
| } |
| |
| switch d.Interpretation() { |
| case build.ProtobufJSON: |
| v := b.instance.Value().LookupPath(path) |
| if b.useList { |
| v, _ = v.Elem() |
| } |
| if !v.Exists() { |
| break |
| } |
| if err := jsonpb.NewDecoder(v).RewriteFile(file); err != nil { |
| return nil, err |
| } |
| } |
| |
| if b.useList { |
| idx := index |
| for _, e := range labels { |
| idx = idx.label(e) |
| } |
| if idx.field.Value == nil { |
| idx.field.Value = &ast.ListLit{ |
| Lbrack: token.NoSpace.Pos(), |
| Rbrack: token.NoSpace.Pos(), |
| } |
| } |
| list := idx.field.Value.(*ast.ListLit) |
| list.Elts = append(list.Elts, expr) |
| } else if len(labels) == 0 { |
| obj, ok := expr.(*ast.StructLit) |
| if !ok { |
| if _, ok := expr.(*ast.ListLit); ok { |
| return nil, fmt.Errorf("expected struct as object root, did you mean to use the --list flag?") |
| } |
| return nil, fmt.Errorf("cannot map non-struct to object root") |
| } |
| f.Decls = append(f.Decls, obj.Elts...) |
| } else { |
| field := &ast.Field{Label: labels[0]} |
| f.Decls = append(f.Decls, field) |
| if p != nil { |
| astutil.CopyComments(field, p) |
| } |
| for _, e := range labels[1:] { |
| newField := &ast.Field{Label: e} |
| newVal := ast.NewStruct(newField) |
| field.Value = newVal |
| field = newField |
| } |
| field.Value = expr |
| } |
| } |
| |
| if pkg != "" { |
| internal.SetPackage(f, pkg, false) |
| } |
| |
| if b.useList { |
| switch x := index.field.Value.(type) { |
| case *ast.StructLit: |
| f.Decls = append(f.Decls, x.Elts...) |
| case *ast.ListLit: |
| f.Decls = append(f.Decls, &ast.EmbedDecl{Expr: x}) |
| default: |
| panic("unreachable") |
| } |
| } |
| |
| return f, astutil.Sanitize(f) |
| } |
| |
| func parseFullPath(exprs string) (p []ast.Label, err error) { |
| f, err := parser.ParseFile("--path", exprs+"_") |
| if err != nil { |
| return p, fmt.Errorf("parser error in path %q: %v", exprs, err) |
| } |
| |
| if len(f.Decls) != 1 { |
| return p, errors.New("path flag must be a space-separated sequence of labels") |
| } |
| |
| for d := f.Decls[0]; ; { |
| field, ok := d.(*ast.Field) |
| if !ok { |
| // This should never happen |
| return p, errors.New("%q not a sequence of labels") |
| } |
| |
| switch x := field.Label.(type) { |
| case *ast.Ident, *ast.BasicLit: |
| p = append(p, x) |
| |
| case ast.Expr: |
| p = append(p, &ast.ParenExpr{X: x}) |
| |
| default: |
| return p, fmt.Errorf("unsupported label type %T", x) |
| } |
| |
| v, ok := field.Value.(*ast.StructLit) |
| if !ok { |
| break |
| } |
| |
| if len(v.Elts) != 1 { |
| return p, errors.New("path value may not contain a struct") |
| } |
| |
| d = v.Elts[0] |
| } |
| return p, nil |
| } |
| |
| type listIndex struct { |
| index map[string]*listIndex |
| field *ast.Field |
| } |
| |
| func newIndex() *listIndex { |
| return &listIndex{ |
| index: map[string]*listIndex{}, |
| field: &ast.Field{}, |
| } |
| } |
| |
| func (x *listIndex) label(label ast.Label) *listIndex { |
| key := astinternal.DebugStr(label) |
| idx := x.index[key] |
| if idx == nil { |
| if x.field.Value == nil { |
| x.field.Value = &ast.StructLit{} |
| } |
| obj := x.field.Value.(*ast.StructLit) |
| newField := &ast.Field{Label: label} |
| obj.Elts = append(obj.Elts, newField) |
| idx = &listIndex{ |
| index: map[string]*listIndex{}, |
| field: newField, |
| } |
| x.index[key] = idx |
| } |
| return idx |
| } |
| |
| func newName(filename string, i int) string { |
| if filename == "-" { |
| return filename |
| } |
| ext := filepath.Ext(filename) |
| filename = filename[:len(filename)-len(ext)] |
| if i > 0 { |
| filename += fmt.Sprintf("-%d", i) |
| } |
| filename += ".cue" |
| return filename |
| } |