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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/attributor/attribute.rb +101 -84
- data/lib/attributor/extras/field_selector.rb +4 -0
- data/lib/attributor/families/numeric.rb +19 -6
- data/lib/attributor/families/temporal.rb +16 -9
- data/lib/attributor/hash_dsl_compiler.rb +6 -5
- data/lib/attributor/type.rb +26 -3
- data/lib/attributor/types/bigdecimal.rb +6 -1
- data/lib/attributor/types/boolean.rb +5 -0
- data/lib/attributor/types/collection.rb +19 -0
- data/lib/attributor/types/csv.rb +4 -0
- data/lib/attributor/types/date.rb +7 -1
- data/lib/attributor/types/date_time.rb +7 -1
- data/lib/attributor/types/float.rb +4 -3
- data/lib/attributor/types/hash.rb +86 -23
- data/lib/attributor/types/integer.rb +7 -1
- data/lib/attributor/types/model.rb +9 -21
- data/lib/attributor/types/object.rb +5 -0
- data/lib/attributor/types/polymorphic.rb +0 -1
- data/lib/attributor/types/string.rb +19 -0
- data/lib/attributor/types/symbol.rb +5 -0
- data/lib/attributor/types/tempfile.rb +4 -0
- data/lib/attributor/types/time.rb +6 -2
- data/lib/attributor/types/uri.rb +8 -0
- data/lib/attributor/version.rb +1 -1
- data/lib/attributor.rb +3 -7
- data/spec/attribute_spec.rb +148 -124
- data/spec/extras/field_selector/field_selector_spec.rb +9 -0
- data/spec/hash_dsl_compiler_spec.rb +5 -5
- data/spec/spec_helper.rb +0 -2
- data/spec/support/integers.rb +7 -0
- data/spec/support/models.rb +7 -7
- data/spec/types/bigdecimal_spec.rb +8 -0
- data/spec/types/boolean_spec.rb +10 -0
- data/spec/types/collection_spec.rb +16 -0
- data/spec/types/date_spec.rb +9 -0
- data/spec/types/date_time_spec.rb +9 -0
- data/spec/types/float_spec.rb +8 -0
- data/spec/types/hash_spec.rb +181 -22
- data/spec/types/integer_spec.rb +9 -0
- data/spec/types/model_spec.rb +7 -1
- data/spec/types/string_spec.rb +10 -0
- data/spec/types/temporal_spec.rb +5 -1
- data/spec/types/time_spec.rb +9 -0
- data/spec/types/uri_spec.rb +9 -0
- metadata +5 -6
- data/lib/attributor/attribute_resolver.rb +0 -111
- 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
|
441
|
-
|
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
|
-
|
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] |=
|
453
|
-
|
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
|
459
|
-
hash[:requirements] << {
|
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
|
-
|
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 =
|
584
|
-
|
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
|
-
|
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(
|
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
|
-
|
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
|
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
|
-
|
133
|
-
|
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
|
@@ -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
|
@@ -1,8 +1,8 @@
|
|
1
1
|
require 'date'
|
2
2
|
|
3
3
|
module Attributor
|
4
|
-
class Time
|
5
|
-
include
|
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
|
data/lib/attributor/types/uri.rb
CHANGED
data/lib/attributor/version.rb
CHANGED
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
|
-
|
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
|