Skip to content

Commit

Permalink
feat(json.Marshaler): marshal schemas back to json properly
Browse files Browse the repository at this point in the history
Merge pull request #14 from qri-io/feat_coding
  • Loading branch information
b5 committed Jan 18, 2018
2 parents 05ef7f0 + 8452aaf commit d7cbe4b
Show file tree
Hide file tree
Showing 18 changed files with 366 additions and 84 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
steps:
- checkout
- run: mkdir -p $TEST_RESULTS
- run: go get github.com/jstemmer/go-junit-report github.com/golang/lint/golint
- run: go get github.com/jstemmer/go-junit-report github.com/golang/lint/golint github.com/sergi/go-diff/diffmatchpatch
- run:
name: Install deps
command: >
Expand Down
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@
[![CI](https://img.shields.io/circleci/project/github/qri-io/jsonschema.svg?style=flat-square)](https://circleci.com/gh/qri-io/jsonschema)
[![Go Report Card](https://goreportcard.com/badge/github.com/qri-io/jsonschema)](https://goreportcard.com/report/github.com/qri-io/jsonschema)

### 🚧🚧 Under Construction 🚧🚧
golang implementation of the [JSON Schema Specification](http://json-schema.org/), which lets you write JSON that validates some other json. Rad.

### 🚧 New Package Alert 🚧
This is a very new implementation and hasn't been tested by things that aren't computers. If you run into issues, please file an... issue, and we'll all work together to address it.

### Package Features

* Encode schemas back to JSON
* Supply Your own Custom Validators
* Uses Standard Go idioms

### Getting Involved

We would love involvement from more people! If you notice any errors or would
Expand Down Expand Up @@ -97,3 +105,55 @@ It involves two steps that should happen _before_ allocating any RootSchema inst
1. create a custom type that implements the `Validator` interface
2. call RegisterValidator with the keyword you'd like to detect in JSON, and a `ValMaker` function.


```go
package main

import (
"encoding/json"
"fmt"
"github.com/qri-io/jsonschema"
)

// your custom validator
type IsFoo bool

// newIsFoo is a jsonschama.ValMaker
func newIsFoo() jsonschema.Validator {
return new(IsFoo)
}

// Validate implements jsonschema.Validator
func (f IsFoo) Validate(data interface{}) error {
if str, ok := data.(string); ok {
if str != "foo" {
return fmt.Errorf("'%s' is not foo. It should be foo. plz make '%s' == foo. plz", str, str)
}
}
return nil
}

func main() {
// register a custom validator by supplying a function
// that creates new instances of your Validator.
jsonschema.RegisterValidator("foo", newIsFoo)

schBytes := []byte(`{ "foo": true }`)

// parse a schema that uses your custom keyword
rs := new(jsonschema.RootSchema)
if err := json.Unmarshal(schBytes, rs); err != nil {
// Real programs handle errors.
panic(err)
}

// validate some JSON
err := rs.ValidateBytes([]byte(`"bar"`))

// print le error
fmt.Println(err.Error())

// Output: 'bar' is not foo. It should be foo. plz make 'bar' == foo. plz
}
```

38 changes: 22 additions & 16 deletions keywords.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ func DataType(data interface{}) string {
// String values MUST be one of the six primitive types ("null", "boolean", "object", "array", "number", or "string"), or
// "integer" which matches any number with a zero fractional part.
// An instance validates if and only if the instance is in any of the sets listed for this keyword.
type tipe []string
type tipe struct {
strVal bool // set to true if tipe decoded from a string, false if an array
vals []string
}

func newTipe() Validator {
return &tipe{}
Expand All @@ -57,17 +60,17 @@ func newTipe() Validator {
// Validate checks to see if input data satisfies the type constraint
func (t tipe) Validate(data interface{}) error {
jt := DataType(data)
for _, typestr := range t {
for _, typestr := range t.vals {
if jt == typestr || jt == "integer" && typestr == "number" {
return nil
}
}
if len(t) == 1 {
return fmt.Errorf(`expected "%v" to be of type %s`, data, t[0])
if len(t.vals) == 1 {
return fmt.Errorf(`expected "%v" to be of type %s`, data, t.vals[0])
}

str := ""
for _, ts := range t {
for _, ts := range t.vals {
str += ts + ","
}
return fmt.Errorf(`expected "%v" to be one of type: %s`, data, str[:len(str)-1])
Expand All @@ -79,27 +82,27 @@ func (t tipe) JSONProp(name string) interface{} {
if err != nil {
return nil
}
if idx > len(t) || idx < 0 {
if idx > len(t.vals) || idx < 0 {
return nil
}
return t[idx]
return t.vals[idx]
}

// UnmarshalJSON implements the json.Unmarshaler interface for tipe
func (t *tipe) UnmarshalJSON(data []byte) error {
var single string
if err := json.Unmarshal(data, &single); err == nil {
*t = tipe{single}
*t = tipe{strVal: true, vals: []string{single}}
} else {
var set []string
if err := json.Unmarshal(data, &set); err == nil {
*t = tipe(set)
*t = tipe{vals: set}
} else {
return err
}
}

for _, pr := range *t {
for _, pr := range t.vals {
if !primitiveTypes[pr] {
return fmt.Errorf(`"%s" is not a valid type`, pr)
}
Expand All @@ -109,12 +112,10 @@ func (t *tipe) UnmarshalJSON(data []byte) error {

// MarshalJSON implements the json.Marshaler interface for tipe
func (t tipe) MarshalJSON() ([]byte, error) {
if len(t) == 1 {
return json.Marshal(t[0])
} else if len(t) > 1 {
return json.Marshal([]string(t))
if t.strVal {
return json.Marshal(t.vals[0])
}
return []byte(`""`), nil
return json.Marshal(t.vals)
}

// enum validates successfully against this keyword if its value is equal to one of the
Expand Down Expand Up @@ -170,7 +171,7 @@ func (e enum) JSONChildren() (res map[string]JSONPather) {
// konst MAY be of any type, including null.
// An instance validates successfully against this keyword if its
// value is equal to the value of the keyword.
type konst []byte
type konst json.RawMessage

func newKonst() Validator {
return &konst{}
Expand Down Expand Up @@ -204,3 +205,8 @@ func (c *konst) UnmarshalJSON(data []byte) error {
*c = data
return nil
}

// MarshalJSON implements json.Marshaler for konst
func (c konst) MarshalJSON() ([]byte, error) {
return json.Marshal(json.RawMessage(c))
}
83 changes: 44 additions & 39 deletions keywords_booleans.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import (
"strconv"
)

// AllOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
// allOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
// An instance validates successfully against this keyword if it validates successfully against all schemas defined by this keyword's value.
type AllOf []*Schema
type allOf []*Schema

func newAllOf() Validator {
return &AllOf{}
return &allOf{}
}

// Validate implements the validator interface for AllOf
func (a AllOf) Validate(data interface{}) error {
// Validate implements the validator interface for allOf
func (a allOf) Validate(data interface{}) error {
for i, sch := range a {
if err := sch.Validate(data); err != nil {
return fmt.Errorf("allOf element %d error: %s", i, err.Error())
Expand All @@ -24,8 +24,8 @@ func (a AllOf) Validate(data interface{}) error {
return nil
}

// JSONProp implements JSON property name indexing for AllOf
func (a AllOf) JSONProp(name string) interface{} {
// JSONProp implements JSON property name indexing for allOf
func (a allOf) JSONProp(name string) interface{} {
idx, err := strconv.Atoi(name)
if err != nil {
return nil
Expand All @@ -36,26 +36,26 @@ func (a AllOf) JSONProp(name string) interface{} {
return a[idx]
}

// JSONChildren implements the JSONContainer interface for AllOf
func (a AllOf) JSONChildren() (res map[string]JSONPather) {
// JSONChildren implements the JSONContainer interface for allOf
func (a allOf) JSONChildren() (res map[string]JSONPather) {
res = map[string]JSONPather{}
for i, sch := range a {
res[strconv.Itoa(i)] = sch
}
return
}

// AnyOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
// anyOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
// An instance validates successfully against this keyword if it validates successfully against at
// least one schema defined by this keyword's value.
type AnyOf []*Schema
type anyOf []*Schema

func newAnyOf() Validator {
return &AnyOf{}
return &anyOf{}
}

// Validate implements the validator interface for AnyOf
func (a AnyOf) Validate(data interface{}) error {
// Validate implements the validator interface for anyOf
func (a anyOf) Validate(data interface{}) error {
for _, sch := range a {
if err := sch.Validate(data); err == nil {
return nil
Expand All @@ -64,8 +64,8 @@ func (a AnyOf) Validate(data interface{}) error {
return fmt.Errorf("value did not match any specified anyOf schemas: %v", data)
}

// JSONProp implements JSON property name indexing for AnyOf
func (a AnyOf) JSONProp(name string) interface{} {
// JSONProp implements JSON property name indexing for anyOf
func (a anyOf) JSONProp(name string) interface{} {
idx, err := strconv.Atoi(name)
if err != nil {
return nil
Expand All @@ -76,25 +76,25 @@ func (a AnyOf) JSONProp(name string) interface{} {
return a[idx]
}

// JSONChildren implements the JSONContainer interface for AnyOf
func (a AnyOf) JSONChildren() (res map[string]JSONPather) {
// JSONChildren implements the JSONContainer interface for anyOf
func (a anyOf) JSONChildren() (res map[string]JSONPather) {
res = map[string]JSONPather{}
for i, sch := range a {
res[strconv.Itoa(i)] = sch
}
return
}

// OneOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
// oneOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
// An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value.
type OneOf []*Schema
type oneOf []*Schema

func newOneOf() Validator {
return &OneOf{}
return &oneOf{}
}

// Validate implements the validator interface for OneOf
func (o OneOf) Validate(data interface{}) error {
// Validate implements the validator interface for oneOf
func (o oneOf) Validate(data interface{}) error {
matched := false
for _, sch := range o {
if err := sch.Validate(data); err == nil {
Expand All @@ -110,8 +110,8 @@ func (o OneOf) Validate(data interface{}) error {
return nil
}

// JSONProp implements JSON property name indexing for OneOf
func (o OneOf) JSONProp(name string) interface{} {
// JSONProp implements JSON property name indexing for oneOf
func (o oneOf) JSONProp(name string) interface{} {
idx, err := strconv.Atoi(name)
if err != nil {
return nil
Expand All @@ -122,26 +122,26 @@ func (o OneOf) JSONProp(name string) interface{} {
return o[idx]
}

// JSONChildren implements the JSONContainer interface for OneOf
func (o OneOf) JSONChildren() (res map[string]JSONPather) {
// JSONChildren implements the JSONContainer interface for oneOf
func (o oneOf) JSONChildren() (res map[string]JSONPather) {
res = map[string]JSONPather{}
for i, sch := range o {
res[strconv.Itoa(i)] = sch
}
return
}

// Not MUST be a valid JSON Schema.
// not MUST be a valid JSON Schema.
// An instance is valid against this keyword if it fails to validate successfully against the schema defined
// by this keyword.
type Not Schema
type not Schema

func newNot() Validator {
return &Not{}
return &not{}
}

// Validate implements the validator interface for Not
func (n *Not) Validate(data interface{}) error {
// Validate implements the validator interface for not
func (n *not) Validate(data interface{}) error {
sch := Schema(*n)
if sch.Validate(data) == nil {
// TODO - make this error actually make sense
Expand All @@ -150,26 +150,31 @@ func (n *Not) Validate(data interface{}) error {
return nil
}

// JSONProp implements JSON property name indexing for Not
func (n Not) JSONProp(name string) interface{} {
// JSONProp implements JSON property name indexing for not
func (n not) JSONProp(name string) interface{} {
return Schema(n).JSONProp(name)
}

// JSONChildren implements the JSONContainer interface for Not
func (n Not) JSONChildren() (res map[string]JSONPather) {
// JSONChildren implements the JSONContainer interface for not
func (n not) JSONChildren() (res map[string]JSONPather) {
if n.Ref != "" {
s := Schema(n)
return map[string]JSONPather{"$ref": &s}
}
return Schema(n).JSONChildren()
}

// UnmarshalJSON implements the json.Unmarshaler interface for Not
func (n *Not) UnmarshalJSON(data []byte) error {
// UnmarshalJSON implements the json.Unmarshaler interface for not
func (n *not) UnmarshalJSON(data []byte) error {
var sch Schema
if err := json.Unmarshal(data, &sch); err != nil {
return err
}
*n = Not(sch)
*n = not(sch)
return nil
}

// MarshalJSON implements json.Marshaller for not
func (n not) MarshalJSON() ([]byte, error) {
return json.Marshal(Schema(n))
}
Loading

0 comments on commit d7cbe4b

Please sign in to comment.