Skip to content

Commit

Permalink
encoding/jsonschema: use new definition mapping
Browse files Browse the repository at this point in the history
This allows JSON Schema to now directly be compared to
data without qualifiers or remapping:

   $ cue eval schema.json data.yaml

This works now more intuively as data and schema now map
one-to-one.

This relies on the change to allow `...` at the file level. This means
it is not necessary to use the new `isSchema` field. It seems
sensible to track this though and we may need it in the future.

One huge caveat of this new mapping is that it is now no longer
possible to have a non-struct schema with definitions remapped
to a different label. For this we need to allow embedded scalars in
structs. It still seems worth it and rather to allow this in the language.

Change-Id: I16988ca1c0d4436d591ba239964c4bb7445e6fd9
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5942
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
  • Loading branch information
mpvl committed May 13, 2020
1 parent c174a08 commit 435989a
Show file tree
Hide file tree
Showing 19 changed files with 238 additions and 176 deletions.
34 changes: 20 additions & 14 deletions cmd/cue/cmd/testdata/script/def_jsonschema.txt
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
cue def jsonschema: schema.json -p schema -l 'Person::'
cue def jsonschema: schema.json -p schema -l '#Person:'
cmp stdout expect-stdout

# auto mode
cue def schema.json -p schema -l 'Person::'
cue def schema.json -p schema -l '#Person:'
cmp stdout expect-stdout

cue def jsonschema: bad.json

! cue def jsonschema: bad.json --strict
cmp stderr expect-stderr

! cue eval data.yaml schema.json
cmp stderr expect-stderr2

-- expect-stdout --
package schema

Person :: {
// Person
Schema :: {
// The person's first name.
firstName?: string
#Person: {
// The person's first name.
firstName?: string

// The person's last name.
lastName?: string
// The person's last name.
lastName?: string

// Age in years which must be equal to or greater than zero.
age?: >=0
...
} @jsonschema(schema="http://json-schema.org/draft-07/schema#",id="https://example.com/person.schema.json")
// Age in years which must be equal to or greater than zero.
age?: >=0
...
}
-- schema.json --
{
Expand Down Expand Up @@ -55,8 +55,14 @@ Person :: {
"type": "number",
"foo": "bar"
}

-- expect-stderr --
unsupported constraint "foo":
./bad.json:3:10
-- data.yaml --
age: twenty

-- expect-stderr2 --
age: conflicting values "twenty" and >=0 (mismatched types string and number):
11:7
./data.yaml:1:7
-- cue.mod --
26 changes: 13 additions & 13 deletions cmd/cue/cmd/testdata/script/def_openapi.txt
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,11 @@ info: {
version: *"v1alpha1" | string
}

Bar :: {
foo: Foo
#Bar: {
foo: #Foo
...
}
Foo :: {
#Foo: {
a: int
b: >=0 & <10
...
Expand All @@ -226,11 +226,11 @@ info: {
title: *"Some clever title." | string
version: *"v1" | string
}
Bar :: {
foo: Foo
#Bar: {
foo: #Foo
...
}
Foo :: {
#Foo: {
a: int
b: >=0 & <10
...
Expand All @@ -240,11 +240,11 @@ info: {
title: *"Some clever title." | string
version: *"v1" | string
}
Bar :: {
foo: Foo
#Bar: {
foo: #Foo
...
}
Foo :: {
#Foo: {
a: int
b: >=0 & <10
...
Expand All @@ -257,16 +257,16 @@ info: {
title: string | *_|_
version: *"v1alpha1" | string
}
Bar :: {
foo: Foo
#Bar: {
foo: #Foo
...
}
Foo :: {
#Foo: {
a: int
b: >=0 & <10
...
}
Baz :: {
#Baz: {
a: int
b: >=0 & <10
...
Expand Down
6 changes: 3 additions & 3 deletions cmd/cue/cmd/testdata/script/import_auto.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ info: {
version: *"v1beta1" | string
}

Foo :: {
#Foo: {
a: int
b: >=0 & <10
...
}
Bar :: {
foo: Foo
#Bar: {
foo: #Foo
...
}
-- openapi.yaml --
Expand Down
46 changes: 32 additions & 14 deletions encoding/jsonschema/constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,19 @@ func addDefinitions(n cue.Value, s *state) {
s.errf(n, `"definitions" expected an object, found %v`, n.Kind)
}

old := s.isSchema
s.isSchema = true
defer func() { s.isSchema = old }()

s.processMap(n, func(key string, n cue.Value) {
f := &ast.Field{
Label: ast.NewString(s.path[len(s.path)-1]),
Token: token.ISA,
Value: s.schema(n),
}
f = &ast.Field{
Label: ast.NewIdent(rootDefs),
Value: ast.NewStruct(f),
name := s.path[len(s.path)-1]
a, _ := jsonSchemaRef(n.Pos(), []string{"definitions", name})

f := &ast.Field{Label: a[len(a)-1], Value: s.schema(n)}
for i := len(a) - 2; i >= 0; i-- {
f = &ast.Field{Label: a[i], Value: ast.NewStruct(f)}
}

ast.SetRelPos(f, token.NewSection)
s.definitions = append(s.definitions, f)
})
Expand Down Expand Up @@ -211,15 +214,29 @@ var constraints = []*constraint{
p0("$ref", func(n cue.Value, s *state) {
s.usedTypes = allTypes
str, _ := s.strValue(n)
a := s.parseRef(n.Pos(), str)
if a != nil {
a = s.mapRef(n.Pos(), str, a)
refs := s.parseRef(n.Pos(), str)
var a []ast.Label
if refs != nil {
a = s.mapRef(n.Pos(), str, refs)
}
if a == nil {
s.addConjunct(&ast.BadExpr{From: n.Pos()})
return
}
s.addConjunct(ast.NewSel(ast.NewIdent(a[0]), a[1:]...))
sel, ok := a[0].(ast.Expr)
if !ok {
sel = &ast.BadExpr{}
}
for _, l := range a[1:] {
switch x := l.(type) {
case *ast.Ident:
sel = &ast.SelectorExpr{X: sel, Sel: x}

case *ast.BasicLit:
sel = &ast.IndexExpr{X: sel, Index: x}
}
}
s.addConjunct(sel)
}),

// Combinators
Expand Down Expand Up @@ -384,8 +401,9 @@ var constraints = []*constraint{

s.processMap(n, func(key string, n cue.Value) {
// property?: value
label := ast.NewString(key)
expr, state := s.schemaState(n, allTypes, false)
f := &ast.Field{Label: ast.NewString(key), Value: expr}
f := &ast.Field{Label: label, Value: expr}
state.doc(f)
f.Optional = token.Blank.Pos()
if len(s.obj.Elts) > 0 && len(f.Comments()) > 0 {
Expand All @@ -396,7 +414,7 @@ var constraints = []*constraint{
if state.deprecated {
switch expr.(type) {
case *ast.StructLit:
s.obj.Elts = append(s.obj.Elts, addTag(key, "deprecated", ""))
s.obj.Elts = append(s.obj.Elts, addTag(label, "deprecated", ""))
default:
f.Attrs = append(f.Attrs, internal.NewAttr("deprecated", ""))
}
Expand Down
77 changes: 54 additions & 23 deletions encoding/jsonschema/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ import (
"cuelang.org/go/internal"
)

const rootDefs = "def"
// rootDefs defines the top-level name of the map of definitions that do not
// have a valid identifier name.
//
// TODO: find something more principled, like allowing #."a-b" or `#a-b`.
const rootDefs = "#def"

// A decoder converts JSON schema to CUE.
type decoder struct {
Expand Down Expand Up @@ -64,7 +68,7 @@ func (d *decoder) decode(v cue.Value) *ast.File {
var a []ast.Decl

if d.cfg.Root == "" {
a = append(a, d.schema([]string{"Schema"}, v)...)
a = append(a, d.schema(nil, v)...)
} else {
ref := d.parseRef(token.NoPos, d.cfg.Root)
if ref == nil {
Expand All @@ -77,11 +81,11 @@ func (d *decoder) decode(v cue.Value) *ast.File {
}
for i.Next() {
ref := append(ref, i.Label())
ref = d.mapRef(i.Value().Pos(), "", ref)
if len(ref) == 0 {
lab := d.mapRef(i.Value().Pos(), "", ref)
if len(lab) == 0 {
return nil
}
decls := d.schema(ref, i.Value())
decls := d.schema(lab, i.Value())
a = append(a, decls...)
}
}
Expand All @@ -106,11 +110,16 @@ func (d *decoder) decode(v cue.Value) *ast.File {
return f
}

func (d *decoder) schema(ref []string, v cue.Value) (a []ast.Decl) {
func (d *decoder) schema(ref []ast.Label, v cue.Value) (a []ast.Decl) {
root := state{decoder: d}

var name ast.Label
inner := len(ref) - 1
name := ref[inner]

if inner >= 0 {
name = ref[inner]
root.isSchema = true
}

expr, state := root.schemaState(v, allTypes, false)

Expand All @@ -121,30 +130,48 @@ func (d *decoder) schema(ref []string, v cue.Value) (a []ast.Decl) {
if state.id != "" {
tags = append(tags, fmt.Sprintf("id=%q", state.id))
}
if len(tags) > 0 {
a = append(a, addTag(name, "jsonschema", strings.Join(tags, ",")))
}

if state.deprecated {
a = append(a, addTag(name, "deprecated", ""))
if name == nil {
if len(tags) > 0 {
body := strings.Join(tags, ",")
a = append(a, &ast.Attribute{
Text: fmt.Sprintf("@jsonschema(%s)", body)})
}

if state.deprecated {
a = append(a, &ast.Attribute{Text: "@deprecated()"})
}
} else {
if len(tags) > 0 {
a = append(a, addTag(name, "jsonschema", strings.Join(tags, ",")))
}

if state.deprecated {
a = append(a, addTag(name, "deprecated", ""))
}
}

f := &ast.Field{
Label: ast.NewIdent(name),
Token: token.ISA,
Value: expr,
if name != nil {
f := &ast.Field{
Label: name,
Value: expr,
}

a = append(a, f)
} else if st, ok := expr.(*ast.StructLit); ok {
a = append(a, st.Elts...)
} else {
a = append(a, &ast.EmbedDecl{Expr: expr})
}

a = append(a, f)
state.doc(a[0])

for i := inner - 1; i >= 0; i-- {
a = []ast.Decl{&ast.Field{
Label: ast.NewIdent(ref[i]),
Token: token.ISA,
Label: ref[i],
Value: &ast.StructLit{Elts: a},
}}
expr = ast.NewStruct(ref[i], token.ISA, expr)
expr = ast.NewStruct(ref[i], expr)
}

return a
Expand Down Expand Up @@ -205,6 +232,8 @@ func (d *decoder) strValue(n cue.Value) (s string, ok bool) {
type state struct {
*decoder

isSchema bool // for omitting ellipsis in an ast.File

parent *state

path []string
Expand Down Expand Up @@ -285,6 +314,7 @@ func (s *state) finalize() (e ast.Expr) {
conjuncts = append(conjuncts, s.conjuncts...)

if s.obj != nil {
// TODO: may need to explicitly close.
if !s.closeStruct {
s.obj.Elts = append(s.obj.Elts, &ast.Ellipsis{})
}
Expand Down Expand Up @@ -360,6 +390,7 @@ func (s *state) schema(n cue.Value) ast.Expr {
// caller is a logical operator like anyOf, allOf, oneOf, or not.
func (s *state) schemaState(n cue.Value, types cue.Kind, isLogical bool) (ast.Expr, *state) {
state := &state{
isSchema: s.isSchema,
decoder: s.decoder,
allowedTypes: types,
path: s.path,
Expand Down Expand Up @@ -414,6 +445,7 @@ func (s *state) value(n cue.Value) ast.Expr {
Value: s.value(n),
})
})
// TODO: only open when s.isSchema?
a = append(a, &ast.Ellipsis{})
return setPos(&ast.StructLit{Elts: a}, n)

Expand Down Expand Up @@ -485,10 +517,9 @@ func excludeFields(decls []ast.Decl) ast.Expr {
return &ast.UnaryExpr{Op: token.NMAT, X: ast.NewString(re)}
}

func addTag(field, tag, value string) *ast.Field {
func addTag(field ast.Label, tag, value string) *ast.Field {
return &ast.Field{
Label: ast.NewIdent(field),
Token: token.ISA,
Label: field,
Value: ast.NewIdent("_"),
Attrs: []*ast.Attribute{
{Text: fmt.Sprintf("@%s(%s)", tag, value)},
Expand Down

0 comments on commit 435989a

Please sign in to comment.