attributor 5.0.2 → 5.1.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/.rubocop.yml +30 -0
- data/.travis.yml +6 -4
- data/CHANGELOG.md +6 -1
- data/Gemfile +1 -1
- data/Guardfile +14 -8
- data/Rakefile +4 -5
- data/attributor.gemspec +34 -29
- data/lib/attributor.rb +23 -29
- data/lib/attributor/attribute.rb +108 -127
- data/lib/attributor/attribute_resolver.rb +12 -26
- data/lib/attributor/dsl_compiler.rb +17 -21
- data/lib/attributor/dumpable.rb +1 -2
- data/lib/attributor/example_mixin.rb +5 -8
- data/lib/attributor/exceptions.rb +5 -6
- data/lib/attributor/extensions/randexp.rb +3 -5
- data/lib/attributor/extras/field_selector.rb +4 -4
- data/lib/attributor/extras/field_selector/transformer.rb +6 -7
- data/lib/attributor/families/numeric.rb +0 -2
- data/lib/attributor/families/temporal.rb +1 -4
- data/lib/attributor/hash_dsl_compiler.rb +22 -25
- data/lib/attributor/type.rb +24 -32
- data/lib/attributor/types/bigdecimal.rb +7 -14
- data/lib/attributor/types/boolean.rb +5 -8
- data/lib/attributor/types/class.rb +9 -10
- data/lib/attributor/types/collection.rb +34 -44
- data/lib/attributor/types/container.rb +9 -15
- data/lib/attributor/types/csv.rb +7 -10
- data/lib/attributor/types/date.rb +20 -25
- data/lib/attributor/types/date_time.rb +7 -14
- data/lib/attributor/types/float.rb +4 -6
- data/lib/attributor/types/hash.rb +171 -196
- data/lib/attributor/types/ids.rb +2 -6
- data/lib/attributor/types/integer.rb +12 -17
- data/lib/attributor/types/model.rb +39 -48
- data/lib/attributor/types/object.rb +2 -4
- data/lib/attributor/types/polymorphic.rb +118 -0
- data/lib/attributor/types/regexp.rb +4 -5
- data/lib/attributor/types/string.rb +6 -7
- data/lib/attributor/types/struct.rb +8 -15
- data/lib/attributor/types/symbol.rb +3 -6
- data/lib/attributor/types/tempfile.rb +5 -6
- data/lib/attributor/types/time.rb +11 -11
- data/lib/attributor/types/uri.rb +9 -10
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_resolver_spec.rb +57 -78
- data/spec/attribute_spec.rb +174 -216
- data/spec/attributor_spec.rb +11 -15
- data/spec/dsl_compiler_spec.rb +19 -33
- data/spec/dumpable_spec.rb +6 -7
- data/spec/extras/field_selector/field_selector_spec.rb +1 -1
- data/spec/families_spec.rb +1 -3
- data/spec/hash_dsl_compiler_spec.rb +65 -74
- data/spec/spec_helper.rb +9 -3
- data/spec/support/hashes.rb +2 -3
- data/spec/support/models.rb +30 -36
- data/spec/support/polymorphics.rb +10 -0
- data/spec/type_spec.rb +38 -61
- data/spec/types/bigdecimal_spec.rb +11 -15
- data/spec/types/boolean_spec.rb +12 -39
- data/spec/types/class_spec.rb +10 -11
- data/spec/types/collection_spec.rb +72 -81
- data/spec/types/container_spec.rb +22 -26
- data/spec/types/csv_spec.rb +15 -16
- data/spec/types/date_spec.rb +16 -33
- data/spec/types/date_time_spec.rb +16 -33
- data/spec/types/file_upload_spec.rb +1 -2
- data/spec/types/float_spec.rb +7 -14
- data/spec/types/hash_spec.rb +285 -289
- data/spec/types/ids_spec.rb +5 -7
- data/spec/types/integer_spec.rb +37 -46
- data/spec/types/model_spec.rb +111 -128
- data/spec/types/polymorphic_spec.rb +134 -0
- data/spec/types/regexp_spec.rb +4 -7
- data/spec/types/string_spec.rb +17 -21
- data/spec/types/struct_spec.rb +40 -47
- data/spec/types/tempfile_spec.rb +1 -2
- data/spec/types/temporal_spec.rb +9 -0
- data/spec/types/time_spec.rb +16 -32
- data/spec/types/type_spec.rb +15 -0
- data/spec/types/uri_spec.rb +6 -7
- metadata +77 -25
data/lib/attributor/types/ids.rb
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
module Attributor
|
2
|
-
|
3
2
|
class Ids < CSV
|
4
|
-
|
5
|
-
|
6
3
|
def self.for(type)
|
7
4
|
identity_name = type.options.fetch(:identity) do
|
8
5
|
raise AttributorException, "no identity found for #{type.name}"
|
@@ -16,11 +13,10 @@ module Attributor
|
|
16
13
|
@member_attribute = identity_attribute
|
17
14
|
@member_type = identity_attribute.type
|
18
15
|
end
|
19
|
-
|
20
16
|
end
|
21
17
|
|
22
|
-
def self.of(
|
23
|
-
raise
|
18
|
+
def self.of(_type)
|
19
|
+
raise 'Invalid definition of Ids type. Defining Ids.of(type) is not allowed, you probably meant to do Ids.for(type) instead'
|
24
20
|
end
|
25
21
|
end
|
26
22
|
end
|
@@ -1,17 +1,14 @@
|
|
1
1
|
|
2
2
|
|
3
3
|
module Attributor
|
4
|
-
|
5
4
|
class Integer < Numeric
|
6
|
-
|
7
|
-
EXAMPLE_RANGE = 1000.freeze
|
5
|
+
EXAMPLE_RANGE = 1000
|
8
6
|
|
9
7
|
def self.native_type
|
10
|
-
|
8
|
+
::Integer
|
11
9
|
end
|
12
10
|
|
13
|
-
|
14
|
-
def self.example(context=nil, options: {})
|
11
|
+
def self.example(_context = nil, options: {})
|
15
12
|
validate_options(options)
|
16
13
|
|
17
14
|
# Set default values
|
@@ -30,33 +27,31 @@ module Attributor
|
|
30
27
|
end
|
31
28
|
|
32
29
|
# Generate random number on interval [min,max]
|
33
|
-
rand(max-min+1) + min
|
30
|
+
rand(max - min + 1) + min
|
34
31
|
end
|
35
32
|
|
36
|
-
def self.load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
33
|
+
def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
37
34
|
Integer(value)
|
38
35
|
rescue TypeError
|
39
36
|
super
|
40
37
|
end
|
41
38
|
|
42
39
|
def self.validate_options(options)
|
43
|
-
if options.
|
40
|
+
if options.key?(:min) && options.key?(:max)
|
44
41
|
# Both :max and :min must be integers
|
45
|
-
raise AttributorException
|
42
|
+
raise AttributorException, "Invalid range: [#{options[:min].inspect}, #{options[:max].inspect}]" if !options[:min].is_a?(::Integer) || !options[:max].is_a?(::Integer)
|
46
43
|
|
47
44
|
# :max cannot be less than :min
|
48
|
-
raise AttributorException
|
49
|
-
elsif !options.
|
45
|
+
raise AttributorException, "Invalid range: [#{options[:min].inspect}, #{options[:max].inspect}]" if options[:max] < options[:min]
|
46
|
+
elsif !options.key?(:min) && options.key?(:max)
|
50
47
|
# :max must be an integer
|
51
|
-
raise AttributorException
|
52
|
-
elsif options.
|
48
|
+
raise AttributorException, "Invalid range: [, #{options[:max].inspect}]" unless options[:max].is_a?(::Integer)
|
49
|
+
elsif options.key?(:min) && !options.key?(:max)
|
53
50
|
# :min must be an integer
|
54
|
-
raise AttributorException
|
55
|
-
else
|
51
|
+
raise AttributorException, "Invalid range: [#{options[:min].inspect},]" unless options[:min].is_a?(::Integer)
|
56
52
|
# Neither :min nor :max were given, noop
|
57
53
|
end
|
58
54
|
true
|
59
55
|
end
|
60
|
-
|
61
56
|
end
|
62
57
|
end
|
@@ -1,10 +1,13 @@
|
|
1
1
|
module Attributor
|
2
2
|
class Model < Hash
|
3
|
-
|
4
3
|
# FIXME: this is not the way to fix this. Really we should add finalize! to Models.
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
begin
|
5
|
+
undef :timeout
|
6
|
+
undef :format
|
7
|
+
undef :test
|
8
|
+
rescue
|
9
|
+
nil
|
10
|
+
end
|
8
11
|
|
9
12
|
if RUBY_ENGINE =~ /^jruby/
|
10
13
|
# We are "forced" to require it here (in case hasn't been yet) to make sure the added methods have been applied
|
@@ -25,13 +28,12 @@ module Attributor
|
|
25
28
|
@key_attribute = Attribute.new(@key_type)
|
26
29
|
@value_attribute = Attribute.new(@value_type)
|
27
30
|
|
28
|
-
|
29
31
|
def self.inherited(klass)
|
30
|
-
k =
|
31
|
-
ka =
|
32
|
+
k = key_type
|
33
|
+
ka = key_attribute
|
32
34
|
|
33
|
-
v =
|
34
|
-
va =
|
35
|
+
v = value_type
|
36
|
+
va = value_attribute
|
35
37
|
|
36
38
|
klass.instance_eval do
|
37
39
|
@saved_blocks = []
|
@@ -54,8 +56,8 @@ module Attributor
|
|
54
56
|
#
|
55
57
|
def self.define_accessors(name)
|
56
58
|
name = name.to_sym
|
57
|
-
|
58
|
-
|
59
|
+
define_reader(name)
|
60
|
+
define_writer(name)
|
59
61
|
end
|
60
62
|
|
61
63
|
def self.define_reader(name)
|
@@ -66,12 +68,11 @@ module Attributor
|
|
66
68
|
RUBY
|
67
69
|
end
|
68
70
|
|
69
|
-
|
70
71
|
def self.define_writer(name)
|
71
|
-
context = [
|
72
|
+
context = ['assignment', "of(#{name})"].freeze
|
72
73
|
module_eval do
|
73
|
-
define_method(name.to_s +
|
74
|
-
|
74
|
+
define_method(name.to_s + '=') do |value|
|
75
|
+
set(name, value, context: context)
|
75
76
|
end
|
76
77
|
end
|
77
78
|
end
|
@@ -79,12 +80,12 @@ module Attributor
|
|
79
80
|
def self.check_option!(name, value)
|
80
81
|
case name
|
81
82
|
when :identity
|
82
|
-
raise AttributorException, "Invalid identity type #{value.inspect}" unless value.
|
83
|
-
:ok # FIXME ... actually do something smart, that doesn't break lazy attribute creation
|
83
|
+
raise AttributorException, "Invalid identity type #{value.inspect}" unless value.is_a?(::Symbol)
|
84
|
+
:ok # FIXME: ... actually do something smart, that doesn't break lazy attribute creation
|
84
85
|
when :reference
|
85
|
-
:ok # FIXME ... actually do something smart
|
86
|
+
:ok # FIXME: ... actually do something smart
|
86
87
|
when :dsl_compiler
|
87
|
-
:ok # FIXME ... actually do something smart
|
88
|
+
:ok # FIXME: ... actually do something smart
|
88
89
|
when :dsl_compiler_options
|
89
90
|
:ok
|
90
91
|
else
|
@@ -96,17 +97,17 @@ module Attributor
|
|
96
97
|
context + [subname]
|
97
98
|
end
|
98
99
|
|
99
|
-
def self.example(context=nil, **values)
|
100
|
-
context ||= ["#{
|
100
|
+
def self.example(context = nil, **values)
|
101
|
+
context ||= ["#{name || 'Struct'}-#{rand(10_000_000)}"]
|
101
102
|
context = Array(context)
|
102
103
|
|
103
|
-
if
|
104
|
-
result =
|
104
|
+
if keys.any?
|
105
|
+
result = new
|
105
106
|
result.extend(ExampleMixin)
|
106
107
|
|
107
|
-
result.lazy_attributes =
|
108
|
+
result.lazy_attributes = example_contents(context, result, values)
|
108
109
|
else
|
109
|
-
result =
|
110
|
+
result = new
|
110
111
|
end
|
111
112
|
result
|
112
113
|
end
|
@@ -120,13 +121,11 @@ module Attributor
|
|
120
121
|
end
|
121
122
|
end
|
122
123
|
|
123
|
-
|
124
124
|
# TODO: memoize validation results here, but only after rejiggering how we store the context.
|
125
125
|
# Two calls to validate() with different contexts should return get the same errors,
|
126
126
|
# but with their respective contexts.
|
127
|
-
def validate(context=Attributor::DEFAULT_ROOT_CONTEXT)
|
128
|
-
|
129
|
-
raise AttributorException, "validation conflict" if @validating
|
127
|
+
def validate(context = Attributor::DEFAULT_ROOT_CONTEXT)
|
128
|
+
raise AttributorException, 'validation conflict' if @validating
|
130
129
|
@validating = true
|
131
130
|
|
132
131
|
context = [context] if context.is_a? ::String
|
@@ -134,22 +133,20 @@ module Attributor
|
|
134
133
|
errors = []
|
135
134
|
|
136
135
|
self.class.attributes.each do |sub_attribute_name, sub_attribute|
|
137
|
-
sub_context = self.class.generate_subcontext(context,sub_attribute_name)
|
136
|
+
sub_context = self.class.generate_subcontext(context, sub_attribute_name)
|
138
137
|
|
139
|
-
value =
|
140
|
-
unless value.nil?
|
141
|
-
keys_with_values << sub_attribute_name
|
142
|
-
end
|
138
|
+
value = __send__(sub_attribute_name)
|
139
|
+
keys_with_values << sub_attribute_name unless value.nil?
|
143
140
|
|
144
141
|
if value.respond_to?(:validating) # really, it's a thing with sub-attributes
|
145
142
|
next if value.validating
|
146
143
|
end
|
147
144
|
|
148
|
-
errors.
|
145
|
+
errors.concat sub_attribute.validate(value, sub_context)
|
149
146
|
end
|
150
147
|
self.class.requirements.each do |req|
|
151
148
|
validation_errors = req.validate(keys_with_values, context)
|
152
|
-
errors.
|
149
|
+
errors.concat(validation_errors) unless validation_errors.empty?
|
153
150
|
end
|
154
151
|
|
155
152
|
errors
|
@@ -157,13 +154,11 @@ module Attributor
|
|
157
154
|
@validating = false
|
158
155
|
end
|
159
156
|
|
160
|
-
|
161
157
|
def attributes
|
162
158
|
@contents
|
163
159
|
end
|
164
160
|
|
165
|
-
|
166
|
-
def respond_to_missing?(name,*)
|
161
|
+
def respond_to_missing?(name, *)
|
167
162
|
attribute_name = name.to_s
|
168
163
|
attribute_name.chomp!('=')
|
169
164
|
|
@@ -172,25 +167,23 @@ module Attributor
|
|
172
167
|
super
|
173
168
|
end
|
174
169
|
|
175
|
-
|
176
170
|
def method_missing(name, *args)
|
177
171
|
attribute_name = name.to_s
|
178
172
|
attribute_name.chomp!('=')
|
179
173
|
|
180
|
-
if self.class.attributes.
|
174
|
+
if self.class.attributes.key?(attribute_name.to_sym)
|
181
175
|
self.class.define_accessors(attribute_name)
|
182
|
-
return
|
176
|
+
return __send__(name, *args)
|
183
177
|
end
|
184
178
|
|
185
179
|
super
|
186
180
|
end
|
187
181
|
|
188
|
-
|
189
|
-
def dump(context: Attributor::DEFAULT_ROOT_CONTEXT, **opts)
|
182
|
+
def dump(context: Attributor::DEFAULT_ROOT_CONTEXT, **_opts)
|
190
183
|
return CIRCULAR_REFERENCE_MARKER if @dumping
|
191
184
|
@dumping = true
|
192
185
|
|
193
|
-
|
186
|
+
attributes.each_with_object({}) do |(name, value), hash|
|
194
187
|
attribute = self.class.attributes[name]
|
195
188
|
|
196
189
|
# skip dumping undefined attributes
|
@@ -199,12 +192,10 @@ module Attributor
|
|
199
192
|
next
|
200
193
|
end
|
201
194
|
|
202
|
-
hash[name.to_sym] = attribute.dump(value, context: context + [name]
|
195
|
+
hash[name.to_sym] = attribute.dump(value, context: context + [name])
|
203
196
|
end
|
204
197
|
ensure
|
205
198
|
@dumping = false
|
206
199
|
end
|
207
|
-
|
208
200
|
end
|
209
|
-
|
210
201
|
end
|
@@ -3,17 +3,15 @@
|
|
3
3
|
require_relative '../exceptions'
|
4
4
|
|
5
5
|
module Attributor
|
6
|
-
|
7
6
|
class Object
|
8
7
|
include Type
|
9
8
|
|
10
9
|
def self.native_type
|
11
|
-
|
10
|
+
::BasicObject
|
12
11
|
end
|
13
12
|
|
14
|
-
def self.example(
|
13
|
+
def self.example(_context = nil, options: {})
|
15
14
|
'An Object'
|
16
15
|
end
|
17
|
-
|
18
16
|
end
|
19
17
|
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
require_relative '../exceptions'
|
4
|
+
|
5
|
+
module Attributor
|
6
|
+
class Polymorphic
|
7
|
+
include Type
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_reader :discriminator
|
11
|
+
attr_reader :types
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.on(discriminator)
|
15
|
+
::Class.new(self) do
|
16
|
+
@discriminator = discriminator
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.given(value, type)
|
21
|
+
@types[value] = type
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.inherited(klass)
|
25
|
+
klass.instance_eval do
|
26
|
+
@types = {}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.example(context = nil, **values)
|
31
|
+
types.values.pick.example(context, **values)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.valid_type?(value)
|
35
|
+
self.types.values.include?(value.class)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.constructable?
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.native_type
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.construct(constructor_block, **_options)
|
47
|
+
return self if constructor_block.nil?
|
48
|
+
|
49
|
+
self.instance_eval(&constructor_block)
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
54
|
+
return nil if value.nil?
|
55
|
+
|
56
|
+
return value if self.types.values.include?(value.class)
|
57
|
+
|
58
|
+
parsed_value = self.parse(value, context)
|
59
|
+
|
60
|
+
discriminator_value = discriminator_value_for(parsed_value)
|
61
|
+
|
62
|
+
type = self.types.fetch(discriminator_value) do
|
63
|
+
raise LoadError, "invalid value for discriminator: #{discriminator_value}"
|
64
|
+
end
|
65
|
+
type.load(parsed_value)
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.discriminator_value_for(parsed_value)
|
69
|
+
return parsed_value[self.discriminator] if parsed_value.key?(self.discriminator)
|
70
|
+
|
71
|
+
value = case self.discriminator
|
72
|
+
when ::String
|
73
|
+
parsed_value[self.discriminator.to_sym]
|
74
|
+
when ::Symbol
|
75
|
+
parsed_value[self.discriminator.to_s]
|
76
|
+
end
|
77
|
+
|
78
|
+
return value if value
|
79
|
+
|
80
|
+
raise LoadError, "can't find key #{self.discriminator.inspect} in #{parsed_value.inspect}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.dump(value, **opts)
|
84
|
+
if (loaded = load(value))
|
85
|
+
loaded.dump(**opts)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.parse(value, context)
|
90
|
+
if value.nil?
|
91
|
+
{}
|
92
|
+
elsif value.is_a?(Attributor::Hash)
|
93
|
+
value.contents
|
94
|
+
elsif value.is_a?(::Hash)
|
95
|
+
value
|
96
|
+
elsif value.is_a?(::String)
|
97
|
+
decode_json(value, context)
|
98
|
+
elsif value.respond_to?(:to_hash)
|
99
|
+
value.to_hash
|
100
|
+
else
|
101
|
+
raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.describe(shallow = false, example: nil)
|
106
|
+
super.merge(
|
107
|
+
discriminator: self.discriminator,
|
108
|
+
types: self.describe_types
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.describe_types
|
113
|
+
self.types.each_with_object({}) do |(key, value), description|
|
114
|
+
description[key] = { type: value.describe(true) }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -5,11 +5,11 @@ module Attributor
|
|
5
5
|
include Type
|
6
6
|
|
7
7
|
def self.native_type
|
8
|
-
|
8
|
+
::Regexp
|
9
9
|
end
|
10
10
|
|
11
|
-
def self.load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
12
|
-
unless value.
|
11
|
+
def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
12
|
+
unless value.is_a?(::String) || value.nil?
|
13
13
|
raise IncompatibleTypeError, context: context, value_type: value.class, type: self
|
14
14
|
end
|
15
15
|
|
@@ -18,13 +18,12 @@ module Attributor
|
|
18
18
|
super
|
19
19
|
end
|
20
20
|
|
21
|
-
def self.example(
|
21
|
+
def self.example(_context = nil, options: {})
|
22
22
|
::Regexp.new(/^pattern\d{0,3}$/).to_s
|
23
23
|
end
|
24
24
|
|
25
25
|
def self.family
|
26
26
|
'string'
|
27
27
|
end
|
28
|
-
|
29
28
|
end
|
30
29
|
end
|