diff --git a/Gemfile.lock b/Gemfile.lock index 8e3e063..5f08ad3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,8 +9,13 @@ PATH GEM remote: https://rubygems.org/ specs: + concurrent-ruby (1.2.2) docile (1.4.0) hana (1.3.7) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + i18n-debug (1.2.0) + i18n (< 2) minitest (5.15.0) rake (13.0.6) regexp_parser (2.8.1) @@ -33,6 +38,8 @@ PLATFORMS DEPENDENCIES bundler (~> 2.0) + i18n + i18n-debug json_schemer! minitest (~> 5.0) rake (~> 13.0) diff --git a/README.md b/README.md index c2729d4..fca5266 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,166 @@ JSONSchemer.schema( ) ``` +## Custom Error Messages + +Error messages can be customized using the `x-error` keyword and/or [I18n](https://github.com/ruby-i18n/i18n) translations. `x-error` takes precedence if both are defined. + +### `x-error` Keyword + +```ruby +# override all errors for a schema +schemer = JSONSchemer.schema({ + 'type' => 'string', + 'x-error' => 'custom error for schema and all keywords' +}) + +schemer.validate(1).first +# => {"data"=>1, +# "data_pointer"=>"", +# "schema"=>{"type"=>"string", "x-error"=>"custom error for schema and all keywords"}, +# "schema_pointer"=>"", +# "root_schema"=>{"type"=>"string", "x-error"=>"custom error for schema and all keywords"}, +# "type"=>"string", +# "error"=>"custom error for schema and all keywords", +# "x-error"=>true} + +schemer.validate(1, :output_format => 'basic') +# => {"valid"=>false, +# "keywordLocation"=>"", +# "absoluteKeywordLocation"=>"json-schemer://schema#", +# "instanceLocation"=>"", +# "error"=>"custom error for schema and all keywords", +# "x-error"=>true, +# "errors"=>#} + +# keyword-specific errors +schemer = JSONSchemer.schema({ + 'type' => 'string', + 'minLength' => 10, + 'x-error' => { + 'type' => 'custom error for `type` keyword', + # special `^` keyword for schema-level error + '^' => 'custom error for schema', + # same behavior as when `x-error` is a string + '*' => 'fallback error for schema and all keywords' + } +}) + +schemer.validate(1).map { _1.fetch('error') } +# => ["custom error for `type` keyword"] + +schemer.validate('1').map { _1.fetch('error') } +# => ["custom error for schema and all keywords"] + +schemer.validate(1, :output_format => 'basic').fetch('error') +# => "custom error for schema" + +# variable interpolation (instance/instanceLocation/keywordLocation/absoluteKeywordLocation) +schemer = JSONSchemer.schema({ + '$id' => 'https://example.com/schema', + 'properties' => { + 'abc' => { + 'type' => 'string', + 'x-error' => <<~ERROR + instance: %{instance} + instance location: %{instanceLocation} + keyword location: %{keywordLocation} + absolute keyword location: %{absoluteKeywordLocation} + ERROR + } + } +}) + +puts schemer.validate({ 'abc' => 1 }).first.fetch('error') +# instance: 1 +# instance location: /abc +# keyword location: /properties/abc/type +# absolute keyword location: https://example.com/schema#/properties/abc/type +``` + +### I18n + +When the [I18n gem](https://github.com/ruby-i18n/i18n) is loaded, custom error messages are looked up under the `json_schemer` key. It may be necessary to restart your application after adding the root key because the existence check is cached for performance reasons. + +Translation keys are looked up in this order: + +1. `$LOCALE.json_schemer.errors.$ABSOLUTE_KEYWORD_LOCATION` +2. `$LOCALE.json_schemer.errors.$SCHEMA_ID.$KEYWORD_LOCATION` +3. `$LOCALE.json_schemer.errors.$KEYWORD_LOCATION` +4. `$LOCALE.json_schemer.errors.$SCHEMA_ID.$KEYWORD` +5. `$LOCALE.json_schemer.errors.$SCHEMA_ID.*` +6. `$LOCALE.json_schemer.errors.$META_SCHEMA_ID.$KEYWORD` +7. `$LOCALE.json_schemer.errors.$META_SCHEMA_ID.*` +8. `$LOCALE.json_schemer.errors.$KEYWORD` +9. `$LOCALE.json_schemer.errors.*` + +Example translations file: + +```yaml +en: + json_schemer: + errors: + 'https://example.com/schema#/properties/abc/type': custom error for absolute keyword location + 'https://example.com/schema': + '#/properties/abc/type': custom error for keyword location, nested under schema $id + 'type': custom error for `type` keyword, nested under schema $id + '^': custom error for schema, nested under schema $id + '*': fallback error for schema and all keywords, nested under schema $id + '#/properties/abc/type': custom error for keyword location + 'http://json-schema.org/draft-07/schema#': + 'type': custom error for `type` keyword, nested under meta-schema $id ($schema) + '^': custom error for schema, nested under meta-schema $id + '*': fallback error for schema and all keywords, nested under meta-schema $id ($schema) + 'type': custom error for `type` keyword + '^': custom error for schema + # variable interpolation (instance/instanceLocation/keywordLocation/absoluteKeywordLocation) + '*': | + fallback error for schema and all keywords + instance: %{instance} + instance location: %{instanceLocation} + keyword location: %{keywordLocation} + absolute keyword location: %{absoluteKeywordLocation} +``` + +And output: + +```ruby +require 'i18n' +I18n.locale = :en # $LOCALE=en + +schemer = JSONSchemer.schema({ + '$id' => 'https://example.com/schema', # $SCHEMA_ID=https://example.com/schema + '$schema' => 'http://json-schema.org/draft-07/schema#', # $META_SCHEMA_ID=http://json-schema.org/draft-07/schema# + 'properties' => { + 'abc' => { + 'type' => 'integer' # $KEYWORD=type + } # $KEYWORD_LOCATION=#/properties/abc/type + } # $ABSOLUTE_KEYWORD_LOCATION=https://example.com/schema#/properties/abc/type +}) + +schemer.validate({ 'abc' => 'not-an-integer' }).first +# => {"data"=>"not-an-integer", +# "data_pointer"=>"/abc", +# "schema"=>{"type"=>"integer"}, +# "schema_pointer"=>"/properties/abc", +# "root_schema"=>{"$id"=>"https://example.com/schema", "$schema"=>"http://json-schema.org/draft-07/schema#", "properties"=>{"abc"=>{"type"=>"integer"}}}, +# "type"=>"integer", +# "error"=>"custom error for absolute keyword location", +# "i18n"=>true +``` + +In the example above, custom error messsages are looked up using the following keys (in order until one is found): + +1. `en.json_schemer.errors.'https://example.com/schema#/properties/abc/type'` +2. `en.json_schemer.errors.'https://example.com/schema'.'#/properties/abc/type'` +3. `en.json_schemer.errors.'#/properties/abc/type'` +4. `en.json_schemer.errors.'https://example.com/schema'.type` +5. `en.json_schemer.errors.'https://example.com/schema'.*` +6. `en.json_schemer.errors.'http://json-schema.org/draft-07/schema#'.type` +7. `en.json_schemer.errors.'http://json-schema.org/draft-07/schema#'.*` +8. `en.json_schemer.errors.type` +9. `en.json_schemer.errors.*` + ## OpenAPI ```ruby diff --git a/json_schemer.gemspec b/json_schemer.gemspec index be40b9d..122a2dc 100644 --- a/json_schemer.gemspec +++ b/json_schemer.gemspec @@ -26,6 +26,8 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "minitest", "~> 5.0" spec.add_development_dependency "simplecov", "~> 0.22" + spec.add_development_dependency "i18n" + spec.add_development_dependency "i18n-debug" spec.add_runtime_dependency "hana", "~> 1.3" spec.add_runtime_dependency "regexp_parser", "~> 2.0" diff --git a/lib/json_schemer/draft202012/vocab.rb b/lib/json_schemer/draft202012/vocab.rb index 7eaafc5..a77b70b 100644 --- a/lib/json_schemer/draft202012/vocab.rb +++ b/lib/json_schemer/draft202012/vocab.rb @@ -16,7 +16,9 @@ module Vocab '$defs' => Core::Defs, 'definitions' => Core::Defs, # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01#section-8.3 - '$comment' => Core::Comment + '$comment' => Core::Comment, + # https://github.com/orgs/json-schema-org/discussions/329 + 'x-error' => Core::XError } # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01#section-10 APPLICATOR = { diff --git a/lib/json_schemer/draft202012/vocab/core.rb b/lib/json_schemer/draft202012/vocab/core.rb index 8d288d0..66fea2c 100644 --- a/lib/json_schemer/draft202012/vocab/core.rb +++ b/lib/json_schemer/draft202012/vocab/core.rb @@ -119,6 +119,12 @@ def parse class Comment < Keyword; end + class XError < Keyword + def message(error_key) + value.is_a?(Hash) ? (value[error_key] || value[CATCHALL]) : value + end + end + class UnknownKeyword < Keyword def parse if value.is_a?(Hash) diff --git a/lib/json_schemer/keyword.rb b/lib/json_schemer/keyword.rb index 5b8a5cc..091d7d8 100644 --- a/lib/json_schemer/keyword.rb +++ b/lib/json_schemer/keyword.rb @@ -26,6 +26,10 @@ def schema_pointer @schema_pointer ||= "#{parent.schema_pointer}/#{escaped_keyword}" end + def error_key + keyword + end + private def parse diff --git a/lib/json_schemer/output.rb b/lib/json_schemer/output.rb index 9ca0c13..74c55ec 100644 --- a/lib/json_schemer/output.rb +++ b/lib/json_schemer/output.rb @@ -5,6 +5,11 @@ module Output attr_reader :keyword, :schema + def x_error + return @x_error if defined?(@x_error) + @x_error = schema.parsed['x-error']&.message(error_key) + end + private def result(instance, instance_location, keyword_location, valid, nested = nil, type: nil, annotation: nil, details: nil, ignore_nested: false) diff --git a/lib/json_schemer/result.rb b/lib/json_schemer/result.rb index 3612c89..52b05d9 100644 --- a/lib/json_schemer/result.rb +++ b/lib/json_schemer/result.rb @@ -1,10 +1,15 @@ # frozen_string_literal: true module JSONSchemer - Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do - CLASSIC_ERROR_TYPES = Hash.new do |hash, klass| - hash[klass] = klass.name.rpartition('::').last.sub(/\A[[:alpha:]]/, &:downcase) - end + CATCHALL = '*' + I18N_SEPARATOR = "\x1F" # unit separator + I18N_SCOPE = 'json_schemer' + I18N_ERRORS_SCOPE = "#{I18N_SCOPE}#{I18N_SEPARATOR}errors" + X_ERROR_REGEX = /%\{(instance|instanceLocation|keywordLocation|absoluteKeywordLocation)\}/ + CLASSIC_ERROR_TYPES = Hash.new do |hash, klass| + hash[klass] = klass.name.rpartition('::').last.sub(/\A[[:alpha:]]/, &:downcase) + end + Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do def output(output_format) case output_format when 'classic' @@ -24,10 +29,59 @@ def output(output_format) def error return @error if defined?(@error) - resolved_instance_location = Location.resolve(instance_location) - @error = source.error( - :formatted_instance_location => resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`", - :details => details + if source.x_error + # not using sprintf because it warns: "too many arguments for format string" + @error = source.x_error.gsub( + X_ERROR_REGEX, + '%{instance}' => instance, + '%{instanceLocation}' => Location.resolve(instance_location), + '%{keywordLocation}' => Location.resolve(keyword_location), + '%{absoluteKeywordLocation}' => source.absolute_keyword_location + ) + @x_error = true + else + resolved_instance_location = Location.resolve(instance_location) + formatted_instance_location = resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`" + @error = source.error(:formatted_instance_location => formatted_instance_location, :details => details) + if i18n? + begin + @error = i18n! + @i18n = true + rescue I18n::MissingTranslationData + end + end + end + @error + end + + def i18n? + return @@i18n if defined?(@@i18n) + @@i18n = defined?(I18n) && I18n.exists?(I18N_SCOPE) + end + + def i18n! + base_uri_str = source.schema.base_uri.to_s + meta_schema_base_uri_str = source.schema.meta_schema.base_uri.to_s + resolved_keyword_location = Location.resolve(keyword_location) + error_key = source.error_key + I18n.translate!( + source.absolute_keyword_location, + :default => [ + "#{base_uri_str}#{I18N_SEPARATOR}##{resolved_keyword_location}", + "##{resolved_keyword_location}", + "#{base_uri_str}#{I18N_SEPARATOR}#{error_key}", + "#{base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}", + "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{error_key}", + "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}", + error_key, + CATCHALL + ].map!(&:to_sym), + :separator => I18N_SEPARATOR, + :scope => I18N_ERRORS_SCOPE, + :instance => instance, + :instanceLocation => Location.resolve(instance_location), + :keywordLocation => resolved_keyword_location, + :absoluteKeywordLocation => source.absolute_keyword_location ) end @@ -38,8 +92,13 @@ def to_output_unit 'absoluteKeywordLocation' => source.absolute_keyword_location, 'instanceLocation' => Location.resolve(instance_location) } - out['error'] = error unless valid - out['annotation'] = annotation if valid && annotation + if valid + out['annotation'] = annotation if annotation + else + out['error'] = error + out['x-error'] = true if @x_error + out['i18n'] = true if @i18n + end out end @@ -54,6 +113,8 @@ def to_classic 'type' => type || CLASSIC_ERROR_TYPES[source.class] } out['error'] = error + out['x-error'] = true if @x_error + out['i18n'] = true if @i18n out['details'] = details if details out end diff --git a/lib/json_schemer/schema.rb b/lib/json_schemer/schema.rb index 1c2985b..d77bfa7 100644 --- a/lib/json_schemer/schema.rb +++ b/lib/json_schemer/schema.rb @@ -295,6 +295,10 @@ def schema_pointer end end + def error_key + '^' + end + def fetch_format(format, *args, &block) if meta_schema == self formats.fetch(format, *args, &block) diff --git a/test/errors_test.rb b/test/errors_test.rb new file mode 100644 index 0000000..6a57bd9 --- /dev/null +++ b/test/errors_test.rb @@ -0,0 +1,272 @@ +require 'test_helper' + +class ErrorsTest < Minitest::Test + def test_x_error + schema = { + 'oneOf' => [ + { + 'x-error' => 'properties a and b were provided, however only one or the other may be specified', + 'required' => ['a'], + 'not' => { 'required' => ['b'] } + }, + { + 'x-error' => { + 'not' => '%{instance} `%{instanceLocation}` %{keywordLocation} %{absoluteKeywordLocation}' + }, + 'required' => ['b'], + 'not' => { 'required' => ['a'] } + } + ], + 'x-error' => { + '*' => 'schema error', + 'oneOf' => 'oneOf error' + } + } + data = { + 'a' => 'foo', + 'b' => 'bar' + } + assert_equal( + [ + 'properties a and b were provided, however only one or the other may be specified', + '{"a"=>"foo", "b"=>"bar"} `` /oneOf/1/not json-schemer://schema#/oneOf/1/not' + ].sort, + JSONSchemer.schema(schema).validate(data).map { |error| error.fetch('error') }.sort + ) + + assert_equal('schema error', JSONSchemer.schema(schema).validate(data, :output_format => 'basic').fetch('error')) + assert_equal('oneOf error', JSONSchemer.schema(schema).validate(data, :output_format => 'detailed').fetch('error')) + + assert_equal(true, JSONSchemer.schema(schema).validate(data, :output_format => 'basic').fetch('x-error')) + assert_equal(true, JSONSchemer.schema(schema).validate(data, :output_format => 'detailed').fetch('x-error')) + assert_equal([true, true], JSONSchemer.schema(schema).validate(data).map { |error| error.fetch('x-error') }) + + refute(JSONSchemer.schema(schema).validate(data, :output_format => 'basic').key?('i18n')) + refute(JSONSchemer.schema(schema).validate(data, :output_format => 'detailed').key?('i18n')) + assert_equal([false, false], JSONSchemer.schema(schema).validate(data).map { |error| error.key?('i18n') }) + end + + def test_x_error_override + schema = { + 'required' => ['a'], + 'minProperties' => 2 + } + assert_equal( + ['object at root is missing required properties: a', 'object size at root is less than: 2'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + + schema.merge!('x-error' => 'schema error') + assert_equal( + ['schema error', 'schema error'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + assert_equal( + 'schema error', + JSONSchemer.schema(schema).validate({}, :output_format => 'basic').fetch('error') + ) + + schema.merge!('x-error' => { 'required' => 'required error' }) + assert_equal( + ['required error', 'object size at root is less than: 2'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + + schema.merge!('x-error' => { 'required' => 'required error', 'minProperties' => 'minProperties error' }) + assert_equal( + ['required error', 'minProperties error'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + + schema.merge!('x-error' => { '*' => 'catchall', 'minProperties' => 'minProperties error' }) + assert_equal( + ['catchall', 'minProperties error'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + assert_equal( + 'catchall', + JSONSchemer.schema(schema).validate({}, :output_format => 'basic').fetch('error') + ) + + schema.merge!('x-error' => { '^' => 'schema error', 'minProperties' => 'minProperties error' }) + assert_equal( + ['object at root is missing required properties: a', 'minProperties error'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + assert_equal( + 'schema error', + JSONSchemer.schema(schema).validate({}, :output_format => 'basic').fetch('error') + ) + + schema.merge!('x-error' => { '^' => 'schema error', '*' => 'catchall' }) + assert_equal( + ['catchall', 'catchall'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + assert_equal( + 'schema error', + JSONSchemer.schema(schema).validate({}, :output_format => 'basic').fetch('error') + ) + end + + def test_x_error_precedence + schema = { + '$id' => 'https://example.com/schema', + 'required' => ['a'] + } + x_error_schema = schema.merge( + 'x-error' => { + 'required' => 'x error' + } + ) + + assert_equal('x error', JSONSchemer.schema(x_error_schema).validate({}).first.fetch('error')) + assert_equal('object at root is missing required properties: a', JSONSchemer.schema(schema).validate({}).first.fetch('error')) + + i18n({ 'https://example.com/schema#/required' => 'i18n error' }) do + assert_equal('x error', JSONSchemer.schema(x_error_schema).validate({}).first.fetch('error')) + assert_equal('i18n error', JSONSchemer.schema(schema).validate({}).first.fetch('error')) + end + end + + def test_i18n_error + schema = { + '$id' => 'https://example.com/schema', + '$schema' => 'https://json-schema.org/draft/2019-09/schema', + 'properties' => { + 'yah' => { + 'type' => 'string' + } + } + } + schemer = JSONSchemer.schema(schema) + data = { 'yah' => 1 } + + errors = { + 'https://example.com/schema#' => 'A', + 'https://example.com/schema#/properties/yah/type' => '1', + 'https://example.com/schema' => { + '#' => 'B', + '#/properties/yah/type' => '2', + '^' => 'D', + 'type' => '4', + '*' => 'E/5' + }, + '#/properties/yah/type' => '3', + '#' => 'C', + 'https://json-schema.org/draft/2019-09/schema' => { + '^' => 'F', + 'type' => '6', + '*' => 'G/7' + }, + '^' => 'H', + 'type' => '8', + '*' => 'I/9: %{instance} `%{instanceLocation}` %{keywordLocation} %{absoluteKeywordLocation}', + + 'https://example.com/differentschema#/properties/yah/type' => '?', + 'https://example.com/differentschema' => { + '#/properties/yah/type' => '?', + 'type' => '?', + '*' => '?' + }, + '?' => '?' + } + + i18n(errors) do + assert_equal('A', schemer.validate(data, :output_format => 'basic').fetch('error')) + assert_equal('1', schemer.validate(data).first.fetch('error')) + end + + errors.delete('https://example.com/schema#') + assert_equal('B', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.delete('https://example.com/schema#/properties/yah/type') + assert_equal('2', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://example.com/schema').delete('#') + assert_equal('C', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.fetch('https://example.com/schema').delete('#/properties/yah/type') + assert_equal('3', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.delete('#') + assert_equal('D', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.delete('#/properties/yah/type') + assert_equal('4', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://example.com/schema').delete('^') + assert_equal('E/5', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.fetch('https://example.com/schema').delete('type') + assert_equal('E/5', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://example.com/schema').delete('*') + i18n(errors) do + assert_equal('F', schemer.validate(data, :output_format => 'basic').fetch('error')) + assert_equal('6', schemer.validate(data).first.fetch('error')) + end + + errors.fetch('https://json-schema.org/draft/2019-09/schema').delete('^') + assert_equal('G/7', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.fetch('https://json-schema.org/draft/2019-09/schema').delete('type') + assert_equal('G/7', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://json-schema.org/draft/2019-09/schema').delete('*') + i18n(errors) do + assert_equal('H', schemer.validate(data, :output_format => 'basic').fetch('error')) + assert_equal('8', schemer.validate(data).first.fetch('error')) + end + + errors.delete('^') + assert_equal('I/9: {"yah"=>1} `` https://example.com/schema#', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.delete('type') + assert_equal('I/9: 1 `/yah` /properties/yah/type https://example.com/schema#/properties/yah/type', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + i18n(errors) do + assert_equal(true, schemer.validate(data).first.fetch('i18n')) + refute(schemer.validate(data).first.key?('x-error')) + assert_equal(true, schemer.validate(data, :output_format => 'basic').fetch('i18n')) + refute(schemer.validate(data, :output_format => 'basic').key?('x-error')) + end + + errors.delete('*') + i18n(errors) do + assert_equal('value at root does not match schema', schemer.validate(data, :output_format => 'basic').fetch('error')) + assert_equal('value at `/yah` is not a string', schemer.validate(data).first.fetch('error')) + + refute(schemer.validate(data).first.key?('i18n')) + refute(schemer.validate(data).first.key?('x-error')) + refute(schemer.validate(data, :output_format => 'basic').key?('i18n')) + refute(schemer.validate(data, :output_format => 'basic').key?('x-error')) + end + end + +private + + def i18n(errors) + require 'yaml' + require 'i18n' + # require 'i18n/debug' + + JSONSchemer.remove_class_variable(:@@i18n) if JSONSchemer.class_variable_defined?(:@@i18n) + # @on_lookup ||= I18n::Debug.on_lookup + # I18n::Debug.on_lookup(&@on_lookup) + + Tempfile.create(['translations', '.yml']) do |file| + file.write(YAML.dump({ 'en' => { 'json_schemer' => { 'errors' => errors } } })) + file.flush + + I18n.load_path += [file.path] + + yield + ensure + I18n.load_path -= [file.path] + end + ensure + JSONSchemer.remove_class_variable(:@@i18n) if JSONSchemer.class_variable_defined?(:@@i18n) + # I18n::Debug.on_lookup {} + end +end diff --git a/test/exe_test.rb b/test/exe_test.rb index 8f37a1c..9b73a38 100644 --- a/test/exe_test.rb +++ b/test/exe_test.rb @@ -175,7 +175,9 @@ def test_stdin def exe(*args, **kwargs) Open3.capture3('bundle', 'exec', 'json_schemer', *args, **kwargs).tap do |_stdout, stderr, _status| + # :nocov: stderr.gsub!(RUBY_2_5_WARNING_REGEX, '') if RUBY_ENGINE == 'ruby' && RUBY_VERSION.match?(/\A2\.5\.\d+\z/) + # :nocov: end end