jschema 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/lib/jschema.rb +41 -0
  3. data/lib/jschema/json_reference.rb +65 -0
  4. data/lib/jschema/schema.rb +84 -0
  5. data/lib/jschema/schema_ref.rb +13 -0
  6. data/lib/jschema/simple_validator.rb +86 -0
  7. data/lib/jschema/string_length_validator.rb +17 -0
  8. data/lib/jschema/validator.rb +14 -0
  9. data/lib/jschema/validator/all_of.rb +29 -0
  10. data/lib/jschema/validator/any_of.rb +29 -0
  11. data/lib/jschema/validator/dependencies.rb +63 -0
  12. data/lib/jschema/validator/enum.rb +23 -0
  13. data/lib/jschema/validator/format.rb +75 -0
  14. data/lib/jschema/validator/items.rb +79 -0
  15. data/lib/jschema/validator/max_items.rb +28 -0
  16. data/lib/jschema/validator/max_length.rb +23 -0
  17. data/lib/jschema/validator/max_properties.rb +31 -0
  18. data/lib/jschema/validator/maximum.rb +31 -0
  19. data/lib/jschema/validator/min_items.rb +28 -0
  20. data/lib/jschema/validator/min_length.rb +23 -0
  21. data/lib/jschema/validator/min_properties.rb +31 -0
  22. data/lib/jschema/validator/minimum.rb +31 -0
  23. data/lib/jschema/validator/multiple_of.rb +32 -0
  24. data/lib/jschema/validator/not.rb +23 -0
  25. data/lib/jschema/validator/one_of.rb +29 -0
  26. data/lib/jschema/validator/pattern.rb +36 -0
  27. data/lib/jschema/validator/properties.rb +106 -0
  28. data/lib/jschema/validator/required.rb +34 -0
  29. data/lib/jschema/validator/type.rb +57 -0
  30. data/lib/jschema/validator/unique_items.rb +27 -0
  31. data/test/assert_received.rb +15 -0
  32. data/test/string_length_validator_tests.rb +18 -0
  33. data/test/test_assert_received.rb +39 -0
  34. data/test/test_integration.rb +30 -0
  35. data/test/test_json_reference.rb +84 -0
  36. data/test/test_schema.rb +125 -0
  37. data/test/test_schema_ref.rb +11 -0
  38. data/test/test_validator.rb +29 -0
  39. data/test/validator/argument_is_array_of_schemas_tests.rb +22 -0
  40. data/test/validator/assertions.rb +56 -0
  41. data/test/validator/properties_limit_tests.rb +13 -0
  42. data/test/validator/schema_validation_helpers.rb +29 -0
  43. data/test/validator/test_all_of.rb +30 -0
  44. data/test/validator/test_any_of.rb +31 -0
  45. data/test/validator/test_dependencies.rb +106 -0
  46. data/test/validator/test_enum.rb +30 -0
  47. data/test/validator/test_format.rb +70 -0
  48. data/test/validator/test_items.rb +113 -0
  49. data/test/validator/test_max_items.rb +26 -0
  50. data/test/validator/test_max_length.rb +30 -0
  51. data/test/validator/test_max_properties.rb +29 -0
  52. data/test/validator/test_maximum.rb +58 -0
  53. data/test/validator/test_min_items.rb +27 -0
  54. data/test/validator/test_min_length.rb +30 -0
  55. data/test/validator/test_min_properties.rb +29 -0
  56. data/test/validator/test_minimum.rb +58 -0
  57. data/test/validator/test_multiple_of.rb +38 -0
  58. data/test/validator/test_not.rb +32 -0
  59. data/test/validator/test_one_of.rb +31 -0
  60. data/test/validator/test_pattern.rb +32 -0
  61. data/test/validator/test_properties.rb +136 -0
  62. data/test/validator/test_required.rb +30 -0
  63. data/test/validator/test_simple_validator.rb +100 -0
  64. data/test/validator/test_type.rb +97 -0
  65. data/test/validator/test_unique_items.rb +30 -0
  66. data/test/validator/validation_against_schemas_tests.rb +24 -0
  67. metadata +144 -0
@@ -0,0 +1,23 @@
1
+ module JSchema
2
+ module Validator
3
+ class Not < SimpleValidator
4
+ private
5
+
6
+ self.keywords = ['not']
7
+
8
+ def validate_args(schema)
9
+ valid_schema?(schema, 'not') || invalid_schema('not', schema)
10
+ end
11
+
12
+ def post_initialize(not_schema)
13
+ @schema = Schema.build(not_schema, parent, 'not')
14
+ end
15
+
16
+ def validate_instance(instance)
17
+ if @schema.valid?(instance)
18
+ "#{instance} must not validate against #{@schema}"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ module JSchema
2
+ module Validator
3
+ class OneOf < SimpleValidator
4
+ private
5
+
6
+ self.keywords = ['oneOf']
7
+
8
+ def validate_args(one_of)
9
+ schema_array?(one_of, 'oneOf') || invalid_schema('oneOf', one_of)
10
+ end
11
+
12
+ def post_initialize(one_of)
13
+ @one_of = one_of.map.with_index do |sch, index|
14
+ Schema.build(sch, parent, "oneOf/#{index}")
15
+ end
16
+ end
17
+
18
+ def validate_instance(instance)
19
+ valid = @one_of.one? do |schema|
20
+ schema.valid?(instance)
21
+ end
22
+
23
+ unless valid
24
+ "#{instance} must be valid against exactly one schema"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ module JSchema
2
+ module Validator
3
+ class Pattern < SimpleValidator
4
+ private
5
+
6
+ # Fix because of Rubinius
7
+ unless defined? PrimitiveFailure
8
+ class PrimitiveFailure < Exception
9
+ end
10
+ end
11
+
12
+ self.keywords = ['pattern']
13
+
14
+ def validate_args(pattern)
15
+ Regexp.new(pattern)
16
+ true
17
+ rescue TypeError, PrimitiveFailure
18
+ invalid_schema 'pattern', pattern
19
+ end
20
+
21
+ def post_initialize(pattern)
22
+ @pattern = pattern
23
+ end
24
+
25
+ def validate_instance(instance)
26
+ unless instance.match(@pattern)
27
+ "#{instance} must match pattern #{@pattern.inspect}"
28
+ end
29
+ end
30
+
31
+ def applicable_types
32
+ [String]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,106 @@
1
+ module JSchema
2
+ module Validator
3
+ class Properties < SimpleValidator
4
+ private
5
+
6
+ self.keywords = [
7
+ 'properties',
8
+ 'patternProperties',
9
+ 'additionalProperties'
10
+ ]
11
+
12
+ def validate_args(properties, pattern_properties, additional_properties)
13
+ validate_properties(properties)
14
+ validate_pattern_properties(pattern_properties)
15
+ validate_additional_properties(additional_properties)
16
+ end
17
+
18
+ def validate_properties(properties)
19
+ valid_properties?(properties) ||
20
+ invalid_schema('properties', properties)
21
+ end
22
+
23
+ def validate_pattern_properties(pattern_properties)
24
+ valid_properties?(pattern_properties) ||
25
+ invalid_schema('patternProperties', pattern_properties)
26
+ end
27
+
28
+ def validate_additional_properties(additional_properties)
29
+ (valid_properties?(additional_properties) ||
30
+ boolean?(additional_properties)) ||
31
+ invalid_schema('additionalProperties', additional_properties)
32
+ end
33
+
34
+ def valid_properties?(properties)
35
+ properties.nil? ||
36
+ (properties.is_a?(Hash) && properties.all? { |_, v| v.is_a?(Hash) })
37
+ end
38
+
39
+ def post_initialize(properties, pattern_properties, additional_properties)
40
+ @properties = properties
41
+ @additional_properties = additional_properties
42
+ @pattern_properties = pattern_properties
43
+ end
44
+
45
+ def applicable_types
46
+ [Hash]
47
+ end
48
+
49
+ def validate_instance(instance)
50
+ instance.each do |field, value|
51
+ schemas = schemas_for(field)
52
+
53
+ if schemas.empty? && @additional_properties == false
54
+ return "#{instance} must not have any additional fields"
55
+ end
56
+
57
+ schemas.each do |schema|
58
+ validation_errors = schema.validate(value)
59
+ unless validation_errors.empty?
60
+ return validation_errors.first
61
+ end
62
+ end
63
+ end
64
+ nil
65
+ end
66
+
67
+ private
68
+
69
+ def schemas_for(field)
70
+ [
71
+ properties_schema(field),
72
+ additional_properties_schema(field),
73
+ *pattern_properties_schema(field)
74
+ ].compact
75
+ end
76
+
77
+ def properties_schema(field)
78
+ if @properties.is_a?(Hash)
79
+ if (sch = @properties[field])
80
+ Schema.build(sch, parent, 'properties')
81
+ end
82
+ end
83
+ end
84
+
85
+ def additional_properties_schema(field)
86
+ if @additional_properties.is_a?(Hash)
87
+ if (sch = @additional_properties[field])
88
+ Schema.build(sch, parent, 'additionalProperties')
89
+ end
90
+ end
91
+ end
92
+
93
+ def pattern_properties_schema(field)
94
+ schemas = []
95
+ if @pattern_properties.is_a?(Hash)
96
+ @pattern_properties.each do |pattern, sch|
97
+ if field.match(pattern)
98
+ schemas << Schema.build(sch, parent, 'patternProperties')
99
+ end
100
+ end
101
+ end
102
+ schemas
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,34 @@
1
+ module JSchema
2
+ module Validator
3
+ class Required < SimpleValidator
4
+ private
5
+
6
+ self.keywords = ['required']
7
+
8
+ def validate_args(required)
9
+ valid_required?(required) || invalid_schema('required', required)
10
+ end
11
+
12
+ def post_initialize(required)
13
+ @required = required
14
+ end
15
+
16
+ def applicable_types
17
+ [Hash]
18
+ end
19
+
20
+ def validate_instance(instance)
21
+ @required.each do |required_property|
22
+ if instance[required_property].nil?
23
+ return "#{instance} must have property `#{required_property}`"
24
+ end
25
+ end and nil
26
+ end
27
+
28
+ def valid_required?(required)
29
+ unique_non_empty_array?(required) &&
30
+ required.all? { |req| req.is_a?(String) }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,57 @@
1
+ module JSchema
2
+ module Validator
3
+ class Type < SimpleValidator
4
+ private
5
+
6
+ self.keywords = ['type']
7
+
8
+ def validate_args(type)
9
+ if type.is_a?(String) || unique_non_empty_array?(type)
10
+ true
11
+ else
12
+ invalid_schema 'type', type
13
+ end
14
+ end
15
+
16
+ def post_initialize(type)
17
+ @json_types = Array(type)
18
+ @ruby_classes = @json_types.map do |json_type|
19
+ json_type_to_ruby_class(json_type)
20
+ end.flatten.compact
21
+ end
22
+
23
+ def validate_instance(instance)
24
+ unless @ruby_classes.one? { |type| instance.is_a?(type) }
25
+ error_message(instance)
26
+ end
27
+ end
28
+
29
+ def json_type_to_ruby_class(json_type)
30
+ case json_type
31
+ when 'object' then Hash
32
+ when 'null' then NilClass
33
+ when 'string' then String
34
+ when 'integer' then [Fixnum, Bignum]
35
+ when 'array' then Array
36
+ when 'boolean' then [TrueClass, FalseClass]
37
+ when 'number' then [Fixnum, Float, BigDecimal, Bignum]
38
+ else invalid_schema('type', json_type)
39
+ end
40
+ end
41
+
42
+ def error_message(instance)
43
+ types =
44
+ case @json_types.size
45
+ when 1
46
+ @json_types.first
47
+ when 2
48
+ @json_types.join(' or ')
49
+ when 3..Float::INFINITY
50
+ @json_types[0..-2].join(', ') << ", or #{@json_types.last}"
51
+ end
52
+
53
+ "#{instance.inspect} must be a #{types}"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,27 @@
1
+ module JSchema
2
+ module Validator
3
+ class UniqueItems < SimpleValidator
4
+ private
5
+
6
+ self.keywords = ['uniqueItems']
7
+
8
+ def validate_args(unique_items)
9
+ boolean?(unique_items) || invalid_schema('uniqueItems', unique_items)
10
+ end
11
+
12
+ def post_initialize(unique_items)
13
+ @unique_items = unique_items
14
+ end
15
+
16
+ def validate_instance(instance)
17
+ if @unique_items && instance.size != instance.uniq.size
18
+ "#{instance} must contain only unique items"
19
+ end
20
+ end
21
+
22
+ def applicable_types
23
+ [Array]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ module MiniTest
2
+ module Assertions
3
+ def assert_received(receiver, message, args = [])
4
+ mock = MiniTest::Mock.new
5
+ mock.expect(message, nil, args)
6
+ receiver_stub = ->(*arg) { mock.__send__ message, *arg }
7
+
8
+ receiver.stub message, receiver_stub do
9
+ yield if block_given?
10
+ end
11
+
12
+ mock.verify
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ module StringLengthValidatorTests
2
+ def test_that_argument_can_be_big_integer
3
+ validator(2**64)
4
+ end
5
+
6
+ def test_that_argument_is_integer
7
+ assert_raises(JSchema::InvalidSchema) { validator('invalid') }
8
+ end
9
+
10
+ def test_that_argument_is_not_null
11
+ assert_raises(JSchema::InvalidSchema) { validator(nil) }
12
+ end
13
+
14
+ def test_that_validation_always_passes_for_non_string_instances
15
+ assert validator(1).valid?(nil)
16
+ assert validator(1).valid?([1, 2])
17
+ end
18
+ end
@@ -0,0 +1,39 @@
1
+ require 'minitest/autorun'
2
+ require_relative 'assert_received'
3
+
4
+ class TestAssertReceived < Minitest::Test
5
+ def test_message_sending_without_params
6
+ assert_received(Object, :class) do
7
+ Object.class
8
+ end
9
+ end
10
+
11
+ def test_message_sending_with_params
12
+ assert_received(String, :==, [Fixnum]) do
13
+ String == Fixnum
14
+ end
15
+ end
16
+
17
+ def test_assertion_failure_when_message_is_not_received
18
+ assert_raises(MockExpectationError) do
19
+ assert_received(String, :class) do
20
+ end
21
+ end
22
+ end
23
+
24
+ def test_assertion_failure_when_unexpected_message_is_received
25
+ assert_raises(MockExpectationError) do
26
+ assert_received(String, :class) do
27
+ String.name
28
+ end
29
+ end
30
+ end
31
+
32
+ def test_assertion_failure_when_unexpected_params_are_received
33
+ assert_raises(MockExpectationError) do
34
+ assert_received(String, :==, [Fixnum]) do
35
+ String == Bignum
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ require 'minitest/autorun'
2
+ require 'webmock/minitest'
3
+ require 'jschema'
4
+ require 'json'
5
+
6
+ class TestIntegration < Minitest::Test
7
+ def test_simple_schema
8
+ stub_request(:get, 'http://json-schema.org/geo')
9
+ .to_return(body: Pathname.new('test/fixtures/geo.json'))
10
+
11
+ validate 'json_schema1.json', 'json_data1.json'
12
+ end
13
+
14
+ def test_advanced_schema
15
+ validate 'json_schema2.json', 'json_data2.json'
16
+ end
17
+
18
+ private
19
+
20
+ def validate(schema_file, data_file)
21
+ sch = json_fixture(schema_file).freeze
22
+ schema = JSchema::Schema.build(sch)
23
+ data = json_fixture(data_file)
24
+ assert schema.valid?(data)
25
+ end
26
+
27
+ def json_fixture(filename)
28
+ JSON.parse open(File.join('test', 'fixtures', filename)).read
29
+ end
30
+ end
@@ -0,0 +1,84 @@
1
+ require 'minitest/autorun'
2
+ require 'webmock/minitest'
3
+ require 'jschema'
4
+ require 'ostruct'
5
+
6
+ require_relative 'assert_received'
7
+
8
+ class TestJSONReference < Minitest::Test
9
+ def test_schema_registration_and_dereferencing
10
+ schema = generate_schema('registered')
11
+ JSchema::JSONReference.register_schema schema
12
+ assert_equal dereference(schema), schema
13
+ end
14
+
15
+ def test_schema_dereferencing_with_same_uri
16
+ schema1 = generate_schema('uri')
17
+ schema2 = generate_schema('uri')
18
+ JSchema::JSONReference.register_schema schema1
19
+ JSchema::JSONReference.register_schema schema2
20
+
21
+ assert_equal dereference(schema1), schema1
22
+ assert_equal dereference(schema2), schema2
23
+ end
24
+
25
+ def test_dereferencing_external_schema
26
+ external_schema = { 'type' => 'number' }
27
+ schema_uri = 'http://example.com/geo'
28
+
29
+ expected_schema_args = [external_schema, URI(schema_uri), nil]
30
+ assert_received JSchema::Schema, :new, expected_schema_args do
31
+ JSchema::JSONReference.stub :register_schema, nil do
32
+ dereference_external_schema schema_uri, external_schema.to_json
33
+ end
34
+ end
35
+ end
36
+
37
+ def test_that_external_schema_is_cached_after_dereferencing
38
+ schema = Object.new
39
+ JSchema::Schema.stub :new, schema do
40
+ assert_received JSchema::JSONReference, :register_schema, [schema] do
41
+ dereference_external_schema 'http://example.com', '{}'
42
+ end
43
+ end
44
+ end
45
+
46
+ def test_dereferencing_external_schema_when_it_is_not_valid
47
+ assert_raises(JSchema::InvalidSchema) do
48
+ dereference_external_schema 'http://example.com/', '}'
49
+ end
50
+ end
51
+
52
+ def test_dereferencing_external_schema_when_it_is_not_available
53
+ assert_raises(JSchema::InvalidSchema) do
54
+ stub_request(:get, //).to_timeout
55
+ dereference generate_schema('http://example.com/')
56
+ end
57
+ end
58
+
59
+ def test_dereferencing_external_schema_when_protocol_is_not_supported
60
+ assert_raises(JSchema::InvalidSchema) do
61
+ dereference_external_schema 'ftp://example.com/', '{}'
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def dereference_external_schema(uri, response_schema)
68
+ stub_request(:get, uri).to_return(body: response_schema)
69
+ dereference generate_schema(uri)
70
+ end
71
+
72
+ def dereference(schema)
73
+ JSchema::JSONReference.dereference(schema.uri, schema)
74
+ end
75
+
76
+ def generate_schema(uri)
77
+ OpenStruct.new(uri: URI(uri), id: generate_id)
78
+ end
79
+
80
+ def generate_id
81
+ @id ||= 0
82
+ @id += 1
83
+ end
84
+ end