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 +4 -4
- data/CHANGELOG.md +11 -0
- data/lib/attributor/dsl_compiler.rb +71 -28
- data/lib/attributor/type.rb +58 -1
- data/lib/attributor/types/bigdecimal.rb +1 -0
- data/lib/attributor/types/collection.rb +2 -6
- data/lib/attributor/types/float.rb +1 -0
- data/lib/attributor/types/struct.rb +10 -2
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_spec.rb +2 -2
- data/spec/dsl_compiler_spec.rb +287 -16
- data/spec/support/models.rb +5 -0
- data/spec/type_spec.rb +31 -0
- data/spec/types/bigdecimal_spec.rb +6 -1
- data/spec/types/float_spec.rb +9 -1
- data/spec/types/model_spec.rb +40 -4
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b5fc3c6809e4a0e21b21752683196ce018beafbb7d8b615f2217abbcb55b3c3
|
4
|
+
data.tar.gz: 55bba33e0b5ee71c581a029cfd3045c6f71ce5d1f578b2654d99f919626dcbc8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/attributor/type.rb
CHANGED
@@ -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
|
@@ -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
|
-
|
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 = {}
|
@@ -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
|
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
|
-
|
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
|
data/lib/attributor/version.rb
CHANGED
data/spec/attribute_spec.rb
CHANGED
@@ -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
|
data/spec/dsl_compiler_spec.rb
CHANGED
@@ -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(/
|
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 '
|
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,
|
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
|
-
|
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
|
data/spec/support/models.rb
CHANGED
@@ -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(:
|
57
|
+
expect(js[:type]).to eq(:string)
|
53
58
|
expect(js[:'x-type_name']).to eq('BigDecimal')
|
54
59
|
end
|
55
60
|
end
|
data/spec/types/float_spec.rb
CHANGED
@@ -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
|
data/spec/types/model_spec.rb
CHANGED
@@ -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
|
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: '
|
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-
|
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.
|
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
|