diff --git a/.gitignore b/.gitignore index c378da3..8234cf7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store coverage.txt -.vscode \ No newline at end of file +.vscode +.idea diff --git a/README.md b/README.md index 134f7fc..489a68b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ +Changes from upstream and other notes: +* Fixes [#81](https://github.com/qri-io/jsonschema/issues/81) +* I have looked at [*76](https://github.com/qri-io/jsonschema/issues/76) / [#80](https://github.com/qri-io/jsonschema/issues/80). My opinion, after reading the code, is that the global schema registry (which is the source of these concurrency issues) only gets involved when you have "$ref" elements. Since we only run this against schemas we write / generate, and I can guarantee that we don't generate "$ref" elements, this shouldn't be an issue for us, and we should be able to use this library across goroutines without additional synchronization +* [#91](https://github.com/qri-io/jsonschema/issues/91) may still be an issue, but I don't understand from reading it exactly what the problem is +* Fixes [#53](https://github.com/qri-io/jsonschema/issues/53) by adding a ValidateReader method that takes an io.Reader + + # jsonschema [![Qri](https://img.shields.io/badge/made%20by-qri-magenta.svg?style=flat-square)](https://qri.io) [![GoDoc](https://godoc.org/github.com/qri-io/jsonschema?status.svg)](http://godoc.org/github.com/qri-io/jsonschema) diff --git a/keywords_object.go b/keywords_object.go index 62ce0de..1627dba 100644 --- a/keywords_object.go +++ b/keywords_object.go @@ -326,7 +326,7 @@ func (ap *AdditionalProperties) ValidateKeyword(ctx context.Context, currentStat continue } if ap.schemaType == schemaTypeFalse { - currentState.AddError(data, "additional properties are not allowed") + currentState.AddError(data, "additional properties are not allowed: "+key) return } currentState.SetEvaluatedKey(key) @@ -669,7 +669,7 @@ func (up *UnevaluatedProperties) ValidateKeyword(ctx context.Context, currentSta continue } if up.schemaType == schemaTypeFalse { - currentState.AddError(data, "unevaluated properties are not allowed") + currentState.AddError(data, "unevaluated properties are not allowed: "+key) return } subState.DescendInstanceFromState(currentState, key) diff --git a/modo_test.go b/modo_test.go new file mode 100644 index 0000000..e001240 --- /dev/null +++ b/modo_test.go @@ -0,0 +1,41 @@ +package jsonschema + +import ( + "context" + "encoding/json" + "io" + "io/fs" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadingGeneratedSchemas(t *testing.T) { + t.Parallel() + + filesystem := os.DirFS("testdata") + _ = fs.WalkDir(filesystem, "modo", func(path string, d fs.DirEntry, err error) error { + require.NoError(t, err) + if d.IsDir() { + return nil + } + + t.Run(path, func(t *testing.T) { + assert.NotEqual(t, "", d.Name()) + f, err := filesystem.Open(path) + require.NoError(t, err) + data, err := io.ReadAll(f) + require.NoError(t, err) + + var s Schema + err = json.Unmarshal(data, &s) + require.NoError(t, err) + + _, err = s.ValidateBytes(context.Background(), []byte(`{}`)) + require.NoError(t, err) + }) + return nil + }) +} diff --git a/schema.go b/schema.go index 751a63f..486ae3f 100644 --- a/schema.go +++ b/schema.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/url" "sort" "strings" @@ -331,6 +332,17 @@ func (s *Schema) ValidateBytes(ctx context.Context, data []byte) ([]KeyError, er return *vs.Errs, nil } +// ValidateReader performs schema validation against a stream of json +// byte data +func (s *Schema) ValidateReader(ctx context.Context, data io.Reader) ([]KeyError, error) { + var doc interface{} + if err := json.NewDecoder(data).Decode(&doc); err != nil { + return nil, fmt.Errorf("error parsing JSON bytes: %w", err) + } + vs := s.Validate(ctx, doc) + return *vs.Errs, nil +} + // TopLevelType returns a string representing the schema's top-level type. func (s *Schema) TopLevelType() string { if t, ok := s.keywords["type"].(*Type); ok { diff --git a/testdata/modo/app_session-session.json b/testdata/modo/app_session-session.json new file mode 100644 index 0000000..d9e2d39 --- /dev/null +++ b/testdata/modo/app_session-session.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"device_identifier":{"description":"mobile device/app identifier","type":"string"},"ip_address":{"description":"ip address of the session","type":"string"},"payment_frame_size":{"required":["height","width"],"type":"object","properties":{"height":{"type":"number"},"unit":{"type":"string","default":"pixel"},"width":{"type":"number"}},"additionalProperties":false}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/bank_direct_details-payment.json b/testdata/modo/bank_direct_details-payment.json new file mode 100644 index 0000000..f72ed83 --- /dev/null +++ b/testdata/modo/bank_direct_details-payment.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","required":["debitor_account_number","debitor_account_type","debitor_bank_id_code"],"type":"object","properties":{"bank_network":{"description":"The network by which the payment will be cleared. Such as SEPA or ACH.","type":"string"},"country":{"description":"ISO-3166 Alpha-2 Country Code","type":"string"},"creditor_account_number":{"description":"Account number at a given bank for the recipient of funds, likely an IBAN if outside the U.S. and Canada. This is an optional field and would override any previously configured merchant account information","type":"string"},"creditor_bank_id_code":{"description":"Bank identifier of the bank in which funds will be credited, sometimes referred to as a routing number in the US or BIC elsewhere. This is an optional field and would override any previously configured merchant account information","type":"string"},"debitor_account_number":{"description":"Account number at a given bank for the source of funds, likely an IBAN if outside the U.S. and Canada.","type":"string"},"debitor_account_type":{"description":"Account number at a given bank for the source of funds, likely an IBAN if outside the U.S. and Canada.","type":"string"},"debitor_bank_id_code":{"description":"Bank identifier of the bank in which funds will be debited, sometimes referred to as a routing number in the US or BIC elsewhere.","type":"string"},"payment_information":{"description":"a description or note for the bank direct payment being made","type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/bank_direct_psp_response-paymentResponse.json b/testdata/modo/bank_direct_psp_response-paymentResponse.json new file mode 100644 index 0000000..20916f8 --- /dev/null +++ b/testdata/modo/bank_direct_psp_response-paymentResponse.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"bank_response_reason":{"description":"the response/reason from the acquirer bank","type":"string"},"payment_method":{"type":"string"},"psp_name":{"description":"Name of the payment service provider","type":"string"},"response_time":{"description":"The delta (in seconds) between when Modo sends a request to psp and receives the response","type":"number"},"status_code":{"description":"The status code returned during the processing of the payment","type":"string"},"status_message":{"description":"The status message (human readable) returned during the processing of the payment","type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/card_details-payment.json b/testdata/modo/card_details-payment.json new file mode 100644 index 0000000..dcecc1f --- /dev/null +++ b/testdata/modo/card_details-payment.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","description":"A credit, debit or prepaid card issued on a network (like Visa or Mastercard) by one or more banks.","required":["card_number","expiration_date"],"type":"object","properties":{"brand":{"description":"The brand of the card","type":"string"},"card_number":{"type":"string"},"card_type":{"description":"type of card being used (credit, debit, pre-paid)","type":"string"},"country":{"description":"ISO-3166 Alpha-2 Country Code","type":"string"},"expiration_date":{"type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/card_psp_response-paymentResponse.json b/testdata/modo/card_psp_response-paymentResponse.json new file mode 100644 index 0000000..242d06b --- /dev/null +++ b/testdata/modo/card_psp_response-paymentResponse.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"acquirer_response_code":{"description":"the raw acquirer response code to the authorization request","type":"string"},"address_verification_response_code":{"description":"This is the AVs response code","type":"string"},"authorization_id":{"description":"id assigned by the acquirer for the authorization","type":"string"},"available_balance":{"type":"object","properties":{"currency":{"type":"object","properties":{"currency_code":{"type":"object","properties":{"isocode":{"type":"string"}},"additionalProperties":false},"decimal_places":{"type":"integer"},"name":{"type":"string"},"separator":{"type":"string"},"symbol":{"type":"string"}},"additionalProperties":false},"value":{"type":"integer"}},"additionalProperties":false},"card_verification_response_code":{"description":"This is the CVV2 response code","type":"string"},"international_card_flag":{"description":"This flag indicates if this transaction was made with a international card","type":"boolean"},"network_unique_id":{"description":"Global or domestic network id assigned to the transaction","type":"string"},"payment_method":{"type":"string"},"prepaid_card_flag":{"description":"This flag indicates if this transaction was made with a pre-paid card","type":"boolean"},"psp_name":{"description":"Name of the payment service provider","type":"string"},"response_time":{"description":"The delta (in seconds) between when Modo sends a request to psp and receives the response","type":"number"},"status_code":{"description":"The status code returned during the processing of the payment","type":"string"},"status_message":{"description":"The status message (human readable) returned during the processing of the payment","type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/card_verification_details-paymentVerification.json b/testdata/modo/card_verification_details-paymentVerification.json new file mode 100644 index 0000000..49b4671 --- /dev/null +++ b/testdata/modo/card_verification_details-paymentVerification.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","description":"Verification details for a card","type":"object","properties":{"card_verification_code":{"description":"Plaintext card verification code","type":"string"},"device_fingerprint_ref":{"description":"A fingerprint of the device used for card verification (necessary for 3DS scenarios)","type":"string"},"emv_data":{"description":"data sent in specific to chip card transactions","type":"string"},"pin_block":{"description":"card pin block","type":"string"},"track_1":{"description":"1st track on the card magstripe","type":"string"},"track_2":{"description":"2nd track on the card magstripe","type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/line_item_details_airfare-order.json b/testdata/modo/line_item_details_airfare-order.json new file mode 100644 index 0000000..c5da575 --- /dev/null +++ b/testdata/modo/line_item_details_airfare-order.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"agency_invoice_number":{"type":"string"},"agency_plan_name":{"type":"string"},"airline_code":{"type":"string"},"airline_designator_code":{"type":"string"},"cabin_class":{"type":"string"},"customer_reference_identifier":{"type":"string"},"itinerary":{"type":"array","items":{"type":"object","properties":{"arrival_time":{"type":"string","format":"date-time"},"departure_time":{"type":"string","format":"date-time"},"destination_airport_city":{"type":"string"},"destination_airport_code":{"type":"string"},"flight_number":{"type":"string"},"origin_airport_city":{"type":"string"},"origin_airport_code":{"type":"string"},"seat_assignment":{"type":"string"}},"additionalProperties":false}},"ticket_number":{"type":"string"},"travel_agency_code":{"type":"string"},"travel_agency_name":{"type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/line_item_details_cashback-order.json b/testdata/modo/line_item_details_cashback-order.json new file mode 100644 index 0000000..57e4490 --- /dev/null +++ b/testdata/modo/line_item_details_cashback-order.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"product_id":{"type":"string"},"your_product_reference":{"type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/line_item_details_fuel-order.json b/testdata/modo/line_item_details_fuel-order.json new file mode 100644 index 0000000..d72c680 --- /dev/null +++ b/testdata/modo/line_item_details_fuel-order.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"fuel_grade":{"type":"string"},"product_id":{"type":"string"},"service_level":{"type":"string"},"unit_of_measure":{"type":"string"},"your_product_reference":{"type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/line_item_details_invoice-order.json b/testdata/modo/line_item_details_invoice-order.json new file mode 100644 index 0000000..40cd291 --- /dev/null +++ b/testdata/modo/line_item_details_invoice-order.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"customer_identifier":{"type":"string"},"invoice_date":{"type":"string","format":"date"},"invoice_identifier":{"type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/line_item_details_product-order.json b/testdata/modo/line_item_details_product-order.json new file mode 100644 index 0000000..1201ddd --- /dev/null +++ b/testdata/modo/line_item_details_product-order.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"SKU":{"type":"string"},"manufacturer":{"type":"string"},"model":{"type":"string"},"serial_number":{"type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/line_item_details_shipping-order.json b/testdata/modo/line_item_details_shipping-order.json new file mode 100644 index 0000000..92c2c75 --- /dev/null +++ b/testdata/modo/line_item_details_shipping-order.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"shipping_date":{"type":"string"},"shipping_method":{"type":"string"},"your_shipping_reference":{"type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/line_item_details_subscription-order.json b/testdata/modo/line_item_details_subscription-order.json new file mode 100644 index 0000000..ec66635 --- /dev/null +++ b/testdata/modo/line_item_details_subscription-order.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"description":{"type":"string"},"frequency":{"type":"string"},"subscription_name":{"type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/line_item_details_tax-order.json b/testdata/modo/line_item_details_tax-order.json new file mode 100644 index 0000000..57e4490 --- /dev/null +++ b/testdata/modo/line_item_details_tax-order.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"product_id":{"type":"string"},"your_product_reference":{"type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/offline_session-session.json b/testdata/modo/offline_session-session.json new file mode 100644 index 0000000..49d0fba --- /dev/null +++ b/testdata/modo/offline_session-session.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"application_name":{"description":"the name of the system requesting checkout.","type":"string"},"ip_address":{"description":"originating IP address of the session","type":"string"},"system_id":{"description":"the identifier of the system requesting the checkout.","type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/ps_adyen_details-paymentService.json b/testdata/modo/ps_adyen_details-paymentService.json new file mode 100644 index 0000000..b9c7caf --- /dev/null +++ b/testdata/modo/ps_adyen_details-paymentService.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","description":"The payment service uses this field for payment specific","type":"object","properties":{"ps_payment_options":{"description":"Sometimes a payment_service has a reference or optional value for a payment associated with optional services that you may want to take advantage of (fraud flags, purchase financing, installment plans). Use this to communicate those options to the payment_service.\n// ","type":"object","properties":{"option_type":{"description":"The type of payments option","type":"string"},"option_value":{"description":"The actual option value","type":"string"}},"additionalProperties":false},"settlement_splits":{"type":"array","items":{"type":"object","properties":{"account":{"description":"If split_type is MarketPlace, the account_id for the accountholder's settlement account must be provided","type":"string"},"amount":{"type":"object","properties":{"currency":{"type":"object","properties":{"currency_code":{"type":"object","properties":{"isocode":{"type":"string"}},"additionalProperties":false},"decimal_places":{"type":"integer"},"name":{"type":"string"},"separator":{"type":"string"},"symbol":{"type":"string"}},"additionalProperties":false},"value":{"type":"integer"}},"additionalProperties":false},"reference":{"description":"The reference of this split. Used to link other operations (e.g. captures and refunds) to this split.","type":"string"},"split_type":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/terminal_session-session.json b/testdata/modo/terminal_session-session.json new file mode 100644 index 0000000..909978d --- /dev/null +++ b/testdata/modo/terminal_session-session.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"ip_address":{"description":"IP address of the session","type":"string"},"lane":{"description":"your lane identifier","type":"string"},"register":{"description":"your register identifier","type":"string"},"terminal_id":{"description":"your terminal identifier","type":"string"}},"additionalProperties":false} \ No newline at end of file diff --git a/testdata/modo/web_session-session.json b/testdata/modo/web_session-session.json new file mode 100644 index 0000000..21d2269 --- /dev/null +++ b/testdata/modo/web_session-session.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2019-09/schema","type":"object","properties":{"browser":{"type":"object","properties":{"accept_header":{"type":"string"},"user_agent":{"type":"string"}},"additionalProperties":false},"cookie":{"type":"string"},"ip_address":{"description":"ip address of the session","type":"string"},"payment_frame_size":{"required":["height","width"],"type":"object","properties":{"height":{"type":"number"},"unit":{"type":"string","default":"pixel"},"width":{"type":"number"}},"additionalProperties":false}},"additionalProperties":false} \ No newline at end of file