schemacop 2.4.7 → 3.0.0.rc0

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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +25 -1
  4. data/.travis.yml +2 -1
  5. data/CHANGELOG.md +8 -0
  6. data/README.md +41 -708
  7. data/README_V2.md +775 -0
  8. data/README_V3.md +683 -0
  9. data/Rakefile +8 -12
  10. data/VERSION +1 -1
  11. data/lib/schemacop.rb +35 -37
  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} +4 -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/{node_supporting_field.rb → v2/node_supporting_field.rb} +8 -10
  26. data/lib/schemacop/{node_supporting_type.rb → v2/node_supporting_type.rb} +6 -3
  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 +217 -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 +775 -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 -145
  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 -833
  133. data/doc/file_list.html +0 -56
  134. data/doc/frames.html +0 -17
  135. data/doc/index.html +0 -833
  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/root_node.rb +0 -4
  145. data/lib/schemacop/validator/array_validator.rb +0 -30
  146. data/lib/schemacop/validator/float_validator.rb +0 -5
  147. data/lib/schemacop/validator/hash_validator.rb +0 -35
  148. data/lib/schemacop/validator/integer_validator.rb +0 -5
  149. data/lib/schemacop/validator/number_validator.rb +0 -19
  150. data/lib/schemacop/validator/object_validator.rb +0 -27
  151. data/lib/schemacop/validator/string_validator.rb +0 -37
  152. data/test/casting_test.rb +0 -118
  153. data/test/collector_test.rb +0 -45
  154. data/test/custom_check_test.rb +0 -93
  155. data/test/custom_if_test.rb +0 -95
  156. data/test/defaults_test.rb +0 -93
  157. data/test/empty_test.rb +0 -14
  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,775 @@
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('_foo39sjfdoi 345893(%' => 'bar', 'foo' => 'bar') do
65
+ error '/', 'Property name "_foo39sjfdoi 345893(%" does not match "^[a-zA-Z0-9]+$".'
66
+ end
67
+
68
+ assert_json(
69
+ type: :object,
70
+ additionalProperties: true,
71
+ propertyNames: '^[a-zA-Z0-9]+$'
72
+ )
73
+ end
74
+
75
+ def test_required
76
+ schema do
77
+ str! :foo
78
+ int? :bar
79
+ end
80
+
81
+ assert_validation(foo: 'sdfsd')
82
+ assert_validation(foo: 'sdfsd', bar: 42)
83
+
84
+ assert_validation(bar: 42) do
85
+ error '/foo', 'Value must be given.'
86
+ end
87
+
88
+ assert_validation({}) do
89
+ error '/foo', 'Value must be given.'
90
+ end
91
+
92
+ assert_json(
93
+ type: :object,
94
+ properties: {
95
+ foo: { type: :string },
96
+ bar: { type: :integer }
97
+ },
98
+ required: %i[foo],
99
+ additionalProperties: false
100
+ )
101
+ end
102
+
103
+ def test_min_properties
104
+ schema :hash, min_properties: 2, additional_properties: true
105
+ assert_validation(foo: :bar, bar: :baz)
106
+ assert_validation(foo: :bar, bar: :baz, baz: :foo)
107
+
108
+ assert_validation(foo: :bar) do
109
+ error '/', 'Has 1 properties but needs at least 2.'
110
+ end
111
+
112
+ assert_validation({}) do
113
+ error '/', 'Has 0 properties but needs at least 2.'
114
+ end
115
+
116
+ assert_json(
117
+ type: :object,
118
+ minProperties: 2,
119
+ additionalProperties: true
120
+ )
121
+ end
122
+
123
+ def test_max_properties
124
+ schema :hash, max_properties: 3, additional_properties: true
125
+ assert_validation(foo: :bar, bar: :baz)
126
+ assert_validation(foo: :bar, bar: :baz, baz: :foo)
127
+
128
+ assert_validation(foo: :bar, bar: :baz, baz: :foo, answer: 42) do
129
+ error '/', 'Has 4 properties but needs at most 3.'
130
+ end
131
+
132
+ assert_json(
133
+ type: :object,
134
+ maxProperties: 3,
135
+ additionalProperties: true
136
+ )
137
+ end
138
+
139
+ def test_min_max_properties
140
+ schema :hash, min_properties: 3, max_properties: 3, additional_properties: true
141
+ assert_validation(foo: :bar, bar: :baz, baz: :foo)
142
+
143
+ assert_validation(foo: :bar, bar: :baz, baz: :foo, answer: 42) do
144
+ error '/', 'Has 4 properties but needs at most 3.'
145
+ end
146
+
147
+ assert_validation(foo: :bar, bar: :baz) do
148
+ error '/', 'Has 2 properties but needs at least 3.'
149
+ end
150
+
151
+ assert_json(
152
+ type: :object,
153
+ minProperties: 3,
154
+ maxProperties: 3,
155
+ additionalProperties: true
156
+ )
157
+ end
158
+
159
+ def test_dependencies
160
+ schema :hash do
161
+ str! :name
162
+ str? :credit_card
163
+ str? :billing_address
164
+ str? :phone_number
165
+
166
+ dep :credit_card, :billing_address, :phone_number
167
+ dep :billing_address, :credit_card
168
+ end
169
+
170
+ assert_validation(name: 'John')
171
+ assert_validation(name: 'John', credit_card: '23423523', billing_address: 'Example 3', phone_number: '234')
172
+
173
+ assert_validation(name: 'John', credit_card: '23423523') do
174
+ error '/', 'Missing property "billing_address" because "credit_card" is given.'
175
+ error '/', 'Missing property "phone_number" because "credit_card" is given.'
176
+ end
177
+
178
+ assert_validation(name: 'John', billing_address: 'Example 3') do
179
+ error '/', 'Missing property "credit_card" because "billing_address" is given.'
180
+ end
181
+
182
+ assert_json(
183
+ type: :object,
184
+ properties: {
185
+ name: { type: :string },
186
+ credit_card: { type: :string },
187
+ billing_address: { type: :string },
188
+ phone_number: { type: :string }
189
+ },
190
+ required: %i[name],
191
+ dependencies: {
192
+ credit_card: %i[billing_address phone_number],
193
+ billing_address: %i[credit_card]
194
+ },
195
+ additionalProperties: false
196
+ )
197
+ end
198
+
199
+ def test_pattern_properties_wo_additional
200
+ schema additional_properties: false do
201
+ str! :name
202
+ str?(/^foo_.*$/)
203
+ int?(/^bar_.*$/)
204
+ end
205
+
206
+ assert_validation(name: 'John', foo_bar: 'John')
207
+ assert_validation(name: 'John', foo_bar: 'John', bar_baz: 42, foo_baz: '42')
208
+ assert_validation(name: 'John', foo_baz: 'John', bar_baz: 42)
209
+
210
+ assert_validation(name: 'John', xy: 'John', bar_baz: 'Doe') do
211
+ error '/', 'Obsolete property "xy".'
212
+ error '/bar_baz', 'Invalid type, expected "integer".'
213
+ end
214
+
215
+ assert_json(
216
+ type: :object,
217
+ properties: {
218
+ name: { type: :string }
219
+ },
220
+ patternProperties: {
221
+ '^foo_.*$': { type: :string },
222
+ '^bar_.*$': { type: :integer }
223
+ },
224
+ additionalProperties: false,
225
+ required: %i[name]
226
+ )
227
+ end
228
+
229
+ def test_pattern_properties_w_additional
230
+ schema additional_properties: true do
231
+ int? :builtin
232
+ str?(/^S_/)
233
+ int?(/^I_/)
234
+ add :string
235
+ end
236
+
237
+ assert_validation(builtin: 42)
238
+ assert_validation(keyword: 'value')
239
+
240
+ assert_validation(keyword: 42) do
241
+ error '/keyword', 'Invalid type, expected "string".'
242
+ end
243
+
244
+ assert_json(
245
+ type: 'object',
246
+ properties: {
247
+ builtin: { type: :integer }
248
+ },
249
+ patternProperties: {
250
+ '^S_': { type: :string },
251
+ '^I_': { type: :integer }
252
+ },
253
+ additionalProperties: { type: :string }
254
+ )
255
+ end
256
+
257
+ def test_defaults
258
+ schema do
259
+ str? :first_name, default: 'John'
260
+ str? :last_name, default: 'Doe'
261
+ str! :active, format: :boolean
262
+ hsh? :address, default: {} do
263
+ str? :street, default: 'Example 42'
264
+ end
265
+ end
266
+
267
+ data = { last_name: 'Doeringer', active: 'true' }
268
+ data_was = data.dup
269
+
270
+ assert_equal({ first_name: 'John', last_name: 'Doeringer', active: true, address: { street: 'Example 42' } }, @schema.validate(data).data)
271
+ assert_equal data_was, data
272
+
273
+ schema do
274
+ hsh? :address do
275
+ str? :street, default: 'Example 42'
276
+ end
277
+ end
278
+
279
+ assert_equal({}, @schema.validate({}).data)
280
+ end
281
+
282
+ def test_all_of
283
+ schema do
284
+ all_of! :str do
285
+ str min_length: 3
286
+ str max_length: 5
287
+ end
288
+ end
289
+
290
+ assert_validation(str: '123')
291
+ assert_validation(str: '1234')
292
+ assert_validation(str: '12345')
293
+ assert_validation(str: '0') do
294
+ error '/str', 'Does not match all allOf conditions.'
295
+ end
296
+ end
297
+
298
+ def test_one_of_required
299
+ schema do
300
+ one_of! :str do
301
+ str min_length: 4
302
+ str min_length: 0, max_length: 4
303
+ end
304
+ end
305
+
306
+ assert_validation(str: '12345')
307
+ assert_validation(str: '123')
308
+ assert_validation(str: nil) do
309
+ error '/str', 'Value must be given.'
310
+ end
311
+ assert_validation(str: '1234') do
312
+ error '/str', 'Matches 2 definitions but should match exactly 1.'
313
+ end
314
+ end
315
+
316
+ def test_one_of_optional
317
+ schema do
318
+ one_of? :str do
319
+ str min_length: 4
320
+ str min_length: 0, max_length: 4
321
+ end
322
+ end
323
+
324
+ assert_validation(str: '12345')
325
+ assert_validation(str: '123')
326
+ assert_validation(str: nil)
327
+ assert_validation({})
328
+ assert_validation(str: '1234') do
329
+ error '/str', 'Matches 2 definitions but should match exactly 1.'
330
+ end
331
+ end
332
+
333
+ def test_any_of_required
334
+ schema do
335
+ any_of! :str_or_int do
336
+ str
337
+ int
338
+ end
339
+ end
340
+
341
+ assert_validation(str_or_int: 'Hello World')
342
+ assert_validation(str_or_int: 42)
343
+ assert_validation(str_or_int: :foo) do
344
+ error '/str_or_int', 'Does not match any anyOf condition.'
345
+ end
346
+ end
347
+
348
+ def test_any_of_optional
349
+ schema do
350
+ any_of? :str_or_int do
351
+ str
352
+ int
353
+ end
354
+ end
355
+
356
+ assert_validation(str_or_int: 'Hello World')
357
+ assert_validation(str_or_int: 42)
358
+ assert_validation(str_or_int: nil)
359
+ assert_validation({})
360
+ assert_validation(str_or_int: :foo) do
361
+ error '/str_or_int', 'Does not match any anyOf condition.'
362
+ end
363
+ end
364
+
365
+ def test_is_not_required
366
+ schema do
367
+ is_not! :foo, required: true do
368
+ str
369
+ end
370
+ end
371
+
372
+ assert_validation(foo: 42)
373
+ assert_validation(foo: true)
374
+ assert_validation(foo: { bar: :baz })
375
+ assert_validation(foo: nil) do
376
+ error '/foo', 'Value must be given.'
377
+ end
378
+ assert_validation(foo: 'string') do
379
+ error '/foo', 'Must not match schema: {"type"=>"string"}.'
380
+ end
381
+ end
382
+
383
+ def test_is_not_optional
384
+ schema do
385
+ is_not? :foo do
386
+ str
387
+ end
388
+ end
389
+
390
+ assert_validation(foo: 42)
391
+ assert_validation(foo: true)
392
+ assert_validation(foo: { bar: :baz })
393
+ assert_validation(foo: nil)
394
+ assert_validation(foo: 'string') do
395
+ error '/foo', 'Must not match schema: {"type"=>"string"}.'
396
+ end
397
+ end
398
+
399
+ # Helper function that checks for all the options if the option is
400
+ # an integer or something else, in which case it needs to raise
401
+ def validate_self_should_error(value_to_check)
402
+ assert_raises_with_message Exceptions::InvalidSchemaError,
403
+ 'Option "min_properties" must be an "integer"' do
404
+ schema :hash, min_properties: value_to_check
405
+ end
406
+
407
+ assert_raises_with_message Exceptions::InvalidSchemaError,
408
+ 'Option "max_properties" must be an "integer"' do
409
+ schema :hash, max_properties: value_to_check
410
+ end
411
+ end
412
+
413
+ def test_validate_self
414
+ assert_raises_with_message Exceptions::InvalidSchemaError,
415
+ 'Pattern properties can\'t be required.' do
416
+ schema :hash do
417
+ str!(/[a-z]+/)
418
+ end
419
+ end
420
+
421
+ validate_self_should_error(1.0)
422
+ validate_self_should_error(4r)
423
+ validate_self_should_error(true)
424
+ validate_self_should_error(false)
425
+ validate_self_should_error((4 + 6i))
426
+ validate_self_should_error('13')
427
+ validate_self_should_error('Lorem ipsum')
428
+ end
429
+
430
+ def test_doc_example
431
+ schema :hash do
432
+ scm :address do
433
+ str! :street
434
+ int! :number
435
+ str! :zip
436
+ end
437
+ int? :id
438
+ str! :name
439
+ ref! :address, :address
440
+ ary! :additional_addresses, default: [] do
441
+ ref :address
442
+ end
443
+ ary? :comments, :array, default: [] do
444
+ str
445
+ end
446
+ hsh! :jobs, min_properties: 1 do
447
+ str?(/^[0-9]+$/)
448
+ end
449
+ end
450
+
451
+ assert_validation(
452
+ id: 42,
453
+ name: 'John Doe',
454
+ address: {
455
+ street: 'Silver Street',
456
+ number: 4,
457
+ zip: '38234C'
458
+ },
459
+ additional_addresses: [
460
+ { street: 'Example street', number: 42, zip: '8048' }
461
+ ],
462
+ comments: [
463
+ 'This is a comment'
464
+ ],
465
+ jobs: {
466
+ 2020 => 'Software Engineer'
467
+ }
468
+ )
469
+ end
470
+
471
+ def test_cast_without_additional
472
+ schema :hash do
473
+ str! :foo, format: :integer
474
+ end
475
+
476
+ assert_validation(nil)
477
+ assert_validation(foo: '1')
478
+ assert_cast({ foo: '1' }, { foo: 1 })
479
+
480
+ assert_validation(foo: '1', bar: '2') do
481
+ error '/', 'Obsolete property "bar".'
482
+ end
483
+
484
+ assert_json(
485
+ type: 'object',
486
+ properties: {
487
+ foo: {
488
+ type: :string,
489
+ format: :integer
490
+ }
491
+ },
492
+ additionalProperties: false,
493
+ required: %i[foo]
494
+ )
495
+ end
496
+
497
+ def test_cast_with_additional
498
+ schema :hash, additional_properties: true do
499
+ str! :foo, format: :integer
500
+ end
501
+
502
+ assert_validation(nil)
503
+ assert_validation(foo: '1')
504
+ assert_cast({ foo: '1' }, { foo: 1 })
505
+
506
+ assert_validation(foo: '1', bar: nil)
507
+ assert_validation(foo: '1', bar: '2')
508
+ assert_cast({ foo: '1', bar: '2' }, { foo: 1, bar: '2' })
509
+
510
+ assert_json(
511
+ type: 'object',
512
+ properties: {
513
+ foo: {
514
+ type: :string,
515
+ format: :integer
516
+ }
517
+ },
518
+ additionalProperties: true,
519
+ required: %i[foo]
520
+ )
521
+ end
522
+
523
+ def test_multiple_add_in_schema
524
+ assert_raises_with_message Exceptions::InvalidSchemaError,
525
+ 'You can only use "add" once to specify additional properties.' do
526
+ schema :hash do
527
+ add :integer
528
+ add :string
529
+ end
530
+ end
531
+ end
532
+
533
+ def test_cast_with_additional_in_block
534
+ schema :hash do
535
+ str! :foo, format: :integer
536
+ add :string
537
+ end
538
+
539
+ assert_validation(nil)
540
+ assert_validation(foo: '1')
541
+ assert_cast({ foo: '1' }, { foo: 1 })
542
+
543
+ assert_validation(foo: '1', bar: nil)
544
+ assert_validation(foo: '1', bar: '2')
545
+ assert_cast({ foo: '1', bar: '2' }, { foo: 1, bar: '2' })
546
+
547
+ assert_json(
548
+ type: 'object',
549
+ properties: {
550
+ foo: {
551
+ type: :string,
552
+ format: :integer
553
+ }
554
+ },
555
+ additionalProperties: { type: :string },
556
+ required: %i[foo]
557
+ )
558
+ end
559
+
560
+ def test_cast_with_additional_in_block_with_casting
561
+ schema :hash do
562
+ str! :foo, format: :integer
563
+ add :string, format: :integer
564
+ end
565
+
566
+ assert_validation(nil)
567
+ assert_validation(foo: '1')
568
+ assert_cast({ foo: '1' }, { foo: 1 })
569
+
570
+ assert_validation(foo: '1', bar: nil)
571
+ assert_validation(foo: '1', bar: '2')
572
+ assert_cast({ foo: '1', bar: '2' }, { foo: 1, bar: 2 })
573
+ end
574
+
575
+ def test_cast_with_additional_any_of
576
+ schema :hash do
577
+ str! :foo, format: :integer
578
+ add :any_of do
579
+ str
580
+ int
581
+ end
582
+ end
583
+
584
+ assert_validation(nil)
585
+ assert_validation(foo: '1')
586
+ assert_cast({ foo: '1' }, { foo: 1 })
587
+
588
+ assert_validation(foo: '1', bar: nil)
589
+ assert_validation(foo: '1', bar: '2')
590
+ assert_validation(foo: '1', bar: '2', baz: 3)
591
+ assert_validation(foo: '1', bar: '2', baz: 3, qux: [1, 2]) do
592
+ error '/qux', 'Does not match any anyOf condition.'
593
+ end
594
+
595
+ assert_cast({ foo: '1', bar: '2' }, { foo: 1, bar: '2' })
596
+
597
+ assert_json(
598
+ type: 'object',
599
+ properties: {
600
+ foo: {
601
+ type: :string,
602
+ format: :integer
603
+ }
604
+ },
605
+ additionalProperties: {
606
+ anyOf: [
607
+ { type: :string },
608
+ { type: :integer }
609
+ ]
610
+ },
611
+ required: %i[foo]
612
+ )
613
+ end
614
+
615
+ def test_cast_with_additional_any_of_with_casting
616
+ schema :hash do
617
+ str! :foo, format: :integer
618
+ add :any_of do
619
+ str format: :integer
620
+ str format: :date
621
+ int
622
+ end
623
+ end
624
+
625
+ assert_validation(nil)
626
+ assert_validation(foo: '1')
627
+ assert_cast({ foo: '1' }, { foo: 1 })
628
+
629
+ assert_validation(foo: '1', bar: nil)
630
+ assert_validation(foo: '1', bar: '2')
631
+ assert_validation(foo: '1', bar: '2', baz: 3)
632
+ assert_validation(foo: '1', bar: '2', baz: 3, qux: [1, 2]) do
633
+ error '/qux', 'Does not match any anyOf condition.'
634
+ end
635
+
636
+ assert_cast({ foo: '1', bar: '2' }, { foo: 1, bar: 2 })
637
+ assert_cast({ foo: '1', bar: '2', qux: '2020-01-13', asd: 1 }, { foo: 1, bar: 2, qux: Date.new(2020, 1, 13), asd: 1 })
638
+
639
+ assert_json(
640
+ type: 'object',
641
+ properties: {
642
+ foo: {
643
+ type: :string,
644
+ format: :integer
645
+ }
646
+ },
647
+ additionalProperties: {
648
+ anyOf: [
649
+ {
650
+ type: :string,
651
+ format: :integer
652
+ },
653
+ {
654
+ type: :string,
655
+ format: :date
656
+ },
657
+ {
658
+ type: :integer
659
+ }
660
+ ]
661
+ },
662
+ required: %i[foo]
663
+ )
664
+ end
665
+
666
+ def test_enum_schema
667
+ schema :hash do
668
+ str! :foo, enum: ['bar', 'qux', 123, :faz]
669
+ end
670
+
671
+ assert_json({
672
+ type: :object,
673
+ additionalProperties: false,
674
+ properties: {
675
+ foo: {
676
+ type: :string,
677
+ enum: ['bar', 'qux', 123, :faz]
678
+ }
679
+ },
680
+ required: [:foo]
681
+ })
682
+
683
+ assert_validation(nil)
684
+ assert_validation({ foo: 'bar' })
685
+ assert_validation({ foo: 'qux' })
686
+
687
+ # Even we put those types in the enum, they need to fail the validations,
688
+ # as they are not strings
689
+ assert_validation({ foo: 123 }) do
690
+ error '/foo', 'Invalid type, expected "string".'
691
+ end
692
+ assert_validation({ foo: :faz }) do
693
+ error '/foo', 'Invalid type, expected "string".'
694
+ end
695
+
696
+ # These need to fail validation, as they are not in the enum list
697
+ assert_validation({ foo: 'Lorem ipsum' }) do
698
+ error '/foo', 'Value not included in enum ["bar", "qux", 123, :faz].'
699
+ end
700
+ end
701
+
702
+ def test_with_generic_keywords
703
+ schema :hash, title: 'Hash', description: 'A hash with a description' do
704
+ str! :foo,
705
+ enum: ['bar', 'qux', 123, :faz],
706
+ title: 'A string',
707
+ description: 'A string in the hash',
708
+ examples: [
709
+ 'foo'
710
+ ]
711
+ end
712
+
713
+ assert_json({
714
+ type: :object,
715
+ additionalProperties: false,
716
+ title: 'Hash',
717
+ description: 'A hash with a description',
718
+ properties: {
719
+ foo: {
720
+ type: :string,
721
+ enum: ['bar', 'qux', 123, :faz],
722
+ title: 'A string',
723
+ examples: ['foo'],
724
+ description: 'A string in the hash'
725
+ }
726
+ },
727
+ required: [:foo]
728
+ })
729
+ end
730
+
731
+ def test_hash_with_indifferent_access
732
+ schema :hash do
733
+ str! :foo
734
+ int? :bar
735
+ add :symbol
736
+ end
737
+
738
+ # Test with symbol notation
739
+ hash = ActiveSupport::HashWithIndifferentAccess.new
740
+
741
+ assert_validation(hash) do
742
+ error '/foo', 'Value must be given.'
743
+ end
744
+ hash[:foo] = 'Foo'
745
+ assert_validation(hash)
746
+ hash[:bar] = 123
747
+ assert_validation(hash)
748
+ hash[:qux] = :ruby
749
+ assert_validation(hash)
750
+
751
+ # Test with string notation
752
+ hash = ActiveSupport::HashWithIndifferentAccess.new
753
+
754
+ assert_validation(hash) do
755
+ error '/foo', 'Value must be given.'
756
+ end
757
+ hash['foo'] = 'Foo'
758
+ assert_validation(hash)
759
+ hash['bar'] = 123
760
+ assert_validation(hash)
761
+ hash['qux'] = :ruby
762
+ assert_validation(hash)
763
+ end
764
+
765
+ def test_invalid_schema
766
+ assert_raises_with_message Exceptions::InvalidSchemaError,
767
+ 'Child nodes must have a name.' do
768
+ schema :hash do
769
+ int!
770
+ end
771
+ end
772
+ end
773
+ end
774
+ end
775
+ end