Skip to content

Commit

Permalink
vocab with embedded schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
santhosh-tekuri committed May 2, 2024
1 parent 2b1ce1d commit 2ccf96b
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 3 deletions.
166 changes: 166 additions & 0 deletions example_vocab_discriminator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package jsonschema_test

import (
"fmt"
"log"
"strings"

"github.com/santhosh-tekuri/jsonschema/v6"
)

// SchemaExt --

type discriminator struct {
pname string
values map[string]*jsonschema.Schema
}

func (d *discriminator) Validate(ctx *jsonschema.ValidatorContext, v any) {
obj, ok := v.(map[string]any)
if !ok {
return
}
pvalue, ok := obj[d.pname]
if !ok {
return
}
value, ok := pvalue.(string)
if !ok {
return
}
sch := d.values[value]
if sch == nil {
return
}
if err := ctx.Validate(sch, v, nil); err != nil {
ctx.AddErr(err)
} else {
ctx.EvaluatedProp(d.pname)
}
}

// Vocab --

func discriminatorVocab() *jsonschema.Vocabulary {
url := "http://example.com/meta/discriminator"
schema, err := jsonschema.UnmarshalJSON(strings.NewReader(`{
"discriminator": {
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"patternProperties": {
".*": {
"type": "object",
"patternProperties": {
".*": {
"$ref": "https://json-schema.org/draft/2020-12/schema"
}
}
}
}
}
}`))
if err != nil {
log.Fatal(err)
}

c := jsonschema.NewCompiler()
if err := c.AddResource(url, schema); err != nil {
log.Fatal(err)
}
sch, err := c.Compile(url)
if err != nil {
log.Fatal(err)
}

return &jsonschema.Vocabulary{
URL: url,
Schema: sch,
Compile: compileDiscriminator,
}
}

func compileDiscriminator(ctx *jsonschema.CompilerContext, obj map[string]any) (jsonschema.SchemaExt, error) {
v, ok := obj["discriminator"]
if !ok {
return nil, nil
}
d, ok := v.(map[string]any)
if !ok {
return nil, nil
}
var pname string
var pvalue any
for key, value := range d {
pname = key
pvalue = value
break
}
values := map[string]*jsonschema.Schema{}
vm, ok := pvalue.(map[string]any)
if !ok {
return nil, nil
}
for value := range vm {
values[value] = ctx.Enqueue([]string{"discriminator", pname, value})
}
return &discriminator{pname, values}, nil
}

// Example --

func Example_vocab_discriminator() {
// if kind is fish, swimmingSpeed is required
// if kind is dog, runningSpeed is required
schema, err := jsonschema.UnmarshalJSON(strings.NewReader(`{
"type": "object",
"properties": {
"kind": { "type": "string" }
},
"required": ["kind"],
"discriminator": {
"kind": {
"fish": {
"type": "object",
"properties": {
"swimmingSpeed": { "type": "number" }
},
"required": ["swimmingSpeed"]
},
"dog": {
"type": "object",
"properties": {
"runningSpeed": { "type": "number" }
},
"required": ["runningSpeed"]
}
}
}
}`))
if err != nil {
fmt.Println("xxx", err)
log.Fatal(err)
}
inst, err := jsonschema.UnmarshalJSON(strings.NewReader(`{
"kind": "fish",
"runningSpeed": 5
}`))
if err != nil {
log.Fatal(err)
}
c := jsonschema.NewCompiler()
c.AssertVocabs()
c.RegisterVocabulary(discriminatorVocab())
if err := c.AddResource("schema.json", schema); err != nil {
log.Fatal(err)
}
sch, err := c.Compile("schema.json")
if err != nil {
log.Fatal(err)
}

err = sch.Validate(inst)
fmt.Println("valid:", err == nil)
// Output:
// valid: false
}
File renamed without changes.
6 changes: 4 additions & 2 deletions objcompiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@ func (c *objCompiler) compile(s *Schema) error {
if v == nil {
continue
}
ext, err := v.Compile(&CompilerContext{}, c.obj)
ext, err := v.Compile(&CompilerContext{c}, c.obj)
if err != nil {
return err
}
s.Extensions = append(s.Extensions, ext)
if ext != nil {
s.Extensions = append(s.Extensions, ext)
}
}

return nil
Expand Down
23 changes: 23 additions & 0 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,29 @@ func (vd *validator) validateVal(sch *Schema, v any, vtok string) error {
return err
}

func (vd *validator) validateValue(sch *Schema, v any, vpath []string) error {
vloc := append(vd.vloc, vpath...)
scp := vd.scp.child(sch, "", vd.scp.vid+1)
uneval := unevalFrom(v, sch, false)
subvd := validator{
v: v,
vloc: vloc,
sch: sch,
scp: scp,
uneval: uneval,
errors: nil,
boolResult: vd.boolResult,
regexpEngine: vd.regexpEngine,
meta: vd.meta,
resources: vd.resources,
assertVocabs: vd.assertVocabs,
vocabularies: vd.vocabularies,
}
subvd.handleMeta()
_, err := subvd.validate()
return err
}

func (vd *validator) metaResource(sch *Schema) *resource {
if sch != vd.meta {
return nil
Expand Down
24 changes: 23 additions & 1 deletion vocab.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@ package jsonschema

// CompilerContext provides helpers for
// compiling a [Vocabulary].
type CompilerContext struct{}
type CompilerContext struct {
c *objCompiler
}

func (ctx *CompilerContext) Enqueue(schPath []string) *Schema {
ptr := ctx.c.up.ptr
for _, tok := range schPath {
ptr = ptr.append(tok)
}
return ctx.c.enqueuePtr(ptr)
}

// Vocabulary defines a set of keywords, their syntax and
// their semantics.
Expand Down Expand Up @@ -35,6 +45,18 @@ type ValidatorContext struct {
vd *validator
}

// Validate validates v with sch. vpath gives path of v from current context value.
func (ctx *ValidatorContext) Validate(sch *Schema, v any, vpath []string) error {
switch len(vpath) {
case 0:
return ctx.vd.validateSelf(sch, "", false)
case 1:
return ctx.vd.validateVal(sch, v, vpath[0])
default:
return ctx.vd.validateValue(sch, v, vpath)
}
}

// EvaluatedProp marks given property of current object as evaluated.
func (ctx *ValidatorContext) EvaluatedProp(pname string) {
delete(ctx.vd.uneval.props, pname)
Expand Down

0 comments on commit 2ccf96b

Please sign in to comment.