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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +30 -0
  3. data/.travis.yml +6 -4
  4. data/CHANGELOG.md +6 -1
  5. data/Gemfile +1 -1
  6. data/Guardfile +14 -8
  7. data/Rakefile +4 -5
  8. data/attributor.gemspec +34 -29
  9. data/lib/attributor.rb +23 -29
  10. data/lib/attributor/attribute.rb +108 -127
  11. data/lib/attributor/attribute_resolver.rb +12 -26
  12. data/lib/attributor/dsl_compiler.rb +17 -21
  13. data/lib/attributor/dumpable.rb +1 -2
  14. data/lib/attributor/example_mixin.rb +5 -8
  15. data/lib/attributor/exceptions.rb +5 -6
  16. data/lib/attributor/extensions/randexp.rb +3 -5
  17. data/lib/attributor/extras/field_selector.rb +4 -4
  18. data/lib/attributor/extras/field_selector/transformer.rb +6 -7
  19. data/lib/attributor/families/numeric.rb +0 -2
  20. data/lib/attributor/families/temporal.rb +1 -4
  21. data/lib/attributor/hash_dsl_compiler.rb +22 -25
  22. data/lib/attributor/type.rb +24 -32
  23. data/lib/attributor/types/bigdecimal.rb +7 -14
  24. data/lib/attributor/types/boolean.rb +5 -8
  25. data/lib/attributor/types/class.rb +9 -10
  26. data/lib/attributor/types/collection.rb +34 -44
  27. data/lib/attributor/types/container.rb +9 -15
  28. data/lib/attributor/types/csv.rb +7 -10
  29. data/lib/attributor/types/date.rb +20 -25
  30. data/lib/attributor/types/date_time.rb +7 -14
  31. data/lib/attributor/types/float.rb +4 -6
  32. data/lib/attributor/types/hash.rb +171 -196
  33. data/lib/attributor/types/ids.rb +2 -6
  34. data/lib/attributor/types/integer.rb +12 -17
  35. data/lib/attributor/types/model.rb +39 -48
  36. data/lib/attributor/types/object.rb +2 -4
  37. data/lib/attributor/types/polymorphic.rb +118 -0
  38. data/lib/attributor/types/regexp.rb +4 -5
  39. data/lib/attributor/types/string.rb +6 -7
  40. data/lib/attributor/types/struct.rb +8 -15
  41. data/lib/attributor/types/symbol.rb +3 -6
  42. data/lib/attributor/types/tempfile.rb +5 -6
  43. data/lib/attributor/types/time.rb +11 -11
  44. data/lib/attributor/types/uri.rb +9 -10
  45. data/lib/attributor/version.rb +1 -1
  46. data/spec/attribute_resolver_spec.rb +57 -78
  47. data/spec/attribute_spec.rb +174 -216
  48. data/spec/attributor_spec.rb +11 -15
  49. data/spec/dsl_compiler_spec.rb +19 -33
  50. data/spec/dumpable_spec.rb +6 -7
  51. data/spec/extras/field_selector/field_selector_spec.rb +1 -1
  52. data/spec/families_spec.rb +1 -3
  53. data/spec/hash_dsl_compiler_spec.rb +65 -74
  54. data/spec/spec_helper.rb +9 -3
  55. data/spec/support/hashes.rb +2 -3
  56. data/spec/support/models.rb +30 -36
  57. data/spec/support/polymorphics.rb +10 -0
  58. data/spec/type_spec.rb +38 -61
  59. data/spec/types/bigdecimal_spec.rb +11 -15
  60. data/spec/types/boolean_spec.rb +12 -39
  61. data/spec/types/class_spec.rb +10 -11
  62. data/spec/types/collection_spec.rb +72 -81
  63. data/spec/types/container_spec.rb +22 -26
  64. data/spec/types/csv_spec.rb +15 -16
  65. data/spec/types/date_spec.rb +16 -33
  66. data/spec/types/date_time_spec.rb +16 -33
  67. data/spec/types/file_upload_spec.rb +1 -2
  68. data/spec/types/float_spec.rb +7 -14
  69. data/spec/types/hash_spec.rb +285 -289
  70. data/spec/types/ids_spec.rb +5 -7
  71. data/spec/types/integer_spec.rb +37 -46
  72. data/spec/types/model_spec.rb +111 -128
  73. data/spec/types/polymorphic_spec.rb +134 -0
  74. data/spec/types/regexp_spec.rb +4 -7
  75. data/spec/types/string_spec.rb +17 -21
  76. data/spec/types/struct_spec.rb +40 -47
  77. data/spec/types/tempfile_spec.rb +1 -2
  78. data/spec/types/temporal_spec.rb +9 -0
  79. data/spec/types/time_spec.rb +16 -32
  80. data/spec/types/type_spec.rb +15 -0
  81. data/spec/types/uri_spec.rb +6 -7
  82. metadata +77 -25
@@ -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(type)
23
- raise "Invalid definition of Ids type. Defining Ids.of(type) is not allowed, you probably meant to do Ids.for(type) instead"
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
- return ::Integer
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.has_key?(:min) && options.has_key?(:max)
40
+ if options.key?(:min) && options.key?(:max)
44
41
  # Both :max and :min must be integers
45
- raise AttributorException.new("Invalid range: [#{options[:min].inspect}, #{options[:max].inspect}]") if !options[:min].is_a?(::Integer) || !options[:max].is_a?(::Integer)
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.new("Invalid range: [#{options[:min].inspect}, #{options[:max].inspect}]") if options[:max] < options[:min]
49
- elsif !options.has_key?(:min) && options.has_key?(:max)
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.new("Invalid range: [, #{options[:max].inspect}]") if !options[:max].is_a?(::Integer)
52
- elsif options.has_key?(:min) && !options.has_key?(:max)
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.new("Invalid range: [#{options[:min].inspect},]") if !options[:min].is_a?(::Integer)
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
- undef :timeout
6
- undef :format
7
- undef :test rescue nil
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 = self.key_type
31
- ka = self.key_attribute
32
+ k = key_type
33
+ ka = key_attribute
32
34
 
33
- v = self.value_type
34
- va = self.value_attribute
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
- self.define_reader(name)
58
- self.define_writer(name)
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 = ["assignment","of(#{name})"].freeze
72
+ context = ['assignment', "of(#{name})"].freeze
72
73
  module_eval do
73
- define_method(name.to_s + "=") do |value|
74
- self.set(name, value, context: context)
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.kind_of?(::Symbol)
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 ||= ["#{self.name || 'Struct'}-#{rand(10000000)}"]
100
+ def self.example(context = nil, **values)
101
+ context ||= ["#{name || 'Struct'}-#{rand(10_000_000)}"]
101
102
  context = Array(context)
102
103
 
103
- if self.keys.any?
104
- result = self.new
104
+ if keys.any?
105
+ result = new
105
106
  result.extend(ExampleMixin)
106
107
 
107
- result.lazy_attributes = self.example_contents(context, result, values)
108
+ result.lazy_attributes = example_contents(context, result, values)
108
109
  else
109
- result = self.new
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 = self.__send__(sub_attribute_name)
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.push(*sub_attribute.validate(value, sub_context))
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.push(*validation_errors) unless validation_errors.empty?
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.has_key?(attribute_name.to_sym)
174
+ if self.class.attributes.key?(attribute_name.to_sym)
181
175
  self.class.define_accessors(attribute_name)
182
- return self.__send__(name, *args)
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
- self.attributes.each_with_object({}) do |(name, value), hash|
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
- return ::BasicObject
10
+ ::BasicObject
12
11
  end
13
12
 
14
- def self.example(context=nil, options:{})
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
- return ::Regexp
8
+ ::Regexp
9
9
  end
10
10
 
11
- def self.load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
12
- unless value.kind_of?(::String) || value.nil?
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(context=nil, options:{})
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