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