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.
Files changed (53) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +4 -3
  3. data/CHANGELOG.md +146 -140
  4. data/attributor.gemspec +4 -5
  5. data/lib/attributor.rb +16 -2
  6. data/lib/attributor/attribute.rb +42 -9
  7. data/lib/attributor/extras/field_selector.rb +4 -0
  8. data/lib/attributor/families/numeric.rb +19 -6
  9. data/lib/attributor/families/temporal.rb +16 -9
  10. data/lib/attributor/hash_dsl_compiler.rb +6 -6
  11. data/lib/attributor/type.rb +27 -4
  12. data/lib/attributor/types/bigdecimal.rb +7 -2
  13. data/lib/attributor/types/boolean.rb +7 -2
  14. data/lib/attributor/types/class.rb +2 -2
  15. data/lib/attributor/types/collection.rb +24 -5
  16. data/lib/attributor/types/container.rb +3 -3
  17. data/lib/attributor/types/csv.rb +5 -1
  18. data/lib/attributor/types/date.rb +9 -3
  19. data/lib/attributor/types/date_time.rb +8 -2
  20. data/lib/attributor/types/float.rb +4 -3
  21. data/lib/attributor/types/hash.rb +82 -18
  22. data/lib/attributor/types/integer.rb +7 -1
  23. data/lib/attributor/types/model.rb +2 -2
  24. data/lib/attributor/types/object.rb +5 -0
  25. data/lib/attributor/types/polymorphic.rb +3 -2
  26. data/lib/attributor/types/string.rb +20 -1
  27. data/lib/attributor/types/struct.rb +1 -1
  28. data/lib/attributor/types/symbol.rb +5 -0
  29. data/lib/attributor/types/tempfile.rb +4 -0
  30. data/lib/attributor/types/time.rb +7 -3
  31. data/lib/attributor/types/uri.rb +9 -1
  32. data/lib/attributor/version.rb +1 -1
  33. data/spec/attribute_spec.rb +42 -7
  34. data/spec/dsl_compiler_spec.rb +7 -7
  35. data/spec/extras/field_selector/field_selector_spec.rb +9 -0
  36. data/spec/hash_dsl_compiler_spec.rb +2 -2
  37. data/spec/support/integers.rb +7 -0
  38. data/spec/type_spec.rb +1 -1
  39. data/spec/types/bigdecimal_spec.rb +8 -0
  40. data/spec/types/boolean_spec.rb +10 -0
  41. data/spec/types/class_spec.rb +0 -1
  42. data/spec/types/collection_spec.rb +16 -0
  43. data/spec/types/date_spec.rb +9 -0
  44. data/spec/types/date_time_spec.rb +9 -0
  45. data/spec/types/float_spec.rb +8 -0
  46. data/spec/types/hash_spec.rb +127 -9
  47. data/spec/types/integer_spec.rb +10 -1
  48. data/spec/types/model_spec.rb +14 -3
  49. data/spec/types/string_spec.rb +10 -0
  50. data/spec/types/temporal_spec.rb +5 -1
  51. data/spec/types/time_spec.rb +9 -0
  52. data/spec/types/uri_spec.rb +9 -0
  53. metadata +21 -34
@@ -5,7 +5,9 @@ require_relative '../exceptions'
5
5
  require 'date'
6
6
 
7
7
  module Attributor
8
- class DateTime < Temporal
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, context: context, from: value.class, encoding: 'DateTime', value: value
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 Type
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.family
26
- 'numeric'
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?(:to_hash)
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, context: context, value_type: value.class, type: self
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.nil?
435
- required_names = []
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
- required_names << sub_name if sub_attribute.options[:required] == true
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] |= required_names
447
- required_names = []
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 required_names.empty?
453
- hash[:requirements] << { type: :all, attributes: required_names }
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 < 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
@@ -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
@@ -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
 
@@ -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?(:to_hash)
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, context: context, value_type: value.class, type: self
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
@@ -26,7 +26,7 @@ module Attributor
26
26
  end
27
27
 
28
28
  ::Class.new(self) do
29
- attributes options, &attribute_definition
29
+ attributes **options, &attribute_definition
30
30
  end
31
31
  end
32
32
 
@@ -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
@@ -29,11 +29,15 @@ module Attributor
29
29
  begin
30
30
  return ::Time.parse(value)
31
31
  rescue ArgumentError
32
- raise Attributor::DeserializationError, context: context, from: value.class, encoding: 'Time', value: value
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
@@ -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, context: context, from: value.class, to: self, value: value
45
+ raise CoercionError.new(context: context, from: value.class, to: self, value: value)
38
46
  end
39
47
  end
40
48
 
@@ -1,3 +1,3 @@
1
1
  module Attributor
2
- VERSION = '5.2.0'.freeze
2
+ VERSION = '5.6'.freeze
3
3
  end
@@ -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) { { required: true, values: ['one'], description: 'something', min: 0 } }
70
+ let(:attribute_options) { {required: true, values: ['one'], description: "something", min: 0} }
41
71
  let(:expected) do
42
- h = { type: { name: 'String', id: type.id, family: type.family } }
43
- common = attribute_options.select { |k, _v| Attributor::Attribute::TOP_LEVEL_OPTIONS.include? k }
44
- h.merge!(common)
45
- h[:options] = { min: 0 }
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
- its(:describe) { should eq expected }
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
- expect(type).to receive(:load).with(value, context, {})
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