schemacop 2.4.4 → 3.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (172) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +25 -1
  4. data/.travis.yml +3 -1
  5. data/CHANGELOG.md +21 -0
  6. data/README.md +53 -698
  7. data/README_V2.md +775 -0
  8. data/README_V3.md +1051 -0
  9. data/Rakefile +8 -12
  10. data/VERSION +1 -1
  11. data/lib/schemacop.rb +35 -36
  12. data/lib/schemacop/base_schema.rb +37 -0
  13. data/lib/schemacop/railtie.rb +10 -0
  14. data/lib/schemacop/schema.rb +1 -60
  15. data/lib/schemacop/schema2.rb +22 -0
  16. data/lib/schemacop/schema3.rb +21 -0
  17. data/lib/schemacop/scoped_env.rb +25 -13
  18. data/lib/schemacop/v2.rb +26 -0
  19. data/lib/schemacop/{caster.rb → v2/caster.rb} +16 -2
  20. data/lib/schemacop/{collector.rb → v2/collector.rb} +5 -2
  21. data/lib/schemacop/{dupper.rb → v2/dupper.rb} +1 -1
  22. data/lib/schemacop/{field_node.rb → v2/field_node.rb} +5 -3
  23. data/lib/schemacop/v2/node.rb +142 -0
  24. data/lib/schemacop/{node_resolver.rb → v2/node_resolver.rb} +1 -1
  25. data/lib/schemacop/v2/node_supporting_field.rb +70 -0
  26. data/lib/schemacop/{node_supporting_type.rb → v2/node_supporting_type.rb} +14 -11
  27. data/lib/schemacop/{node_with_block.rb → v2/node_with_block.rb} +3 -2
  28. data/lib/schemacop/v2/root_node.rb +6 -0
  29. data/lib/schemacop/v2/validator/array_validator.rb +32 -0
  30. data/lib/schemacop/{validator → v2/validator}/boolean_validator.rb +1 -1
  31. data/lib/schemacop/v2/validator/float_validator.rb +7 -0
  32. data/lib/schemacop/v2/validator/hash_validator.rb +37 -0
  33. data/lib/schemacop/v2/validator/integer_validator.rb +7 -0
  34. data/lib/schemacop/{validator → v2/validator}/nil_validator.rb +1 -1
  35. data/lib/schemacop/v2/validator/number_validator.rb +21 -0
  36. data/lib/schemacop/v2/validator/object_validator.rb +29 -0
  37. data/lib/schemacop/v2/validator/string_validator.rb +39 -0
  38. data/lib/schemacop/{validator → v2/validator}/symbol_validator.rb +1 -1
  39. data/lib/schemacop/v3.rb +45 -0
  40. data/lib/schemacop/v3/all_of_node.rb +27 -0
  41. data/lib/schemacop/v3/any_of_node.rb +28 -0
  42. data/lib/schemacop/v3/array_node.rb +219 -0
  43. data/lib/schemacop/v3/boolean_node.rb +16 -0
  44. data/lib/schemacop/v3/combination_node.rb +45 -0
  45. data/lib/schemacop/v3/context.rb +17 -0
  46. data/lib/schemacop/v3/dsl_scope.rb +46 -0
  47. data/lib/schemacop/v3/global_context.rb +114 -0
  48. data/lib/schemacop/v3/hash_node.rb +238 -0
  49. data/lib/schemacop/v3/integer_node.rb +13 -0
  50. data/lib/schemacop/v3/is_not_node.rb +32 -0
  51. data/lib/schemacop/v3/node.rb +214 -0
  52. data/lib/schemacop/v3/node_registry.rb +49 -0
  53. data/lib/schemacop/v3/number_node.rb +18 -0
  54. data/lib/schemacop/v3/numeric_node.rb +76 -0
  55. data/lib/schemacop/v3/object_node.rb +40 -0
  56. data/lib/schemacop/v3/one_of_node.rb +28 -0
  57. data/lib/schemacop/v3/reference_node.rb +49 -0
  58. data/lib/schemacop/v3/result.rb +58 -0
  59. data/lib/schemacop/v3/string_node.rb +124 -0
  60. data/lib/schemacop/v3/symbol_node.rb +13 -0
  61. data/schemacop.gemspec +24 -27
  62. data/test/lib/test_helper.rb +152 -0
  63. data/test/schemas/nested/group.rb +6 -0
  64. data/test/schemas/user.rb +7 -0
  65. data/test/unit/schemacop/v2/casting_test.rb +120 -0
  66. data/test/unit/schemacop/v2/collector_test.rb +47 -0
  67. data/test/unit/schemacop/v2/custom_check_test.rb +95 -0
  68. data/test/unit/schemacop/v2/custom_if_test.rb +97 -0
  69. data/test/unit/schemacop/v2/defaults_test.rb +95 -0
  70. data/test/unit/schemacop/v2/empty_test.rb +16 -0
  71. data/test/unit/schemacop/v2/nil_dis_allow_test.rb +43 -0
  72. data/test/unit/schemacop/v2/node_resolver_test.rb +28 -0
  73. data/test/unit/schemacop/v2/short_forms_test.rb +351 -0
  74. data/test/unit/schemacop/v2/types_test.rb +88 -0
  75. data/test/unit/schemacop/v2/validator_array_test.rb +99 -0
  76. data/test/unit/schemacop/v2/validator_boolean_test.rb +17 -0
  77. data/test/unit/schemacop/v2/validator_float_test.rb +59 -0
  78. data/test/unit/schemacop/v2/validator_hash_test.rb +95 -0
  79. data/test/unit/schemacop/v2/validator_integer_test.rb +48 -0
  80. data/test/unit/schemacop/v2/validator_nil_test.rb +15 -0
  81. data/test/unit/schemacop/v2/validator_number_test.rb +62 -0
  82. data/test/unit/schemacop/v2/validator_object_test.rb +141 -0
  83. data/test/unit/schemacop/v2/validator_string_test.rb +78 -0
  84. data/test/unit/schemacop/v2/validator_symbol_test.rb +18 -0
  85. data/test/unit/schemacop/v3/all_of_node_test.rb +199 -0
  86. data/test/unit/schemacop/v3/any_of_node_test.rb +218 -0
  87. data/test/unit/schemacop/v3/array_node_test.rb +805 -0
  88. data/test/unit/schemacop/v3/boolean_node_test.rb +126 -0
  89. data/test/unit/schemacop/v3/global_context_test.rb +164 -0
  90. data/test/unit/schemacop/v3/hash_node_test.rb +826 -0
  91. data/test/unit/schemacop/v3/integer_node_test.rb +323 -0
  92. data/test/unit/schemacop/v3/is_not_node_test.rb +173 -0
  93. data/test/unit/schemacop/v3/node_test.rb +148 -0
  94. data/test/unit/schemacop/v3/number_node_test.rb +292 -0
  95. data/test/unit/schemacop/v3/object_node_test.rb +170 -0
  96. data/test/unit/schemacop/v3/one_of_node_test.rb +187 -0
  97. data/test/unit/schemacop/v3/reference_node_test.rb +351 -0
  98. data/test/unit/schemacop/v3/string_node_test.rb +334 -0
  99. data/test/unit/schemacop/v3/symbol_node_test.rb +75 -0
  100. metadata +152 -143
  101. data/doc/Schemacop.html +0 -146
  102. data/doc/Schemacop/ArrayValidator.html +0 -329
  103. data/doc/Schemacop/BooleanValidator.html +0 -145
  104. data/doc/Schemacop/Caster.html +0 -379
  105. data/doc/Schemacop/Collector.html +0 -787
  106. data/doc/Schemacop/Dupper.html +0 -214
  107. data/doc/Schemacop/Exceptions.html +0 -115
  108. data/doc/Schemacop/Exceptions/InvalidSchemaError.html +0 -124
  109. data/doc/Schemacop/Exceptions/ValidationError.html +0 -124
  110. data/doc/Schemacop/FieldNode.html +0 -421
  111. data/doc/Schemacop/FloatValidator.html +0 -158
  112. data/doc/Schemacop/HashValidator.html +0 -293
  113. data/doc/Schemacop/IntegerValidator.html +0 -158
  114. data/doc/Schemacop/NilValidator.html +0 -145
  115. data/doc/Schemacop/Node.html +0 -1438
  116. data/doc/Schemacop/NodeResolver.html +0 -258
  117. data/doc/Schemacop/NodeSupportingField.html +0 -590
  118. data/doc/Schemacop/NodeSupportingType.html +0 -612
  119. data/doc/Schemacop/NodeWithBlock.html +0 -289
  120. data/doc/Schemacop/NumberValidator.html +0 -232
  121. data/doc/Schemacop/ObjectValidator.html +0 -298
  122. data/doc/Schemacop/RootNode.html +0 -171
  123. data/doc/Schemacop/Schema.html +0 -699
  124. data/doc/Schemacop/StringValidator.html +0 -295
  125. data/doc/Schemacop/SymbolValidator.html +0 -145
  126. data/doc/ScopedEnv.html +0 -351
  127. data/doc/_index.html +0 -379
  128. data/doc/class_list.html +0 -51
  129. data/doc/css/common.css +0 -1
  130. data/doc/css/full_list.css +0 -58
  131. data/doc/css/style.css +0 -496
  132. data/doc/file.README.html +0 -821
  133. data/doc/file_list.html +0 -56
  134. data/doc/frames.html +0 -17
  135. data/doc/index.html +0 -821
  136. data/doc/inheritance.graphml +0 -524
  137. data/doc/inheritance.pdf +0 -825
  138. data/doc/js/app.js +0 -303
  139. data/doc/js/full_list.js +0 -216
  140. data/doc/js/jquery.js +0 -4
  141. data/doc/method_list.html +0 -587
  142. data/doc/top-level-namespace.html +0 -112
  143. data/lib/schemacop/node.rb +0 -139
  144. data/lib/schemacop/node_supporting_field.rb +0 -58
  145. data/lib/schemacop/root_node.rb +0 -4
  146. data/lib/schemacop/validator/array_validator.rb +0 -30
  147. data/lib/schemacop/validator/float_validator.rb +0 -5
  148. data/lib/schemacop/validator/hash_validator.rb +0 -35
  149. data/lib/schemacop/validator/integer_validator.rb +0 -5
  150. data/lib/schemacop/validator/number_validator.rb +0 -19
  151. data/lib/schemacop/validator/object_validator.rb +0 -27
  152. data/lib/schemacop/validator/string_validator.rb +0 -37
  153. data/test/casting_test.rb +0 -90
  154. data/test/collector_test.rb +0 -45
  155. data/test/custom_check_test.rb +0 -93
  156. data/test/custom_if_test.rb +0 -95
  157. data/test/defaults_test.rb +0 -71
  158. data/test/nil_dis_allow_test.rb +0 -41
  159. data/test/node_resolver_test.rb +0 -26
  160. data/test/short_forms_test.rb +0 -349
  161. data/test/test_helper.rb +0 -13
  162. data/test/types_test.rb +0 -84
  163. data/test/validator_array_test.rb +0 -97
  164. data/test/validator_boolean_test.rb +0 -15
  165. data/test/validator_float_test.rb +0 -57
  166. data/test/validator_hash_test.rb +0 -93
  167. data/test/validator_integer_test.rb +0 -46
  168. data/test/validator_nil_test.rb +0 -13
  169. data/test/validator_number_test.rb +0 -60
  170. data/test/validator_object_test.rb +0 -139
  171. data/test/validator_string_test.rb +0 -76
  172. data/test/validator_symbol_test.rb +0 -16
@@ -0,0 +1,126 @@
1
+ require 'test_helper'
2
+ # rubocop:disable Lint/BooleanSymbol
3
+
4
+ module Schemacop
5
+ module V3
6
+ class BooleanNodeTest < V3Test
7
+ EXP_INVALID_TYPE = 'Invalid type, expected "boolean".'.freeze
8
+
9
+ def test_basic
10
+ schema :boolean
11
+
12
+ assert_validation true
13
+ assert_validation false
14
+
15
+ assert_json(type: :boolean)
16
+ end
17
+
18
+ def test_required
19
+ schema :boolean, required: true
20
+
21
+ assert_validation true
22
+ assert_validation false
23
+ assert_validation nil do
24
+ error '/', 'Value must be given.'
25
+ end
26
+
27
+ assert_json(type: :boolean)
28
+ end
29
+
30
+ def test_hash
31
+ schema { boo! :alive }
32
+ assert_json(
33
+ type: :object,
34
+ properties: {
35
+ alive: { type: :boolean }
36
+ },
37
+ required: %i[alive],
38
+ additionalProperties: false
39
+ )
40
+ assert_validation alive: true
41
+ assert_validation alive: false
42
+ end
43
+
44
+ def test_type
45
+ schema :boolean
46
+
47
+ assert_json(type: :boolean)
48
+
49
+ assert_validation 42 do
50
+ error '/', EXP_INVALID_TYPE
51
+ end
52
+
53
+ [:true, 'true', :false, 'false', 0, 1].each do |value|
54
+ assert_validation value do
55
+ error '/', EXP_INVALID_TYPE
56
+ end
57
+ end
58
+
59
+ schema { boo? :name }
60
+
61
+ assert_json(
62
+ type: :object,
63
+ properties: {
64
+ name: { type: :boolean }
65
+ },
66
+ additionalProperties: false
67
+ )
68
+
69
+ [:true, 'true', :false, 'false', 0, 1].each do |value|
70
+ assert_validation name: value do
71
+ error '/name', EXP_INVALID_TYPE
72
+ end
73
+ end
74
+ end
75
+
76
+ def test_enum_schema
77
+ schema :boolean, enum: [1, 2, 'foo', :bar, { qux: 42 }, true]
78
+
79
+ assert_json({
80
+ type: :boolean,
81
+ enum: [1, 2, 'foo', :bar, { qux: 42 }, true]
82
+ })
83
+
84
+ assert_validation(nil)
85
+ assert_validation(true)
86
+
87
+ # Even we put those types in the enum, they need to fail the validations,
88
+ # as they are not booleans
89
+ assert_validation('foo') do
90
+ error '/', 'Invalid type, expected "boolean".'
91
+ end
92
+ assert_validation(:bar) do
93
+ error '/', 'Invalid type, expected "boolean".'
94
+ end
95
+ assert_validation({ qux: 42 }) do
96
+ error '/', 'Invalid type, expected "boolean".'
97
+ end
98
+
99
+ # These need to fail validation, as they are not in the enum list
100
+ assert_validation(false) do
101
+ error '/', 'Value not included in enum [1, 2, "foo", :bar, {:qux=>42}, true].'
102
+ end
103
+ end
104
+
105
+ def test_with_generic_keywords
106
+ schema :boolean, enum: [1, 'foo', true],
107
+ title: 'Boolean schema',
108
+ description: 'Boolean schema holding generic keywords',
109
+ examples: [
110
+ true
111
+ ]
112
+
113
+ assert_json({
114
+ type: :boolean,
115
+ enum: [1, 'foo', true],
116
+ title: 'Boolean schema',
117
+ description: 'Boolean schema holding generic keywords',
118
+ examples: [
119
+ true
120
+ ]
121
+ })
122
+ end
123
+ end
124
+ end
125
+ end
126
+ # rubocop:enable Lint/BooleanSymbol
@@ -0,0 +1,164 @@
1
+ require 'test_helper'
2
+
3
+ module Schemacop
4
+ module V3
5
+ class GlobalContextTest < V3Test
6
+ def setup
7
+ super
8
+ GlobalContext.instance_variable_set(:@instance, GlobalContext.send(:new))
9
+ Schemacop.load_paths = ['test/schemas']
10
+ end
11
+
12
+ def test_instance
13
+ assert_equal GlobalContext.instance, GlobalContext.instance
14
+ end
15
+
16
+ def test_eager_load
17
+ refute GlobalContext.instance.instance_variable_get(:@eager_loaded)
18
+ GlobalContext.instance.eager_load!
19
+ assert GlobalContext.instance.instance_variable_get(:@eager_loaded)
20
+
21
+ assert_raises_with_message RuntimeError, /can't be eager loaded more than once/ do
22
+ GlobalContext.instance.eager_load!
23
+ end
24
+ end
25
+
26
+ def test_schemas
27
+ assert_equal({}, GlobalContext.instance.schemas)
28
+ GlobalContext.instance.eager_load!
29
+ assert_equal(%i[nested/group user], GlobalContext.instance.schemas.keys)
30
+ assert_is_a Node, GlobalContext.instance.schemas.values.first
31
+ end
32
+
33
+ def test_schema_for_w_eager_loading
34
+ GlobalContext.instance.eager_load!
35
+
36
+ 2.times do
37
+ assert_is_a HashNode, GlobalContext.instance.schema_for('user')
38
+ assert_equal(%i[nested/group user], GlobalContext.instance.schemas.keys)
39
+ assert_is_a HashNode, GlobalContext.instance.schema_for('user')
40
+ end
41
+ end
42
+
43
+ def test_schema_for_wo_eager_loading
44
+ assert_is_a HashNode, GlobalContext.instance.schema_for('user')
45
+ assert_equal(%i[user], GlobalContext.instance.schemas.keys)
46
+ assert_is_a HashNode, GlobalContext.instance.schema_for('user')
47
+ assert_is_a HashNode, GlobalContext.instance.schema_for('nested/group')
48
+ assert_equal(%i[user nested/group], GlobalContext.instance.schemas.keys)
49
+ end
50
+
51
+ def test_file_reload
52
+ dir = Dir.mktmpdir
53
+ Schemacop.load_paths << dir
54
+ IO.write(File.join(dir, 'foo.rb'), %(schema :string))
55
+ assert_is_a StringNode, GlobalContext.instance.schema_for('foo')
56
+ IO.write(File.join(dir, 'foo.rb'), %(schema :integer))
57
+ assert_is_a IntegerNode, GlobalContext.instance.schema_for('foo')
58
+ end
59
+
60
+ def test_file_not_reloaded_in_eager
61
+ dir = Dir.mktmpdir
62
+ Schemacop.load_paths << dir
63
+
64
+ IO.write(File.join(dir, 'foo.rb'), %(schema :string))
65
+
66
+ GlobalContext.instance.eager_load!
67
+
68
+ assert_is_a StringNode, GlobalContext.instance.schema_for('foo')
69
+ IO.write(File.join(dir, 'foo.rb'), %(schema :integer))
70
+ assert_is_a StringNode, GlobalContext.instance.schema_for('foo')
71
+ end
72
+
73
+ def test_schema_not_found
74
+ assert_nil GlobalContext.instance.schema_for('foo')
75
+ GlobalContext.instance.eager_load!
76
+ assert_nil GlobalContext.instance.schema_for('foo')
77
+ end
78
+
79
+ def test_inter_references
80
+ schema = GlobalContext.instance.schema_for('user')
81
+
82
+ assert schema.validate(
83
+ first_name: 'John',
84
+ last_name: 'Doe',
85
+ groups: [
86
+ { name: 'Group 1' }
87
+ ]
88
+ ).valid?
89
+
90
+ refute schema.validate(
91
+ first_name: 'John',
92
+ last_name: 'Doe',
93
+ groups: [
94
+ { name_x: 'Group 1' }
95
+ ]
96
+ ).valid?
97
+
98
+ schema = GlobalContext.instance.schema_for('nested/group')
99
+
100
+ assert schema.validate(
101
+ name: 'Group 1',
102
+ users: [
103
+ { first_name: 'John', last_name: 'Doe' }
104
+ ]
105
+ ).valid?
106
+
107
+ refute schema.validate(
108
+ name: 'Group 1',
109
+ users: [
110
+ { first_name_x: 'John', last_name: 'Doe' }
111
+ ]
112
+ ).valid?
113
+ end
114
+
115
+ def test_empty_schema
116
+ dir = Dir.mktmpdir
117
+ Schemacop.load_paths << dir
118
+ IO.write(File.join(dir, 'foo.rb'), %())
119
+ assert_raises_with_message RuntimeError, /does not define any schema/ do
120
+ GlobalContext.instance.schema_for('foo')
121
+ end
122
+ end
123
+
124
+ def test_multiple_schemas
125
+ dir = Dir.mktmpdir
126
+ Schemacop.load_paths << dir
127
+ IO.write(File.join(dir, 'foo.rb'), %(schema :string\nschema :integer))
128
+ assert_raises_with_message RuntimeError, /Schema "#{File.join(dir, 'foo.rb')}" defines multiple schemas/ do
129
+ GlobalContext.instance.schema_for('foo')
130
+ end
131
+ end
132
+
133
+ def test_invalid_schema
134
+ dir = Dir.mktmpdir
135
+ Schemacop.load_paths << dir
136
+ IO.write(File.join(dir, 'foo.rb'), %(foobarbaz))
137
+
138
+ assert_raises_with_message RuntimeError, /Could not load schema/ do
139
+ GlobalContext.schema_for('foo')
140
+ end
141
+ end
142
+
143
+ def test_overrides_with_eager_load
144
+ dir = Dir.mktmpdir
145
+ Schemacop.load_paths << dir
146
+ IO.write(File.join(dir, 'user.rb'), %(schema :string))
147
+
148
+ assert_raises_with_message RuntimeError, %r{in both load paths "test/schemas" and "#{dir}"} do
149
+ GlobalContext.eager_load!
150
+ end
151
+ end
152
+
153
+ def test_overrides_with_lazy_load
154
+ dir = Dir.mktmpdir
155
+ Schemacop.load_paths << dir
156
+ IO.write(File.join(dir, 'user.rb'), %(schema :string))
157
+
158
+ assert_raises_with_message RuntimeError, %r{in both load paths "test/schemas" and "#{dir}"} do
159
+ GlobalContext.instance.schema_for('user')
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,826 @@
1
+ require 'test_helper'
2
+
3
+ module Schemacop
4
+ module V3
5
+ class HashNodeTest < V3Test
6
+ EXP_INVALID_TYPE = 'Invalid type, expected "hash".'.freeze
7
+
8
+ def test_basic
9
+ schema
10
+ assert_validation({})
11
+ assert_json(type: :object, additionalProperties: false)
12
+
13
+ schema :hash
14
+ assert_validation({})
15
+
16
+ assert_json(type: :object, additionalProperties: false)
17
+ end
18
+
19
+ def test_additional_properties_false
20
+ schema
21
+ assert_validation({})
22
+ assert_validation(foo: :bar, bar: :baz) do
23
+ error '/', 'Obsolete property "foo".'
24
+ error '/', 'Obsolete property "bar".'
25
+ end
26
+ assert_json(type: :object, additionalProperties: false)
27
+ end
28
+
29
+ def test_additional_properties_true
30
+ schema :hash, additional_properties: true
31
+ assert_validation({})
32
+ assert_validation(foo: :bar)
33
+ assert_validation(foo: { bar: :baz })
34
+
35
+ assert_json(type: :object, additionalProperties: true)
36
+ end
37
+
38
+ def test_additional_properties_schema
39
+ schema :hash do
40
+ str! :foo
41
+ add :string
42
+ end
43
+
44
+ assert_validation(foo: 'bar', baz: 'foo', answer: '42')
45
+ assert_validation(foo: 'bar', baz: 'foo', answer: 42) do
46
+ error '/answer', 'Invalid type, expected "string".'
47
+ end
48
+
49
+ assert_json(
50
+ properties: {
51
+ foo: { type: :string }
52
+ },
53
+ required: %i[foo],
54
+ type: :object,
55
+ additionalProperties: { type: :string }
56
+ )
57
+ end
58
+
59
+ def test_property_names
60
+ schema :hash, additional_properties: true, property_names: '^[a-zA-Z0-9]+$'
61
+ assert_validation({})
62
+ assert_validation(foo: :bar)
63
+ assert_validation('foo' => 'bar')
64
+ assert_validation(Foo: :bar)
65
+ assert_validation('_foo39sjfdoi 345893(%' => 'bar', 'foo' => 'bar') do
66
+ error '/', 'Property name "_foo39sjfdoi 345893(%" does not match "^[a-zA-Z0-9]+$".'
67
+ end
68
+
69
+ assert_json(
70
+ type: :object,
71
+ additionalProperties: true,
72
+ propertyNames: '^[a-zA-Z0-9]+$'
73
+ )
74
+
75
+ assert_cast({ foo: 123 }, { foo: 123 })
76
+ assert_cast({ Foo: 123 }, { Foo: 123 })
77
+
78
+ # New schema
79
+ schema :hash, additional_properties: true, property_names: '^[a-z]+$'
80
+
81
+ assert_validation({})
82
+ assert_validation(foo: :bar)
83
+ assert_validation('foo' => 'bar')
84
+ assert_validation(Foo: :bar) do
85
+ error '/', 'Property name :Foo does not match "^[a-z]+$".'
86
+ end
87
+ assert_validation('_foo39sjfdoi 345893(%' => 'bar', 'foo' => 'bar') do
88
+ error '/', 'Property name "_foo39sjfdoi 345893(%" does not match "^[a-z]+$".'
89
+ end
90
+
91
+ assert_cast({ foo: 123 }, { foo: 123 })
92
+ end
93
+
94
+ def test_required
95
+ schema do
96
+ str! :foo
97
+ int? :bar
98
+ end
99
+
100
+ assert_validation(foo: 'sdfsd')
101
+ assert_validation(foo: 'sdfsd', bar: 42)
102
+
103
+ assert_validation(bar: 42) do
104
+ error '/foo', 'Value must be given.'
105
+ end
106
+
107
+ assert_validation({}) do
108
+ error '/foo', 'Value must be given.'
109
+ end
110
+
111
+ assert_json(
112
+ type: :object,
113
+ properties: {
114
+ foo: { type: :string },
115
+ bar: { type: :integer }
116
+ },
117
+ required: %i[foo],
118
+ additionalProperties: false
119
+ )
120
+ end
121
+
122
+ def test_min_properties
123
+ schema :hash, min_properties: 2, additional_properties: true
124
+ assert_validation(foo: :bar, bar: :baz)
125
+ assert_validation(foo: :bar, bar: :baz, baz: :foo)
126
+
127
+ assert_validation(foo: :bar) do
128
+ error '/', 'Has 1 properties but needs at least 2.'
129
+ end
130
+
131
+ assert_validation({}) do
132
+ error '/', 'Has 0 properties but needs at least 2.'
133
+ end
134
+
135
+ assert_json(
136
+ type: :object,
137
+ minProperties: 2,
138
+ additionalProperties: true
139
+ )
140
+ end
141
+
142
+ def test_max_properties
143
+ schema :hash, max_properties: 3, additional_properties: true
144
+ assert_validation(foo: :bar, bar: :baz)
145
+ assert_validation(foo: :bar, bar: :baz, baz: :foo)
146
+
147
+ assert_validation(foo: :bar, bar: :baz, baz: :foo, answer: 42) do
148
+ error '/', 'Has 4 properties but needs at most 3.'
149
+ end
150
+
151
+ assert_json(
152
+ type: :object,
153
+ maxProperties: 3,
154
+ additionalProperties: true
155
+ )
156
+ end
157
+
158
+ def test_min_max_properties
159
+ schema :hash, min_properties: 3, max_properties: 3, additional_properties: true
160
+ assert_validation(foo: :bar, bar: :baz, baz: :foo)
161
+
162
+ assert_validation(foo: :bar, bar: :baz, baz: :foo, answer: 42) do
163
+ error '/', 'Has 4 properties but needs at most 3.'
164
+ end
165
+
166
+ assert_validation(foo: :bar, bar: :baz) do
167
+ error '/', 'Has 2 properties but needs at least 3.'
168
+ end
169
+
170
+ assert_json(
171
+ type: :object,
172
+ minProperties: 3,
173
+ maxProperties: 3,
174
+ additionalProperties: true
175
+ )
176
+ end
177
+
178
+ def test_dependencies
179
+ schema :hash do
180
+ str! :name
181
+ str? :credit_card
182
+ str? :billing_address
183
+ str? :phone_number
184
+
185
+ dep :credit_card, :billing_address, :phone_number
186
+ dep :billing_address, :credit_card
187
+ end
188
+
189
+ assert_validation(name: 'John')
190
+ assert_validation(name: 'John', credit_card: '23423523', billing_address: 'Example 3', phone_number: '234')
191
+
192
+ assert_validation(name: 'John', credit_card: '23423523') do
193
+ error '/', 'Missing property "billing_address" because "credit_card" is given.'
194
+ error '/', 'Missing property "phone_number" because "credit_card" is given.'
195
+ end
196
+
197
+ assert_validation(name: 'John', billing_address: 'Example 3') do
198
+ error '/', 'Missing property "credit_card" because "billing_address" is given.'
199
+ end
200
+
201
+ assert_json(
202
+ type: :object,
203
+ properties: {
204
+ name: { type: :string },
205
+ credit_card: { type: :string },
206
+ billing_address: { type: :string },
207
+ phone_number: { type: :string }
208
+ },
209
+ required: %i[name],
210
+ dependencies: {
211
+ credit_card: %i[billing_address phone_number],
212
+ billing_address: %i[credit_card]
213
+ },
214
+ additionalProperties: false
215
+ )
216
+ end
217
+
218
+ def test_pattern_properties_wo_additional
219
+ schema additional_properties: false do
220
+ str! :name
221
+ str?(/^foo_.*$/)
222
+ int?(/^bar_.*$/)
223
+ end
224
+
225
+ assert_validation(name: 'John', foo_bar: 'John')
226
+ assert_validation(name: 'John', foo_bar: 'John', bar_baz: 42, foo_baz: '42')
227
+ assert_validation(name: 'John', foo_baz: 'John', bar_baz: 42)
228
+
229
+ assert_validation(name: 'John', xy: 'John', bar_baz: 'Doe') do
230
+ error '/', 'Obsolete property "xy".'
231
+ error '/bar_baz', 'Invalid type, expected "integer".'
232
+ end
233
+
234
+ assert_json(
235
+ type: :object,
236
+ properties: {
237
+ name: { type: :string }
238
+ },
239
+ patternProperties: {
240
+ '^foo_.*$': { type: :string },
241
+ '^bar_.*$': { type: :integer }
242
+ },
243
+ additionalProperties: false,
244
+ required: %i[name]
245
+ )
246
+ end
247
+
248
+ def test_pattern_properties_w_additional
249
+ schema additional_properties: true do
250
+ int? :builtin
251
+ str?(/^S_/)
252
+ int?(/^I_/)
253
+ add :string
254
+ end
255
+
256
+ assert_validation(builtin: 42)
257
+ assert_validation(keyword: 'value')
258
+
259
+ assert_validation(keyword: 42) do
260
+ error '/keyword', 'Invalid type, expected "string".'
261
+ end
262
+
263
+ assert_json(
264
+ type: 'object',
265
+ properties: {
266
+ builtin: { type: :integer }
267
+ },
268
+ patternProperties: {
269
+ '^S_': { type: :string },
270
+ '^I_': { type: :integer }
271
+ },
272
+ additionalProperties: { type: :string }
273
+ )
274
+ end
275
+
276
+ def test_pattern_properties_casting
277
+ schema do
278
+ int?(/^id_.*$/)
279
+ int?(/^val.*$/)
280
+ end
281
+
282
+ assert_json({
283
+ type: :object,
284
+ patternProperties: {
285
+ '^id_.*$': { type: :integer },
286
+ '^val.*$': { type: :integer }
287
+ },
288
+ additionalProperties: false
289
+ })
290
+
291
+ assert_validation({})
292
+ assert_validation({ id_foo: 1 })
293
+ assert_validation({ id_foo: 1, id_bar: 2 })
294
+ assert_validation({ id_foo: 1, id_bar: 2, value: 4 })
295
+
296
+ assert_cast({ id_foo: 1 }, { id_foo: 1 })
297
+ assert_cast({ id_foo: 1, id_bar: 2 }, { id_foo: 1, id_bar: 2 })
298
+ assert_cast({ id_foo: 1, id_bar: 2, value: 4 }, { id_foo: 1, id_bar: 2, value: 4 })
299
+ end
300
+
301
+ def test_defaults
302
+ schema do
303
+ str? :first_name, default: 'John'
304
+ str? :last_name, default: 'Doe'
305
+ str! :active, format: :boolean
306
+ hsh? :address, default: {} do
307
+ str? :street, default: 'Example 42'
308
+ end
309
+ end
310
+
311
+ data = { last_name: 'Doeringer', active: 'true' }
312
+ data_was = data.dup
313
+
314
+ assert_equal({ first_name: 'John', last_name: 'Doeringer', active: true, address: { street: 'Example 42' } }, @schema.validate(data).data)
315
+ assert_equal data_was, data
316
+
317
+ schema do
318
+ hsh? :address do
319
+ str? :street, default: 'Example 42'
320
+ end
321
+ end
322
+
323
+ assert_equal({}, @schema.validate({}).data)
324
+ end
325
+
326
+ def test_all_of
327
+ schema do
328
+ all_of! :str do
329
+ str min_length: 3
330
+ str max_length: 5
331
+ end
332
+ end
333
+
334
+ assert_validation(str: '123')
335
+ assert_validation(str: '1234')
336
+ assert_validation(str: '12345')
337
+ assert_validation(str: '0') do
338
+ error '/str', 'Does not match all allOf conditions.'
339
+ end
340
+ end
341
+
342
+ def test_one_of_required
343
+ schema do
344
+ one_of! :str do
345
+ str min_length: 4
346
+ str min_length: 0, max_length: 4
347
+ end
348
+ end
349
+
350
+ assert_validation(str: '12345')
351
+ assert_validation(str: '123')
352
+ assert_validation(str: nil) do
353
+ error '/str', 'Value must be given.'
354
+ end
355
+ assert_validation(str: '1234') do
356
+ error '/str', 'Matches 2 definitions but should match exactly 1.'
357
+ end
358
+ end
359
+
360
+ def test_one_of_optional
361
+ schema do
362
+ one_of? :str do
363
+ str min_length: 4
364
+ str min_length: 0, max_length: 4
365
+ end
366
+ end
367
+
368
+ assert_validation(str: '12345')
369
+ assert_validation(str: '123')
370
+ assert_validation(str: nil)
371
+ assert_validation({})
372
+ assert_validation(str: '1234') do
373
+ error '/str', 'Matches 2 definitions but should match exactly 1.'
374
+ end
375
+ end
376
+
377
+ def test_any_of_required
378
+ schema do
379
+ any_of! :str_or_int do
380
+ str
381
+ int
382
+ end
383
+ end
384
+
385
+ assert_validation(str_or_int: 'Hello World')
386
+ assert_validation(str_or_int: 42)
387
+ assert_validation(str_or_int: :foo) do
388
+ error '/str_or_int', 'Does not match any anyOf condition.'
389
+ end
390
+ end
391
+
392
+ def test_any_of_optional
393
+ schema do
394
+ any_of? :str_or_int do
395
+ str
396
+ int
397
+ end
398
+ end
399
+
400
+ assert_validation(str_or_int: 'Hello World')
401
+ assert_validation(str_or_int: 42)
402
+ assert_validation(str_or_int: nil)
403
+ assert_validation({})
404
+ assert_validation(str_or_int: :foo) do
405
+ error '/str_or_int', 'Does not match any anyOf condition.'
406
+ end
407
+ end
408
+
409
+ def test_is_not_required
410
+ schema do
411
+ is_not! :foo, required: true do
412
+ str
413
+ end
414
+ end
415
+
416
+ assert_validation(foo: 42)
417
+ assert_validation(foo: true)
418
+ assert_validation(foo: { bar: :baz })
419
+ assert_validation(foo: nil) do
420
+ error '/foo', 'Value must be given.'
421
+ end
422
+ assert_validation(foo: 'string') do
423
+ error '/foo', 'Must not match schema: {"type"=>"string"}.'
424
+ end
425
+ end
426
+
427
+ def test_is_not_optional
428
+ schema do
429
+ is_not? :foo do
430
+ str
431
+ end
432
+ end
433
+
434
+ assert_validation(foo: 42)
435
+ assert_validation(foo: true)
436
+ assert_validation(foo: { bar: :baz })
437
+ assert_validation(foo: nil)
438
+ assert_validation(foo: 'string') do
439
+ error '/foo', 'Must not match schema: {"type"=>"string"}.'
440
+ end
441
+ end
442
+
443
+ # Helper function that checks for all the options if the option is
444
+ # an integer or something else, in which case it needs to raise
445
+ def validate_self_should_error(value_to_check)
446
+ assert_raises_with_message Exceptions::InvalidSchemaError,
447
+ 'Option "min_properties" must be an "integer"' do
448
+ schema :hash, min_properties: value_to_check
449
+ end
450
+
451
+ assert_raises_with_message Exceptions::InvalidSchemaError,
452
+ 'Option "max_properties" must be an "integer"' do
453
+ schema :hash, max_properties: value_to_check
454
+ end
455
+ end
456
+
457
+ def test_validate_self
458
+ assert_raises_with_message Exceptions::InvalidSchemaError,
459
+ 'Pattern properties can\'t be required.' do
460
+ schema :hash do
461
+ str!(/[a-z]+/)
462
+ end
463
+ end
464
+
465
+ validate_self_should_error(1.0)
466
+ validate_self_should_error(4r)
467
+ validate_self_should_error(true)
468
+ validate_self_should_error(false)
469
+ validate_self_should_error((4 + 6i))
470
+ validate_self_should_error('13')
471
+ validate_self_should_error('Lorem ipsum')
472
+
473
+ # rubocop:disable Lint/BooleanSymbol
474
+ assert_raises_with_message Exceptions::InvalidSchemaError,
475
+ 'Option "additional_properties" must be a boolean value' do
476
+ schema :hash, additional_properties: :true
477
+ end
478
+ # rubocop:enable Lint/BooleanSymbol
479
+ end
480
+
481
+ def test_doc_example
482
+ schema :hash do
483
+ scm :address do
484
+ str! :street
485
+ int! :number
486
+ str! :zip
487
+ end
488
+ int? :id
489
+ str! :name
490
+ ref! :address, :address
491
+ ary! :additional_addresses, default: [] do
492
+ ref :address
493
+ end
494
+ ary? :comments, :array, default: [] do
495
+ str
496
+ end
497
+ hsh! :jobs, min_properties: 1 do
498
+ str?(/^[0-9]+$/)
499
+ end
500
+ end
501
+
502
+ assert_validation(
503
+ id: 42,
504
+ name: 'John Doe',
505
+ address: {
506
+ street: 'Silver Street',
507
+ number: 4,
508
+ zip: '38234C'
509
+ },
510
+ additional_addresses: [
511
+ { street: 'Example street', number: 42, zip: '8048' }
512
+ ],
513
+ comments: [
514
+ 'This is a comment'
515
+ ],
516
+ jobs: {
517
+ 2020 => 'Software Engineer'
518
+ }
519
+ )
520
+ end
521
+
522
+ def test_cast_without_additional
523
+ schema :hash do
524
+ str! :foo, format: :integer
525
+ end
526
+
527
+ assert_validation(nil)
528
+ assert_validation(foo: '1')
529
+ assert_cast({ foo: '1' }, { foo: 1 })
530
+
531
+ assert_validation(foo: '1', bar: '2') do
532
+ error '/', 'Obsolete property "bar".'
533
+ end
534
+
535
+ assert_json(
536
+ type: 'object',
537
+ properties: {
538
+ foo: {
539
+ type: :string,
540
+ format: :integer
541
+ }
542
+ },
543
+ additionalProperties: false,
544
+ required: %i[foo]
545
+ )
546
+ end
547
+
548
+ def test_cast_with_additional
549
+ schema :hash, additional_properties: true do
550
+ str! :foo, format: :integer
551
+ end
552
+
553
+ assert_validation(nil)
554
+ assert_validation(foo: '1')
555
+ assert_cast({ foo: '1' }, { foo: 1 })
556
+
557
+ assert_validation(foo: '1', bar: nil)
558
+ assert_validation(foo: '1', bar: '2')
559
+ assert_cast({ foo: '1', bar: '2' }, { foo: 1, bar: '2' })
560
+
561
+ assert_json(
562
+ type: 'object',
563
+ properties: {
564
+ foo: {
565
+ type: :string,
566
+ format: :integer
567
+ }
568
+ },
569
+ additionalProperties: true,
570
+ required: %i[foo]
571
+ )
572
+ end
573
+
574
+ def test_multiple_add_in_schema
575
+ assert_raises_with_message Exceptions::InvalidSchemaError,
576
+ 'You can only use "add" once to specify additional properties.' do
577
+ schema :hash do
578
+ add :integer
579
+ add :string
580
+ end
581
+ end
582
+ end
583
+
584
+ def test_cast_with_additional_in_block
585
+ schema :hash do
586
+ str! :foo, format: :integer
587
+ add :string
588
+ end
589
+
590
+ assert_validation(nil)
591
+ assert_validation(foo: '1')
592
+ assert_cast({ foo: '1' }, { foo: 1 })
593
+
594
+ assert_validation(foo: '1', bar: nil)
595
+ assert_validation(foo: '1', bar: '2')
596
+ assert_cast({ foo: '1', bar: '2' }, { foo: 1, bar: '2' })
597
+
598
+ assert_json(
599
+ type: 'object',
600
+ properties: {
601
+ foo: {
602
+ type: :string,
603
+ format: :integer
604
+ }
605
+ },
606
+ additionalProperties: { type: :string },
607
+ required: %i[foo]
608
+ )
609
+ end
610
+
611
+ def test_cast_with_additional_in_block_with_casting
612
+ schema :hash do
613
+ str! :foo, format: :integer
614
+ add :string, format: :integer
615
+ end
616
+
617
+ assert_validation(nil)
618
+ assert_validation(foo: '1')
619
+ assert_cast({ foo: '1' }, { foo: 1 })
620
+
621
+ assert_validation(foo: '1', bar: nil)
622
+ assert_validation(foo: '1', bar: '2')
623
+ assert_cast({ foo: '1', bar: '2' }, { foo: 1, bar: 2 })
624
+ end
625
+
626
+ def test_cast_with_additional_any_of
627
+ schema :hash do
628
+ str! :foo, format: :integer
629
+ add :any_of do
630
+ str
631
+ int
632
+ end
633
+ end
634
+
635
+ assert_validation(nil)
636
+ assert_validation(foo: '1')
637
+ assert_cast({ foo: '1' }, { foo: 1 })
638
+
639
+ assert_validation(foo: '1', bar: nil)
640
+ assert_validation(foo: '1', bar: '2')
641
+ assert_validation(foo: '1', bar: '2', baz: 3)
642
+ assert_validation(foo: '1', bar: '2', baz: 3, qux: [1, 2]) do
643
+ error '/qux', 'Does not match any anyOf condition.'
644
+ end
645
+
646
+ assert_cast({ foo: '1', bar: '2' }, { foo: 1, bar: '2' })
647
+
648
+ assert_json(
649
+ type: 'object',
650
+ properties: {
651
+ foo: {
652
+ type: :string,
653
+ format: :integer
654
+ }
655
+ },
656
+ additionalProperties: {
657
+ anyOf: [
658
+ { type: :string },
659
+ { type: :integer }
660
+ ]
661
+ },
662
+ required: %i[foo]
663
+ )
664
+ end
665
+
666
+ def test_cast_with_additional_any_of_with_casting
667
+ schema :hash do
668
+ str! :foo, format: :integer
669
+ add :any_of do
670
+ str format: :integer
671
+ str format: :date
672
+ int
673
+ end
674
+ end
675
+
676
+ assert_validation(nil)
677
+ assert_validation(foo: '1')
678
+ assert_cast({ foo: '1' }, { foo: 1 })
679
+
680
+ assert_validation(foo: '1', bar: nil)
681
+ assert_validation(foo: '1', bar: '2')
682
+ assert_validation(foo: '1', bar: '2', baz: 3)
683
+ assert_validation(foo: '1', bar: '2', baz: 3, qux: [1, 2]) do
684
+ error '/qux', 'Does not match any anyOf condition.'
685
+ end
686
+
687
+ assert_cast({ foo: '1', bar: '2' }, { foo: 1, bar: 2 })
688
+ assert_cast({ foo: '1', bar: '2', qux: '2020-01-13', asd: 1 }, { foo: 1, bar: 2, qux: Date.new(2020, 1, 13), asd: 1 })
689
+
690
+ assert_json(
691
+ type: 'object',
692
+ properties: {
693
+ foo: {
694
+ type: :string,
695
+ format: :integer
696
+ }
697
+ },
698
+ additionalProperties: {
699
+ anyOf: [
700
+ {
701
+ type: :string,
702
+ format: :integer
703
+ },
704
+ {
705
+ type: :string,
706
+ format: :date
707
+ },
708
+ {
709
+ type: :integer
710
+ }
711
+ ]
712
+ },
713
+ required: %i[foo]
714
+ )
715
+ end
716
+
717
+ def test_enum_schema
718
+ schema :hash do
719
+ str! :foo, enum: ['bar', 'qux', 123, :faz]
720
+ end
721
+
722
+ assert_json({
723
+ type: :object,
724
+ additionalProperties: false,
725
+ properties: {
726
+ foo: {
727
+ type: :string,
728
+ enum: ['bar', 'qux', 123, :faz]
729
+ }
730
+ },
731
+ required: [:foo]
732
+ })
733
+
734
+ assert_validation(nil)
735
+ assert_validation({ foo: 'bar' })
736
+ assert_validation({ foo: 'qux' })
737
+
738
+ # Even we put those types in the enum, they need to fail the validations,
739
+ # as they are not strings
740
+ assert_validation({ foo: 123 }) do
741
+ error '/foo', 'Invalid type, expected "string".'
742
+ end
743
+ assert_validation({ foo: :faz }) do
744
+ error '/foo', 'Invalid type, expected "string".'
745
+ end
746
+
747
+ # These need to fail validation, as they are not in the enum list
748
+ assert_validation({ foo: 'Lorem ipsum' }) do
749
+ error '/foo', 'Value not included in enum ["bar", "qux", 123, :faz].'
750
+ end
751
+ end
752
+
753
+ def test_with_generic_keywords
754
+ schema :hash, title: 'Hash', description: 'A hash with a description' do
755
+ str! :foo,
756
+ enum: ['bar', 'qux', 123, :faz],
757
+ title: 'A string',
758
+ description: 'A string in the hash',
759
+ examples: [
760
+ 'foo'
761
+ ]
762
+ end
763
+
764
+ assert_json({
765
+ type: :object,
766
+ additionalProperties: false,
767
+ title: 'Hash',
768
+ description: 'A hash with a description',
769
+ properties: {
770
+ foo: {
771
+ type: :string,
772
+ enum: ['bar', 'qux', 123, :faz],
773
+ title: 'A string',
774
+ examples: ['foo'],
775
+ description: 'A string in the hash'
776
+ }
777
+ },
778
+ required: [:foo]
779
+ })
780
+ end
781
+
782
+ def test_hash_with_indifferent_access
783
+ schema :hash do
784
+ str! :foo
785
+ int? :bar
786
+ add :symbol
787
+ end
788
+
789
+ # Test with symbol notation
790
+ hash = ActiveSupport::HashWithIndifferentAccess.new
791
+
792
+ assert_validation(hash) do
793
+ error '/foo', 'Value must be given.'
794
+ end
795
+ hash[:foo] = 'Foo'
796
+ assert_validation(hash)
797
+ hash[:bar] = 123
798
+ assert_validation(hash)
799
+ hash[:qux] = :ruby
800
+ assert_validation(hash)
801
+
802
+ # Test with string notation
803
+ hash = ActiveSupport::HashWithIndifferentAccess.new
804
+
805
+ assert_validation(hash) do
806
+ error '/foo', 'Value must be given.'
807
+ end
808
+ hash['foo'] = 'Foo'
809
+ assert_validation(hash)
810
+ hash['bar'] = 123
811
+ assert_validation(hash)
812
+ hash['qux'] = :ruby
813
+ assert_validation(hash)
814
+ end
815
+
816
+ def test_invalid_schema
817
+ assert_raises_with_message Exceptions::InvalidSchemaError,
818
+ 'Child nodes must have a name.' do
819
+ schema :hash do
820
+ int!
821
+ end
822
+ end
823
+ end
824
+ end
825
+ end
826
+ end