diff --git a/lib/jsonapi_parameters.rb b/lib/jsonapi_parameters.rb index 15610f8..70b2d1c 100644 --- a/lib/jsonapi_parameters.rb +++ b/lib/jsonapi_parameters.rb @@ -1,6 +1,6 @@ require 'jsonapi_parameters/parameters' require 'jsonapi_parameters/handlers' -require 'jsonapi_parameters/validator' +require 'jsonapi_parameters/schema_validator' require 'jsonapi_parameters/translator' require 'jsonapi_parameters/core_ext' require 'jsonapi_parameters/stack_limit' diff --git a/lib/jsonapi_parameters/schema_validator.rb b/lib/jsonapi_parameters/schema_validator.rb new file mode 100644 index 0000000..0affbd8 --- /dev/null +++ b/lib/jsonapi_parameters/schema_validator.rb @@ -0,0 +1,51 @@ +require 'json_schemer' + +module JsonApi::Parameters + SCHEMA_PATH = Pathname.new(__dir__).join('jsonapi_schema.json').to_s.freeze + + private + + def should_prevalidate? + JsonApi::Parameters.enforce_schema_prevalidation && !JsonApi::Parameters.suppress_schema_validation_errors + end + + class SchemaValidator + class ValidationError < StandardError; end + + def initialize(payload) + @payload = payload.deep_stringify_keys + end + + attr_reader :payload + + def validate! + schema = JSONSchemer.schema(File.read(SCHEMA_PATH)) + + unless schema.valid?(payload) # rubocop:disable Style/GuardClause + errors = [] + + schema.validate(payload).each do |validation_error| + errors << nice_error(validation_error) + end + + raise SchemaValidator::ValidationError.new(errors.join(', ')) + end + end + + private + + # Based on & thanks to https://polythematik.de/2020/02/17/ruby-json-schema/ + def nice_error(err) + case err['type'] + when 'required' + "path '#{err['data_pointer']}' is missing keys: #{err['details']['missing_keys'].join ', '}" + when 'format' + "path '#{err['data_pointer']}' is not in required format (#{err['schema']['format']})" + when 'minLength' + "path '#{err['data_pointer']}' is not long enough (min #{err['schema']['minLength']})" + else + "path '#{err['data_pointer']}' is invalid according to the JsonApi schema" + end + end + end +end diff --git a/lib/jsonapi_parameters/translator.rb b/lib/jsonapi_parameters/translator.rb index eba0833..a26961e 100644 --- a/lib/jsonapi_parameters/translator.rb +++ b/lib/jsonapi_parameters/translator.rb @@ -18,12 +18,12 @@ def jsonapi_translate(params, naming_convention:) @jsonapi_unsafe_hash = ensure_naming(params, naming_convention) - JsonApi::Parameters::Validator.new(@jsonapi_unsafe_hash.deep_dup).validate! if should_prevalidate? + JsonApi::Parameters::SchemaValidator.new(@jsonapi_unsafe_hash.deep_dup).validate! if should_prevalidate? formed_parameters rescue StandardError => err # Validate the payload and raise errors... - JsonApi::Parameters::Validator.new(@jsonapi_unsafe_hash.deep_dup).validate! unless JsonApi::Parameters.suppress_schema_validation_errors + JsonApi::Parameters::SchemaValidator.new(@jsonapi_unsafe_hash.deep_dup).validate! unless JsonApi::Parameters.suppress_schema_validation_errors raise err # ... or if there were none, re-raise initial error end diff --git a/lib/jsonapi_parameters/validator.rb b/lib/jsonapi_parameters/validator.rb deleted file mode 100644 index 5627588..0000000 --- a/lib/jsonapi_parameters/validator.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'active_model' -require 'json_schemer' - -module JsonApi::Parameters - SCHEMA_PATH = Pathname.new(__dir__).join('jsonapi_schema.json').to_s.freeze - - private - - def should_prevalidate? - JsonApi::Parameters.enforce_schema_prevalidation && !JsonApi::Parameters.suppress_schema_validation_errors - end - - class Validator - include ActiveModel::Validations - - attr_reader :payload - - class PayloadValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - @schema = JSONSchemer.schema(File.read(SCHEMA_PATH)) - - unless @schema.valid?(value) # rubocop:disable Style/GuardClause - @schema.validate(value).each do |validation_error| - record.errors[attribute] << nice_error(validation_error) - end - end - end - - private - - # Based on & thanks to https://polythematik.de/2020/02/17/ruby-json-schema/ - def nice_error(err) - case err['type'] - when 'required' - "path '#{err['data_pointer']}' is missing keys: #{err['details']['missing_keys'].join ', '}" - when 'format' - "path '#{err['data_pointer']}' is not in required format (#{err['schema']['format']})" - when 'minLength' - "path '#{err['data_pointer']}' is not long enough (min #{err['schema']['minLength']})" - else - "path '#{err['data_pointer']}' is invalid according to the JsonApi schema" - end - end - end - - validates :payload, presence: true, payload: true - - def initialize(payload) - @payload = payload.deep_stringify_keys - end - end -end diff --git a/spec/lib/jsonapi_parameters/validator_spec.rb b/spec/lib/jsonapi_parameters/validator_spec.rb index 65c6536..9e37f32 100644 --- a/spec/lib/jsonapi_parameters/validator_spec.rb +++ b/spec/lib/jsonapi_parameters/validator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe JsonApi::Parameters::Validator do # rubocop:disable RSpec/FilePath +describe JsonApi::Parameters::SchemaValidator do # rubocop:disable RSpec/FilePath describe 'initializer' do it 'ensures @payload has keys deeply stringified' do validator = described_class.new(payload: { sample: 'value' }) @@ -26,7 +26,7 @@ class Translator it 'raises validation errors' do payload = { payload: { sample: 'value' } } - expect { translator.jsonapify(payload) }.to raise_error(ActiveModel::ValidationError) + expect { translator.jsonapify(payload) }.to raise_error(JsonApi::Parameters::SchemaValidator::ValidationError) end it 'does not raise TranslatorError' do @@ -42,7 +42,7 @@ class Translator begin translator.jsonapify(payload) - rescue ActiveModel::ValidationError => _ # rubocop:disable Lint/HandleExceptions + rescue JsonApi::Parameters::SchemaValidator::ValidationError => _ # rubocop:disable Lint/HandleExceptions end end end @@ -55,7 +55,7 @@ class Translator it 'does not raise validation errors' do payload = { payload: { sample: 'value' } } - expect { translator.jsonapify(payload) }.not_to raise_error(ActiveModel::ValidationError) + expect { translator.jsonapify(payload) }.not_to raise_error(JsonApi::Parameters::SchemaValidator::ValidationError) end it 'still raises any other errors' do @@ -77,7 +77,7 @@ class Translator it 'raises validation errors' do payload = { payload: { sample: 'value' } } - expect { translator.jsonapify(payload) }.to raise_error(ActiveModel::ValidationError) + expect { translator.jsonapify(payload) }.to raise_error(JsonApi::Parameters::SchemaValidator::ValidationError) end end @@ -88,7 +88,7 @@ class Translator expect(File).to receive(:read).with(JsonApi::Parameters::SCHEMA_PATH).and_call_original expect(JSONSchemer).to receive(:schema).and_call_original - expect { validator.validate! }.to raise_error(ActiveModel::ValidationError) + expect { validator.validate! }.to raise_error(JsonApi::Parameters::SchemaValidator::ValidationError) end end @@ -98,11 +98,11 @@ class Translator payload = { controller: 'examples_controller', action: 'create', commit: 'Sign up' } validator = described_class.new(payload) - expect { validator.validate! }.to raise_error(ActiveModel::ValidationError) + expect { validator.validate! }.to raise_error(JsonApi::Parameters::SchemaValidator::ValidationError) begin validator.validate! - rescue ActiveModel::ValidationError => err + rescue JsonApi::Parameters::SchemaValidator::ValidationError => err rails_specific_params.each do |param| expect(err.message).not_to include("Payload path '/#{param}'") end