Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change json scema lib #305

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bundle/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ func TestValuesOrDefaults_DefaultFailsValidation(t *testing.T) {
_, err := ValuesOrDefaults(map[string]interface{}{}, b, "install")
is.Error(err)
is.Contains(err.Error(), "cannot use value")
is.Contains(err.Error(), "type should be boolean")
is.Contains(err.Error(), "got string, want boolean")
}

func TestValidateVersionTag(t *testing.T) {
Expand Down
19 changes: 16 additions & 3 deletions bundle/definition/schema.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package definition

import (
"bytes"
"encoding/json"
"strconv"
"strings"

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

type Definitions map[string]*Schema
Expand Down Expand Up @@ -91,12 +93,23 @@ func (s *Schema) GetTypes() ([]string, bool, error) {
// github.com/qri-io/jsonschema to load and validate a given schema. If it is valid,
// then the json is unmarshaled.
func (s *Schema) UnmarshalJSON(data []byte) error {

schema, err := jsonschema.UnmarshalJSON(bytes.NewReader(data))
if err != nil {
return err
}
// Before we unmarshal into the cnab-go bundle/definition/Schema type, unmarshal into
// the library struct so we can handle any validation errors in the schema. If there
// are any errors, return those.
js := NewRootSchema()
if err := js.UnmarshalJSON(data); err != nil {
c := NewCompiler()
err = c.AddResource("schema.json", schema)
if err != nil {
return err
}
_, err = c.Compile("schema.json")
if err != nil {
if e, ok := err.(*jsonschema.SchemaValidationError); ok {
return e.Err
}
return err
}
// The schema is valid at this point, so now use an indirect wrapper type to actually
Expand Down
11 changes: 5 additions & 6 deletions bundle/definition/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ func TestSimpleUnMarshalDefinition(t *testing.T) {
def := `{
"$comment": "schema comment",
"$id": "schema id",
"$ref": "schema ref",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"items": [
Expand Down Expand Up @@ -97,7 +96,7 @@ func TestUnknownSchemaType(t *testing.T) {
definition := new(Schema)
err := json.Unmarshal([]byte(s), definition)
assert.Error(t, err, "should not have been able to marshall definition")
assert.EqualError(t, err, "error unmarshaling type from json: \"cnab\" is not a valid type")
assert.Contains(t, err.Error(), "jsonschema validation failed with 'https://json-schema.org/draft/2019-09/schema#'")
}

func TestSingleSchemaType(t *testing.T) {
Expand Down Expand Up @@ -181,7 +180,7 @@ func TestBooleanTypeValidation(t *testing.T) {
assert.Len(t, valErrors, 1, "expected a validation error")
valErr := valErrors[0]
assert.Equal(t, "/", valErr.Path, "expected validation to fail at the root")
assert.Equal(t, "should be one of [true]", valErr.Error)
assert.Equal(t, "at '': value must be true", valErr.Error)

boolValue2 := "true, false"
s2 := valueTestJSON("boolean", boolValue, boolValue2)
Expand Down Expand Up @@ -210,7 +209,7 @@ func TestStringTypeValidationEnum(t *testing.T) {
assert.NoError(t, err)
valErr := valErrors[0]
assert.Equal(t, "/", valErr.Path, "expected validation to fail at the root")
assert.Equal(t, "should be one of [\"dog\"]", valErr.Error)
assert.Equal(t, "at '': value must be 'dog'", valErr.Error)

anotherSchema := `{
"type" : "string",
Expand All @@ -224,7 +223,7 @@ func TestStringTypeValidationEnum(t *testing.T) {
valErrors, err = definition2.Validate("pig")
assert.NoError(t, err, "shouldn't have gotten an actual error")
assert.Len(t, valErrors, 1, "expected validation failure for pig")
assert.Equal(t, "should be one of [\"chicken\", \"duck\"]", valErrors[0].Error)
assert.Equal(t, "at '': value must be one of 'chicken', 'duck'", valErrors[0].Error)
}

func TestStringMinLengthValidator(t *testing.T) {
Expand All @@ -238,7 +237,7 @@ func TestStringMinLengthValidator(t *testing.T) {

valErrors, err := definition.Validate("four")
assert.Len(t, valErrors, 1, "expected the validation to fail with four characters")
assert.Equal(t, "min length of 10 characters required: four", valErrors[0].Error)
assert.Equal(t, "at '': minLength: got 4, want 10", valErrors[0].Error)
assert.NoError(t, err)

valErrors, err = definition.Validate("abcdefghijklmnopqrstuvwxyz")
Expand Down
45 changes: 31 additions & 14 deletions bundle/definition/validation.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package definition

import (
"context"
"bytes"
"encoding/json"
"strings"

"github.com/pkg/errors"
"github.com/qri-io/jsonschema"
jsonschema "github.com/santhosh-tekuri/jsonschema/v6"
)

// ValidationError error represents a validation error
Expand All @@ -23,10 +24,18 @@ func (s *Schema) ValidateSchema() (*jsonschema.Schema, error) {
if err != nil {
return nil, errors.Wrap(err, "unable to load schema")
}
rs := NewRootSchema()
err = rs.UnmarshalJSON(b)
schema, err := jsonschema.UnmarshalJSON(bytes.NewReader(b))
if err != nil {
return nil, errors.Wrap(err, "schema not valid")
return nil, err
}
c := NewCompiler()
err = c.AddResource("schema.json", schema)
if err != nil {
return nil, err
}
rs, err := c.Compile("schema.json")
if err != nil {
return nil, err
}
return rs, nil
}
Expand All @@ -40,27 +49,35 @@ func (s *Schema) Validate(data interface{}) ([]ValidationError, error) {
return nil, err
}

payload, err := json.Marshal(data)
jsonPayload, err := json.Marshal(data)
if err != nil {
return nil, errors.Wrap(err, "unable to process data")
}
valErrs, err := def.ValidateBytes(context.Background(), payload)

payload, err := jsonschema.UnmarshalJSON(bytes.NewReader(jsonPayload))
if err != nil {
return nil, errors.Wrap(err, "unable to perform validation")
return nil, errors.Wrap(err, "unable to process data")
}

err = def.Validate(payload)
if err == nil {
return nil, nil
}
if len(valErrs) > 0 {
valErrors := []ValidationError{}

for _, err := range valErrs {
if verr, ok := err.(*jsonschema.ValidationError); ok {
valErrors := make([]ValidationError, 0, len(verr.Causes))
for _, e := range verr.Causes {
path := strings.Join(e.InstanceLocation, "/")
path = "/" + path
valError := ValidationError{
Path: err.PropertyPath,
Error: err.Message,
Path: path,
Error: e.Error(),
}
valErrors = append(valErrors, valError)
}
return valErrors, nil
}
return nil, nil
return nil, err
}

// CoerceValue can be used to turn float and other numeric types into integers. When
Expand Down
38 changes: 6 additions & 32 deletions bundle/definition/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,33 +82,7 @@ func TestObjectValidationValid_CustomValidator_ContentEncoding_base64(t *testing
valErrors, err = definition.Validate(invalidVal)
require.NoError(t, err)
require.Len(t, valErrors, 1, "expected 1 validation error")
assert.Equal(t, "invalid base64 value: SGVsbG8gV29ybGQhCg===", valErrors[0].Error)
}

func TestObjectValidationValid_CustomValidator_ContentEncoding_InvalidEncoding(t *testing.T) {
s := `{
"type": "object",
"properties" : {
"file" : {
"type": "string",
"contentEncoding": "base65"
}
},
"required" : ["file"]
}`
definition := new(Schema)
err := json.Unmarshal([]byte(s), definition)
require.NoError(t, err, "should have been able to marshal definition")

val := struct {
File string `json:"file"`
}{
File: "SGVsbG8gV29ybGQhCg==",
}
valErrors, err := definition.Validate(val)
assert.NoError(t, err)
assert.Len(t, valErrors, 1, "expected 1 validation error")
assert.Equal(t, "unsupported or invalid contentEncoding type of base65", valErrors[0].Error)
assert.Equal(t, "at '/file': value is not 'base64' encoded: illegal base64 data at input byte 20", valErrors[0].Error)
}

func TestObjectValidationInValidMinimum(t *testing.T) {
Expand Down Expand Up @@ -146,7 +120,7 @@ func TestObjectValidationInValidMinimum(t *testing.T) {
valErr := valErrors[0]
assert.NotNil(t, valErr, "expected the obtain the validation error")
assert.Equal(t, "/port", valErr.Path, "expected validation error to reference port")
assert.Equal(t, "must be greater than or equal to 100", valErr.Error, "expected validation error to reference port")
assert.Equal(t, "at '/port': minimum: got 80, want 100", valErr.Error, "expected validation error to reference port")
}

func TestObjectValidationPropertyRequired(t *testing.T) {
Expand Down Expand Up @@ -184,7 +158,7 @@ func TestObjectValidationPropertyRequired(t *testing.T) {
valErrors, err := definition.Validate(val)
assert.Len(t, valErrors, 1, "expected a validation error")
assert.NoError(t, err)
assert.Equal(t, "\"host\" value is required", valErrors[0].Error)
assert.Equal(t, "at '': missing property 'host'", valErrors[0].Error)

}

Expand Down Expand Up @@ -228,8 +202,8 @@ func TestObjectValidationNoAdditionalPropertiesAllowed(t *testing.T) {
valErrors, err := definition.Validate(val)
assert.Len(t, valErrors, 1, "expected a validation error")
assert.NoError(t, err)
assert.Equal(t, "/badActor", valErrors[0].Path, "expected the error to be on badActor")
assert.Equal(t, "additional properties are not allowed", valErrors[0].Error)
assert.Equal(t, "/", valErrors[0].Path, "expected the error to be on badActor")
assert.Equal(t, "at '': additional properties 'badActor' not allowed", valErrors[0].Error)
}

func TestObjectValidationAdditionalPropertiesAreStrings(t *testing.T) {
Expand Down Expand Up @@ -276,5 +250,5 @@ func TestObjectValidationAdditionalPropertiesAreStrings(t *testing.T) {
valErrors, err := definition.Validate(val)
assert.Len(t, valErrors, 1, "expected a validation error")
assert.NoError(t, err)
assert.Equal(t, "type should be string, got boolean", valErrors[0].Error)
assert.Equal(t, "at '/badActor': got boolean, want string", valErrors[0].Error)
}
58 changes: 10 additions & 48 deletions bundle/definition/validators.go
Original file line number Diff line number Diff line change
@@ -1,54 +1,16 @@
package definition

import (
"context"
"encoding/base64"
"fmt"

"github.com/qri-io/jsonpointer"
"github.com/qri-io/jsonschema"
jsonschema "github.com/santhosh-tekuri/jsonschema/v6"
)

// ContentEncoding represents a "custom" Schema property
type ContentEncoding string

// NewContentEncoding allocates a new ContentEncoding validator
func NewContentEncoding() jsonschema.Keyword {
return new(ContentEncoding)
}

func (c ContentEncoding) Validate(propPath string, data interface{}, errs *[]jsonschema.KeyError) {}

func (c ContentEncoding) ValidateKeyword(ctx context.Context, currentState *jsonschema.ValidationState, data interface{}) {
if obj, ok := data.(string); ok {
switch c {
case "base64":
_, err := base64.StdEncoding.DecodeString(obj)
if err != nil {
currentState.AddError(data, fmt.Sprintf("invalid %s value: %s", c, obj))
}
// Add validation support for other encodings as needed
// See https://json-schema.org/latest/json-schema-validation.html#rfc.section.8.3
default:
currentState.AddError(data, fmt.Sprintf("unsupported or invalid contentEncoding type of %s", c))
}
}
}

func (c ContentEncoding) Register(uri string, registry *jsonschema.SchemaRegistry) {}

func (c ContentEncoding) Resolve(pointer jsonpointer.Pointer, uri string) *jsonschema.Schema {
return nil
}

// NewRootSchema returns a jsonschema.RootSchema with any needed custom
// jsonschema.Validators pre-registered
func NewRootSchema() *jsonschema.Schema {
// Register custom validators here
// Note: as of writing, jsonschema doesn't have a stock validator for instances of type `contentEncoding`
// There may be others missing in the library that exist in http://json-schema.org/draft-07/schema#
// and thus, we'd need to create/register them here (if not included upstream)
jsonschema.RegisterKeyword("contentEncoding", NewContentEncoding)
jsonschema.LoadDraft2019_09()
return &jsonschema.Schema{}
// NewCompiler returns a jsonschema.Compiler configured for fully support
// https://json-schema.org/draft/2019-09/schema
func NewCompiler() *jsonschema.Compiler {
c := jsonschema.NewCompiler()
c.DefaultDraft(jsonschema.Draft2019)
c.AssertVocabs()
c.AssertFormat()
c.AssertContent()
return c
}
2 changes: 1 addition & 1 deletion bundle/outputs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestOutputValidate(t *testing.T) {
b.Definitions["output-definition"].Default = 1
err := o.Validate("output", b)
assert.Error(t, err)
assert.Contains(t, err.Error(), `encountered an error validating the default value 1 for output "output": type should be string`)
assert.Contains(t, err.Error(), `encountered an error validating the default value 1 for output "output": at '': got number, want string`)
})

t.Run("successful validation", func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion bundle/parameters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func TestParameterValidate(t *testing.T) {
b.Definitions["param-definition"].Default = 1
err := p.Validate("param", b)
assert.Error(t, err)
assert.Contains(t, err.Error(), `encountered an error validating the default value 1 for parameter "param": type should be string`)
assert.Contains(t, err.Error(), `encountered an error validating the default value 1 for parameter "param": at '': got number, want string`)
})

t.Run("successful validation", func(t *testing.T) {
Expand Down
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/cnabio/cnab-go

go 1.19
go 1.21

toolchain go1.22.3

require (
github.com/Masterminds/semver v1.5.0
Expand All @@ -15,8 +17,7 @@ require (
github.com/oklog/ulid v1.3.1
github.com/opencontainers/go-digest v1.0.0
github.com/pkg/errors v0.9.1
github.com/qri-io/jsonpointer v0.1.1
github.com/qri-io/jsonschema v0.2.2-0.20210723092138-2eb22ee8115f
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
github.com/stretchr/testify v1.7.0
github.com/xeipuuv/gojsonschema v1.2.0
gopkg.in/yaml.v2 v2.4.0
Expand Down Expand Up @@ -110,7 +111,7 @@ require (
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/term v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
Expand Down
Loading