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,114 @@
1
+ module Schemacop
2
+ module V3
3
+ class GlobalContext < Context
4
+ DSL_METHODS = %i[schema].freeze
5
+
6
+ def self.instance
7
+ @instance ||= new
8
+ end
9
+
10
+ def self.eager_load!
11
+ instance.eager_load!
12
+ end
13
+
14
+ def self.schemas
15
+ instance.schemas
16
+ end
17
+
18
+ def self.schema_for(path)
19
+ instance.schema_for(path)
20
+ end
21
+
22
+ def schema(type = :hash, **options, &block)
23
+ @current_schemas << Node.create(type, **options, &block)
24
+ end
25
+
26
+ def schema_for(path)
27
+ path = path.to_sym
28
+ load_schema(path) unless @eager_loaded
29
+ @schemas[path]
30
+ end
31
+
32
+ def eager_load!
33
+ @schemas = {}
34
+
35
+ fail "Global context can't be eager loaded more than once." if @eager_loaded
36
+
37
+ Schemacop.load_paths.each do |load_path|
38
+ Dir.glob(File.join(load_path, '**', '*.rb')).sort.each do |file|
39
+ load_file(file, load_path)
40
+ end
41
+ end
42
+
43
+ @eager_loaded = true
44
+ end
45
+
46
+ private
47
+
48
+ def initialize
49
+ super
50
+ @schemas = {}
51
+ @load_paths_by_schemas = {}
52
+ @eager_loaded = false
53
+ @current_virtual_path = nil
54
+ end
55
+
56
+ def path_for(virtual_path)
57
+ "#{virtual_path.to_s.underscore}.rb"
58
+ end
59
+
60
+ def virtual_path_for(path, load_path)
61
+ Pathname.new(path).relative_path_from(load_path).to_s.underscore.gsub(/\.rb$/, '').to_sym
62
+ end
63
+
64
+ def load_schema(virtual_path)
65
+ path = path_for(virtual_path)
66
+
67
+ @schemas = schemas.except(virtual_path).freeze
68
+ @load_paths_by_schemas = @load_paths_by_schemas.except(virtual_path)
69
+
70
+ Schemacop.load_paths.each do |load_path|
71
+ path_in_load_path = File.join(load_path, path)
72
+
73
+ if File.exist?(path_in_load_path)
74
+ load_file(path_in_load_path, load_path)
75
+ end
76
+ end
77
+ end
78
+
79
+ def load_file(path, load_path)
80
+ return false unless File.exist?(path)
81
+
82
+ # Determine virtual path
83
+ virtual_path = virtual_path_for(path, load_path)
84
+
85
+ # Run file and collect schemas
86
+ begin
87
+ @current_schemas = []
88
+ env = ScopedEnv.new(self, DSL_METHODS)
89
+ env.instance_eval IO.read(path)
90
+ rescue StandardError => e
91
+ fail "Could not load schema #{path.inspect}: #{e.message}"
92
+ end
93
+
94
+ # Load schemas
95
+ case @current_schemas.size
96
+ when 0
97
+ fail "Schema #{path.inspect} does not define any schema."
98
+ when 1
99
+ if @schemas.include?(virtual_path)
100
+ fail "Schema #{virtual_path.to_s.inspect} is defined in both load paths "\
101
+ "#{@load_paths_by_schemas[virtual_path].inspect} and #{load_path.inspect}."
102
+ end
103
+
104
+ @load_paths_by_schemas[virtual_path] = load_path
105
+ @schemas = @schemas.merge(virtual_path => @current_schemas.first)
106
+ else
107
+ fail "Schema #{path.inspect} defines multiple schemas."
108
+ end
109
+
110
+ return true
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,217 @@
1
+ module Schemacop
2
+ module V3
3
+ class HashNode < Node
4
+ ATTRIBUTES = %i[
5
+ type
6
+ min_properties
7
+ max_properties
8
+ dependencies
9
+ property_names
10
+ ].freeze
11
+
12
+ supports_children(name: true)
13
+
14
+ def self.allowed_options
15
+ super + ATTRIBUTES - %i[dependencies] + %i[additional_properties]
16
+ end
17
+
18
+ def self.dsl_methods
19
+ super + NodeRegistry.dsl_methods(true) + %i[dsl_dep dsl_add]
20
+ end
21
+
22
+ def add_child(node)
23
+ unless node.name
24
+ fail Exceptions::InvalidSchemaError, 'Child nodes must have a name.'
25
+ end
26
+
27
+ @properties[node.name] = node
28
+ end
29
+
30
+ def dsl_add(type, **options, &block)
31
+ if @options[:additional_properties].is_a?(Node)
32
+ fail Exceptions::InvalidSchemaError, 'You can only use "add" once to specify additional properties.'
33
+ end
34
+
35
+ @options[:additional_properties] = create(type, **options, &block)
36
+ end
37
+
38
+ def dsl_dep(source, *targets)
39
+ @options[:dependencies] ||= {}
40
+ @options[:dependencies][source] = targets
41
+ end
42
+
43
+ def as_json
44
+ properties = {}
45
+ pattern_properties = {}
46
+
47
+ @properties.each do |name, property|
48
+ if name.is_a?(Regexp)
49
+ pattern_properties[name] = property
50
+ else
51
+ properties[name] = property
52
+ end
53
+ end
54
+
55
+ json = {}
56
+ json[:properties] = Hash[properties.values.map { |p| [p.name, p.as_json] }] if properties.any?
57
+ json[:patternProperties] = Hash[pattern_properties.values.map { |p| [sanitize_exp(p.name), p.as_json] }] if pattern_properties.any?
58
+
59
+ # In schemacop, by default, additional properties are not allowed,
60
+ # the users explicitly need to enable additional properties
61
+ if options[:additional_properties] == true
62
+ json[:additionalProperties] = true
63
+ elsif options[:additional_properties].is_a?(Node)
64
+ json[:additionalProperties] = options[:additional_properties].as_json
65
+ else
66
+ json[:additionalProperties] = false
67
+ end
68
+
69
+ required_properties = @properties.values.select(&:required?).map(&:name)
70
+
71
+ if required_properties.any?
72
+ json[:required] = required_properties
73
+ end
74
+
75
+ return process_json(ATTRIBUTES, json)
76
+ end
77
+
78
+ def sanitize_exp(exp)
79
+ exp = exp.to_s
80
+ if exp.start_with?('(?-mix:')
81
+ exp = exp.to_s.gsub(/^\(\?-mix:/, '').gsub(/\)$/, '')
82
+ end
83
+ return exp
84
+ end
85
+
86
+ def allowed_types
87
+ { Hash => :object }
88
+ end
89
+
90
+ def _validate(data, result: Result.new)
91
+ super_data = super
92
+ return if super_data.nil?
93
+
94
+ # Validate min_properties #
95
+ if options[:min_properties] && super_data.size < options[:min_properties]
96
+ result.error "Has #{super_data.size} properties but needs at least #{options[:min_properties]}."
97
+ end
98
+
99
+ # Validate max_properties #
100
+ if options[:max_properties] && super_data.size > options[:max_properties]
101
+ result.error "Has #{super_data.size} properties but needs at most #{options[:max_properties]}."
102
+ end
103
+
104
+ # Validate specified properties #
105
+ @properties.each_value do |node|
106
+ result.in_path(node.name) do
107
+ next if node.name.is_a?(Regexp)
108
+
109
+ node._validate(super_data[node.name], result: result)
110
+ end
111
+ end
112
+
113
+ # Validate additional properties #
114
+ specified_properties = @properties.keys.to_set
115
+ additional_properties = super_data.reject { |k, _v| specified_properties.include?(k.to_s.to_sym) }
116
+
117
+ property_patterns = {}
118
+
119
+ @properties.each_value do |property|
120
+ if property.name.is_a?(Regexp)
121
+ property_patterns[property.name] = property
122
+ end
123
+ end
124
+
125
+ property_names = options[:property_names]
126
+ property_names = Regexp.compile(property_names) if property_names
127
+
128
+ additional_properties.each do |name, additional_property|
129
+ if property_names && !property_names.match?(name)
130
+ result.error "Property name #{name.inspect} does not match #{options[:property_names].inspect}."
131
+ end
132
+
133
+ if options[:additional_properties].blank?
134
+ match = property_patterns.keys.find { |p| p.match?(name.to_s) }
135
+ if match
136
+ result.in_path(name) do
137
+ property_patterns[match]._validate(additional_property, result: result)
138
+ end
139
+ else
140
+ result.error "Obsolete property #{name.to_s.inspect}."
141
+ end
142
+ elsif options[:additional_properties].is_a?(Node)
143
+ result.in_path(name) do
144
+ options[:additional_properties]._validate(additional_property, result: result)
145
+ end
146
+ end
147
+ end
148
+
149
+ # Validate dependencies #
150
+ options[:dependencies]&.each do |source, targets|
151
+ targets.each do |target|
152
+ if super_data[source].present? && super_data[target].blank?
153
+ result.error "Missing property #{target.to_s.inspect} because #{source.to_s.inspect} is given."
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ def children
160
+ @properties.values
161
+ end
162
+
163
+ def cast(data)
164
+ result = {}
165
+ data ||= default
166
+ return nil if data.nil?
167
+
168
+ # TODO: How to handle regex / etc.?
169
+ @properties.each_value do |prop|
170
+ result[prop.name] = prop.cast(data[prop.name])
171
+
172
+ if result[prop.name].nil? && !data.include?(prop.name)
173
+ result.delete(prop.name)
174
+ end
175
+ end
176
+
177
+ # Handle additional properties
178
+ if options[:additional_properties] == true
179
+ result = data.merge(result)
180
+ elsif options[:additional_properties].is_a?(Node)
181
+ specified_properties = @properties.keys.to_set
182
+ additional_properties = data.reject { |k, _v| specified_properties.include?(k.to_s.to_sym) }
183
+ if additional_properties.any?
184
+ additional_properties_result = {}
185
+ additional_properties.each do |key, value|
186
+ additional_properties_result[key] = options[:additional_properties].cast(value)
187
+ end
188
+ result = additional_properties_result.merge(result)
189
+ end
190
+ end
191
+
192
+ return result
193
+ end
194
+
195
+ protected
196
+
197
+ def init
198
+ @properties = {}
199
+ @options[:type] = :object
200
+ end
201
+
202
+ def validate_self
203
+ unless options[:min_properties].nil? || options[:min_properties].is_a?(Integer)
204
+ fail 'Option "min_properties" must be an "integer"'
205
+ end
206
+
207
+ unless options[:max_properties].nil? || options[:max_properties].is_a?(Integer)
208
+ fail 'Option "max_properties" must be an "integer"'
209
+ end
210
+
211
+ if @properties.values.any? { |p| p.name.is_a?(Regexp) && p.required? }
212
+ fail 'Pattern properties can\'t be required.'
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,13 @@
1
+ module Schemacop
2
+ module V3
3
+ class IntegerNode < NumericNode
4
+ def as_json
5
+ process_json(ATTRIBUTES, type: :integer)
6
+ end
7
+
8
+ def allowed_types
9
+ { Integer => :integer }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ module Schemacop
2
+ module V3
3
+ class IsNotNode < CombinationNode
4
+ def type
5
+ :not
6
+ end
7
+
8
+ def _validate(data, result:)
9
+ super_data = super
10
+ return if super_data.nil?
11
+
12
+ if matches(super_data).any?
13
+ result.error "Must not match schema: #{@items.first.as_json.as_json.inspect}."
14
+ end
15
+ end
16
+
17
+ def as_json
18
+ process_json([], type => @items.first.as_json)
19
+ end
20
+
21
+ def validate_self
22
+ if @items.count != 1
23
+ fail 'Node "is_not" only allows exactly one item.'
24
+ end
25
+ end
26
+
27
+ def cast(data)
28
+ data
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,214 @@
1
+ module Schemacop
2
+ module V3
3
+ class Node
4
+ attr_reader :name
5
+ attr_reader :default
6
+ attr_reader :title
7
+ attr_reader :description
8
+ attr_reader :options
9
+ attr_reader :parent
10
+
11
+ class_attribute :_supports_children
12
+ self._supports_children = nil
13
+
14
+ def self.supports_children(name: false)
15
+ self._supports_children = { name: name }
16
+ end
17
+
18
+ def self.supports_children_options
19
+ _supports_children
20
+ end
21
+
22
+ def self.resolve_class(type)
23
+ NodeRegistry.find(type)
24
+ end
25
+
26
+ def self.create(type = self, **options, &block)
27
+ klass = resolve_class(type)
28
+ fail "Could not find node for type #{type.inspect}." unless klass
29
+
30
+ node = klass.new(**options, &block)
31
+
32
+ if options.delete(:cast_str)
33
+ format = NodeRegistry.name(klass)
34
+ one_of_options = {
35
+ required: options.delete(:required),
36
+ name: options.delete(:name)
37
+ }
38
+ node = create(:one_of, **one_of_options) do
39
+ self.node node
40
+ str format: format, format_options: options
41
+ end
42
+ end
43
+
44
+ return node
45
+ end
46
+
47
+ def self.allowed_options
48
+ %i[name required default description examples enum parent options cast_str title]
49
+ end
50
+
51
+ def self.dsl_methods
52
+ %i[dsl_scm dsl_node]
53
+ end
54
+
55
+ def allowed_types
56
+ {}
57
+ end
58
+
59
+ def used_external_schemas
60
+ children.map(&:used_external_schemas).flatten.uniq
61
+ end
62
+
63
+ def children
64
+ []
65
+ end
66
+
67
+ def initialize(**options, &block)
68
+ # Check options #
69
+ disallowed_options = options.keys - self.class.allowed_options
70
+
71
+ if disallowed_options.any?
72
+ fail "Options #{disallowed_options.inspect} are not allowed for this node."
73
+ end
74
+
75
+ # Assign attributes #
76
+ @name = options.delete(:name)
77
+ @required = !!options.delete(:required)
78
+ @default = options.delete(:default)
79
+ @title = options.delete(:title)
80
+ @description = options.delete(:description)
81
+ @examples = options.delete(:examples)
82
+ @enum = options.delete(:enum)&.to_set
83
+ @parent = options.delete(:parent)
84
+ @options = options
85
+ @schemas = {}
86
+
87
+ # Run subclass init #
88
+ init
89
+
90
+ # Run DSL block #
91
+ if block_given?
92
+ unless self.class.supports_children_options
93
+ fail "Node #{self.class} does not support blocks."
94
+ end
95
+
96
+ scope = DslScope.new(self)
97
+ env = ScopedEnv.new(self, self.class.dsl_methods, scope, :dsl_)
98
+ env.instance_exec(&block)
99
+ end
100
+
101
+ # Validate self #
102
+ begin
103
+ validate_self
104
+ rescue StandardError => e
105
+ fail Exceptions::InvalidSchemaError, e.message
106
+ end
107
+ end
108
+
109
+ def create(type, **options, &block)
110
+ options[:parent] = self
111
+ return Node.create(type, **options, &block)
112
+ end
113
+
114
+ def init; end
115
+
116
+ def dsl_scm(name, type = :hash, **options, &block)
117
+ @schemas[name] = create(type, **options, &block)
118
+ end
119
+
120
+ def dsl_node(node)
121
+ add_child node
122
+ end
123
+
124
+ def schemas
125
+ (parent&.schemas || {}).merge(@schemas)
126
+ end
127
+
128
+ def required?
129
+ @required
130
+ end
131
+
132
+ def as_json
133
+ process_json([], {})
134
+ end
135
+
136
+ def cast(value)
137
+ value || default
138
+ end
139
+
140
+ def validate(data)
141
+ result = Result.new(self, data)
142
+ _validate(data, result: result)
143
+ return result
144
+ end
145
+
146
+ protected
147
+
148
+ def item_matches?(item, data)
149
+ item_result = Result.new(self)
150
+ item._validate(data, result: item_result)
151
+ return item_result.errors.none?
152
+ end
153
+
154
+ def process_json(attrs, json)
155
+ if @schemas.any?
156
+ json[:definitions] = {}
157
+ @schemas.each do |name, subschema|
158
+ json[:definitions][name] = subschema.as_json
159
+ end
160
+ end
161
+ attrs.each do |attr|
162
+ if options.include?(attr)
163
+ json[attr.to_s.camelize(:lower).to_sym] = options[attr]
164
+ end
165
+ end
166
+
167
+ json[:title] = @title if @title
168
+ json[:examples] = @examples if @examples
169
+ json[:description] = @description if @description
170
+ json[:default] = @default if @default
171
+ json[:enum] = @enum.to_a if @enum
172
+
173
+ return json.as_json
174
+ end
175
+
176
+ def type_assertion_method
177
+ :is_a?
178
+ end
179
+
180
+ def _validate(data, result:)
181
+ # Validate nil #
182
+ if data.nil? && required?
183
+ result.error 'Value must be given.'
184
+ return nil
185
+ end
186
+
187
+ # Apply default #
188
+ if data.nil?
189
+ if default
190
+ data = default
191
+ else
192
+ return nil
193
+ end
194
+ end
195
+
196
+ # Validate type #
197
+ if allowed_types.any? && allowed_types.keys.none? { |c| data.send(type_assertion_method, c) }
198
+ collection = allowed_types.values.map { |t| "\"#{t}\"" }.uniq.sort.join(' or ')
199
+ result.error %(Invalid type, expected #{collection}.)
200
+ return nil
201
+ end
202
+
203
+ # Validate enums #
204
+ if @enum && !@enum.include?(data)
205
+ result.error "Value not included in enum #{@enum.to_a.inspect}."
206
+ end
207
+
208
+ return data
209
+ end
210
+
211
+ def validate_self; end
212
+ end
213
+ end
214
+ end