attributor 5.2.0 → 5.6
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 +5 -5
- data/.travis.yml +4 -3
- data/CHANGELOG.md +146 -140
- data/attributor.gemspec +4 -5
- data/lib/attributor.rb +16 -2
- data/lib/attributor/attribute.rb +42 -9
- 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 -6
- data/lib/attributor/type.rb +27 -4
- data/lib/attributor/types/bigdecimal.rb +7 -2
- data/lib/attributor/types/boolean.rb +7 -2
- data/lib/attributor/types/class.rb +2 -2
- data/lib/attributor/types/collection.rb +24 -5
- data/lib/attributor/types/container.rb +3 -3
- data/lib/attributor/types/csv.rb +5 -1
- data/lib/attributor/types/date.rb +9 -3
- data/lib/attributor/types/date_time.rb +8 -2
- data/lib/attributor/types/float.rb +4 -3
- data/lib/attributor/types/hash.rb +82 -18
- data/lib/attributor/types/integer.rb +7 -1
- data/lib/attributor/types/model.rb +2 -2
- data/lib/attributor/types/object.rb +5 -0
- data/lib/attributor/types/polymorphic.rb +3 -2
- data/lib/attributor/types/string.rb +20 -1
- data/lib/attributor/types/struct.rb +1 -1
- data/lib/attributor/types/symbol.rb +5 -0
- data/lib/attributor/types/tempfile.rb +4 -0
- data/lib/attributor/types/time.rb +7 -3
- data/lib/attributor/types/uri.rb +9 -1
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_spec.rb +42 -7
- data/spec/dsl_compiler_spec.rb +7 -7
- data/spec/extras/field_selector/field_selector_spec.rb +9 -0
- data/spec/hash_dsl_compiler_spec.rb +2 -2
- data/spec/support/integers.rb +7 -0
- data/spec/type_spec.rb +1 -1
- data/spec/types/bigdecimal_spec.rb +8 -0
- data/spec/types/boolean_spec.rb +10 -0
- data/spec/types/class_spec.rb +0 -1
- 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 +127 -9
- data/spec/types/integer_spec.rb +10 -1
- data/spec/types/model_spec.rb +14 -3
- 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 +21 -34
@@ -5,7 +5,9 @@ require_relative '../exceptions'
|
|
5
5
|
require 'date'
|
6
6
|
|
7
7
|
module Attributor
|
8
|
-
class DateTime
|
8
|
+
class DateTime
|
9
|
+
include Temporal
|
10
|
+
|
9
11
|
def self.native_type
|
10
12
|
::DateTime
|
11
13
|
end
|
@@ -24,8 +26,12 @@ module Attributor
|
|
24
26
|
begin
|
25
27
|
return ::DateTime.parse(value)
|
26
28
|
rescue ArgumentError
|
27
|
-
raise Attributor::DeserializationError
|
29
|
+
raise Attributor::DeserializationError.new(context: context, from: value.class, encoding: 'DateTime', value: value)
|
28
30
|
end
|
29
31
|
end
|
32
|
+
|
33
|
+
def self.json_schema_string_format
|
34
|
+
:'date-time'
|
35
|
+
end
|
30
36
|
end
|
31
37
|
end
|
@@ -2,8 +2,9 @@
|
|
2
2
|
# See: http://ruby-doc.org/core-2.1.0/Float.html
|
3
3
|
|
4
4
|
module Attributor
|
5
|
+
|
5
6
|
class Float
|
6
|
-
include
|
7
|
+
include Numeric
|
7
8
|
|
8
9
|
def self.native_type
|
9
10
|
::Float
|
@@ -22,8 +23,8 @@ module Attributor
|
|
22
23
|
super
|
23
24
|
end
|
24
25
|
|
25
|
-
def self.
|
26
|
-
|
26
|
+
def self.json_schema_type
|
27
|
+
:number
|
27
28
|
end
|
28
29
|
end
|
29
30
|
end
|
@@ -82,7 +82,7 @@ module Attributor
|
|
82
82
|
def self.attributes(**options, &key_spec)
|
83
83
|
raise @error if @error
|
84
84
|
|
85
|
-
keys(options, &key_spec)
|
85
|
+
keys(**options, &key_spec)
|
86
86
|
end
|
87
87
|
|
88
88
|
def self.keys(**options, &key_spec)
|
@@ -111,7 +111,7 @@ module Attributor
|
|
111
111
|
}.merge(@options)
|
112
112
|
|
113
113
|
blocks = @saved_blocks.shift(@saved_blocks.size)
|
114
|
-
compiler = dsl_class.new(self, opts)
|
114
|
+
compiler = dsl_class.new(self, **opts)
|
115
115
|
compiler.parse(*blocks)
|
116
116
|
|
117
117
|
if opts[:case_insensitive_load] == true
|
@@ -171,7 +171,7 @@ module Attributor
|
|
171
171
|
raise Attributor::AttributorException, ":case_insensitive_load may not be used with keys of type #{key_type.name}"
|
172
172
|
end
|
173
173
|
|
174
|
-
keys(options, &constructor_block)
|
174
|
+
keys(**options, &constructor_block)
|
175
175
|
self
|
176
176
|
end
|
177
177
|
|
@@ -221,7 +221,7 @@ module Attributor
|
|
221
221
|
result = new
|
222
222
|
result.extend(ExampleMixin)
|
223
223
|
|
224
|
-
result.lazy_attributes = example_contents(context, result, values)
|
224
|
+
result.lazy_attributes = example_contents(context, result, **values)
|
225
225
|
else
|
226
226
|
hash = ::Hash.new
|
227
227
|
|
@@ -262,11 +262,11 @@ module Attributor
|
|
262
262
|
end
|
263
263
|
|
264
264
|
def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **_options)
|
265
|
-
context = Array(context)
|
266
265
|
|
267
266
|
return value if value.is_a?(self)
|
268
267
|
return nil if value.nil? && !recurse
|
269
268
|
|
269
|
+
context = Array(context)
|
270
270
|
loaded_value = self.parse(value, context)
|
271
271
|
|
272
272
|
return from_hash(loaded_value, context, recurse: recurse) if keys.any?
|
@@ -282,10 +282,12 @@ module Attributor
|
|
282
282
|
value
|
283
283
|
elsif value.is_a?(::String)
|
284
284
|
decode_json(value, context)
|
285
|
-
elsif value.respond_to?(:
|
285
|
+
elsif value.respond_to?(:to_h)
|
286
|
+
value.to_h
|
287
|
+
elsif value.respond_to?(:to_hash) # Deprecate this in lieu of to_h only?
|
286
288
|
value.to_hash
|
287
289
|
else
|
288
|
-
raise Attributor::IncompatibleTypeError
|
290
|
+
raise Attributor::IncompatibleTypeError.new(context: context, value_type: value.class, type: self)
|
289
291
|
end
|
290
292
|
end
|
291
293
|
|
@@ -301,6 +303,10 @@ module Attributor
|
|
301
303
|
context + ["key(#{key_name.inspect})"]
|
302
304
|
end
|
303
305
|
|
306
|
+
def to_h
|
307
|
+
Attributor.recursive_to_h(@contents)
|
308
|
+
end
|
309
|
+
|
304
310
|
def generate_subcontext(context, key_name)
|
305
311
|
self.class.generate_subcontext(context, key_name)
|
306
312
|
end
|
@@ -431,11 +437,11 @@ module Attributor
|
|
431
437
|
|
432
438
|
if keys.any?
|
433
439
|
# Spit keys if it's the root or if it's an anonymous structures
|
434
|
-
if !shallow || name
|
435
|
-
|
440
|
+
if ( !shallow || self.name == nil)
|
441
|
+
required_names_from_attr = []
|
436
442
|
# FIXME: change to :keys when the praxis doc browser supports displaying those
|
437
|
-
hash[:attributes] = keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
|
438
|
-
|
443
|
+
hash[:attributes] = self.keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
|
444
|
+
required_names_from_attr << sub_name if sub_attribute.options[:required] == true
|
439
445
|
sub_example = example.get(sub_name) if example
|
440
446
|
sub_attributes[sub_name] = sub_attribute.describe(true, example: sub_example)
|
441
447
|
end
|
@@ -443,14 +449,14 @@ module Attributor
|
|
443
449
|
described_req = req.describe(shallow)
|
444
450
|
if described_req[:type] == :all
|
445
451
|
# Add the names of the attributes that have the required flag too
|
446
|
-
described_req[:attributes] |=
|
447
|
-
|
452
|
+
described_req[:attributes] |= required_names_from_attr
|
453
|
+
required_names_from_attr = []
|
448
454
|
end
|
449
455
|
list << described_req
|
450
456
|
end
|
451
457
|
# Make sure we create an :all requirement, if there wasn't one so we can add the required: true attributes
|
452
|
-
unless
|
453
|
-
hash[:requirements] << {
|
458
|
+
unless required_names_from_attr.empty?
|
459
|
+
hash[:requirements] << {type: :all, attributes: required_names_from_attr }
|
454
460
|
end
|
455
461
|
end
|
456
462
|
else
|
@@ -461,6 +467,61 @@ module Attributor
|
|
461
467
|
hash
|
462
468
|
end
|
463
469
|
|
470
|
+
def self.as_json_schema( shallow: false, example: nil, attribute_options: {} )
|
471
|
+
hash = super
|
472
|
+
opts = self.options.merge( attribute_options )
|
473
|
+
|
474
|
+
if key_type
|
475
|
+
hash[:'x-key_type'] = key_type.as_json_schema
|
476
|
+
end
|
477
|
+
|
478
|
+
if self.keys.any?
|
479
|
+
# Spit keys if it's the root or if it's an anonymous structures
|
480
|
+
if ( !shallow || self.name == nil)
|
481
|
+
required_names_from_attr = []
|
482
|
+
# FIXME: change to :keys when the praxis doc browser supports displaying those
|
483
|
+
hash[:properties] = self.keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
|
484
|
+
required_names_from_attr << sub_name if sub_attribute.options[:required] == true
|
485
|
+
sub_example = example.get(sub_name) if example
|
486
|
+
sub_attributes[sub_name] = sub_attribute.as_json_schema(shallow: true, example: sub_example)
|
487
|
+
end
|
488
|
+
|
489
|
+
# Expose the more complex requirements to in the x-tended attribute
|
490
|
+
extended_requirements = self.requirements.each_with_object([]) do |req, list|
|
491
|
+
described_req = req.describe(shallow)
|
492
|
+
if described_req[:type] == :all
|
493
|
+
# Add the names of the attributes that have the required flag too
|
494
|
+
described_req[:attributes] |= required_names_from_attr
|
495
|
+
required_names_from_attr = []
|
496
|
+
end
|
497
|
+
list << described_req
|
498
|
+
end
|
499
|
+
all = extended_requirements.find{|r| r[:type] == :all }
|
500
|
+
if ( all && !all[:attributes].empty? )
|
501
|
+
hash[:required] = all[:attributes]
|
502
|
+
end
|
503
|
+
hash[:'x-requirements'] = extended_requirements unless extended_requirements.empty?
|
504
|
+
end
|
505
|
+
else
|
506
|
+
hash[:'x-value_type'] = value_type.as_json_schema(shallow:true)
|
507
|
+
end
|
508
|
+
|
509
|
+
if opts[:allow_extra]
|
510
|
+
hash[:additionalProperties] = if value_type == Attributor::Object
|
511
|
+
true
|
512
|
+
else
|
513
|
+
value_type.as_json_schema(shallow: true)
|
514
|
+
end
|
515
|
+
end
|
516
|
+
# TODO: minProperties and maxProperties and patternProperties
|
517
|
+
# TODO: map our required_if (and possible our above requirements 'at_least...' to json schema dependencies)
|
518
|
+
hash
|
519
|
+
end
|
520
|
+
|
521
|
+
def self.json_schema_type
|
522
|
+
:object
|
523
|
+
end
|
524
|
+
|
464
525
|
# TODO: Think about the format of the subcontexts to use: let's use .at(key.to_s)
|
465
526
|
attr_reader :contents
|
466
527
|
|
@@ -548,6 +609,7 @@ module Attributor
|
|
548
609
|
end
|
549
610
|
|
550
611
|
def validate(context = Attributor::DEFAULT_ROOT_CONTEXT)
|
612
|
+
@validating = true
|
551
613
|
context = [context] if context.is_a? ::String
|
552
614
|
|
553
615
|
if self.class.keys.any?
|
@@ -555,6 +617,8 @@ module Attributor
|
|
555
617
|
else
|
556
618
|
self.validate_generic(context)
|
557
619
|
end
|
620
|
+
ensure
|
621
|
+
@validating = false
|
558
622
|
end
|
559
623
|
|
560
624
|
def validate_keys(context)
|
@@ -607,12 +671,12 @@ module Attributor
|
|
607
671
|
@dumping = true
|
608
672
|
|
609
673
|
contents.each_with_object({}) do |(k, v), hash|
|
610
|
-
k = key_attribute.dump(k, opts)
|
674
|
+
k = key_attribute.dump(k, **opts)
|
611
675
|
|
612
676
|
v = if (attribute_for_value = self.class.keys[k])
|
613
|
-
attribute_for_value.dump(v, opts)
|
677
|
+
attribute_for_value.dump(v, **opts)
|
614
678
|
else
|
615
|
-
value_attribute.dump(v, opts)
|
679
|
+
value_attribute.dump(v, **opts)
|
616
680
|
end
|
617
681
|
|
618
682
|
hash[k] = v
|
@@ -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
|
@@ -105,7 +105,7 @@ module Attributor
|
|
105
105
|
result = new
|
106
106
|
result.extend(ExampleMixin)
|
107
107
|
|
108
|
-
result.lazy_attributes = example_contents(context, result, values)
|
108
|
+
result.lazy_attributes = example_contents(context, result, **values)
|
109
109
|
else
|
110
110
|
result = new
|
111
111
|
end
|
@@ -192,7 +192,7 @@ module Attributor
|
|
192
192
|
next
|
193
193
|
end
|
194
194
|
|
195
|
-
hash[name.to_sym] = attribute.dump(value, context: context + [name])
|
195
|
+
hash[name.to_sym] = attribute.dump(value, context: context + [name], **_opts)
|
196
196
|
end
|
197
197
|
ensure
|
198
198
|
@dumping = false
|
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'active_support'
|
2
1
|
|
3
2
|
require_relative '../exceptions'
|
4
3
|
|
@@ -95,7 +94,9 @@ module Attributor
|
|
95
94
|
value
|
96
95
|
elsif value.is_a?(::String)
|
97
96
|
decode_json(value, context)
|
98
|
-
elsif value.respond_to?(:
|
97
|
+
elsif value.respond_to?(:to_h)
|
98
|
+
value.to_h
|
99
|
+
elsif value.respond_to?(:to_hash) # Deprecate this in lieu of to_h only?
|
99
100
|
value.to_hash
|
100
101
|
else
|
101
102
|
raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
|
@@ -8,7 +8,7 @@ module Attributor
|
|
8
8
|
|
9
9
|
def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
10
10
|
if value.is_a?(Enumerable)
|
11
|
-
raise IncompatibleTypeError
|
11
|
+
raise IncompatibleTypeError.new(context: context, value_type: value.class, type: self)
|
12
12
|
end
|
13
13
|
|
14
14
|
value && String(value)
|
@@ -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
|
@@ -29,11 +29,15 @@ module Attributor
|
|
29
29
|
begin
|
30
30
|
return ::Time.parse(value)
|
31
31
|
rescue ArgumentError
|
32
|
-
raise Attributor::DeserializationError
|
32
|
+
raise Attributor::DeserializationError.new(context: context, from: value.class, encoding: 'Time', value: value)
|
33
33
|
end
|
34
34
|
else
|
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
@@ -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
|
@@ -34,7 +42,7 @@ module Attributor
|
|
34
42
|
when ::String
|
35
43
|
URI(value)
|
36
44
|
else
|
37
|
-
raise CoercionError
|
45
|
+
raise CoercionError.new(context: context, from: value.class, to: self, value: value)
|
38
46
|
end
|
39
47
|
end
|
40
48
|
|
data/lib/attributor/version.rb
CHANGED
data/spec/attribute_spec.rb
CHANGED
@@ -36,17 +36,48 @@ describe Attributor::Attribute do
|
|
36
36
|
it { should eq other_attribute }
|
37
37
|
end
|
38
38
|
|
39
|
+
context 'describe_json_schema' do
|
40
|
+
let(:type) { PositiveIntegerType }
|
41
|
+
|
42
|
+
let(:attribute_options) do
|
43
|
+
{
|
44
|
+
values: [1,20],
|
45
|
+
description: "something",
|
46
|
+
example: 20,
|
47
|
+
max: 1000,
|
48
|
+
default: 1
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'reports all of the possible attributes' do
|
53
|
+
let(:js){ subject.as_json_schema(example: 20) }
|
54
|
+
|
55
|
+
it 'including the attribute-specific ones' do
|
56
|
+
expect(js[:enum]).to eq( [1,20])
|
57
|
+
expect(js[:description]).to eq( "something")
|
58
|
+
expect(js[:default]).to eq(1)
|
59
|
+
expect(js[:example]).to eq(20)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'as well as the type-specific ones' do
|
63
|
+
expect(js[:type]).to eq(:integer)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
39
69
|
context 'describe' do
|
40
|
-
let(:attribute_options) { {
|
70
|
+
let(:attribute_options) { {required: true, values: ['one'], description: "something", min: 0} }
|
41
71
|
let(:expected) do
|
42
|
-
h = {
|
43
|
-
common = attribute_options.select
|
44
|
-
h.merge!(common)
|
45
|
-
h[:options] = {
|
72
|
+
h = {type: {name: 'String', id: type.id, family: type.family}}
|
73
|
+
common = attribute_options.select{|k,v| Attributor::Attribute::TOP_LEVEL_OPTIONS.include? k }
|
74
|
+
h.merge!( common )
|
75
|
+
h[:options] = {:min => 0 }
|
46
76
|
h
|
47
77
|
end
|
48
78
|
|
49
|
-
|
79
|
+
# It has both the type-included options (min) as well as the attribute options (max)
|
80
|
+
its(:describe) { should == expected }
|
50
81
|
|
51
82
|
context 'with example options' do
|
52
83
|
let(:attribute_options) { { description: 'something', example: 'ex_def' } }
|
@@ -313,7 +344,11 @@ describe Attributor::Attribute do
|
|
313
344
|
let(:value) { '1' }
|
314
345
|
|
315
346
|
it 'delegates to type.load' do
|
316
|
-
|
347
|
+
# Need to add the "anything" of the 3rd element, as in ruby < 2.7 it comes as an empty hash
|
348
|
+
expect(type).to receive(:load) do |v, c, _other|
|
349
|
+
expect(v).to eq(value)
|
350
|
+
expect(c).to eq(context)
|
351
|
+
end
|
317
352
|
attribute.load(value, context)
|
318
353
|
end
|
319
354
|
|