diff --git a/bundle/definition/schema.go b/bundle/definition/schema.go index e756cc34..25269f7c 100644 --- a/bundle/definition/schema.go +++ b/bundle/definition/schema.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/pkg/errors" - "github.com/qri-io/jsonschema" ) type Definitions map[string]*Schema @@ -96,7 +95,7 @@ func (s *Schema) UnmarshalJSON(data []byte) error { // 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 := new(jsonschema.RootSchema) + js := NewRootSchema() if err := js.UnmarshalJSON(data); err != nil { return err } diff --git a/bundle/definition/validation.go b/bundle/definition/validation.go index 4a1511e2..058d9f92 100644 --- a/bundle/definition/validation.go +++ b/bundle/definition/validation.go @@ -4,7 +4,6 @@ import ( "encoding/json" "github.com/pkg/errors" - "github.com/qri-io/jsonschema" ) // ValidationError error represents a validation error @@ -24,7 +23,7 @@ func (s *Schema) Validate(data interface{}) ([]ValidationError, error) { if err != nil { return nil, errors.Wrap(err, "unable to load schema") } - def := new(jsonschema.RootSchema) + def := NewRootSchema() err = json.Unmarshal([]byte(b), def) if err != nil { return nil, errors.Wrap(err, "unable to build schema") diff --git a/bundle/definition/validation_test.go b/bundle/definition/validation_test.go index 809a8c37..b1a471fd 100644 --- a/bundle/definition/validation_test.go +++ b/bundle/definition/validation_test.go @@ -42,6 +42,75 @@ func TestObjectValidationValid(t *testing.T) { assert.NoError(t, err) } +func TestObjectValidationValid_CustomValidator_ContentEncoding_base64(t *testing.T) { + s := `{ + "type": "object", + "properties" : { + "file" : { + "type": "string", + "contentEncoding": "base64" + } + }, + "required" : ["file"] + }` + definition := new(Schema) + err := json.Unmarshal([]byte(s), definition) + require.NoError(t, err, "should have been able to marshal definition") + assert.Equal(t, "object", definition.Type, "type should have been an object") + props := definition.Properties + assert.NotNil(t, props, "should have found properties") + assert.Equal(t, 1, len(props), "should have had a single property") + propSchema, ok := props["file"] + assert.True(t, ok, "should have found file property") + assert.Equal(t, "string", propSchema.Type, "file type should have been a string") + assert.Equal(t, "base64", propSchema.ContentEncoding, "file contentEncoding should have been base64") + + val := struct { + File string `json:"file"` + }{ + File: "SGVsbG8gV29ybGQhCg==", + } + valErrors, err := definition.Validate(val) + assert.NoError(t, err) + assert.Len(t, valErrors, 0, "expected no validation errors") + + invalidVal := struct { + File string `json:"file"` + }{ + File: "SGVsbG8gV29ybGQhCg===", + } + valErrors, err = definition.Validate(invalidVal) + assert.NoError(t, err) + assert.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) +} + func TestObjectValidationInValidMinimum(t *testing.T) { s := `{ "type": "object", diff --git a/bundle/definition/validators.go b/bundle/definition/validators.go new file mode 100644 index 00000000..ddc56a81 --- /dev/null +++ b/bundle/definition/validators.go @@ -0,0 +1,45 @@ +package definition + +import ( + "encoding/base64" + "fmt" + + "github.com/qri-io/jsonschema" +) + +// ContentEncoding represents a "custom" Schema property +type ContentEncoding string + +// NewContentEncoding allocates a new ContentEncoding validator +func NewContentEncoding() jsonschema.Validator { + return new(ContentEncoding) +} + +// Validate implements the Validator interface for ContentEncoding +// which, as of writing, isn't included by default in the jsonschema library we consume +func (c ContentEncoding) Validate(propPath string, data interface{}, errs *[]jsonschema.ValError) { + if obj, ok := data.(string); ok { + switch c { + case "base64": + _, err := base64.StdEncoding.DecodeString(obj) + if err != nil { + jsonschema.AddError(errs, propPath, 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: + jsonschema.AddError(errs, propPath, data, fmt.Sprintf("unsupported or invalid contentEncoding type of %s", c)) + } + } +} + +// NewRootSchema returns a jsonschema.RootSchema with any needed custom +// jsonschema.Validators pre-registered +func NewRootSchema() *jsonschema.RootSchema { + // 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.RegisterValidator("contentEncoding", NewContentEncoding) + return new(jsonschema.RootSchema) +}