jschema 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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