schemacop 3.0.0.rc0 → 3.0.0.rc5

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.
data/Rakefile CHANGED
@@ -18,6 +18,7 @@ task :gemspec do
18
18
  # needs access to ActiveSupport::HashWithIndifferentAccess and expects
19
19
  # behavior of that as in version 5 of ActiveSupport.
20
20
  spec.add_dependency 'activesupport', '>= 4.0'
21
+ spec.add_dependency 'ruby2_keywords', '0.0.4'
21
22
  spec.add_development_dependency 'bundler'
22
23
  spec.add_development_dependency 'rake'
23
24
  spec.add_development_dependency 'minitest'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.0.rc0
1
+ 3.0.0.rc5
data/lib/schemacop.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # External dependencies
2
+ require 'ruby2_keywords'
2
3
  require 'active_support/all'
3
4
  require 'set'
4
5
 
@@ -1,9 +1,9 @@
1
1
  module Schemacop
2
2
  class Schema2 < BaseSchema
3
- def initialize(*args, &block)
3
+ def initialize(*args, **kwargs, &block)
4
4
  super()
5
5
  @root = V2::HashValidator.new do
6
- req :root, *args, &block
6
+ req :root, *args, **kwargs, &block
7
7
  end
8
8
  end
9
9
 
@@ -7,7 +7,7 @@ module Schemacop
7
7
  @prefix = prefix
8
8
  end
9
9
 
10
- def method_missing(symbol, *args, &block)
10
+ ruby2_keywords def method_missing(symbol, *args, &block)
11
11
  symbol = :"#{@prefix}#{symbol}" if @prefix
12
12
 
13
13
  if @methods.include?(symbol)
data/lib/schemacop/v2.rb CHANGED
@@ -10,7 +10,6 @@ require 'schemacop/v2/node'
10
10
  require 'schemacop/v2/node_with_block'
11
11
  require 'schemacop/v2/node_supporting_type'
12
12
  require 'schemacop/v2/field_node'
13
- require 'schemacop/v2/root_node'
14
13
  require 'schemacop/v2/node_supporting_field'
15
14
  require 'schemacop/v2/caster'
16
15
  require 'schemacop/v2/dupper'
@@ -18,6 +18,7 @@ module Schemacop::V2
18
18
  @data = data
19
19
  @target_type = target_type
20
20
  @caster = nil
21
+ @value = nil
21
22
 
22
23
  if casts.is_a?(Array)
23
24
  from_types = casts
@@ -15,36 +15,50 @@ module Schemacop::V2
15
15
  exec_block(&block)
16
16
  end
17
17
 
18
- def req?(*args, &block)
19
- field(*args, required: true, allow_nil: true, &block)
18
+ def req?(*args, **kwargs, &block)
19
+ kwargs ||= {}
20
+ kwargs[:required] = true
21
+ kwargs[:allow_nil] = true
22
+ field(*args, **kwargs, &block)
20
23
  end
21
24
 
22
- def req!(*args, &block)
23
- field(*args, required: true, allow_nil: false, &block)
25
+ def req!(*args, **kwargs, &block)
26
+ kwargs ||= {}
27
+ kwargs[:required] = true
28
+ kwargs[:allow_nil] = false
29
+ field(*args, **kwargs, &block)
24
30
  end
25
31
 
26
32
  alias req req!
27
33
 
28
- def opt?(*args, &block)
29
- field(*args, required: false, allow_nil: true, &block)
34
+ def opt?(*args, **kwargs, &block)
35
+ kwargs ||= {}
36
+ kwargs[:required] = false
37
+ kwargs[:allow_nil] = true
38
+ field(*args, **kwargs, &block)
30
39
  end
31
40
 
32
- def opt!(*args, &block)
33
- field(*args, required: false, allow_nil: false, &block)
41
+ def opt!(*args, **kwargs, &block)
42
+ kwargs ||= {}
43
+ kwargs[:required] = false
44
+ kwargs[:allow_nil] = false
45
+ field(*args, **kwargs, &block)
34
46
  end
35
47
 
36
48
  alias opt opt?
37
49
 
38
50
  protected
39
51
 
40
- def field(*args, required:, allow_nil:, &block)
52
+ def field(*args, **kwargs, &block)
41
53
  name = args.shift
54
+ required = kwargs.delete(:required)
55
+ allow_nil = kwargs.delete(:allow_nil)
42
56
 
43
57
  if @fields[name]
44
- @fields[name].type(*args, &block)
58
+ @fields[name].type(*args, **kwargs, &block)
45
59
  elsif args.any?
46
60
  @fields[name] = FieldNode.new(name, required) do
47
- type(*args, &block)
61
+ type(*args, **kwargs, &block)
48
62
  end
49
63
  else
50
64
  @fields[name] = FieldNode.new(name, required, &block)
@@ -33,7 +33,7 @@ module Schemacop::V2
33
33
  super
34
34
  rescue NoMethodError
35
35
  @types = []
36
- type :hash, {}, &block
36
+ type :hash, &block
37
37
  end
38
38
 
39
39
  # required signature:
@@ -73,7 +73,7 @@ module Schemacop::V2
73
73
  end
74
74
 
75
75
  child = klass.new do
76
- self.type(*subtypes, options, &block)
76
+ self.type(*subtypes, **options, &block)
77
77
  end
78
78
 
79
79
  # child = klass.build(options)
@@ -172,10 +172,9 @@ module Schemacop
172
172
  list_item.present?
173
173
  end
174
174
 
175
- def item_for_data(data, force: true)
175
+ def item_for_data(data)
176
176
  item = children.find { |c| item_matches?(c, data) }
177
177
  return item if item
178
- return nil unless force
179
178
 
180
179
  fail "Could not find specification for item #{data.inspect}."
181
180
  end
@@ -11,6 +11,8 @@ module Schemacop
11
11
 
12
12
  supports_children(name: true)
13
13
 
14
+ attr_reader :properties
15
+
14
16
  def self.allowed_options
15
17
  super + ATTRIBUTES - %i[dependencies] + %i[additional_properties]
16
18
  end
@@ -19,6 +21,14 @@ module Schemacop
19
21
  super + NodeRegistry.dsl_methods(true) + %i[dsl_dep dsl_add]
20
22
  end
21
23
 
24
+ def self.sanitize_exp(exp)
25
+ exp = exp.to_s
26
+ if exp.start_with?('(?-mix:')
27
+ exp = exp.to_s.gsub(/^\(\?-mix:/, '').gsub(/\)$/, '')
28
+ end
29
+ return exp
30
+ end
31
+
22
32
  def add_child(node)
23
33
  unless node.name
24
34
  fail Exceptions::InvalidSchemaError, 'Child nodes must have a name.'
@@ -35,7 +45,7 @@ module Schemacop
35
45
  @options[:additional_properties] = create(type, **options, &block)
36
46
  end
37
47
 
38
- def dsl_dep(source, *targets)
48
+ def dsl_dep(source, *targets, **_kwargs)
39
49
  @options[:dependencies] ||= {}
40
50
  @options[:dependencies][source] = targets
41
51
  end
@@ -54,11 +64,11 @@ module Schemacop
54
64
 
55
65
  json = {}
56
66
  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?
67
+ json[:patternProperties] = Hash[pattern_properties.values.map { |p| [self.class.sanitize_exp(p.name), p.as_json] }] if pattern_properties.any?
58
68
 
59
69
  # In schemacop, by default, additional properties are not allowed,
60
70
  # the users explicitly need to enable additional properties
61
- if options[:additional_properties] == true
71
+ if options[:additional_properties].is_a?(TrueClass)
62
72
  json[:additionalProperties] = true
63
73
  elsif options[:additional_properties].is_a?(Node)
64
74
  json[:additionalProperties] = options[:additional_properties].as_json
@@ -75,14 +85,6 @@ module Schemacop
75
85
  return process_json(ATTRIBUTES, json)
76
86
  end
77
87
 
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
88
  def allowed_types
87
89
  { Hash => :object }
88
90
  end
@@ -91,14 +93,23 @@ module Schemacop
91
93
  super_data = super
92
94
  return if super_data.nil?
93
95
 
96
+ original_data_hash = super_data.dup
97
+ data_hash = super_data.with_indifferent_access
98
+
99
+ if original_data_hash.size != data_hash.size
100
+ ambiguous_properties = original_data_hash.keys - data_hash.keys
101
+
102
+ result.error "Has #{ambiguous_properties.size} ambiguous properties: #{ambiguous_properties}."
103
+ end
104
+
94
105
  # 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]}."
106
+ if options[:min_properties] && data_hash.size < options[:min_properties]
107
+ result.error "Has #{data_hash.size} properties but needs at least #{options[:min_properties]}."
97
108
  end
98
109
 
99
110
  # 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]}."
111
+ if options[:max_properties] && data_hash.size > options[:max_properties]
112
+ result.error "Has #{data_hash.size} properties but needs at most #{options[:max_properties]}."
102
113
  end
103
114
 
104
115
  # Validate specified properties #
@@ -106,13 +117,13 @@ module Schemacop
106
117
  result.in_path(node.name) do
107
118
  next if node.name.is_a?(Regexp)
108
119
 
109
- node._validate(super_data[node.name], result: result)
120
+ node._validate(data_hash[node.name], result: result)
110
121
  end
111
122
  end
112
123
 
113
124
  # Validate additional properties #
114
125
  specified_properties = @properties.keys.to_set
115
- additional_properties = super_data.reject { |k, _v| specified_properties.include?(k.to_s.to_sym) }
126
+ additional_properties = data_hash.reject { |k, _v| specified_properties.include?(k.to_s) }
116
127
 
117
128
  property_patterns = {}
118
129
 
@@ -130,7 +141,9 @@ module Schemacop
130
141
  result.error "Property name #{name.inspect} does not match #{options[:property_names].inspect}."
131
142
  end
132
143
 
133
- if options[:additional_properties].blank?
144
+ if options[:additional_properties].is_a?(TrueClass)
145
+ next
146
+ elsif options[:additional_properties].is_a?(FalseClass) || options[:additional_properties].blank?
134
147
  match = property_patterns.keys.find { |p| p.match?(name.to_s) }
135
148
  if match
136
149
  result.in_path(name) do
@@ -149,7 +162,7 @@ module Schemacop
149
162
  # Validate dependencies #
150
163
  options[:dependencies]&.each do |source, targets|
151
164
  targets.each do |target|
152
- if super_data[source].present? && super_data[target].blank?
165
+ if data_hash[source].present? && data_hash[target].blank?
153
166
  result.error "Missing property #{target.to_s.inspect} because #{source.to_s.inspect} is given."
154
167
  end
155
168
  end
@@ -161,25 +174,54 @@ module Schemacop
161
174
  end
162
175
 
163
176
  def cast(data)
164
- result = {}
177
+ result = {}.with_indifferent_access
165
178
  data ||= default
166
179
  return nil if data.nil?
167
180
 
168
- # TODO: How to handle regex / etc.?
181
+ data_hash = data.dup.with_indifferent_access
182
+
183
+ property_patterns = {}
184
+ as_names = []
185
+
169
186
  @properties.each_value do |prop|
170
- result[prop.name] = prop.cast(data[prop.name])
187
+ if prop.name.is_a?(Regexp)
188
+ property_patterns[prop.name] = prop
189
+ next
190
+ end
191
+
192
+ as_names << prop.as&.to_s if prop.as.present?
193
+
194
+ prop_name = prop.as&.to_s || prop.name
195
+
196
+ casted_data = prop.cast(data_hash[prop.name])
197
+
198
+ if casted_data.present? || data_hash.include?(prop.name)
199
+ result[prop_name] = casted_data
200
+ end
171
201
 
172
- if result[prop.name].nil? && !data.include?(prop.name)
173
- result.delete(prop.name)
202
+ if result[prop_name].nil? && !data_hash.include?(prop.name) && !as_names.include?(prop.name)
203
+ result.delete(prop_name)
204
+ end
205
+ end
206
+
207
+ # Handle regex properties
208
+ specified_properties = @properties.keys.to_set
209
+ additional_properties = data_hash.reject { |k, _v| specified_properties.include?(k.to_s.to_sym) }
210
+
211
+ if additional_properties.any? && property_patterns.any?
212
+ additional_properties.each do |name, additional_property|
213
+ match_key = property_patterns.keys.find { |p| p.match?(name.to_s) }
214
+ match = property_patterns[match_key]
215
+ result[name] = match.cast(additional_property)
174
216
  end
175
217
  end
176
218
 
177
219
  # Handle additional properties
178
- if options[:additional_properties] == true
179
- result = data.merge(result)
220
+ if options[:additional_properties].is_a?(TrueClass)
221
+ result = data_hash.merge(result)
180
222
  elsif options[:additional_properties].is_a?(Node)
181
223
  specified_properties = @properties.keys.to_set
182
- additional_properties = data.reject { |k, _v| specified_properties.include?(k.to_s.to_sym) }
224
+ additional_properties = data_hash.reject { |k, _v| specified_properties.include?(k.to_s.to_sym) }
183
225
  if additional_properties.any?
184
226
  additional_properties_result = {}
185
227
  additional_properties.each do |key, value|
@@ -197,6 +239,14 @@ module Schemacop
197
239
  def init
198
240
  @properties = {}
199
241
  @options[:type] = :object
242
+ unless @options[:additional_properties].nil? || @options[:additional_properties].is_a?(TrueClass) || @options[:additional_properties].is_a?(FalseClass)
243
+ fail Schemacop::Exceptions::InvalidSchemaError, 'Option "additional_properties" must be a boolean value'
244
+ end
245
+
246
+ # Default the additional_properties option to false if it's not given
247
+ if @options[:additional_properties].nil?
248
+ @options[:additional_properties] = false
249
+ end
200
250
  end
201
251
 
202
252
  def validate_self
@@ -2,6 +2,7 @@ module Schemacop
2
2
  module V3
3
3
  class Node
4
4
  attr_reader :name
5
+ attr_reader :as
5
6
  attr_reader :default
6
7
  attr_reader :title
7
8
  attr_reader :description
@@ -32,8 +33,10 @@ module Schemacop
32
33
  if options.delete(:cast_str)
33
34
  format = NodeRegistry.name(klass)
34
35
  one_of_options = {
35
- required: options.delete(:required),
36
- name: options.delete(:name)
36
+ required: options.delete(:required),
37
+ name: options.delete(:name),
38
+ as: options.delete(:as),
39
+ description: options.delete(:description)
37
40
  }
38
41
  node = create(:one_of, **one_of_options) do
39
42
  self.node node
@@ -45,7 +48,7 @@ module Schemacop
45
48
  end
46
49
 
47
50
  def self.allowed_options
48
- %i[name required default description examples enum parent options cast_str title]
51
+ %i[name required default description examples enum parent options cast_str title as]
49
52
  end
50
53
 
51
54
  def self.dsl_methods
@@ -69,11 +72,13 @@ module Schemacop
69
72
  disallowed_options = options.keys - self.class.allowed_options
70
73
 
71
74
  if disallowed_options.any?
72
- fail "Options #{disallowed_options.inspect} are not allowed for this node."
75
+ fail Schemacop::Exceptions::InvalidSchemaError, "Options #{disallowed_options.inspect} are not allowed for this node."
73
76
  end
74
77
 
75
78
  # Assign attributes #
76
79
  @name = options.delete(:name)
80
+ @name = @name.to_s unless @name.nil? || @name.is_a?(Regexp)
81
+ @as = options.delete(:as)
77
82
  @required = !!options.delete(:required)
78
83
  @default = options.delete(:default)
79
84
  @title = options.delete(:title)
@@ -90,7 +95,7 @@ module Schemacop
90
95
  # Run DSL block #
91
96
  if block_given?
92
97
  unless self.class.supports_children_options
93
- fail "Node #{self.class} does not support blocks."
98
+ fail Schemacop::Exceptions::InvalidSchemaError, "Node #{self.class} does not support blocks."
94
99
  end
95
100
 
96
101
  scope = DslScope.new(self)
@@ -117,7 +122,7 @@ module Schemacop
117
122
  @schemas[name] = create(type, **options, &block)
118
123
  end
119
124
 
120
- def dsl_node(node)
125
+ def dsl_node(node, *_args, **_kwargs)
121
126
  add_child node
122
127
  end
123
128
 
@@ -40,10 +40,6 @@ module Schemacop
40
40
  def self.name(klass)
41
41
  @by_class[klass][:name]
42
42
  end
43
-
44
- def self.short_name(klass)
45
- @by_class[klass][:short_name]
46
- end
47
43
  end
48
44
  end
49
45
  end
@@ -36,7 +36,13 @@ module Schemacop
36
36
  end
37
37
 
38
38
  def used_external_schemas
39
- schemas.include?(@path) ? [] : [@path]
39
+ target_children_schema = target.used_external_schemas
40
+
41
+ if schemas.include?(@path)
42
+ return target_children_schema
43
+ else
44
+ return [@path] + target_children_schema
45
+ end
40
46
  end
41
47
 
42
48
  protected
@@ -66,21 +66,29 @@ module Schemacop
66
66
  end
67
67
 
68
68
  def cast(value)
69
+ if value.present?
70
+ to_cast = value
71
+ elsif default.present?
72
+ to_cast = default
73
+ else
74
+ return nil
75
+ end
76
+
69
77
  case options[:format]
70
78
  when :boolean
71
- return value == 'true'
79
+ return to_cast == 'true'
72
80
  when :date
73
- return Date.parse(value)
81
+ return Date.parse(to_cast)
74
82
  when :'date-time'
75
- return DateTime.parse(value)
83
+ return DateTime.parse(to_cast)
76
84
  when :time
77
- Time.parse(value)
85
+ Time.parse(to_cast)
78
86
  when :integer
79
- return Integer(value)
87
+ return Integer(to_cast)
80
88
  when :number
81
- return Float(value)
89
+ return Float(to_cast)
82
90
  else
83
- return value || default
91
+ return to_cast
84
92
  end
85
93
  end
86
94