attributor 5.0.2 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
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