blob: 946f34f18aaf89ad85f4a9844b6bf41df6ead1bb [file] [log] [blame]
// Copyright 2018 The 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 (
"bytes"
"fmt"
"io/ioutil"
"path/filepath"
"github.com/spf13/cobra"
"golang.org/x/text/message"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/encoding"
"cuelang.org/go/cue/parser"
"cuelang.org/go/internal"
)
const vetDoc = `vet validates CUE and other data files
By default it will only validate if there are no errors.
The -c validates that all regular fields are concrete.
Checking non-CUE files
Vet can also check non-CUE files. The following file formats are
currently supported:
Format Extensions
JSON .json .jsonl .ndjson
YAML .yaml .yml
To activate this mode, the non-cue files must be explicitly mentioned on the
command line. There must also be at least one CUE file to hold the constraints.
In this mode, each file will be verified against a CUE constraint. If the files
contain multiple objects (such as using --- in YAML), they will all be verified
individually.
By default, each file is checked against the root of the loaded CUE files.
The -e can be used to only verify files against the result of an expression
evaluated within the CUE files. This can be useful if the CUE files contain
a set of definitions to pick from.
Examples:
# Check files against a CUE file:
cue vet foo.yaml foo.cue
# Check files against a particular expression
cue vet translations/*.yaml foo.cue -e Translation
If more than one expression is given, all must match all values.
`
func newVetCmd(c *Command) *cobra.Command {
cmd := &cobra.Command{
Use: "vet",
Short: "validate data",
Long: vetDoc,
RunE: mkRunE(c, doVet),
}
cmd.Flags().BoolP(string(flagConcrete), "c", false,
"require the evaluation to be concrete")
cmd.Flags().StringArrayP(string(flagExpression), "e", nil,
"use this expression to validate non-CUE files")
return cmd
}
func doVet(cmd *Command, args []string) error {
builds := loadFromArgs(cmd, args, defaultConfig)
if builds == nil {
return nil
}
instances := buildInstances(cmd, builds)
// Go into a special vet mode if the user explicitly specified non-cue
// files on the command line.
for _, a := range args {
enc := encoding.MapExtension(filepath.Ext(a))
if enc != nil && enc.Name() != "cue" {
vetFiles(cmd, instances[0], builds[0].DataFiles)
return nil
}
}
shown := false
for _, inst := range instances {
// TODO: use ImportPath or some other sanitized path.
concrete := true
hasFlag := false
if flag := cmd.Flag(string(flagConcrete)); flag != nil {
hasFlag = flag.Changed
if hasFlag {
concrete = flagConcrete.Bool(cmd)
}
}
opt := []cue.Option{
cue.Attributes(true),
cue.Definitions(true),
cue.Hidden(true),
}
w := cmd.Stderr()
err := inst.Value().Validate(append(opt, cue.Concrete(concrete))...)
if err != nil && !hasFlag {
err = inst.Value().Validate(append(opt, cue.Concrete(false))...)
if !shown && err == nil {
shown = true
p := message.NewPrinter(getLang())
_, _ = p.Fprintln(w,
"some instances are incomplete; use the -c flag to show errors or suppress this message")
}
}
exitIfErr(cmd, inst, err, false)
}
return nil
}
func vetFiles(cmd *Command, inst *cue.Instance, files []string) {
expressions := flagExpression.StringArray(cmd)
var check cue.Value
if len(expressions) == 0 {
check = inst.Value()
}
for _, e := range expressions {
expr, err := parser.ParseExpr("<expression flag>", e)
exitIfErr(cmd, inst, err, true)
v := inst.Eval(expr)
exitIfErr(cmd, inst, v.Err(), true)
check = check.Unify(v)
}
for _, f := range files {
b, err := ioutil.ReadFile(f)
exitIfErr(cmd, inst, err, true)
ext := filepath.Ext(filepath.Ext(f))
enc := encoding.MapExtension(ext)
if enc == nil {
exitIfErr(cmd, inst, fmt.Errorf("unrecognized extension %q", ext), true)
}
var exprs []ast.Expr
switch enc.Name() {
case "json":
exprs, err = handleJSON(f, bytes.NewReader(b))
case "yaml":
exprs, err = handleYAML(f, bytes.NewReader(b))
default:
exitIfErr(cmd, inst, fmt.Errorf("vet does not support %q", enc.Name()), true)
}
exitIfErr(cmd, inst, err, true)
r := internal.GetRuntime(inst).(*cue.Runtime)
for _, expr := range exprs {
body, err := r.CompileExpr(expr)
exitIfErr(cmd, inst, err, false)
v := body.Value().Unify(check)
if err := v.Err(); err != nil {
exitIfErr(cmd, inst, err, false)
} else {
// Always concrete when checking against concrete files.
err = v.Validate(cue.Concrete(true))
exitIfErr(cmd, inst, err, false)
}
}
}
}