attributor 5.4 → 6.0

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/lib/attributor/attribute.rb +101 -84
  4. data/lib/attributor/extras/field_selector.rb +4 -0
  5. data/lib/attributor/families/numeric.rb +19 -6
  6. data/lib/attributor/families/temporal.rb +16 -9
  7. data/lib/attributor/hash_dsl_compiler.rb +6 -5
  8. data/lib/attributor/type.rb +26 -3
  9. data/lib/attributor/types/bigdecimal.rb +6 -1
  10. data/lib/attributor/types/boolean.rb +5 -0
  11. data/lib/attributor/types/collection.rb +19 -0
  12. data/lib/attributor/types/csv.rb +4 -0
  13. data/lib/attributor/types/date.rb +7 -1
  14. data/lib/attributor/types/date_time.rb +7 -1
  15. data/lib/attributor/types/float.rb +4 -3
  16. data/lib/attributor/types/hash.rb +86 -23
  17. data/lib/attributor/types/integer.rb +7 -1
  18. data/lib/attributor/types/model.rb +9 -21
  19. data/lib/attributor/types/object.rb +5 -0
  20. data/lib/attributor/types/polymorphic.rb +0 -1
  21. data/lib/attributor/types/string.rb +19 -0
  22. data/lib/attributor/types/symbol.rb +5 -0
  23. data/lib/attributor/types/tempfile.rb +4 -0
  24. data/lib/attributor/types/time.rb +6 -2
  25. data/lib/attributor/types/uri.rb +8 -0
  26. data/lib/attributor/version.rb +1 -1
  27. data/lib/attributor.rb +3 -7
  28. data/spec/attribute_spec.rb +148 -124
  29. data/spec/extras/field_selector/field_selector_spec.rb +9 -0
  30. data/spec/hash_dsl_compiler_spec.rb +5 -5
  31. data/spec/spec_helper.rb +0 -2
  32. data/spec/support/integers.rb +7 -0
  33. data/spec/support/models.rb +7 -7
  34. data/spec/types/bigdecimal_spec.rb +8 -0
  35. data/spec/types/boolean_spec.rb +10 -0
  36. data/spec/types/collection_spec.rb +16 -0
  37. data/spec/types/date_spec.rb +9 -0
  38. data/spec/types/date_time_spec.rb +9 -0
  39. data/spec/types/float_spec.rb +8 -0
  40. data/spec/types/hash_spec.rb +181 -22
  41. data/spec/types/integer_spec.rb +9 -0
  42. data/spec/types/model_spec.rb +7 -1
  43. data/spec/types/string_spec.rb +10 -0
  44. data/spec/types/temporal_spec.rb +5 -1
  45. data/spec/types/time_spec.rb +9 -0
  46. data/spec/types/uri_spec.rb +9 -0
  47. metadata +5 -6
  48. data/lib/attributor/attribute_resolver.rb +0 -111
  49. data/spec/attribute_resolver_spec.rb +0 -237
@@ -426,7 +426,6 @@ module Attributor
426
426
  unless object.is_a?(self)
427
427
  raise ArgumentError, "#{name} can not validate object of type #{object.class.name} for #{Attributor.humanize_context(context)}."
428
428
  end
429
-
430
429
  object.validate(context)
431
430
  end
432
431
 
@@ -437,11 +436,11 @@ module Attributor
437
436
 
438
437
  if keys.any?
439
438
  # Spit keys if it's the root or if it's an anonymous structures
440
- if !shallow || name.nil?
441
- required_names = []
439
+ if ( !shallow || self.name == nil)
440
+ required_names_from_attr = []
442
441
  # FIXME: change to :keys when the praxis doc browser supports displaying those
443
- hash[:attributes] = keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
444
- required_names << sub_name if sub_attribute.options[:required] == true
442
+ hash[:attributes] = self.keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
443
+ required_names_from_attr << sub_name if sub_attribute.options[:required] == true
445
444
  sub_example = example.get(sub_name) if example
446
445
  sub_attributes[sub_name] = sub_attribute.describe(true, example: sub_example)
447
446
  end
@@ -449,14 +448,14 @@ module Attributor
449
448
  described_req = req.describe(shallow)
450
449
  if described_req[:type] == :all
451
450
  # Add the names of the attributes that have the required flag too
452
- described_req[:attributes] |= required_names
453
- required_names = []
451
+ described_req[:attributes] |= required_names_from_attr
452
+ required_names_from_attr = []
454
453
  end
455
454
  list << described_req
456
455
  end
457
456
  # Make sure we create an :all requirement, if there wasn't one so we can add the required: true attributes
458
- unless required_names.empty?
459
- hash[:requirements] << { type: :all, attributes: required_names }
457
+ unless required_names_from_attr.empty?
458
+ hash[:requirements] << {type: :all, attributes: required_names_from_attr }
460
459
  end
461
460
  end
462
461
  else
@@ -467,6 +466,60 @@ module Attributor
467
466
  hash
468
467
  end
469
468
 
469
+ def self.as_json_schema( shallow: false, example: nil, attribute_options: {} )
470
+ hash = super
471
+ opts = self.options.merge( attribute_options )
472
+
473
+ if key_type
474
+ hash[:'x-key_type'] = key_type.as_json_schema
475
+ end
476
+
477
+ if self.keys.any?
478
+ # Spit keys if it's the root or if it's an anonymous structures
479
+ if ( !shallow || self.name == nil)
480
+ required_names_from_attr = []
481
+ # FIXME: change to :keys when the praxis doc browser supports displaying those
482
+ hash[:properties] = self.keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
483
+ required_names_from_attr << sub_name if sub_attribute.options[:required] == true
484
+ sub_example = example.get(sub_name) if example
485
+ sub_attributes[sub_name] = sub_attribute.as_json_schema(shallow: true, example: sub_example)
486
+ end
487
+
488
+ # Expose the more complex requirements to in the x-tended attribute
489
+ extended_requirements = self.requirements.each_with_object([]) do |req, list|
490
+ described_req = req.describe(shallow)
491
+ if described_req[:type] == :all
492
+ # Add the names of the attributes that have the required flag too
493
+ described_req[:attributes] |= required_names_from_attr
494
+ required_names_from_attr = []
495
+ end
496
+ list << described_req
497
+ end
498
+ all = extended_requirements.find{|r| r[:type] == :all }
499
+ if ( all && !all[:attributes].empty? )
500
+ hash[:required] = all[:attributes]
501
+ end
502
+ hash[:'x-requirements'] = extended_requirements unless extended_requirements.empty?
503
+ end
504
+ else
505
+ hash[:'x-value_type'] = value_type.as_json_schema(shallow:true)
506
+ end
507
+
508
+ if opts[:allow_extra]
509
+ hash[:additionalProperties] = if value_type == Attributor::Object
510
+ true
511
+ else
512
+ value_type.as_json_schema(shallow: true)
513
+ end
514
+ end
515
+ # TODO: minProperties and maxProperties and patternProperties
516
+ hash
517
+ end
518
+
519
+ def self.json_schema_type
520
+ :object
521
+ end
522
+
470
523
  # TODO: Think about the format of the subcontexts to use: let's use .at(key.to_s)
471
524
  attr_reader :contents
472
525
 
@@ -558,6 +611,12 @@ module Attributor
558
611
  context = [context] if context.is_a? ::String
559
612
 
560
613
  if self.class.keys.any?
614
+ extra_keys = @contents.keys - self.class.keys.keys
615
+ if extra_keys.any? && !self.class.options[:allow_extra]
616
+ return extra_keys.collect do |k|
617
+ "#{Attributor.humanize_context(context)} can not have key: #{k.inspect}"
618
+ end
619
+ end
561
620
  self.validate_keys(context)
562
621
  else
563
622
  self.validate_generic(context)
@@ -567,30 +626,34 @@ module Attributor
567
626
  end
568
627
 
569
628
  def validate_keys(context)
570
- extra_keys = @contents.keys - self.class.keys.keys
571
- if extra_keys.any? && !self.class.options[:allow_extra]
572
- return extra_keys.collect do |k|
573
- "#{Attributor.humanize_context(context)} can not have key: #{k.inspect}"
574
- end
575
- end
576
-
577
629
  errors = []
578
- keys_with_values = []
630
+ keys_provided = []
579
631
 
580
632
  self.class.keys.each do |key, attribute|
581
633
  sub_context = self.class.generate_subcontext(context, key)
582
634
 
583
- value = @contents[key]
584
- keys_with_values << key unless value.nil?
635
+ value = _get_attr(key)
636
+ keys_provided << key if @contents.key?(key)
585
637
 
586
638
  if value.respond_to?(:validating) # really, it's a thing with sub-attributes
587
639
  next if value.validating
588
640
  end
589
-
590
- errors.concat attribute.validate(value, sub_context)
641
+ # Isn't this handled by the requirements validation? NO! we might want to combine
642
+ if attribute.options[:required] && !@contents.key?(key)
643
+ errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is required."]
644
+ end
645
+ if @contents[key].nil?
646
+ if !Attribute.nullable_attribute?(attribute.options) && @contents.key?(key)
647
+ errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is not nullable."]
648
+ end
649
+ # No need to validate the attribute further if the key wasn't passed...(or we would get nullable errors etc..cause the attribute has no
650
+ # context if its containing key was even passed (and there might not be a containing key for a top level attribute anyways))
651
+ else
652
+ errors.concat attribute.validate(value, sub_context)
653
+ end
591
654
  end
592
655
  self.class.requirements.each do |requirement|
593
- validation_errors = requirement.validate(keys_with_values, context)
656
+ validation_errors = requirement.validate(keys_provided, context)
594
657
  errors.concat(validation_errors) unless validation_errors.empty?
595
658
  end
596
659
  errors
@@ -606,7 +669,7 @@ module Attributor
606
669
 
607
670
  unless value_type == Attributor::Object
608
671
  sub_context = context + ["value(#{value.inspect})"]
609
- errors.concat value_attribute.validate(value, sub_context)
672
+ errors.concat value_attribute.validate(value, sub_context)
610
673
  end
611
674
  end
612
675
  end
@@ -1,7 +1,9 @@
1
1
 
2
2
 
3
3
  module Attributor
4
- class Integer < Numeric
4
+ class Integer
5
+ include Attributor::Numeric
6
+
5
7
  EXAMPLE_RANGE = 1000
6
8
 
7
9
  def self.native_type
@@ -53,5 +55,9 @@ module Attributor
53
55
  end
54
56
  true
55
57
  end
58
+
59
+ def self.json_schema_type
60
+ :integer
61
+ end
56
62
  end
57
63
  end
@@ -129,27 +129,9 @@ module Attributor
129
129
  @validating = true
130
130
 
131
131
  context = [context] if context.is_a? ::String
132
- keys_with_values = []
133
- errors = []
134
-
135
- self.class.attributes.each do |sub_attribute_name, sub_attribute|
136
- sub_context = self.class.generate_subcontext(context, sub_attribute_name)
137
-
138
- value = __send__(sub_attribute_name)
139
- keys_with_values << sub_attribute_name unless value.nil?
140
-
141
- if value.respond_to?(:validating) # really, it's a thing with sub-attributes
142
- next if value.validating
143
- end
144
-
145
- errors.concat sub_attribute.validate(value, sub_context)
146
- end
147
- self.class.requirements.each do |req|
148
- validation_errors = req.validate(keys_with_values, context)
149
- errors.concat(validation_errors) unless validation_errors.empty?
150
- end
151
-
152
- errors
132
+ # Use the common, underlying attribute validation of the hash (which will use our _get_attr)
133
+ # to know how to retrieve a value from a model (instead of a hash)
134
+ validate_keys(context)
153
135
  ensure
154
136
  @validating = false
155
137
  end
@@ -198,4 +180,10 @@ module Attributor
198
180
  @dumping = false
199
181
  end
200
182
  end
183
+
184
+ # Override the generic way to get a value from an instance (models need to call the method)
185
+ def _get_attr(k)
186
+ __send__(k)
187
+ end
188
+
201
189
  end
@@ -13,5 +13,10 @@ module Attributor
13
13
  def self.example(_context = nil, options: {})
14
14
  'An Object'
15
15
  end
16
+
17
+ def self.json_schema_type
18
+ :object #FIXME: not sure this is the most appropriate, since an Attributor::Object can be anything
19
+ end
20
+
16
21
  end
17
22
  end
@@ -1,4 +1,3 @@
1
- require 'active_support'
2
1
 
3
2
  require_relative '../exceptions'
4
3
 
@@ -32,5 +32,24 @@ module Attributor
32
32
  def self.family
33
33
  'string'
34
34
  end
35
+
36
+ def self.json_schema_type
37
+ :string
38
+ end
39
+
40
+ # TODO: we're passing the attribute options for now...might need to rethink ...although these are type-specific...
41
+ # TODO: multipleOf, minimum, maximum, exclusiveMinimum and exclusiveMaximum
42
+ def self.as_json_schema( shallow: false, example: nil, attribute_options: {} )
43
+ h = super
44
+ opts = ( self.respond_to?(:options) ) ? self.options.merge( attribute_options ) : attribute_options
45
+ h[:pattern] = self.human_readable_regexp(opts[:regexp]) if opts[:regexp]
46
+ # TODO: minLength, maxLength
47
+ h
48
+ end
49
+
50
+ def self.human_readable_regexp( reg )
51
+ return $1 if reg.to_s =~ /\(\?[^:]+:(.+)\)/
52
+ reg
53
+ end
35
54
  end
36
55
  end
@@ -19,5 +19,10 @@ module Attributor
19
19
  def self.family
20
20
  String.family
21
21
  end
22
+
23
+ def self.json_schema_type
24
+ :string
25
+ end
26
+
22
27
  end
23
28
  end
@@ -38,5 +38,9 @@ module Attributor
38
38
  def self.family
39
39
  String.family
40
40
  end
41
+
42
+ def self.json_schema_type
43
+ :string
44
+ end
41
45
  end
42
46
  end
@@ -1,8 +1,8 @@
1
1
  require 'date'
2
2
 
3
3
  module Attributor
4
- class Time < Temporal
5
- include Type
4
+ class Time
5
+ include Temporal
6
6
 
7
7
  def self.native_type
8
8
  ::Time
@@ -35,5 +35,9 @@ module Attributor
35
35
  raise CoercionError, context: context, from: value.class, to: self, value: value
36
36
  end
37
37
  end
38
+
39
+ def self.json_schema_string_format
40
+ :time
41
+ end
38
42
  end
39
43
  end
@@ -22,6 +22,14 @@ module Attributor
22
22
  ::URI::Generic
23
23
  end
24
24
 
25
+ def self.json_schema_type
26
+ :string
27
+ end
28
+
29
+ def self.json_schema_string_format
30
+ :uri
31
+ end
32
+
25
33
  def self.example(_context = nil, options: {})
26
34
  URI(Randgen.uri)
27
35
  end
@@ -1,3 +1,3 @@
1
1
  module Attributor
2
- VERSION = '5.4'.freeze
2
+ VERSION = '6.0'.freeze
3
3
  end
data/lib/attributor.rb CHANGED
@@ -2,7 +2,7 @@ require 'json'
2
2
  require 'randexp'
3
3
 
4
4
  require 'hashie'
5
-
5
+ require 'active_support/concern'
6
6
  require 'digest/sha1'
7
7
 
8
8
  module Attributor
@@ -13,7 +13,6 @@ module Attributor
13
13
  require_relative 'attributor/type'
14
14
  require_relative 'attributor/dsl_compiler'
15
15
  require_relative 'attributor/hash_dsl_compiler'
16
- require_relative 'attributor/attribute_resolver'
17
16
  require_relative 'attributor/smart_attribute_selector'
18
17
 
19
18
  require_relative 'attributor/example_mixin'
@@ -22,7 +21,8 @@ module Attributor
22
21
 
23
22
  # hierarchical separator string for composing human readable attributes
24
23
  SEPARATOR = '.'.freeze
25
- DEFAULT_ROOT_CONTEXT = ['$'].freeze
24
+ ROOT_PREFIX = '$'.freeze
25
+ DEFAULT_ROOT_CONTEXT = [ROOT_PREFIX].freeze
26
26
 
27
27
  # @param type [Class] The class of the type to resolve
28
28
  #
@@ -56,10 +56,6 @@ module Attributor
56
56
 
57
57
  context = Array(context) if context.is_a? ::String
58
58
 
59
- unless context.is_a? Enumerable
60
- raise "INVALID CONTEXT!!! (got: #{context.inspect})"
61
- end
62
-
63
59
  begin
64
60
  return context.join('.')
65
61
  rescue e