pkg/encoding/yaml: validate using instance check
Assume that the YAML value is final. This seems the
expected behavior in most cases.
ValidatePartial now has the original meaning.
Fixes #266
Change-Id: I7285bc5327ce661c720c9cd2254dc9fe8f9bcdc3
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4821
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/testdata/script/vet_yaml.txt b/cmd/cue/cmd/testdata/script/vet_yaml.txt
new file mode 100644
index 0000000..fe8c513
--- /dev/null
+++ b/cmd/cue/cmd/testdata/script/vet_yaml.txt
@@ -0,0 +1,43 @@
+! cue vet ./yaml.cue
+cmp stderr expect-stderr
+
+-- expect-stderr --
+phrases: error in call to encoding/yaml.Validate: missing field "text":
+ ./yaml.cue:19:10
+ ./yaml.cue:4:12
+ ./yaml.cue:11:17
+ yaml.Validate:4:6
+-- yaml.cue --
+import "encoding/yaml"
+
+// Phrases defines a schema for a valid phrase.
+Phrases :: {
+ phrases: {
+ [string]: Phrase
+ }
+
+ Phrase :: {
+ lang: LanguageTag
+ text: !=""
+ attribution?: !="" // must be non-empty when specified
+ }
+ LanguageTag :: =~"^[a-zA-Z0-9-_]{2,}$" | false
+}
+
+// phrases is a YAML string with a field phrases that is a map of Phrase
+// objects.
+phrases: yaml.Validate(Phrases)
+
+phrases: """
+ phrases:
+ # A quote from Mark Twain.
+ quote1:
+ lang: en
+ attribution: Mark Twain
+
+ # A Norwegian proverb.
+ proverb:
+ lang: no
+ text: Stemmen som sier at du ikke klarer det, lyver.
+ """
+
diff --git a/cue/builtin_test.go b/cue/builtin_test.go
index d1ebbd0..9e427e4 100644
--- a/cue/builtin_test.go
+++ b/cue/builtin_test.go
@@ -120,6 +120,18 @@
test("encoding/yaml", `yaml.Validate("a: 2\n---\na: 4", {a:<5})`),
`true`,
}, {
+ test("encoding/yaml", `yaml.Validate("a: 2\n", {a:<5, b:int})`),
+ `_|_(error in call to encoding/yaml.Validate: value not an instance)`,
+ }, {
+ test("encoding/yaml", `yaml.ValidatePartial("a: 2\n---\na: 4", {a:<3})`),
+ `_|_(error in call to encoding/yaml.ValidatePartial: invalid value 4 (out of bound <3))`,
+ }, {
+ test("encoding/yaml", `yaml.ValidatePartial("a: 2\n---\na: 4", {a:<5})`),
+ `true`,
+ }, {
+ test("encoding/yaml", `yaml.ValidatePartial("a: 2\n", {a:<5, b:int})`),
+ `true`,
+ }, {
test("strconv", `strconv.FormatUint(64, 16)`),
`"40"`,
}, {
diff --git a/cue/builtins.go b/cue/builtins.go
index e8f631b..d94d39f 100644
--- a/cue/builtins.go
+++ b/cue/builtins.go
@@ -30,14 +30,15 @@
"unicode"
"unicode/utf8"
+ "github.com/cockroachdb/apd/v2"
+ goyaml "github.com/ghodss/yaml"
+ "golang.org/x/net/idna"
+
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/literal"
"cuelang.org/go/cue/parser"
"cuelang.org/go/internal"
"cuelang.org/go/internal/third_party/yaml"
- "github.com/cockroachdb/apd/v2"
- goyaml "github.com/ghodss/yaml"
- "golang.org/x/net/idna"
)
func init() {
@@ -723,6 +724,40 @@
return false, err
}
+ if err := v.Subsume(inst.Value(), Final()); err != nil {
+ return false, err
+ }
+ }
+ }()
+ }
+ },
+ }, {
+ Name: "ValidatePartial",
+ Params: []kind{bytesKind | stringKind, topKind},
+ Result: boolKind,
+ Func: func(c *callCtxt) {
+ b, v := c.bytes(0), c.value(1)
+ if c.do() {
+ c.ret, c.err = func() (interface{}, error) {
+ d, err := yaml.NewDecoder("yaml.ValidatePartial", b)
+ if err != nil {
+ return false, err
+ }
+ r := internal.GetRuntime(v).(*Runtime)
+ for {
+ expr, err := d.Decode()
+ if err != nil {
+ if err == io.EOF {
+ return true, nil
+ }
+ return false, err
+ }
+
+ inst, err := r.CompileExpr(expr)
+ if err != nil {
+ return false, err
+ }
+
if x := v.Unify(inst.Value()); x.Err() != nil {
return false, x.Err()
}
@@ -3591,19 +3626,20 @@
native: []*builtin{{}},
cue: `{
Command: {
- tasks: {
- [name=string]: Task
- }
- $type: "tool.Command"
- $name: !=""
- $usage?: =~"^\($name) "
+ $usage?: string
$short?: string
$long?: string
+ Tasks
+ }
+ Tasks: Task | {
+ [name=string]: Tasks
}
Task: {
- $type: "tool.Task"
- $id: =~"\\."
+ $type: "tool.Task"
+ $id: =~"\\."
+ $after?: Task | [...Task]
}
+ Name :: =~"^\\PL([-](\\PL|\\PN))*$"
}`,
},
"tool/cli": &builtinPkg{
@@ -3619,11 +3655,10 @@
native: []*builtin{{}},
cue: `{
Run: {
- $id: *"tool/exec.Run" | "exec"
- cmd: string | [string, ...string]
- install?: string | [string, ...string]
+ $id: *"tool/exec.Run" | "exec"
+ cmd: string | [string, ...string]
env: {
- [string]: string
+ [string]: string | [...=~"="]
}
stdout: *null | string | bytes
stderr: *null | string | bytes
@@ -3704,8 +3739,8 @@
"tool/os": &builtinPkg{
native: []*builtin{{}},
cue: `{
- Value :: bool | number | *string | null
Name :: !="" & !~"^[$]"
+ Value :: bool | number | *string | null
Setenv: {
[Name]: Value
$id: "tool/os.Setenv"
diff --git a/cue/subsume.go b/cue/subsume.go
index b4bc2b7..cc6200e 100644
--- a/cue/subsume.go
+++ b/cue/subsume.go
@@ -30,17 +30,21 @@
s := subsumer{ctx: ctx, mode: mode}
if !s.subsumes(gt, lt) {
var b *bottom
- var ok bool
src := binSrc(token.NoPos, opUnify, gt, lt)
- src2 := src
if s.gt != nil && s.lt != nil {
src := binSrc(token.NoPos, opUnify, s.gt, s.lt)
- b, ok = binOp(ctx, src, opUnify, s.gt, s.lt).(*bottom)
+ var ok bool
+ if s.missing != 0 {
+ b = ctx.mkErr(src, "missing field %q", ctx.labelStr(s.missing))
+ } else if b, ok = binOp(ctx, src, opUnify, s.gt, s.lt).(*bottom); !ok {
+ b = ctx.mkErr(src, "value not an instance")
+ }
}
- if !ok {
+ if b == nil {
b = ctx.mkErr(src, "value not an instance")
+ } else {
+ b = ctx.mkErr(src, b, "%v", b)
}
- b = ctx.mkErr(src2, b, "%v", b)
return w.toErr(b)
}
return nil
@@ -51,7 +55,8 @@
mode subsumeMode
// recorded values where an error occurred.
- gt, lt evaluated
+ gt, lt evaluated
+ missing label
}
type subsumeMode int
@@ -196,7 +201,9 @@
if a.optional && isTop(a.v) {
continue
}
+ s.missing = a.feature
s.gt = a.val()
+ s.lt = o
return false
} else if a.definition != b.definition {
return false
diff --git a/pkg/encoding/yaml/manual.go b/pkg/encoding/yaml/manual.go
index 286f754..053ab6b 100644
--- a/pkg/encoding/yaml/manual.go
+++ b/pkg/encoding/yaml/manual.go
@@ -58,7 +58,7 @@
return yaml.Unmarshal("", data)
}
-// Validate validates YAML and confirms it matches the constraints
+// Validate validates YAML and confirms it is an instance of the schema
// specified by v. If the YAML source is a stream, every object must match v.
func Validate(b []byte, v cue.Value) (bool, error) {
d, err := yaml.NewDecoder("yaml.Validate", b)
@@ -80,6 +80,36 @@
return false, err
}
+ if err := v.Subsume(inst.Value(), cue.Final()); err != nil {
+ return false, err
+ }
+ }
+}
+
+// ValidatePartial validates YAML and confirms it matches the constraints
+// specified by v using unification. This means that b must be consistent with,
+// but does not have to be an instance of v. If the YAML source is a stream,
+// every object must match v.
+func ValidatePartial(b []byte, v cue.Value) (bool, error) {
+ d, err := yaml.NewDecoder("yaml.ValidatePartial", b)
+ if err != nil {
+ return false, err
+ }
+ r := internal.GetRuntime(v).(*cue.Runtime)
+ for {
+ expr, err := d.Decode()
+ if err != nil {
+ if err == io.EOF {
+ return true, nil
+ }
+ return false, err
+ }
+
+ inst, err := r.CompileExpr(expr)
+ if err != nil {
+ return false, err
+ }
+
if x := v.Unify(inst.Value()); x.Err() != nil {
return false, x.Err()
}
diff --git a/pkg/tool/exec/exec_test.go b/pkg/tool/exec/exec_test.go
index fb50513..69e9484 100644
--- a/pkg/tool/exec/exec_test.go
+++ b/pkg/tool/exec/exec_test.go
@@ -39,13 +39,13 @@
WHEN: "Now!"
}
`,
- env: []string{"WHO=World", "WHAT=Hello", "WHEN=Now!" },
+ env: []string{"WHO=World", "WHAT=Hello", "WHEN=Now!"},
}, {
val: `
cmd: "echo"
env: [ "WHO=World", "WHAT=Hello", "WHEN=Now!" ]
`,
- env: []string{ "WHO=World", "WHAT=Hello", "WHEN=Now!" },
+ env: []string{"WHO=World", "WHAT=Hello", "WHEN=Now!"},
}}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {