...

Source file src/github.com/qri-io/jsonschema/schema_test.go

Documentation: github.com/qri-io/jsonschema

     1  package jsonschema
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"path/filepath"
    10  	"strconv"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/sergi/go-diff/diffmatchpatch"
    15  )
    16  
    17  func ExampleBasic() {
    18  	ctx := context.Background()
    19  	var schemaData = []byte(`{
    20  	"title": "Person",
    21  	"type": "object",
    22  	"$id": "https://qri.io/schema/",
    23  	"$comment" : "sample comment",
    24  	"properties": {
    25  	    "firstName": {
    26  	        "type": "string"
    27  	    },
    28  	    "lastName": {
    29  	        "type": "string"
    30  	    },
    31  	    "age": {
    32  	        "description": "Age in years",
    33  	        "type": "integer",
    34  	        "minimum": 0
    35  	    },
    36  	    "friends": {
    37  	    	"type" : "array",
    38  	    	"items" : { "title" : "REFERENCE", "$ref" : "#" }
    39  	    }
    40  	},
    41  	"required": ["firstName", "lastName"]
    42  	}`)
    43  
    44  	rs := &Schema{}
    45  	if err := json.Unmarshal(schemaData, rs); err != nil {
    46  		panic("unmarshal schema: " + err.Error())
    47  	}
    48  
    49  	var valid = []byte(`{
    50  		"firstName" : "George",
    51  		"lastName" : "Michael"
    52  		}`)
    53  	errs, err := rs.ValidateBytes(ctx, valid)
    54  	if err != nil {
    55  		panic(err)
    56  	}
    57  
    58  	if len(errs) > 0 {
    59  		fmt.Println(errs[0].Error())
    60  	}
    61  
    62  	var invalidPerson = []byte(`{
    63  		"firstName" : "Prince"
    64  		}`)
    65  
    66  	errs, err = rs.ValidateBytes(ctx, invalidPerson)
    67  	if err != nil {
    68  		panic(err)
    69  	}
    70  	if len(errs) > 0 {
    71  		fmt.Println(errs[0].Error())
    72  	}
    73  
    74  	var invalidFriend = []byte(`{
    75  		"firstName" : "Jay",
    76  		"lastName" : "Z",
    77  		"friends" : [{
    78  			"firstName" : "Nas"
    79  			}]
    80  		}`)
    81  	errs, err = rs.ValidateBytes(ctx, invalidFriend)
    82  	if err != nil {
    83  		panic(err)
    84  	}
    85  	if len(errs) > 0 {
    86  		fmt.Println(errs[0].Error())
    87  	}
    88  
    89  	// Output: /: {"firstName":"Prince... "lastName" value is required
    90  	// /friends/0: {"firstName":"Nas"} "lastName" value is required
    91  }
    92  
    93  func TestTopLevelType(t *testing.T) {
    94  	schemaObject := []byte(`{
    95      "title": "Car",
    96      "type": "object",
    97      "properties": {
    98          "color": {
    99              "type": "string"
   100          }
   101      },
   102      "required": ["color"]
   103  }`)
   104  	rs := &Schema{}
   105  	if err := json.Unmarshal(schemaObject, rs); err != nil {
   106  		panic("unmarshal schema: " + err.Error())
   107  	}
   108  	if rs.TopLevelType() != "object" {
   109  		t.Errorf("error: schemaObject should be an object")
   110  	}
   111  
   112  	schemaArray := []byte(`{
   113      "title": "Cities",
   114      "type": "array",
   115      "items" : { "title" : "REFERENCE", "$ref" : "#" }
   116  }`)
   117  	rs = &Schema{}
   118  	if err := json.Unmarshal(schemaArray, rs); err != nil {
   119  		panic("unmarshal schema: " + err.Error())
   120  	}
   121  	if rs.TopLevelType() != "array" {
   122  		t.Errorf("error: schemaArray should be an array")
   123  	}
   124  
   125  	schemaUnknown := []byte(`{
   126      "title": "Typeless",
   127      "items" : { "title" : "REFERENCE", "$ref" : "#" }
   128  }`)
   129  	rs = &Schema{}
   130  	if err := json.Unmarshal(schemaUnknown, rs); err != nil {
   131  		panic("unmarshal schema: " + err.Error())
   132  	}
   133  	if rs.TopLevelType() != "unknown" {
   134  		t.Errorf("error: schemaUnknown should have unknown type")
   135  	}
   136  }
   137  
   138  func TestParseUrl(t *testing.T) {
   139  	// Easy case, id is a standard URL
   140  	schemaObject := []byte(`{
   141      "title": "Car",
   142      "type": "object",
   143      "$id": "http://example.com/root.json"
   144  }`)
   145  	rs := &Schema{}
   146  	if err := json.Unmarshal(schemaObject, rs); err != nil {
   147  		panic("unmarshal schema: " + err.Error())
   148  	}
   149  
   150  	// Tricky case, id is only a URL fragment
   151  	schemaObject = []byte(`{
   152      "title": "Car",
   153      "type": "object",
   154      "$id": "#/properites/firstName"
   155  }`)
   156  	rs = &Schema{}
   157  	if err := json.Unmarshal(schemaObject, rs); err != nil {
   158  		panic("unmarshal schema: " + err.Error())
   159  	}
   160  
   161  	// Another tricky case, id is only an empty fragment
   162  	schemaObject = []byte(`{
   163      "title": "Car",
   164      "type": "object",
   165      "$id": "#"
   166  }`)
   167  	rs = &Schema{}
   168  	if err := json.Unmarshal(schemaObject, rs); err != nil {
   169  		panic("unmarshal schema: " + err.Error())
   170  	}
   171  }
   172  
   173  func TestMust(t *testing.T) {
   174  	defer func() {
   175  		if r := recover(); r != nil {
   176  			if err, ok := r.(error); ok {
   177  				if err.Error() != "unexpected end of JSON input" {
   178  					t.Errorf("expected panic error to equal: %s", "unexpected end of JSON input")
   179  				}
   180  			} else {
   181  				t.Errorf("must paniced with a non-error")
   182  			}
   183  		} else {
   184  			t.Errorf("expected invalid call to Must to panic")
   185  		}
   186  	}()
   187  
   188  	// Valid call to Must shouldn't panic
   189  	rs := Must(`{}`)
   190  	if rs == nil {
   191  		t.Errorf("expected parse of empty schema to return *RootSchema, got nil")
   192  		return
   193  	}
   194  
   195  	// This should panic, checked in defer above
   196  	Must(``)
   197  }
   198  
   199  func TestDraft3(t *testing.T) {
   200  	runJSONTests(t, []string{
   201  		"testdata/draft3/additionalProperties.json",
   202  		"testdata/draft3/default.json",
   203  		"testdata/draft3/format.json",
   204  		"testdata/draft3/items.json",
   205  		"testdata/draft3/maxItems.json",
   206  		"testdata/draft3/maxLength.json",
   207  		"testdata/draft3/minItems.json",
   208  		"testdata/draft3/minLength.json",
   209  		"testdata/draft3/pattern.json",
   210  		"testdata/draft3/patternProperties.json",
   211  		"testdata/draft3/properties.json",
   212  		"testdata/draft3/uniqueItems.json",
   213  
   214  		// disabled due to changes in spec
   215  		// "testdata/draft3/disallow.json",
   216  		// "testdata/draft3/divisibleBy.json",
   217  		// "testdata/draft3/enum.json",
   218  		// "testdata/draft3/extends.json",
   219  		// "testdata/draft3/maximum.json",
   220  		// "testdata/draft3/minimum.json",
   221  		// "testdata/draft3/ref.json",
   222  		// "testdata/draft3/refRemote.json",
   223  		// "testdata/draft3/required.json",
   224  		// "testdata/draft3/type.json",
   225  		// "testdata/draft3/optional/format.json",
   226  		// "testdata/draft3/optional/zeroTerminatedFloats.json",
   227  
   228  		// TODO(arqu): implement this
   229  		// "testdata/draft3/additionalItems.json",
   230  		// "testdata/draft3/dependencies.json",
   231  
   232  		// wont fix
   233  		// "testdata/draft3/optional/bignum.json",
   234  		// "testdata/draft3/optional/ecmascript-regex.json",
   235  	})
   236  }
   237  
   238  func TestDraft4(t *testing.T) {
   239  	runJSONTests(t, []string{
   240  		"testdata/draft4/additionalItems.json",
   241  		"testdata/draft4/allOf.json",
   242  		"testdata/draft4/anyOf.json",
   243  		"testdata/draft4/default.json",
   244  		"testdata/draft4/enum.json",
   245  		"testdata/draft4/format.json",
   246  		"testdata/draft4/maxItems.json",
   247  		"testdata/draft4/maxLength.json",
   248  		"testdata/draft4/maxProperties.json",
   249  		"testdata/draft4/minItems.json",
   250  		"testdata/draft4/minLength.json",
   251  		"testdata/draft4/minProperties.json",
   252  		"testdata/draft4/multipleOf.json",
   253  		"testdata/draft4/not.json",
   254  		"testdata/draft4/oneOf.json",
   255  		"testdata/draft4/optional/format.json",
   256  		"testdata/draft4/pattern.json",
   257  		"testdata/draft4/patternProperties.json",
   258  		"testdata/draft4/properties.json",
   259  		"testdata/draft4/required.json",
   260  		"testdata/draft4/type.json",
   261  		"testdata/draft4/uniqueItems.json",
   262  
   263  		// disabled due to changes in spec
   264  		// "testdata/draft4/maximum.json",
   265  		// "testdata/draft4/minimum.json",
   266  		// "testdata/draft4/ref.json",
   267  		// "testdata/draft4/refRemote.json",
   268  		// "testdata/draft4/optional/zeroTerminatedFloats.json",
   269  
   270  		// TODO(arqu): implement this
   271  		// "testdata/draft4/definitions.json",
   272  		// "testdata/draft4/dependencies.json",
   273  		// "testdata/draft4/items.json",
   274  
   275  		// wont fix
   276  		// "testdata/draft4/additionalProperties.json",
   277  		// "testdata/draft4/optional/bignum.json",
   278  		// "testdata/draft4/optional/ecmascript-regex.json",
   279  	})
   280  }
   281  
   282  func TestDraft6(t *testing.T) {
   283  	runJSONTests(t, []string{
   284  		"testdata/draft6/additionalItems.json",
   285  		"testdata/draft6/allOf.json",
   286  		"testdata/draft6/anyOf.json",
   287  		"testdata/draft6/boolean_schema.json",
   288  		"testdata/draft6/const.json",
   289  		"testdata/draft6/contains.json",
   290  		"testdata/draft6/default.json",
   291  		"testdata/draft6/enum.json",
   292  		"testdata/draft6/exclusiveMaximum.json",
   293  		"testdata/draft6/exclusiveMinimum.json",
   294  		"testdata/draft6/format.json",
   295  		"testdata/draft6/maximum.json",
   296  		"testdata/draft6/maxItems.json",
   297  		"testdata/draft6/maxLength.json",
   298  		"testdata/draft6/maxProperties.json",
   299  		"testdata/draft6/minimum.json",
   300  		"testdata/draft6/minItems.json",
   301  		"testdata/draft6/minLength.json",
   302  		"testdata/draft6/minProperties.json",
   303  		"testdata/draft6/multipleOf.json",
   304  		"testdata/draft6/not.json",
   305  		"testdata/draft6/oneOf.json",
   306  		"testdata/draft6/pattern.json",
   307  		"testdata/draft6/patternProperties.json",
   308  		"testdata/draft6/properties.json",
   309  		"testdata/draft6/propertyNames.json",
   310  		"testdata/draft6/required.json",
   311  		"testdata/draft6/type.json",
   312  		"testdata/draft6/uniqueItems.json",
   313  
   314  		"testdata/draft6/optional/format.json",
   315  		"testdata/draft6/optional/zeroTerminatedFloats.json",
   316  
   317  		// TODO(arqu): implement this
   318  		// "testdata/draft6/definitions.json",
   319  		// "testdata/draft6/dependencies.json",
   320  		// "testdata/draft6/items.json",
   321  		// "testdata/draft6/ref.json",
   322  
   323  		// wont fix
   324  		// "testdata/draft6/additionalProperties.json",
   325  		// "testdata/draft6/refRemote.json",
   326  		// "testdata/draft6/optional/bignum.json",
   327  		// "testdata/draft6/optional/ecmascript-regex.json",
   328  	})
   329  }
   330  
   331  func TestDraft7(t *testing.T) {
   332  	path := "testdata/draft-07_schema.json"
   333  	data, err := ioutil.ReadFile(path)
   334  	if err != nil {
   335  		t.Errorf("error reading %s: %s", path, err.Error())
   336  		return
   337  	}
   338  
   339  	rsch := &Schema{}
   340  	if err := json.Unmarshal(data, rsch); err != nil {
   341  		t.Errorf("error unmarshaling schema: %s", err.Error())
   342  		return
   343  	}
   344  
   345  	runJSONTests(t, []string{
   346  		"testdata/draft7/additionalItems.json",
   347  		"testdata/draft7/allOf.json",
   348  		"testdata/draft7/anyOf.json",
   349  		"testdata/draft7/boolean_schema.json",
   350  		"testdata/draft7/const.json",
   351  		"testdata/draft7/contains.json",
   352  		"testdata/draft7/default.json",
   353  		"testdata/draft7/enum.json",
   354  		"testdata/draft7/exclusiveMaximum.json",
   355  		"testdata/draft7/exclusiveMinimum.json",
   356  		"testdata/draft7/format.json",
   357  		"testdata/draft7/if-then-else.json",
   358  		"testdata/draft7/maximum.json",
   359  		"testdata/draft7/maxItems.json",
   360  		"testdata/draft7/maxLength.json",
   361  		"testdata/draft7/maxProperties.json",
   362  		"testdata/draft7/minimum.json",
   363  		"testdata/draft7/minItems.json",
   364  		"testdata/draft7/minLength.json",
   365  		"testdata/draft7/minProperties.json",
   366  		"testdata/draft7/multipleOf.json",
   367  		"testdata/draft7/not.json",
   368  		"testdata/draft7/oneOf.json",
   369  		"testdata/draft7/pattern.json",
   370  		"testdata/draft7/patternProperties.json",
   371  		"testdata/draft7/properties.json",
   372  		"testdata/draft7/propertyNames.json",
   373  		"testdata/draft7/required.json",
   374  		"testdata/draft7/type.json",
   375  		"testdata/draft7/uniqueItems.json",
   376  
   377  		"testdata/draft7/optional/zeroTerminatedFloats.json",
   378  		"testdata/draft7/optional/format/date-time.json",
   379  		"testdata/draft7/optional/format/date.json",
   380  		"testdata/draft7/optional/format/email.json",
   381  		"testdata/draft7/optional/format/hostname.json",
   382  		"testdata/draft7/optional/format/idn-email.json",
   383  		"testdata/draft7/optional/format/idn-hostname.json",
   384  		"testdata/draft7/optional/format/ipv4.json",
   385  		"testdata/draft7/optional/format/ipv6.json",
   386  		"testdata/draft7/optional/format/iri-reference.json",
   387  		"testdata/draft7/optional/format/json-pointer.json",
   388  		"testdata/draft7/optional/format/regex.json",
   389  		"testdata/draft7/optional/format/relative-json-pointer.json",
   390  		"testdata/draft7/optional/format/time.json",
   391  		"testdata/draft7/optional/format/uri-reference.json",
   392  		"testdata/draft7/optional/format/uri-template.json",
   393  		"testdata/draft7/optional/format/uri.json",
   394  
   395  		// TODO(arqu): implement this
   396  		// "testdata/draft7/definitions.json",
   397  		// "testdata/draft7/dependencies.json",
   398  		// "testdata/draft7/items.json",
   399  		// "testdata/draft7/ref.json",
   400  
   401  		// wont fix
   402  		// "testdata/draft7/additionalProperties.json",
   403  		// "testdata/draft7/refRemote.json",
   404  		// "testdata/draft7/optional/bignum.json",
   405  		// "testdata/draft7/optional/content.json",
   406  		// "testdata/draft7/optional/ecmascript-regex.json",
   407  		// "testdata/draft7/optional/format/iri.json",
   408  	})
   409  }
   410  
   411  func TestDraft2019_09(t *testing.T) {
   412  	path := "testdata/draft2019-09_schema.json"
   413  	data, err := ioutil.ReadFile(path)
   414  	if err != nil {
   415  		t.Errorf("error reading %s: %s", path, err.Error())
   416  		return
   417  	}
   418  
   419  	rsch := &Schema{}
   420  	if err := json.Unmarshal(data, rsch); err != nil {
   421  		t.Errorf("error unmarshaling schema: %s", err.Error())
   422  		return
   423  	}
   424  
   425  	runJSONTests(t, []string{
   426  		"testdata/draft2019-09/additionalItems.json",
   427  		// "testdata/draft2019-09/additionalProperties.json",
   428  		"testdata/draft2019-09/allOf.json",
   429  		"testdata/draft2019-09/anchor.json",
   430  		"testdata/draft2019-09/anyOf.json",
   431  		"testdata/draft2019-09/boolean_schema.json",
   432  		"testdata/draft2019-09/const.json",
   433  		"testdata/draft2019-09/contains.json",
   434  		"testdata/draft2019-09/default.json",
   435  		"testdata/draft2019-09/defs.json",
   436  		"testdata/draft2019-09/dependentRequired.json",
   437  		"testdata/draft2019-09/dependentSchemas.json",
   438  		"testdata/draft2019-09/enum.json",
   439  		"testdata/draft2019-09/exclusiveMaximum.json",
   440  		"testdata/draft2019-09/exclusiveMinimum.json",
   441  		"testdata/draft2019-09/format.json",
   442  		"testdata/draft2019-09/if-then-else.json",
   443  		"testdata/draft2019-09/items.json",
   444  		"testdata/draft2019-09/maximum.json",
   445  		"testdata/draft2019-09/maxItems.json",
   446  		"testdata/draft2019-09/maxLength.json",
   447  		"testdata/draft2019-09/maxProperties.json",
   448  		"testdata/draft2019-09/minimum.json",
   449  		"testdata/draft2019-09/minItems.json",
   450  		"testdata/draft2019-09/minLength.json",
   451  		"testdata/draft2019-09/minProperties.json",
   452  		"testdata/draft2019-09/multipleOf.json",
   453  		"testdata/draft2019-09/not.json",
   454  		"testdata/draft2019-09/oneOf.json",
   455  		"testdata/draft2019-09/pattern.json",
   456  		"testdata/draft2019-09/patternProperties.json",
   457  		"testdata/draft2019-09/properties.json",
   458  		"testdata/draft2019-09/propertyNames.json",
   459  		"testdata/draft2019-09/ref.json",
   460  		"testdata/draft2019-09/required.json",
   461  		"testdata/draft2019-09/type.json",
   462  		// "testdata/draft2019-09/unevaluatedProperties.json",
   463  		// "testdata/draft2019-09/unevaluatedItems.json",
   464  		"testdata/draft2019-09/uniqueItems.json",
   465  
   466  		"testdata/draft2019-09/optional/zeroTerminatedFloats.json",
   467  		"testdata/draft2019-09/optional/format/date-time.json",
   468  		"testdata/draft2019-09/optional/format/date.json",
   469  		"testdata/draft2019-09/optional/format/email.json",
   470  		"testdata/draft2019-09/optional/format/hostname.json",
   471  		"testdata/draft2019-09/optional/format/idn-email.json",
   472  		"testdata/draft2019-09/optional/format/idn-hostname.json",
   473  		"testdata/draft2019-09/optional/format/ipv4.json",
   474  		"testdata/draft2019-09/optional/format/ipv6.json",
   475  		"testdata/draft2019-09/optional/format/iri-reference.json",
   476  		"testdata/draft2019-09/optional/format/json-pointer.json",
   477  		"testdata/draft2019-09/optional/format/regex.json",
   478  		"testdata/draft2019-09/optional/format/relative-json-pointer.json",
   479  		"testdata/draft2019-09/optional/format/time.json",
   480  		"testdata/draft2019-09/optional/format/uri-reference.json",
   481  		"testdata/draft2019-09/optional/format/uri-template.json",
   482  		"testdata/draft2019-09/optional/format/uri.json",
   483  
   484  		// TODO(arqu): investigate further, test is modified because
   485  		// if does not formally validate and simply returns
   486  		// when no then or else is present
   487  		"testdata/draft2019-09/unevaluatedProperties_modified.json",
   488  		"testdata/draft2019-09/unevaluatedItems_modified.json",
   489  
   490  		// TODO(arqu): investigate further, test is modified because of inconsistent
   491  		// expectations from spec on how evaluated properties are tracked between
   492  		// additionalProperties and unevaluatedProperties
   493  		"testdata/draft2019-09/additionalProperties_modified.json",
   494  
   495  		// wont fix
   496  		// "testdata/draft2019-09/refRemote.json",
   497  		// "testdata/draft2019-09/optional/bignum.json",
   498  		// "testdata/draft2019-09/optional/content.json",
   499  		// "testdata/draft2019-09/optional/ecmascript-regex.json",
   500  		// "testdata/draft2019-09/optional/refOfUnknownKeyword.json",
   501  
   502  		// TODO(arqu): iri fails on IPV6 not having [] around the address
   503  		// which was a legal format in draft7
   504  		// introduced: https://github.com/json-schema-org/JSON-Schema-Test-Suite/commit/2146b02555b163da40ae98e60bf36b2c2f8d4bd0#diff-b2ca98716e146559819bc49635a149a9
   505  		// relevant RFC: https://tools.ietf.org/html/rfc3986#section-3.2.2
   506  		// relevant 'net/url' package discussion: https://github.com/golang/go/issues/31024
   507  		// "testdata/draft2019-09/optional/format/iri.json",
   508  	})
   509  }
   510  
   511  // TestSet is a json-based set of tests
   512  // JSON-Schema comes with a lovely JSON-based test suite:
   513  // https://github.com/json-schema-org/JSON-Schema-Test-Suite
   514  type TestSet struct {
   515  	Description string     `json:"description"`
   516  	Schema      *Schema    `json:"schema"`
   517  	Tests       []TestCase `json:"tests"`
   518  }
   519  
   520  type TestCase struct {
   521  	Description string      `json:"description"`
   522  	Data        interface{} `json:"data"`
   523  	Valid       bool        `json:"valid"`
   524  }
   525  
   526  func runJSONTests(t *testing.T, testFilepaths []string) {
   527  	tests := 0
   528  	passed := 0
   529  	ctx := context.Background()
   530  	for _, path := range testFilepaths {
   531  		t.Run(path, func(t *testing.T) {
   532  			base := filepath.Base(path)
   533  			testSets := []*TestSet{}
   534  			data, err := ioutil.ReadFile(path)
   535  			if err != nil {
   536  				t.Errorf("error loading test file: %s", err.Error())
   537  				return
   538  			}
   539  
   540  			if err := json.Unmarshal(data, &testSets); err != nil {
   541  				t.Errorf("error unmarshaling test set %s from JSON: %s", base, err.Error())
   542  				return
   543  			}
   544  
   545  			for _, ts := range testSets {
   546  				sc := ts.Schema
   547  				for i, c := range ts.Tests {
   548  					tests++
   549  					validationState := sc.Validate(ctx, c.Data)
   550  					if validationState.IsValid() != c.Valid {
   551  						t.Errorf("%s: %s test case %d: %s. error: %s", base, ts.Description, i, c.Description, *validationState.Errs)
   552  					} else {
   553  						passed++
   554  					}
   555  				}
   556  			}
   557  		})
   558  	}
   559  	t.Logf("%d/%d tests passed", passed, tests)
   560  }
   561  
   562  func TestDataType(t *testing.T) {
   563  	type customObject struct{}
   564  	type customNumber float64
   565  
   566  	cases := []struct {
   567  		data   interface{}
   568  		expect string
   569  	}{
   570  		{nil, "null"},
   571  		{float64(4), "integer"},
   572  		{float64(4.0), "integer"},
   573  		{float64(4.5), "number"},
   574  		{customNumber(4.5), "number"},
   575  		{true, "boolean"},
   576  		{"foo", "string"},
   577  		{[]interface{}{}, "array"},
   578  		{[0]interface{}{}, "array"},
   579  		{map[string]interface{}{}, "object"},
   580  		{struct{}{}, "object"},
   581  		{customObject{}, "object"},
   582  		{int8(42), "unknown"},
   583  		// special cases which should pass with type hints
   584  		{"true", "boolean"},
   585  		{4.0, "number"},
   586  	}
   587  
   588  	for i, c := range cases {
   589  		got := DataTypeWithHint(c.data, c.expect)
   590  		if got != c.expect {
   591  			t.Errorf("case %d result mismatch. expected: '%s', got: '%s'", i, c.expect, got)
   592  		}
   593  	}
   594  }
   595  
   596  func TestJSONCoding(t *testing.T) {
   597  	cases := []string{
   598  		"testdata/coding/false.json",
   599  		"testdata/coding/true.json",
   600  		"testdata/coding/std.json",
   601  		"testdata/coding/booleans.json",
   602  		"testdata/coding/conditionals.json",
   603  		"testdata/coding/numeric.json",
   604  		"testdata/coding/objects.json",
   605  		"testdata/coding/strings.json",
   606  	}
   607  
   608  	for i, c := range cases {
   609  		data, err := ioutil.ReadFile(c)
   610  		if err != nil {
   611  			t.Errorf("case %d error reading file: %s", i, err.Error())
   612  			continue
   613  		}
   614  
   615  		rs := &Schema{}
   616  		if err := json.Unmarshal(data, rs); err != nil {
   617  			t.Errorf("case %d error unmarshaling from json: %s", i, err.Error())
   618  			continue
   619  		}
   620  
   621  		output, err := json.MarshalIndent(rs, "", "  ")
   622  		if err != nil {
   623  			t.Errorf("case %d error marshaling to JSON: %s", i, err.Error())
   624  			continue
   625  		}
   626  
   627  		if !bytes.Equal(data, output) {
   628  			dmp := diffmatchpatch.New()
   629  			diffs := dmp.DiffMain(string(data), string(output), true)
   630  			if len(diffs) == 0 {
   631  				t.Logf("case %d bytes were unequal but computed no difference between results", i)
   632  				continue
   633  			}
   634  
   635  			t.Errorf("case %d %s mismatch:\n", i, c)
   636  			t.Errorf("diff:\n%s", dmp.DiffPrettyText(diffs))
   637  			t.Errorf("expected:\n%s", string(data))
   638  			t.Errorf("got:\n%s", string(output))
   639  			continue
   640  		}
   641  	}
   642  }
   643  
   644  func TestValidateBytes(t *testing.T) {
   645  	ctx := context.Background()
   646  	cases := []struct {
   647  		schema string
   648  		input  string
   649  		errors []string
   650  	}{
   651  		{`true`, `"just a string yo"`, nil},
   652  		{`{"type":"array", "items": {"type":"string"}}`,
   653  			`[1,false,null]`,
   654  			[]string{
   655  				`/0: 1 type should be string, got integer`,
   656  				`/1: false type should be string, got boolean`,
   657  				`/2: type should be string, got null`,
   658  			}},
   659  	}
   660  
   661  	for i, c := range cases {
   662  		rs := &Schema{}
   663  		if err := rs.UnmarshalJSON([]byte(c.schema)); err != nil {
   664  			t.Errorf("case %d error parsing %s", i, err.Error())
   665  			continue
   666  		}
   667  
   668  		errors, err := rs.ValidateBytes(ctx, []byte(c.input))
   669  		if err != nil {
   670  			t.Errorf("case %d error validating: %s", i, err.Error())
   671  			continue
   672  		}
   673  
   674  		if len(errors) != len(c.errors) {
   675  			t.Errorf("case %d: error length mismatch. expected: '%d', got: '%d'", i, len(c.errors), len(errors))
   676  			t.Errorf("%v", errors)
   677  			continue
   678  		}
   679  
   680  		for j, e := range errors {
   681  			if e.Error() != c.errors[j] {
   682  				t.Errorf("case %d: validation error %d mismatch. expected: '%s', got: '%s'", i, j, c.errors[j], e.Error())
   683  				continue
   684  			}
   685  		}
   686  	}
   687  }
   688  
   689  func BenchmarkAdditionalItems(b *testing.B) {
   690  	runBenchmark(b,
   691  		func(sampleSize int) (string, interface{}) {
   692  			data := make([]interface{}, sampleSize)
   693  			for i := 0; i < sampleSize; i++ {
   694  				data[i] = float64(i)
   695  			}
   696  			return `{
   697  				"items": {},
   698  				"additionalItems": false
   699  			}`, data
   700  		},
   701  	)
   702  }
   703  
   704  func BenchmarkAdditionalProperties(b *testing.B) {
   705  	runBenchmark(b,
   706  		func(sampleSize int) (string, interface{}) {
   707  			data := make(map[string]interface{}, sampleSize)
   708  			for i := 0; i < sampleSize; i++ {
   709  				p := fmt.Sprintf("p%v", i)
   710  				data[p] = struct{}{}
   711  			}
   712  			d, err := json.Marshal(data)
   713  			if err != nil {
   714  				b.Errorf("unable to marshal data: %v", err)
   715  				return "", nil
   716  			}
   717  			return `{
   718  				"properties": ` + string(d) + `,
   719  				"additionalProperties": false
   720  			}`, data
   721  		},
   722  	)
   723  }
   724  
   725  func BenchmarkConst(b *testing.B) {
   726  	runBenchmark(b,
   727  		func(sampleSize int) (string, interface{}) {
   728  			data := make(map[string]interface{}, sampleSize)
   729  			for i := 0; i < sampleSize; i++ {
   730  				data[fmt.Sprintf("p%v", i)] = fmt.Sprintf("p%v", 2*i)
   731  			}
   732  			d, err := json.Marshal(data)
   733  			if err != nil {
   734  				b.Errorf("unable to marshal data: %v", err)
   735  				return "", nil
   736  			}
   737  			return `{
   738  				"const": ` + string(d) + `
   739  			}`, data
   740  		},
   741  	)
   742  }
   743  
   744  func BenchmarkContains(b *testing.B) {
   745  	runBenchmark(b,
   746  		func(sampleSize int) (string, interface{}) {
   747  			data := make([]interface{}, sampleSize)
   748  			for i := 0; i < sampleSize; i++ {
   749  				data[i] = float64(i)
   750  			}
   751  			return `{
   752  				"contains": { "const": ` + strconv.Itoa(sampleSize-1) + ` }
   753  			}`, data
   754  		},
   755  	)
   756  }
   757  
   758  func BenchmarkDependencies(b *testing.B) {
   759  	runBenchmark(b,
   760  		func(sampleSize int) (string, interface{}) {
   761  			data := make(map[string]interface{}, sampleSize)
   762  			deps := []string{}
   763  			for i := 0; i < sampleSize; i++ {
   764  				p := fmt.Sprintf("p%v", i)
   765  				data[p] = fmt.Sprintf("p%v", 2*i)
   766  				if i != 0 {
   767  					deps = append(deps, p)
   768  				}
   769  			}
   770  			d, err := json.Marshal(deps)
   771  			if err != nil {
   772  				b.Errorf("unable to marshal data: %v", err)
   773  				return "", nil
   774  			}
   775  			return `{
   776  				"dependencies": {"p0": ` + string(d) + `}
   777  			}`, data
   778  		},
   779  	)
   780  }
   781  
   782  func BenchmarkEnum(b *testing.B) {
   783  	runBenchmark(b,
   784  		func(sampleSize int) (string, interface{}) {
   785  			data := make([]interface{}, sampleSize)
   786  			for i := 0; i < sampleSize; i++ {
   787  				data[i] = float64(i)
   788  			}
   789  			d, err := json.Marshal(data)
   790  			if err != nil {
   791  				b.Errorf("unable to marshal data: %v", err)
   792  				return "", nil
   793  			}
   794  			return `{
   795  				"enum": ` + string(d) + `
   796  			}`, float64(sampleSize / 2)
   797  		},
   798  	)
   799  }
   800  
   801  func BenchmarkMaximum(b *testing.B) {
   802  	runBenchmark(b, func(sampleSize int) (string, interface{}) {
   803  		return `{
   804  			"maximum": 3
   805  		}`, float64(2)
   806  	})
   807  }
   808  
   809  func BenchmarkMinimum(b *testing.B) {
   810  	runBenchmark(b, func(sampleSize int) (string, interface{}) {
   811  		return `{
   812  			"minimum": 3
   813  		}`, float64(4)
   814  	})
   815  }
   816  
   817  func BenchmarkExclusiveMaximum(b *testing.B) {
   818  	runBenchmark(b, func(sampleSize int) (string, interface{}) {
   819  		return `{
   820  			"exclusiveMaximum": 3
   821  		}`, float64(2)
   822  	})
   823  }
   824  
   825  func BenchmarkExclusiveMinimum(b *testing.B) {
   826  	runBenchmark(b, func(sampleSize int) (string, interface{}) {
   827  		return `{
   828  			"exclusiveMinimum": 3
   829  		}`, float64(4)
   830  	})
   831  }
   832  
   833  func BenchmarkMaxItems(b *testing.B) {
   834  	runBenchmark(b,
   835  		func(sampleSize int) (string, interface{}) {
   836  			data := make([]interface{}, sampleSize)
   837  			for i := 0; i < sampleSize; i++ {
   838  				data[i] = float64(i)
   839  			}
   840  			return `{
   841  				"maxItems": 10000
   842  			}`, data
   843  		},
   844  	)
   845  }
   846  
   847  func BenchmarkMinItems(b *testing.B) {
   848  	runBenchmark(b,
   849  		func(sampleSize int) (string, interface{}) {
   850  			data := make([]interface{}, sampleSize)
   851  			for i := 0; i < sampleSize; i++ {
   852  				data[i] = float64(i)
   853  			}
   854  			return `{
   855  				"minItems": 1
   856  			}`, data
   857  		},
   858  	)
   859  }
   860  
   861  func BenchmarkMaxLength(b *testing.B) {
   862  	runBenchmark(b,
   863  		func(sampleSize int) (string, interface{}) {
   864  			data := make([]rune, sampleSize)
   865  			for i := 0; i < sampleSize; i++ {
   866  				data[i] = 'a'
   867  			}
   868  			return `{
   869  				"maxLength": ` + strconv.Itoa(sampleSize) + `
   870  			}`, string(data)
   871  		},
   872  	)
   873  }
   874  
   875  func BenchmarkMinLength(b *testing.B) {
   876  	runBenchmark(b,
   877  		func(sampleSize int) (string, interface{}) {
   878  			data := make([]rune, sampleSize)
   879  			for i := 0; i < sampleSize; i++ {
   880  				data[i] = 'a'
   881  			}
   882  			return `{
   883  				"minLength": 1
   884  			}`, string(data)
   885  		},
   886  	)
   887  }
   888  
   889  func BenchmarkMaxProperties(b *testing.B) {
   890  	runBenchmark(b,
   891  		func(sampleSize int) (string, interface{}) {
   892  			data := make(map[string]interface{}, sampleSize)
   893  			for i := 0; i < sampleSize; i++ {
   894  				data[fmt.Sprintf("p%v", i)] = fmt.Sprintf("p%v", 2*i)
   895  			}
   896  			return `{
   897  				"maxProperties": ` + strconv.Itoa(sampleSize) + `
   898  			}`, data
   899  		},
   900  	)
   901  }
   902  
   903  func BenchmarkMinProperties(b *testing.B) {
   904  	runBenchmark(b,
   905  		func(sampleSize int) (string, interface{}) {
   906  			data := make(map[string]interface{}, sampleSize)
   907  			for i := 0; i < sampleSize; i++ {
   908  				data[fmt.Sprintf("p%v", i)] = fmt.Sprintf("p%v", 2*i)
   909  			}
   910  			return `{
   911  				"minProperties": 1
   912  			}`, data
   913  		},
   914  	)
   915  }
   916  
   917  func BenchmarkMultipleOf(b *testing.B) {
   918  	runBenchmark(b,
   919  		func(sampleSize int) (string, interface{}) {
   920  			return `{
   921  				"multipleOf": 2
   922  			}`, float64(42)
   923  		},
   924  	)
   925  }
   926  
   927  func BenchmarkPattern(b *testing.B) {
   928  	runBenchmark(b,
   929  		func(sampleSize int) (string, interface{}) {
   930  			data := make([]rune, sampleSize)
   931  			for i := 0; i < sampleSize; i++ {
   932  				data[i] = 'a'
   933  			}
   934  			return `{
   935  				"pattern": "^a*$"
   936  			}`, string(data)
   937  		},
   938  	)
   939  }
   940  
   941  func BenchmarkType(b *testing.B) {
   942  	runBenchmark(b,
   943  		func(sampleSize int) (string, interface{}) {
   944  			data := make(map[string]interface{}, sampleSize)
   945  			var schema strings.Builder
   946  
   947  			for i := 0; i < sampleSize; i++ {
   948  				propNull := fmt.Sprintf("n%v", nil)
   949  				propBool := fmt.Sprintf("b%v", i)
   950  				propInt := fmt.Sprintf("i%v", i)
   951  				propFloat := fmt.Sprintf("f%v", i)
   952  				propStr := fmt.Sprintf("s%v", i)
   953  				propArr := fmt.Sprintf("a%v", i)
   954  				propObj := fmt.Sprintf("o%v", i)
   955  
   956  				data[propBool] = true
   957  				data[propInt] = float64(42)
   958  				data[propFloat] = float64(42.5)
   959  				data[propStr] = "foobar"
   960  				data[propArr] = []interface{}{interface{}(1), interface{}(2), interface{}(3)}
   961  				data[propObj] = struct{}{}
   962  
   963  				schema.WriteString(fmt.Sprintf(`"%v": { "type": "null" },`, propNull))
   964  				schema.WriteString(fmt.Sprintf(`"%v": { "type": "boolean" },`, propBool))
   965  				schema.WriteString(fmt.Sprintf(`"%v": { "type": "integer" },`, propInt))
   966  				schema.WriteString(fmt.Sprintf(`"%v": { "type": "number" },`, propFloat))
   967  				schema.WriteString(fmt.Sprintf(`"%v": { "type": "string" },`, propStr))
   968  				schema.WriteString(fmt.Sprintf(`"%v": { "type": "array" },`, propArr))
   969  				schema.WriteString(fmt.Sprintf(`"%v": { "type": "object" }`, propObj))
   970  
   971  				if i != sampleSize-1 {
   972  					schema.WriteString(",")
   973  				}
   974  			}
   975  
   976  			return `{
   977  				"type": "object",
   978  				"properties": { ` + schema.String() + ` }
   979  			}`, data
   980  		},
   981  	)
   982  }
   983  
   984  func runBenchmark(b *testing.B, dataFn func(sampleSize int) (string, interface{})) {
   985  	ctx := context.Background()
   986  	for _, sampleSize := range []int{1, 10, 100, 1000} {
   987  		b.Run(fmt.Sprintf("sample size %v", sampleSize), func(b *testing.B) {
   988  			schema, data := dataFn(sampleSize)
   989  			if data == nil {
   990  				b.Skip("data == nil, skipping")
   991  				return
   992  			}
   993  
   994  			var validator Schema
   995  			if err := json.Unmarshal([]byte(schema), &validator); err != nil {
   996  				b.Errorf("error parsing schema: %s", err.Error())
   997  				return
   998  			}
   999  
  1000  			currentState := NewValidationState(&validator)
  1001  			currentState.ClearState()
  1002  			b.ResetTimer()
  1003  			for i := 0; i < b.N; i++ {
  1004  				validator.ValidateKeyword(ctx, currentState, data)
  1005  			}
  1006  			b.StopTimer()
  1007  
  1008  			if !currentState.IsValid() {
  1009  				b.Errorf("error running benchmark: %s", *currentState.Errs)
  1010  			}
  1011  		})
  1012  	}
  1013  }
  1014  

View as plain text