schemacop 2.4.3 → 3.0.0.rc0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (171) 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 +26 -0
  6. data/README.md +41 -692
  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 -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/{node_supporting_field.rb → v2/node_supporting_field.rb} +8 -10
  26. data/lib/schemacop/{node_supporting_type.rb → v2/node_supporting_type.rb} +12 -9
  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 -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 -289
  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 -818
  133. data/doc/file_list.html +0 -56
  134. data/doc/frames.html +0 -17
  135. data/doc/index.html +0 -818
  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 -31
  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 -90
  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 -71
  157. data/test/nil_dis_allow_test.rb +0 -41
  158. data/test/node_resolver_test.rb +0 -26
  159. data/test/short_forms_test.rb +0 -349
  160. data/test/test_helper.rb +0 -13
  161. data/test/types_test.rb +0 -84
  162. data/test/validator_array_test.rb +0 -97
  163. data/test/validator_boolean_test.rb +0 -15
  164. data/test/validator_float_test.rb +0 -57
  165. data/test/validator_hash_test.rb +0 -71
  166. data/test/validator_integer_test.rb +0 -46
  167. data/test/validator_nil_test.rb +0 -13
  168. data/test/validator_number_test.rb +0 -60
  169. data/test/validator_object_test.rb +0 -139
  170. data/test/validator_string_test.rb +0 -76
  171. 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