Skip to content

Commit

Permalink
compiler: impl AssertVocab flag
Browse files Browse the repository at this point in the history
  • Loading branch information
santhosh-tekuri committed Apr 29, 2024
1 parent c599da8 commit 6d78eb5
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 27 deletions.
10 changes: 10 additions & 0 deletions compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ func (c *Compiler) RegisterVocabulary(vocab *Vocabulary) {
c.roots.vocabularies[vocab.URL] = vocab
}

// AssertVocabs always enables user-defined vocabularies assertions.
//
// Default Behavior:
// for draft-07: enabled.
// for draft/2019-09: disabled unless metaschema enables a vocabulary.
// for draft/2020-12: disabled unless metaschema enables a vocabulary.
func (c *Compiler) AssertVocabs() {
c.roots.assertVocabs = true
}

// AddResource adds schema resource which gets used later in reference
// resolution.
//
Expand Down
18 changes: 18 additions & 0 deletions compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,21 @@ func TestCompileNonStd(t *testing.T) {
t.Fatal(err)
}
}

func TestCustomVocabValidation(t *testing.T) {
schema, err := jsonschema.UnmarshalJSON(strings.NewReader(`{"uniqueKeys": 1}`))
if err != nil {
t.Fatal(err)
}

c := jsonschema.NewCompiler()
c.AssertVocabs()
c.RegisterVocabulary(uniqueKeysVocab())
if err := c.AddResource("schema.json", schema); err != nil {
t.Fatal(err)
}
_, err = c.Compile("schema.json")
if err == nil {
t.Fatal("exception compilation failure")
}
}
38 changes: 34 additions & 4 deletions draft.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,19 +273,49 @@ func (d *dialect) hasVocab(name string) bool {
return slices.Contains(d.draft.defaultVocabs, name)
}

func (d *dialect) getSchema() *Schema {
func (d *dialect) activeVocabs(assertVocabs bool, vocabularies map[string]*Vocabulary) []string {
if len(vocabularies) == 0 {
return d.vocabs
}
if d.draft.version < 2019 {
assertVocabs = true
}
if !assertVocabs {
return d.vocabs
}
var vocabs []string
if d.vocabs == nil {
vocabs = slices.Clone(d.draft.defaultVocabs)
} else {
vocabs = slices.Clone(d.vocabs)
}
for vocab := range vocabularies {
if !slices.Contains(vocabs, vocab) {
vocabs = append(vocabs, vocab)
}
}
return vocabs
}

func (d *dialect) getSchema(assertVocabs bool, vocabularies map[string]*Vocabulary) *Schema {
vocabs := d.activeVocabs(assertVocabs, vocabularies)
if vocabs == nil {
return d.draft.sch
}
// TODO: support custom vocabulary

var allOf []*Schema
for _, vocab := range d.vocabs {
for _, vocab := range vocabs {
sch := d.draft.allVocabs[vocab]
if sch == nil {
if v, ok := vocabularies[vocab]; ok {
sch = v.Schema
}
}
if sch != nil {
allOf = append(allOf, sch)
}
}
if !slices.Contains(d.vocabs, "core") {
if !slices.Contains(vocabs, "core") {
sch := d.draft.allVocabs["core"]
if sch == nil {
sch = d.draft.sch
Expand Down
1 change: 1 addition & 0 deletions example_vocab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func Example_vocab_uniquekeys() {
}

c := jsonschema.NewCompiler()
c.AssertVocabs()
c.RegisterVocabulary(uniqueKeysVocab())
if err := c.AddResource("schema.json", schema); err != nil {
log.Fatal(err)
Expand Down
9 changes: 7 additions & 2 deletions objcompiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,13 @@ func (c *objCompiler) compile(s *Schema) error {
}

// vocabularies
for _, vocab := range c.c.roots.vocabularies {
ext, err := vocab.Compile(&CompilerContext{}, c.obj)
vocabs := c.res.dialect.activeVocabs(c.c.roots.assertVocabs, c.c.roots.vocabularies)
for _, vocab := range vocabs {
v := c.c.roots.vocabularies[vocab]
if v == nil {
continue
}
ext, err := v.Compile(&CompilerContext{}, c.obj)
if err != nil {
return err
}
Expand Down
10 changes: 0 additions & 10 deletions root.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,16 +265,6 @@ func (r *root) collectAnchors(sch any, schPtr jsonPointer, res *resource) error
return nil
}

func (r *root) validate(ptr jsonPointer, v any, regexpEngine RegexpEngine) error {
dialect := r.resource(ptr).dialect
up := urlPtr{r.url, ptr}
meta := dialect.getSchema()
if err := meta.validate(v, regexpEngine, meta, r.resources); err != nil {
return &SchemaValidationError{URL: up.String(), Err: err}
}
return nil
}

func (r *root) clone() *root {
processed := map[jsonPointer]struct{}{}
for k := range r.subschemasProcessed {
Expand Down
15 changes: 13 additions & 2 deletions roots.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type roots struct {
loader defaultLoader
regexpEngine RegexpEngine
vocabularies map[string]*Vocabulary
assertVocabs bool
}

func newRoots() *roots {
Expand Down Expand Up @@ -49,7 +50,7 @@ func (rr *roots) addRoot(u url, doc any) (*root, error) {
}
if !strings.HasPrefix(u.String(), "http://json-schema.org/") &&
!strings.HasPrefix(u.String(), "https://json-schema.org/") {
if err := r.validate("", doc, rr.regexpEngine); err != nil {
if err := rr.validate(r, doc, ""); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -82,13 +83,23 @@ func (rr *roots) ensureSubschema(up urlPtr) error {
if err := rClone.addSubschema(&rr.loader, up.ptr); err != nil {
return err
}
if err := rClone.validate(up.ptr, v, rr.regexpEngine); err != nil {
if err := rr.validate(rClone, v, up.ptr); err != nil {
return err
}
rr.roots[r.url] = rClone
return nil
}

func (rr *roots) validate(r *root, v any, ptr jsonPointer) error {
dialect := r.resource(ptr).dialect
meta := dialect.getSchema(rr.assertVocabs, rr.vocabularies)
if err := meta.validate(v, rr.regexpEngine, meta, r.resources, rr.assertVocabs, rr.vocabularies); err != nil {
up := urlPtr{r.url, ptr}
return &SchemaValidationError{URL: up.String(), Err: err}
}
return nil
}

// --

type InvalidMetaSchemaURLError struct {
Expand Down
26 changes: 17 additions & 9 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import (
)

func (sch *Schema) Validate(v any) error {
return sch.validate(v, nil, nil, nil)
return sch.validate(v, nil, nil, nil, false, nil)
}

func (sch *Schema) validate(v any, regexpEngine RegexpEngine, meta *Schema, resources map[jsonPointer]*resource) error {
func (sch *Schema) validate(v any, regexpEngine RegexpEngine, meta *Schema, resources map[jsonPointer]*resource, assertVocabs bool, vocabularies map[string]*Vocabulary) error {
vd := validator{
v: v,
vloc: make([]string, 0, 8),
Expand All @@ -27,6 +27,8 @@ func (sch *Schema) validate(v any, regexpEngine RegexpEngine, meta *Schema, reso
regexpEngine: regexpEngine,
meta: meta,
resources: resources,
assertVocabs: assertVocabs,
vocabularies: vocabularies,
}
if _, err := vd.validate(); err != nil {
verr := err.(*ValidationError)
Expand Down Expand Up @@ -58,8 +60,10 @@ type validator struct {
regexpEngine RegexpEngine

// meta validation
meta *Schema // set only when validating with metaschema
resources map[jsonPointer]*resource // resources which should be validated with their dialect
meta *Schema // set only when validating with metaschema
resources map[jsonPointer]*resource // resources which should be validated with their dialect
assertVocabs bool
vocabularies map[string]*Vocabulary
}

func (vd *validator) validate() (*uneval, error) {
Expand Down Expand Up @@ -283,10 +287,10 @@ func (vd *validator) objValidate(obj map[string]any) {
sch, meta, resources := s.PropertyNames, vd.meta, vd.resources
res := vd.metaResource(sch)
if res != nil {
meta = res.dialect.getSchema()
meta = res.dialect.getSchema(vd.assertVocabs, vd.vocabularies)
sch = meta
}
if err := sch.validate(pname, vd.regexpEngine, meta, resources); err != nil {
if err := sch.validate(pname, vd.regexpEngine, meta, resources, vd.assertVocabs, vd.vocabularies); err != nil {
verr := err.(*ValidationError)
verr.SchemaURL = s.PropertyNames.Location
verr.ErrorKind = kind.PropertyNames(pname)
Expand Down Expand Up @@ -493,10 +497,10 @@ func (vd *validator) strValidate(str string) {
sch, meta, resources := s.ContentSchema, vd.meta, vd.resources
res := vd.metaResource(sch)
if res != nil {
meta = res.dialect.getSchema()
meta = res.dialect.getSchema(vd.assertVocabs, vd.vocabularies)
sch = meta
}
if err = sch.validate(*deserialized, vd.regexpEngine, meta, resources); err != nil {
if err = sch.validate(*deserialized, vd.regexpEngine, meta, resources, vd.assertVocabs, vd.vocabularies); err != nil {
verr := err.(*ValidationError)
verr.SchemaURL = s.Location
verr.ErrorKind = kind.ContentSchema{}
Expand Down Expand Up @@ -663,6 +667,8 @@ func (vd *validator) validateSelf(sch *Schema, refKw string, boolResult bool) er
regexpEngine: vd.regexpEngine,
meta: vd.meta,
resources: vd.resources,
assertVocabs: vd.assertVocabs,
vocabularies: vd.vocabularies,
}
subvd.handleMeta()
uneval, err := subvd.validate()
Expand All @@ -687,6 +693,8 @@ func (vd *validator) validateVal(sch *Schema, v any, vtok string) error {
regexpEngine: vd.regexpEngine,
meta: vd.meta,
resources: vd.resources,
assertVocabs: vd.assertVocabs,
vocabularies: vd.vocabularies,
}
subvd.handleMeta()
_, err := subvd.validate()
Expand All @@ -710,7 +718,7 @@ func (vd *validator) handleMeta() {
if res == nil {
return
}
sch := res.dialect.getSchema()
sch := res.dialect.getSchema(vd.assertVocabs, vd.vocabularies)
vd.meta = sch
vd.sch = sch
}
Expand Down

0 comments on commit 6d78eb5

Please sign in to comment.