internal/mcp/jsonschema: clearer validations errors
Validation errors now more clearly indicate the path from the root schema. - Enhance schema iteration to yield the path, and generalize it by using reflection. - Store each schema's path inside the schema. - Remove the path arg from validate: we don't need it anymore. - Improve the test for the iterators and add one for validation errors. Change-Id: Ief55133ef4ae2eac0736208a3b9f78ed69933b31 Reviewed-on: https://go-review.googlesource.com/c/tools/+/676375 Reviewed-by: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Jonathan Amsterdam <jba@google.com>
This commit is contained in:
parent
7130f0f081
commit
e3938ce68c
1 changed files with 185 additions and 106 deletions
|
|
@ -27,8 +27,7 @@ func (rs *Resolved) Validate(instance any) error {
|
|||
return fmt.Errorf("cannot validate version %s, only %s", s, draft202012)
|
||||
}
|
||||
st := &state{rs: rs}
|
||||
var pathBuffer [4]any
|
||||
return st.validate(reflect.ValueOf(instance), st.rs.root, nil, pathBuffer[:0])
|
||||
return st.validate(reflect.ValueOf(instance), st.rs.root, nil)
|
||||
}
|
||||
|
||||
// state is the state of single call to ResolvedSchema.Validate.
|
||||
|
|
@ -42,13 +41,10 @@ type state struct {
|
|||
}
|
||||
|
||||
// validate validates the reflected value of the instance.
|
||||
// It keeps track of the path within the instance for better error messages.
|
||||
func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *annotations, path []any) (err error) {
|
||||
func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *annotations) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if p := formatPath(path); p != "" {
|
||||
err = fmt.Errorf("%s: %w", p, err)
|
||||
}
|
||||
err = fmt.Errorf("%s: %w", schema, err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
@ -72,7 +68,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
if schema.Type != "" || schema.Types != nil {
|
||||
gotType, ok := jsonType(instance)
|
||||
if !ok {
|
||||
return fmt.Errorf("%v of type %[1]T is not a valid JSON value", instance)
|
||||
return fmt.Errorf("type: %v of type %[1]T is not a valid JSON value", instance)
|
||||
}
|
||||
if schema.Type != "" {
|
||||
// "number" subsumes integers
|
||||
|
|
@ -163,7 +159,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
|
||||
// $ref: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.1
|
||||
if schema.Ref != "" {
|
||||
if err := st.validate(instance, schema.resolvedRef, &anns, path); err != nil {
|
||||
if err := st.validate(instance, schema.resolvedRef, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -175,7 +171,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
"DynamicRef not resolved properly")
|
||||
if schema.resolvedDynamicRef != nil {
|
||||
// Same as $ref.
|
||||
if err := st.validate(instance, schema.resolvedDynamicRef, &anns, path); err != nil {
|
||||
if err := st.validate(instance, schema.resolvedDynamicRef, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
|
@ -199,7 +195,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
if dynamicSchema == nil {
|
||||
return fmt.Errorf("missing dynamic anchor %q", schema.dynamicRefAnchor)
|
||||
}
|
||||
if err := st.validate(instance, dynamicSchema, &anns, path); err != nil {
|
||||
if err := st.validate(instance, dynamicSchema, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -214,11 +210,11 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
// If any of these fail, then validation fails, even if there is an unevaluatedXXX
|
||||
// keyword in the schema. The spec is unclear about this, but that is the intention.
|
||||
|
||||
valid := func(s *Schema, anns *annotations) bool { return st.validate(instance, s, anns, path) == nil }
|
||||
valid := func(s *Schema, anns *annotations) bool { return st.validate(instance, s, anns) == nil }
|
||||
|
||||
if schema.AllOf != nil {
|
||||
for _, ss := range schema.AllOf {
|
||||
if err := st.validate(instance, ss, &anns, path); err != nil {
|
||||
if err := st.validate(instance, ss, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -264,7 +260,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
ss = schema.Else
|
||||
}
|
||||
if ss != nil {
|
||||
if err := st.validate(instance, ss, &anns, path); err != nil {
|
||||
if err := st.validate(instance, ss, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -281,7 +277,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
if i >= instance.Len() {
|
||||
break // shorter is OK
|
||||
}
|
||||
if err := st.validate(instance.Index(i), ischema, nil, append(path, i)); err != nil {
|
||||
if err := st.validate(instance.Index(i), ischema, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -289,7 +285,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
|
||||
if schema.Items != nil {
|
||||
for i := len(schema.PrefixItems); i < instance.Len(); i++ {
|
||||
if err := st.validate(instance.Index(i), schema.Items, nil, append(path, i)); err != nil {
|
||||
if err := st.validate(instance.Index(i), schema.Items, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -300,14 +296,13 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
nContains := 0
|
||||
if schema.Contains != nil {
|
||||
for i := range instance.Len() {
|
||||
if err := st.validate(instance.Index(i), schema.Contains, nil, append(path, i)); err == nil {
|
||||
if err := st.validate(instance.Index(i), schema.Contains, nil); err == nil {
|
||||
nContains++
|
||||
anns.noteIndex(i)
|
||||
}
|
||||
}
|
||||
if nContains == 0 && (schema.MinContains == nil || *schema.MinContains > 0) {
|
||||
return fmt.Errorf("contains: %s does not have an item matching %s",
|
||||
instance, schema.Contains)
|
||||
return fmt.Errorf("contains: %s does not have an item matching %s", instance, schema.Contains)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -366,7 +361,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
// That includes validations by subschemas on the same instance, like allOf.
|
||||
for i := anns.endIndex; i < instance.Len(); i++ {
|
||||
if !anns.evaluatedIndexes[i] {
|
||||
if err := st.validate(instance.Index(i), schema.UnevaluatedItems, nil, append(path, i)); err != nil {
|
||||
if err := st.validate(instance.Index(i), schema.UnevaluatedItems, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -399,7 +394,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
if instance.Kind() == reflect.Struct && val.IsZero() && !schema.isRequired[prop] {
|
||||
continue
|
||||
}
|
||||
if err := st.validate(val, subschema, nil, append(path, prop)); err != nil {
|
||||
if err := st.validate(val, subschema, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
evalProps[prop] = true
|
||||
|
|
@ -409,7 +404,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
// Check every matching pattern.
|
||||
for re, schema := range schema.patternProperties {
|
||||
if re.MatchString(prop) {
|
||||
if err := st.validate(val, schema, nil, append(path, prop)); err != nil {
|
||||
if err := st.validate(val, schema, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
evalProps[prop] = true
|
||||
|
|
@ -421,7 +416,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
// Apply to all properties not handled above.
|
||||
for prop, val := range properties(instance) {
|
||||
if !evalProps[prop] {
|
||||
if err := st.validate(val, schema.AdditionalProperties, nil, append(path, prop)); err != nil {
|
||||
if err := st.validate(val, schema.AdditionalProperties, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
evalProps[prop] = true
|
||||
|
|
@ -433,7 +428,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
// Note: properties unnecessarily fetches each value. We could define a propertyNames function
|
||||
// if performance ever matters.
|
||||
for prop := range properties(instance) {
|
||||
if err := st.validate(reflect.ValueOf(prop), schema.PropertyNames, nil, append(path, prop)); err != nil {
|
||||
if err := st.validate(reflect.ValueOf(prop), schema.PropertyNames, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -493,7 +488,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
for dprop, ss := range schema.DependentSchemas {
|
||||
if hasProperty(dprop) {
|
||||
// TODO: include dependentSchemas[dprop] in the errors.
|
||||
err := st.validate(instance, ss, &anns, path)
|
||||
err := st.validate(instance, ss, &anns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -505,7 +500,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
|||
// in addition to sibling keywords.
|
||||
for prop, val := range properties(instance) {
|
||||
if !anns.evaluatedProperties[prop] {
|
||||
if err := st.validate(val, schema.UnevaluatedProperties, nil, append(path, prop)); err != nil {
|
||||
if err := st.validate(val, schema.UnevaluatedProperties, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -633,18 +628,3 @@ func jsonName(f reflect.StructField) (string, bool) {
|
|||
}
|
||||
return f.Name, true
|
||||
}
|
||||
|
||||
func formatPath(path []any) string {
|
||||
var b strings.Builder
|
||||
for i, p := range path {
|
||||
if n, ok := p.(int); ok {
|
||||
fmt.Fprintf(&b, "[%d]", n)
|
||||
} else {
|
||||
if i > 0 {
|
||||
b.WriteByte('.')
|
||||
}
|
||||
fmt.Fprintf(&b, "%q", p)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue