1// Copyright 2018 The 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	"github.com/spf13/cobra"
19	"golang.org/x/text/message"
20
21	"cuelang.org/go/cue"
22	"cuelang.org/go/cue/errors"
23)
24
25const vetDoc = `vet validates CUE and other data files
26
27By default it will only validate if there are no errors.
28The -c validates that all regular fields are concrete.
29
30
31Checking non-CUE files
32
33Vet can also check non-CUE files. The following file formats are
34currently supported:
35
36  Format       Extensions
37	JSON       .json .jsonl .ndjson
38	YAML       .yaml .yml
39	TEXT       .txt  (validate a single string value)
40
41To activate this mode, the non-cue files must be explicitly mentioned on the
42command line. There must also be at least one CUE file to hold the constraints.
43
44In this mode, each file will be verified against a CUE constraint. If the files
45contain multiple objects (such as using --- in YAML), they will all be verified
46individually.
47
48By default, each file is checked against the root of the loaded CUE files.
49The -d can be used to only verify files against the result of an expression
50evaluated within the CUE files. This can be useful if the CUE files contain
51a set of definitions to pick from.
52
53Examples:
54
55  # Check files against a CUE file:
56  cue vet foo.yaml foo.cue
57
58  # Check files against a particular expression
59  cue vet translations/*.yaml foo.cue -d '#Translation'
60
61If more than one expression is given, all must match all values.
62`
63
64func newVetCmd(c *Command) *cobra.Command {
65	cmd := &cobra.Command{
66		Use:   "vet",
67		Short: "validate data",
68		Long:  vetDoc,
69		RunE:  mkRunE(c, doVet),
70	}
71
72	addOrphanFlags(cmd.Flags())
73	addInjectionFlags(cmd.Flags(), false)
74
75	cmd.Flags().BoolP(string(flagConcrete), "c", false,
76		"require the evaluation to be concrete")
77
78	return cmd
79}
80
81// doVet validates instances. There are two modes:
82// - Only packages: vet all these packages
83// - Data files: compare each data instance against a single package.
84//
85// It is invalid to have data files with other than exactly one package.
86//
87// TODO: allow unrooted schema, such as JSON schema to compare against
88// other values.
89func doVet(cmd *Command, args []string) error {
90	b, err := parseArgs(cmd, args, &config{
91		noMerge: true,
92	})
93	exitOnErr(cmd, err, true)
94
95	// Go into a special vet mode if the user explicitly specified non-cue
96	// files on the command line.
97	// TODO: unify these two modes.
98	if len(b.orphaned) > 0 {
99		vetFiles(cmd, b)
100		return nil
101	}
102
103	shown := false
104
105	iter := b.instances()
106	defer iter.close()
107	for iter.scan() {
108		v := iter.value()
109		// TODO: use ImportPath or some other sanitized path.
110
111		concrete := true
112		hasFlag := false
113		if flag := cmd.Flag(string(flagConcrete)); flag != nil {
114			hasFlag = flag.Changed
115			if hasFlag {
116				concrete = flagConcrete.Bool(cmd)
117			}
118		}
119		opt := []cue.Option{
120			cue.Attributes(true),
121			cue.Definitions(true),
122			cue.Hidden(true),
123		}
124		w := cmd.Stderr()
125		err := v.Validate(append(opt, cue.Concrete(concrete))...)
126		if err != nil && !hasFlag {
127			err = v.Validate(append(opt, cue.Concrete(false))...)
128			if !shown && err == nil {
129				shown = true
130				p := message.NewPrinter(getLang())
131				_, _ = p.Fprintln(w,
132					"some instances are incomplete; use the -c flag to show errors or suppress this message")
133			}
134		}
135		exitOnErr(cmd, err, false)
136	}
137	exitOnErr(cmd, iter.err(), true)
138	return nil
139}
140
141func vetFiles(cmd *Command, b *buildPlan) {
142	// Use -r type root, instead of -e
143
144	if !b.encConfig.Schema.Exists() {
145		exitOnErr(cmd, errors.New("data files specified without a schema"), true)
146	}
147
148	iter := b.instances()
149	defer iter.close()
150	for iter.scan() {
151		v := iter.value()
152
153		// Always concrete when checking against concrete files.
154		err := v.Validate(cue.Concrete(true))
155		exitOnErr(cmd, err, false)
156	}
157	exitOnErr(cmd, iter.err(), false)
158}
159