attributor 6.5 → 7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9242ef6d878ab79e69393d7bf31b6459873d39a8307c9dac0ea05ce8c4a01912
4
- data.tar.gz: 0b2f333abe4957d1575df87c734c4f642274a83d50562059c33fb261154e44a5
3
+ metadata.gz: 5b5fc3c6809e4a0e21b21752683196ce018beafbb7d8b615f2217abbcb55b3c3
4
+ data.tar.gz: 55bba33e0b5ee71c581a029cfd3045c6f71ce5d1f578b2654d99f919626dcbc8
5
5
  SHA512:
6
- metadata.gz: 48a0ea86f5550dd44b0d162ea96c1a98bbd7019955857f607817efe921fc4250c210e2591664d18a9388fd614d323547c83421b64ba678369c47d248423770ca
7
- data.tar.gz: dcd17fa9b98e35d02e471ef340b641ba15ff4fafd6f05d5ac650204830ba7cc7a44feaf40b6a2386c660ab17d3b8d0b881d01d84c4a83a46fe41c6552a2ec74d
6
+ metadata.gz: f09888249f30f079afcbfbc09695cf828bed15d6c6b621b0ab5a55de086b740c74bfa33cfd4b75fa1c97650ccb0c7ceeb5850401af94a11619ea46b6c0aada33
7
+ data.tar.gz: b3876fbc0f432ff67fc0031234a4da8eea6b2b965d539a71f5a27ab615442cc8ae2420270a315a2d1461fb20d3f9aa7eae8619a1e7979f9f6cb73365d786cddd
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## next
4
4
 
5
+ ## 7.0 (5/23/2023)
6
+ - Support for loading "<digits>." strings in BigDecimal/Float types (These are formats supported by JS, Java ..)
7
+ - Support for defining collections of types using a more terse DSL: T[]
8
+ - For example: Attributor::Integer[] is equivalent to Attributor::Collection.of(Attributor::Integer)
9
+ - It also caches these collection types when they are concrete (i.e., not constructable like Struct[], etc...)
10
+ - Revamped the type and options inheritance (and :reference behavior) when defining attributes:
11
+ - The matrix of behavior is now fully defined, and all possible combinations of parameters are tested
12
+ - tightened up some error cases and messaging (i.e., cannot pass :reference unless there is a block, etc...)
13
+ - while technically this change is compatible, it might surface some errors in previous definitions that somehow
14
+ resolved things ambiguously (and now they will complain loudly). This is the reason for bumping the major version.
15
+
5
16
  ## 6.5 (1/19/2023)
6
17
  - Fix JSON schema reporting for BigDecimal types to be a string, with a format=bigdecimal
7
18
 
@@ -12,6 +12,7 @@ module Attributor
12
12
  class DSLCompiler
13
13
  attr_accessor :options, :target
14
14
 
15
+ include Attributor
15
16
  def initialize(target, **options)
16
17
  @target = target
17
18
  @options = options
@@ -33,6 +34,15 @@ module Attributor
33
34
 
34
35
  def attribute(name, attr_type = nil, **opts, &block)
35
36
  raise AttributorException, "Attribute names must be symbols, got: #{name.inspect}" unless name.is_a? ::Symbol
37
+ if opts[:reference]
38
+ raise AttributorException, ":reference option can only be used when defining blocks" unless block_given?
39
+ if opts[:reference] < Attributor::Collection
40
+ err = ":reference option cannot be a collection. It must be a concrete Struct-like type containing the attribute #{name} you are defining.\n"
41
+ location_file, location_line = block.source_location
42
+ err += "The place where you are trying to define the attribute is here:\n#{location_file} line #{location_line}\n#{block.source}\n"
43
+ raise AttributorException, err
44
+ end
45
+ end
36
46
  target.attributes[name] = define(name, attr_type, **opts, &block)
37
47
  end
38
48
 
@@ -79,7 +89,6 @@ module Attributor
79
89
  # @api semiprivate
80
90
  def define(name, attr_type = nil, **opts, &block)
81
91
  example_given = opts.key? :example
82
-
83
92
  # add to existing attribute if present
84
93
  if (existing_attribute = attributes[name])
85
94
  if existing_attribute.attributes
@@ -87,40 +96,74 @@ module Attributor
87
96
  return existing_attribute
88
97
  end
89
98
  end
90
-
91
- # determine inherited type (giving preference to the direct attribute options)
92
- inherited_type = opts[:reference]
93
- unless inherited_type
94
- reference = options[:reference]
95
- if reference && reference.respond_to?(:attributes) && reference.attributes.key?(name)
96
- inherited_attribute = reference.attributes[name]
97
- opts = inherited_attribute.options.merge(opts) unless attr_type
98
- inherited_type = inherited_attribute.type
99
- opts[:reference] = inherited_type if block_given?
99
+
100
+ if attr_type.nil?
101
+ if block_given?
102
+ final_type, carried_options = resolve_type_for_block(name, **opts, &block)
103
+ else
104
+ final_type, carried_options = resolve_type_for_no_block(name, **opts)
100
105
  end
106
+ else
107
+ final_type = attr_type
108
+ carried_options = {}
101
109
  end
102
110
 
103
- # determine attribute type to use
104
- if attr_type.nil?
105
- if block_given?
106
- # Don't inherit explicit examples if we've redefined the structure
107
- # (but preserve the direct example if given here)
108
- opts.delete :example unless example_given
109
- attr_type = if inherited_type && inherited_type < Attributor::Collection
110
- # override the reference to be the member_attribute's type for collections
111
- opts[:reference] = inherited_type.member_attribute.type
112
- Attributor::Collection.of(Struct)
113
- else
114
- Attributor::Struct
115
- end
116
- elsif inherited_type
117
- attr_type = inherited_type
111
+ final_opts = opts.dup
112
+ final_opts.delete(:reference)
113
+
114
+ # Possibly add a reference for block definitions (No reference for leaves)
115
+ final_opts.merge!(add_reference_to_block(name, opts)) if block_given?
116
+ final_opts = carried_options.merge(final_opts)
117
+ Attributor::Attribute.new(final_type, final_opts, &block)
118
+ end
119
+
120
+
121
+ def resolve_type_for_block(name, **opts)
122
+ resolved_type = nil
123
+ carried_options = {}
124
+ ref = options[:reference]
125
+ if ref && ref.respond_to?(:attributes) && ref.attributes.key?(name)
126
+ type_from_ref = ref.attributes[name]&.type
127
+ resolved_type = type_from_ref < Attributor::Collection ? Attributor::Struct[] : Attributor::Struct
128
+ else
129
+ # Type for attribute with given name could not be determined from reference...or ther is not refrence: defaulting to Struct"
130
+ resolved_type = Attributor::Struct
131
+ end
132
+ [resolved_type, carried_options]
133
+ end
134
+
135
+ def resolve_type_for_no_block(name, **opts)
136
+ resolved_type = nil
137
+ carried_options = {}
138
+ ref = options[:reference]
139
+ if ref && ref.respond_to?(:attributes) && ref.attributes.key?(name)
140
+ resolved_type = ref.attributes[name].type
141
+ carried_options = ref.attributes[name].options
142
+ else
143
+ message = "Type for attribute with name: #{name} could not be determined.\n"
144
+ if ref
145
+ message += "You are defining '#{name}' without a type, and the passed in :reference type (#{ref}) does not have an attribute named '#{name}'.\n" \
118
146
  else
119
- raise AttributorException, "type for attribute with name: #{name} could not be determined"
147
+ message += "You are defining '#{name}' without a type, and there is no :reference type to infer it from (Did you forget to add the type?).\n" \
120
148
  end
149
+ message += "\nIf you are omiting a type thinking that would be inherited from the reference, make sure the right one is passed in," \
150
+ ", which has a #{name} defined, otherwise simply specify the type of the attribute you want.\n"
151
+ raise AttributorException, message
121
152
  end
153
+ [resolved_type, carried_options]
154
+ end
122
155
 
123
- Attributor::Attribute.new(attr_type, opts, &block)
156
+ def add_reference_to_block(name, opts)
157
+ base_reference = options[:reference]
158
+ if opts[:reference] # Direct reference specified in the attribute, just pass it to the block
159
+ {reference: opts[:reference]}
160
+ elsif( base_reference && base_reference.respond_to?(:attributes) && base_reference.attributes.key?(name))
161
+ selected_type = base_reference.attributes[name].type
162
+ selected_type = selected_type.member_attribute.type if selected_type < Attributor::Collection
163
+ {reference: selected_type}
164
+ else
165
+ {}
166
+ end
124
167
  end
125
168
  end
126
169
  end
@@ -4,6 +4,61 @@ module Attributor
4
4
  module Type
5
5
  extend ActiveSupport::Concern
6
6
 
7
+ # Memoized collection classes of specific type (by collection type)
8
+ # i.e., CSV is a Collection of String, which is different from a pure Collection (default)
9
+ # key = membertype class (i.e., String)
10
+ # value = Hash
11
+ # key = concrete collection class: Attributor::Collection (default) or Attributor::CSV...
12
+ # value = memoized constructed collection class of the given member type and collection type
13
+ # example:
14
+ # String => {
15
+ # Attributor::Collection => Attributor::Collection.of(String),
16
+ # Attributor::CSV => Attributor::CSV.of(String)
17
+ # }
18
+ # It memoized them on in the base Type class, properly serializing access through a mutex
19
+ @_memoized_collection_classes = Hash.new do |h, k|
20
+ h[k] = {}
21
+ end
22
+ @_memoized_collection_classes_mutex = Mutex.new
23
+
24
+ class << self
25
+ attr_accessor :_memoized_collection_classes
26
+ def get_memoized_collection_class(member_class, collection_type=Attributor::Collection)
27
+ # No need to serialize on read, Ruby will ensure we can properly read what's there, even if it's being written
28
+ _memoized_collection_classes.dig(member_class,collection_type)
29
+ end
30
+ def set_memoized_collection_class(klass, member_class, collection_type=Attributor::Collection)
31
+ @_memoized_collection_classes_mutex.synchronize do
32
+ _memoized_collection_classes[member_class][collection_type] = klass
33
+ end
34
+ end
35
+ end
36
+
37
+ included do
38
+ # Creates a new type that is a collection of this member types
39
+ # By default, T[] is equivalent to Collection.of(T)
40
+ # But you can pass the collection class to use, e.g. T[CSV] is equivalent to CSV.of(T)
41
+ def self.[](collection_type=Attributor::Collection)
42
+ member_class = self
43
+ existing = Attributor::Type.get_memoized_collection_class(member_class,collection_type)
44
+ return existing if existing
45
+
46
+ unless self.ancestors.include?(Attributor::Type)
47
+ raise Attributor::AttributorException, 'Collections can only have members that are Attributor::Types'
48
+ end
49
+
50
+ # Create and memoize it for non-constructable types here (like we do in Type[])
51
+ new_class = ::Class.new(collection_type) do
52
+ @member_type = member_class
53
+ end
54
+ if self.constructable?
55
+ new_class
56
+ else
57
+ # Unfortunately we cannot freeze the memoized class as it lazily sets the member_attribute inside
58
+ Attributor::Type.set_memoized_collection_class(new_class, member_class, collection_type)
59
+ end
60
+ end
61
+ end
7
62
  module ClassMethods
8
63
  # Does this type support the generation of subtypes?
9
64
  def constructable?
@@ -149,7 +204,9 @@ module Attributor
149
204
  option_value # By default, describing an option returns the hash with the specification
150
205
  end
151
206
  end
152
-
207
+ def options
208
+ {}
209
+ end
153
210
  end
154
211
  end
155
212
  end
@@ -16,6 +16,7 @@ module Attributor
16
16
  return nil if value.nil?
17
17
  return value if value.is_a?(native_type)
18
18
  return BigDecimal(value, 10) if value.is_a?(::Float)
19
+ return BigDecimal(value + '0') if value.is_a?(::String) && value.end_with?('.')
19
20
  BigDecimal(value)
20
21
  end
21
22
 
@@ -12,13 +12,9 @@ module Attributor
12
12
  # @example Collection.of(Integer)
13
13
  #
14
14
  def self.of(type)
15
+ # Favor T[] since that even caches the non-construcable types
15
16
  resolved_type = Attributor.resolve_type(type)
16
- unless resolved_type.ancestors.include?(Attributor::Type)
17
- raise Attributor::AttributorException, 'Collections can only have members that are Attributor::Types'
18
- end
19
- ::Class.new(self) do
20
- @member_type = resolved_type
21
- end
17
+ resolved_type[self]
22
18
  end
23
19
 
24
20
  @options = {}
@@ -18,6 +18,7 @@ module Attributor
18
18
  end
19
19
 
20
20
  def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
21
+ return BigDecimal(value + '0') if value.is_a?(::String) && value.end_with?('.')
21
22
  Float(value)
22
23
  rescue TypeError
23
24
  super
@@ -7,11 +7,19 @@ module Attributor
7
7
 
8
8
  # Construct a new subclass, using attribute_definition to define attributes.
9
9
  def self.construct(attribute_definition, options = {})
10
- # if we're in a subclass of Struct, but not attribute_definition is provided, we're
10
+ # if we're in a subclass of Struct, but no attribute_definition is provided, we're
11
11
  # not REALLY trying to define a new struct. more than likely Collection is calling
12
12
  # construct on us.
13
13
  unless self == Attributor::Struct || attribute_definition.nil?
14
- raise AttributorException, 'can not construct from already-constructed Struct'
14
+ location_file, location_line = attribute_definition.source_location
15
+ message = "You cannot change an already defined Struct type:\n"
16
+ message += "It seems you are trying to define attributes, using a block, on top of an existing concrete Struct type that already has been fully defined.\n"
17
+ message += "The place where you are trying to define the type is here:\n#{location_file} line #{location_line}\n#{attribute_definition.source}\n"
18
+ message += "If what you meant was to define a brand new Struct or Struct[], please make sure you pass the type in the attribute definition,\n"
19
+ message += "rather than leaving it blank.\n"
20
+ message += "Otherwise, what might be happening is that you have left out the explicit type, and the framework has inferred it from the"
21
+ message += "corresponding :reference type attribute (and hence running into the conflict of trying to redefine an existing type)."
22
+ raise AttributorException, message
15
23
  end
16
24
 
17
25
  # TODO: massage the options here to pull out only the relevant ones
@@ -1,3 +1,3 @@
1
1
  module Attributor
2
- VERSION = '6.5'.freeze
2
+ VERSION = '7.0'.freeze
3
3
  end
@@ -323,7 +323,7 @@ describe Attributor::Attribute do
323
323
  end
324
324
 
325
325
  context 'with a regexp' do
326
- let(:example) { /\w+/ }
326
+ let(:example) { Regexp.new(/\w+/) }
327
327
  let(:generated_example) { /\w+/.gen }
328
328
 
329
329
  it 'calls #gen on the regexp' do
@@ -334,7 +334,7 @@ describe Attributor::Attribute do
334
334
 
335
335
  context 'for a type with a non-String native_type' do
336
336
  let(:type) { Attributor::Integer }
337
- let(:example) { /\d{5}/ }
337
+ let(:example) { Regexp.new(/\d{5}/) }
338
338
  let(:generated_example) { /\d{5}/.gen }
339
339
 
340
340
  it 'coerces the example value properly' do
@@ -25,7 +25,7 @@ describe Attributor::DSLCompiler do
25
25
  it 'raises an error for a missing type' do
26
26
  expect do
27
27
  dsl_compiler.attribute(attribute_name)
28
- end.to raise_error(/type for attribute/)
28
+ end.to raise_error(/Type for attribute with name: name could not be determined/)
29
29
  end
30
30
 
31
31
  it 'creates an attribute given a name and type' do
@@ -38,7 +38,6 @@ describe Attributor::DSLCompiler do
38
38
  dsl_compiler.attribute(attribute_name, type, **attribute_options)
39
39
  end
40
40
  end
41
-
42
41
  context 'with a reference' do
43
42
  let(:dsl_compiler_options) { { reference: Turducken } }
44
43
 
@@ -66,16 +65,6 @@ describe Attributor::DSLCompiler do
66
65
  it 'accepts explicit nil type' do
67
66
  dsl_compiler.attribute(attribute_name, nil, **attribute_options)
68
67
  end
69
-
70
- context 'but with the attribute also specifying a reference' do
71
- let(:attribute_options) { { reference: Attributor::CSV } }
72
- let(:expected_type) { Attributor::CSV }
73
- let(:expected_options) { attribute_options }
74
- it 'attribute reference takes precedence over the compiler one (and merges no options)' do
75
- expect(attribute_options[:reference]).to_not eq(dsl_compiler_options[:reference])
76
- dsl_compiler.attribute(attribute_name, **attribute_options)
77
- end
78
- end
79
68
  end
80
69
 
81
70
  context 'for a referenced Model attribute' do
@@ -104,23 +93,305 @@ describe Attributor::DSLCompiler do
104
93
  end
105
94
  end
106
95
 
107
- context 'with a reference' do
96
+ context 'with a reference that contains the same name attribute' do
97
+ before { expect(dsl_compiler_options[:reference].attributes).to have_key(attribute_name) }
108
98
  let(:dsl_compiler_options) { { reference: Turducken } }
109
99
  let(:expected_options) do
110
100
  attribute_options.merge(reference: reference_type)
111
101
  end
112
102
 
113
- it 'sets the type of the attribute to Struct' do
103
+ it 'defaults still to Struct (or Struct[] if reference type is a collection)' do
114
104
  expect(Attributor::Attribute).to receive(:new)
115
- .with(expected_type, description: 'The turkey', reference: Turkey)
105
+ .with(expected_type, expected_options)
116
106
  dsl_compiler.attribute(attribute_name, **attribute_options, &attribute_block)
117
107
  end
108
+ end
118
109
 
119
- it 'passes the correct reference to the created attribute' do
110
+ context 'with a reference that does NOT contains the same name attribute' do
111
+ before { expect(dsl_compiler_options[:reference].attributes).to_not have_key(attribute_name) }
112
+ let(:dsl_compiler_options) { { reference: Person } }
113
+ let(:expected_options) { attribute_options }
114
+
115
+ it 'sets the type of the attribute to Struct (and doe NOT the options from the named ref attribute)' do
116
+ expect(Attributor::Attribute).to receive(:new)
117
+ .with(expected_type, expected_options)
118
+ dsl_compiler.attribute(attribute_name, **attribute_options, &attribute_block)
119
+ end
120
+
121
+ it 'same as above, picks Struct and just brings in the attr options' do
120
122
  expect(Attributor::Attribute).to receive(:new).with(expected_type, expected_options)
121
123
  dsl_compiler.attribute(attribute_name, type, **attribute_options, &attribute_block)
122
124
  end
123
125
  end
124
126
  end
125
127
  end
128
+
129
+ context 'type resolution, option inheritance for attributes with and without references' do
130
+ # Overall strategy
131
+ # 1) When no type is specified:
132
+ # 1.1) if it is a leaf (no block)
133
+ # 1.1.1) with an reference with an attr with the same name
134
+ # - type copied from reference
135
+ # - reference options are inherited as well (and can be overridden by local attribute ones)
136
+ # 1.1.2) without a ref (or the ref does not have same attribute name)
137
+ # - Fail. Cannot determine type
138
+ # 1.2) if it has a block
139
+ # 1.2.1) with an reference with an attr with the same name
140
+ # - Assume you want to define your new Struct (or Struct[]), yet pass ONLY the reference to the matching attribute
141
+ # as you might want to refine the reference attributes (or not, but if you want, you can do it more tersely)
142
+ # - the picked type will be Struct[] (if the reference attribute is a collection), or Struct otherwise
143
+ # 1.2.2) without a ref (or the ref does not have same attribute name)
144
+ # - defaulted to Struct (if you meant Collection.of(Struct) things would fail later somehow)
145
+ # - options are NOT inherited at all (This is something we should ponder more about)
146
+ # 2) When type is specified:
147
+ # 2.1) if it is a leaf (no block)
148
+ # - ignore ref if there is one (with or without matching attribute name).
149
+ # - simply use provided type, and provided options (no inheritance)
150
+ # 2.2) if it has a block
151
+ # - Same as above: use type and options provided, ignore ref if there is one (with or without matching attribute name).
152
+
153
+ let(:mytype) do
154
+ Class.new(Attributor::Struct, &myblock)
155
+ end
156
+ context 'with no explicit type specified' do
157
+ context 'without a block (if it is a leaf)' do
158
+ context 'that has a reference with an attribute with the same name' do
159
+ let(:myblock) {
160
+ Proc.new do
161
+ attributes reference: Duck do
162
+ attribute :age, required: true, min: 42
163
+ end
164
+ end
165
+ }
166
+ it 'uses type from reference' do
167
+ expect(mytype.attributes).to have_key(:age)
168
+ expect(mytype.attributes[:age].type).to eq(Duck.attributes[:age].type)
169
+ end
170
+ it 'copies over reference options and allows the attribute to override/add some' do
171
+ merged_options = Duck.attributes[:age].options.merge(required: true, min: 42)
172
+ expect(mytype.attributes[:age].options).to include(merged_options)
173
+ end
174
+ end
175
+ context 'with a reference, but that does not have a matching attribute name' do
176
+ let(:myblock) {
177
+ Proc.new do
178
+ attributes reference: Cormorant do
179
+ attribute :age
180
+ end
181
+ end
182
+ }
183
+ it 'fails resolving' do
184
+ expect{mytype.attributes}.to raise_error(/Type for attribute with name: age could not be determined./)
185
+ end
186
+ end
187
+ context 'without a reference' do
188
+ let(:myblock) {
189
+ Proc.new do
190
+ attributes do
191
+ attribute :age
192
+ end
193
+ end
194
+ }
195
+ it 'fails resolving' do
196
+ expect{mytype.attributes}.to raise_error(/Type for attribute with name: age could not be determined./)
197
+ end
198
+ end
199
+ end
200
+ context 'with block (if it is NOT a leaf)' do
201
+ context 'that has a reference with an attribute with the same name' do
202
+
203
+ context 'which is not a Collection' do
204
+ let(:myblock) {
205
+ Proc.new do
206
+ attributes reference: Duck do
207
+ attribute :age, description: 'I am redefining'do
208
+ attribute :foobar, Integer, min: 42
209
+ end
210
+ end
211
+ end
212
+ }
213
+ it 'defaults to Struct' do
214
+ expect(mytype.attributes).to have_key(:age)
215
+ age_attribute = mytype.attributes[:age]
216
+ # Resolves to Struct
217
+ expect(age_attribute.type).to be < Attributor::Struct
218
+ # does NOT brings any ref options
219
+ expect(age_attribute.options).to include(description: 'I am redefining')
220
+ expect(age_attribute.options).to include(reference: Duck.attributes[:age].type)
221
+ # And the nested attribute is correctly resolved as well, and ensures options are there
222
+ expect(age_attribute.type.attributes[:foobar].type).to eq(Attributor::Integer)
223
+ expect(age_attribute.type.attributes[:foobar].options).to eq(min: 42)
224
+ end
225
+ end
226
+ context 'which is a Collection' do
227
+ let(:myblock) {
228
+ Proc.new do
229
+ attributes reference: Cormorant do
230
+ # Babies is defined as a collection of structs
231
+ attribute :babies, description: 'I am redefining babies'do
232
+ attribute :name
233
+ attribute :height, Integer, max: 33
234
+ end
235
+ end
236
+ end
237
+ }
238
+ it 'defaults to Struct[]' do
239
+ expect(mytype.attributes).to have_key(:babies)
240
+ babies_attribute = mytype.attributes[:babies]
241
+ # Resolves to Struct[]
242
+ expect(babies_attribute.type).to be < Attributor::Collection
243
+ expect(babies_attribute.type.member_type).to be < Attributor::Struct
244
+ # does NOT brings any ref options
245
+ expect(babies_attribute.options).to include(description: 'I am redefining babies')
246
+ expect(babies_attribute.options).to include(reference: Cormorant.attributes[:babies].type.member_type)
247
+
248
+ babies_member_attribute = babies_attribute.type.member_attribute
249
+ # And the nested attribute is correctly resolved as well, and ensures options are there
250
+ expect(babies_member_attribute.type.attributes[:name].type).to eq(Cormorant.attributes[:babies].type.member_type.attributes[:name].type)
251
+ expect(babies_member_attribute.type.attributes[:name].options).to eq(Cormorant.attributes[:babies].type.member_type.attributes[:name].options)
252
+ # Can add new attributes
253
+ expect(babies_member_attribute.type.attributes[:height].type).to eq(Attributor::Integer)
254
+ expect(babies_member_attribute.type.attributes[:height].options).to eq(max: 33)
255
+ end
256
+ end
257
+
258
+ context 'explicitly passing Collections' do
259
+ context 'using the full collection as reference' do
260
+ let(:myblock) {
261
+ Proc.new do
262
+ attributes do
263
+ # Babies is defined as a collection of structs
264
+ attribute :babies, reference: Cormorant[], description: 'I am redefining babies' do
265
+
266
+ attribute :name
267
+ attribute :height, Integer, max: 33
268
+ end
269
+ end
270
+ end
271
+ }
272
+ it 'complains, as reference must not be a collection' do
273
+ expect{mytype.attributes}.to raise_error(/:reference option cannot be a collection/)
274
+ end
275
+ end
276
+
277
+ context 'overriding the reference for a redefinition of a collection' do
278
+ let(:myblock) {
279
+ Proc.new do
280
+ attributes reference: Cormorant do
281
+ # Babies is defined as a collection of structs, but we're passing a full (compatible) cormorant reference
282
+ attribute :babies, reference: Cormorant, description: 'I am redefining babies'do
283
+ attribute :name
284
+ attribute :height, Integer, max: 33
285
+ end
286
+ end
287
+ end
288
+ }
289
+ it 'still unrolls it, defaults to Struct[] and properly resolves into the member type' do
290
+ expect(mytype.attributes).to have_key(:babies)
291
+ babies_attribute = mytype.attributes[:babies]
292
+ # Resolves to Struct[]
293
+ expect(babies_attribute.type).to be < Attributor::Collection
294
+ expect(babies_attribute.type.member_type).to be < Attributor::Struct
295
+ # does NOT brings any ref options
296
+ expect(babies_attribute.options).to include(description: 'I am redefining babies')
297
+ expect(babies_attribute.options).to include(reference: Cormorant)
298
+ end
299
+ end
300
+ end
301
+ end
302
+ context 'with a reference, but that does not have a matching attribute name' do
303
+ let(:myblock) {
304
+ Proc.new do
305
+ attributes reference: Cormorant do
306
+ attribute :age, description: 'I am redefining' do
307
+ attribute :foobar, Integer, min: 42
308
+ end
309
+ end
310
+ end
311
+ }
312
+ it 'correctly defaults to Struct uses only the local options (same exact as if it had no reference)' do
313
+ expect(mytype.attributes).to have_key(:age)
314
+ age_attribute = mytype.attributes[:age]
315
+ # Resolves to Struct
316
+ expect(age_attribute.type).to be < Attributor::Struct
317
+ # does NOT brings any ref options
318
+ expect(age_attribute.options).to eq(description: 'I am redefining')
319
+ # And the nested attribute is correctly resolved as well, and ensures options are there
320
+ expect(age_attribute.type.attributes[:foobar].type).to eq(Attributor::Integer)
321
+ expect(age_attribute.type.attributes[:foobar].options).to eq(min: 42)
322
+ end
323
+ end
324
+ context 'without a reference' do
325
+ let(:myblock) {
326
+ Proc.new do
327
+ attributes do
328
+ attribute :age, description: 'I am redefining' do
329
+ attribute :foobar, Integer, min: 42
330
+ end
331
+ end
332
+ end
333
+ }
334
+ it 'correctly defaults to Struct uses only the local options' do
335
+ expect(mytype.attributes).to have_key(:age)
336
+ age_attribute = mytype.attributes[:age]
337
+ # Resolves to Struct
338
+ expect(age_attribute.type).to be < Attributor::Struct
339
+ # does NOT brings any ref options
340
+ expect(age_attribute.options).to eq(description: 'I am redefining')
341
+ # And the nested attribute is correctly resolved as well, and ensures options are there
342
+ expect(age_attribute.type.attributes[:foobar].type).to eq(Attributor::Integer)
343
+ expect(age_attribute.type.attributes[:foobar].options).to eq(min: 42)
344
+ end
345
+ end
346
+ end
347
+ end
348
+ context 'with an explicit type specified' do
349
+ context 'without a reference' do
350
+ let(:myblock) {
351
+ Proc.new do
352
+ attributes do
353
+ attribute :age, String, description: 'I am a String now'
354
+ end
355
+ end
356
+ }
357
+ it 'always uses the provided type and local options specified' do
358
+ expect(mytype.attributes).to have_key(:age)
359
+ age_attribute = mytype.attributes[:age]
360
+ # Resolves to String
361
+ expect(age_attribute.type).to eq(Attributor::String)
362
+ # copies local options
363
+ expect(age_attribute.options).to eq(description: 'I am a String now')
364
+ end
365
+ end
366
+ context 'with a reference' do
367
+ let(:myblock) {
368
+ Proc.new do
369
+ attributes reference: Duck do
370
+ attribute :age, String, description: 'I am a String now'
371
+ end
372
+ end
373
+ }
374
+ it 'always uses the provided type and local options specified (same as if it had no reference)' do
375
+ expect(mytype.attributes).to have_key(:age)
376
+ age_attribute = mytype.attributes[:age]
377
+ # Resolves to String
378
+ expect(age_attribute.type).to eq(Attributor::String)
379
+ # copies local options
380
+ expect(age_attribute.options).to eq(description: 'I am a String now')
381
+ end
382
+ end
383
+ context 'always uses the type and options specified ignoring any reference if there is one' do
384
+ end
385
+ end
386
+ context 'no reference no type for leaf' do
387
+ let(:myblock) { Proc.new do
388
+ attributes do
389
+ attribute :name
390
+ end
391
+ end }
392
+ it 'fails as it is not possible to resolve what type to use ' do
393
+ expect{mytype.attributes}.to raise_error(/Type for attribute with name: name could not be determined./)
394
+ end
395
+ end
396
+ end
126
397
  end
@@ -79,6 +79,11 @@ class Address < Attributor::Model
79
79
  attribute :name, String, example: /\w+/, null: true
80
80
  attribute :state, String, values: %w(OR CA), null: false
81
81
  attribute :person, Person, example: proc { |address, context| Person.example(context, address: address) }
82
+ attribute :substruct, reference: Address, null: false do
83
+ attribute :state, Struct do # redefine state as a Struct
84
+ attribute :foo, Integer, default: 1
85
+ end
86
+ end
82
87
  requires :name
83
88
  end
84
89
  end
data/spec/type_spec.rb CHANGED
@@ -175,4 +175,35 @@ describe Attributor::Type do
175
175
  end
176
176
  end
177
177
  end
178
+
179
+ context 'collection sugar' do
180
+ context 'non-constructable types' do
181
+ [:BigDecimal, :Boolean, :Class, :DateTime, :Date, :Float, :Integer,
182
+ :Object, :Regexp, :String, :Symbol, :Tempfile, :Time, :URI]
183
+ .map{|sym| Attributor.const_get(sym)}
184
+ .each do |type|
185
+ before { expect(type.constructable?).to be(false) }
186
+ it "DSL is equivalent to Attributor::Collection.of(#{type})" do
187
+ collection = type[]
188
+ expect(collection).to be < Attributor::Collection
189
+ expect(collection.member_type).to be type
190
+ end
191
+ it "caches the collection type for #{type}" do
192
+ expect(type[]).to be(type[])
193
+ end
194
+ end
195
+ end
196
+ context 'constructable types' do
197
+ [:Hash, :Model, :Struct, :FileUpload]
198
+ .map{|sym| Attributor.const_get(sym)}
199
+ .each do |type|
200
+ before { expect(type.constructable?).to be(true) }
201
+ it "DSL is equivalent to Attributor::Collection.of(#{type})" do
202
+ collection = type[]
203
+ expect(collection).to be < Attributor::Collection
204
+ expect(collection.member_type).to be type
205
+ end
206
+ end
207
+ end
208
+ end
178
209
  end
@@ -42,6 +42,11 @@ describe Attributor::BigDecimal do
42
42
  expect(type.load('0')).to eq(0)
43
43
  expect(type.load('100')).to eq(100)
44
44
  expect(type.load('0.1')).to eq(0.1)
45
+ expect(type.load('.2')).to eq(0.2)
46
+ expect(type.load('-.2')).to eq(-0.2)
47
+ expect(type.load('10.')).to eq(10.0)
48
+ expect(type.load('-10.')).to eq(-10.0)
49
+ expect(type.load('-0.')).to eq(-0.0)
45
50
  end
46
51
  end
47
52
  end
@@ -49,7 +54,7 @@ describe Attributor::BigDecimal do
49
54
  subject(:js){ type.as_json_schema }
50
55
  it 'adds the right attributes' do
51
56
  expect(js.keys).to include(:type, :'x-type_name')
52
- expect(js[:type]).to eq(:number)
57
+ expect(js[:type]).to eq(:string)
53
58
  expect(js[:'x-type_name']).to eq('BigDecimal')
54
59
  end
55
60
  end
@@ -51,11 +51,19 @@ describe Attributor::Float do
51
51
 
52
52
  context 'for incoming String values' do
53
53
  context 'that are valid Floats' do
54
- ['0.0', '-1.0', '1.0', '1e-10'].each do |value|
54
+ ['.2', '-.2', '0.0', '-1.0', '1.0', '1e-10'].each do |value|
55
55
  it 'decodes it if the String represents a Float' do
56
56
  expect(type.load(value)).to eq Float(value)
57
57
  end
58
58
  end
59
+ it 'decodes it if the String represents a Float and ends in .' do
60
+ expect(type.load('10.')).to eq Float('10.0')
61
+ expect(type.load('0.')).to eq Float('0.0')
62
+ end
63
+ it 'decodes it if the String represents a negative Float and ends in .' do
64
+ expect(type.load('-10.')).to eq Float('-10.0')
65
+ expect(type.load('-0.')).to eq Float('-0.0')
66
+ end
59
67
  end
60
68
 
61
69
  context 'that are valid Integers' do
@@ -3,8 +3,6 @@ require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
3
3
  describe Attributor::Model do
4
4
  subject(:chicken) { Chicken }
5
5
 
6
- # TODO: should move most of these specs to hash spec
7
-
8
6
  context 'attributes' do
9
7
  context 'with an exception from the definition block' do
10
8
  subject(:broken_model) do
@@ -46,6 +44,31 @@ describe Attributor::Model do
46
44
  end.to raise_error(Attributor::InvalidDefinition)
47
45
  end
48
46
  end
47
+
48
+ context 'redefining an inheritable attribute name, with a different type' do
49
+ it 'is allowed' do
50
+ aa = Address.attributes
51
+ # Substruct is a Struct with reference Address
52
+ expect(aa[:substruct].options).to include(:reference=>Address, null: false)
53
+ expect(aa[:substruct].type).to be <Attributor::Struct
54
+ expect(aa[:substruct].attributes.keys).to match([:state])
55
+
56
+ state_attribute = aa[:substruct].attributes[:state]
57
+ # We expec the system to have percolated the inherited reference from Address.state ... but it won't be used
58
+ # as we're overriding it with a new Struct
59
+ expect(state_attribute.options).to include(:reference=>Attributor::String)
60
+ # We want to make sure no other options are set since we're starting a new structure from scratch (so it wouldn't
61
+ # major sense to inherit any other options? )
62
+ expect(state_attribute.options.keys).to_not include(:default, :values)
63
+ expect(state_attribute.type).to be < Attributor::Struct
64
+
65
+ # Foo gets its proper integer attribute as a leaf, and properly gets the default option as well
66
+ foo_attribute = state_attribute.attributes[:foo]
67
+ expect(foo_attribute.options).to include(default: 1)
68
+ expect(foo_attribute.attributes).to be_nil
69
+ expect(foo_attribute.type).to eq Attributor::Integer
70
+ end
71
+ end
49
72
  end
50
73
 
51
74
  context 'class methods' do
@@ -141,6 +164,18 @@ describe Attributor::Model do
141
164
  it { should have_key :age }
142
165
  it { should have_key :email }
143
166
  end
167
+
168
+ context 'other' do
169
+ subject(:attributes) { Address.attributes }
170
+ it 'works' do
171
+ expect(subject).to have_key(:substruct)
172
+ expect(subject[:substruct].type).to be < Attributor::Struct
173
+ expect(subject[:substruct].attributes).to have_key(:state)
174
+ expect(subject[:substruct].attributes[:state].type).to be < Attributor::Struct
175
+ expect(subject[:substruct].attributes[:state].attributes).to have_key(:foo)
176
+ expect(subject[:substruct].attributes[:state].attributes[:foo].type).to eq Attributor::Integer
177
+ end
178
+ end
144
179
  end
145
180
 
146
181
  context '.load' do
@@ -473,7 +508,7 @@ describe Attributor::Model do
473
508
  context 'for collections of models' do
474
509
  let(:attributes_block) do
475
510
  proc do
476
- attribute :neighbors, required: true do
511
+ attribute :neighbors, Attributor::Collection.of(Attributor::Struct), required: true do
477
512
  attribute :name, required: true
478
513
  attribute :age, Integer
479
514
  end
@@ -482,8 +517,9 @@ describe Attributor::Model do
482
517
  subject(:struct) { Attributor::Struct.construct(attributes_block, reference: Cormorant) }
483
518
 
484
519
  it 'supports defining sub-attributes using the proper reference' do
520
+ # in construct, we're passing the reference, so it will use it to define the inner name/age attributes
485
521
  expect(struct.attributes[:neighbors].options[:required]).to be true
486
- expect(struct.attributes[:neighbors].options[:null]).to be false
522
+ expect(struct.attributes[:neighbors].options).to_not have_key(:null) # Not inherited from reference
487
523
  expect(struct.attributes[:neighbors].type.member_attribute.type.attributes.keys).to match_array [:name, :age]
488
524
 
489
525
  name_options = struct.attributes[:neighbors].type.member_attribute.type.attributes[:name].options
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attributor
3
3
  version: !ruby/object:Gem::Version
4
- version: '6.5'
4
+ version: '7.0'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep M. Blanquer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-01-19 00:00:00.000000000 Z
12
+ date: 2023-05-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: hashie
@@ -407,7 +407,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
407
407
  - !ruby/object:Gem::Version
408
408
  version: '0'
409
409
  requirements: []
410
- rubygems_version: 3.1.2
410
+ rubygems_version: 3.3.7
411
411
  signing_key:
412
412
  specification_version: 4
413
413
  summary: A powerful attribute and type management library for Ruby