attributor 2.2.1 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 76667cd055dc19dfec12646bbdbf1514c90f71d9
4
- data.tar.gz: 828783ff925e1a52b46fba2b2bd63c658f0cb6b6
3
+ metadata.gz: eaf11f67f19da1559c3abdbcebed02193ac9e5e3
4
+ data.tar.gz: 6ba6a1dbf45d4e2d43450036651f0d4c0fde9829
5
5
  SHA512:
6
- metadata.gz: 8aa70abf93838a54751afb73020cd949ce7aa829fd6d9c679e84d8326d5b93fb2a06275b121b59a7495159b9ae40d5fdfce3d5c81d7b17c0a170eba42d9b3b66
7
- data.tar.gz: 862d52a9800f01e7dd853b2b331a1e9edda95f26634ac50907e658b4cca510888f034fc8e0c69d8bca60c4b857ae3be8ac73976476a6c3e09e2c8ad6dfaa4571
6
+ metadata.gz: 410e51a836135f981bee0cc40aabb254c526ade79aafa224a96881dac78b8c1986ff088b33f89e80da8a0899ebc086a9ff589768130adf78ca3cb84ae7a8b350
7
+ data.tar.gz: 22d78b19144db5a3c19b778a6c9c32e66ce819f86d3a71dc443462ab0f4a8911345cefe193995b43897c8fd30224de0a67a7e000fa79a5b18a66586e70adbb67
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ doc
15
15
  .DS_Store
16
16
 
17
17
  Gemfile.lock
18
+ pkg
@@ -1,9 +1,17 @@
1
1
  Attributor Changelog
2
2
  ============================
3
3
 
4
- next
4
+ 2.3.0
5
5
  ------
6
6
 
7
+ * Added `recurse` option to `Type.load` that is used by `Model` and `Hash` to force the loading of values (specifically, so that default values are assigned) even if the loaded value is `nil`.
8
+ * Fix `Attributor::CSV` to dump `String` values and generate `String` examples.
9
+ * Default values of `false` now work correctly.
10
+ * Added `BigDecimal`, `Date` and `Time` types
11
+ * `DateTime.load` now raises `CoercionError` (instead of returning `nil`) if given values that can not coerced properly.
12
+ * `Hash.dump` now first calls `Hash.load`, and correctly uses defined value types for dumping.
13
+ * Added `Hash#get`, for retrieving keys using the same logic the `case_insensitive_load` and `allow_extra` with defined `extra` key.
14
+
7
15
 
8
16
  2.2.1
9
17
  ------
@@ -22,7 +30,7 @@ next
22
30
  * `:case_insensitive_load` for string-keyed hashes. This allows loading hashes with keys that do not exactly match the case defined in the hash.
23
31
  * Added `:allow_extras` option to allow handling of undefined keys when loading.
24
32
  * Added `Hash#set` to encapsulate the above options and attribute loading.
25
- * Added `extra` command in the `keys` DSL, which lets you define a key (whose value should be a Hash), to group any unspecified keys during load.
33
+ * Added `extra` command in the `keys` DSL, which lets you define a key (whose value should be a Hash), to group any unspecified keys during load.
26
34
 
27
35
  2.1.0
28
36
  ------
@@ -31,7 +39,7 @@ next
31
39
  * Add Collection subclasses for CSVs and Ids
32
40
  * CSV type for Collection of values serialized as comma-separated strings.
33
41
  * Ids type. A helper for creating CSVs with members matching a given a type's :identity option.
34
- * Allow instances of Models to be initialized with initial data.
42
+ * Allow instances of Models to be initialized with initial data.
35
43
  * Supported formats for the data are equivalent to the loading formats (i.e. ruby Hash, a JSON string or another instance of the same model type).
36
44
  * Improved context reporting in errors
37
45
  * Added contextual information while loading and dumping attributes.
@@ -39,7 +47,7 @@ next
39
47
  * `validate` takes a `context` argument that (instead of a string) is now an array of parent segments.
40
48
  * `dump` takes a `context:` option parameter of the same type
41
49
  * Enhanced error messages to report the correct context scope.
42
- * Make Attribute assignments in models to report a special context (not the attributor root)
50
+ * Make Attribute assignments in models to report a special context (not the attributor root)
43
51
  * Instead of reporting "$." as the context , when doing model.field_name=value, they'll now report "assignment.of(field_name)" instead
44
52
  * Truncate the length of values when reporting loading errors when they're long (i.e. >500 chars)
45
53
  * `Model.attributes` may now be called more than once to set add or replace attributes. The exact behavior depends upon the types of the attributes being added or replaced. See [model_spec.rb](spec/types/model_spec.rb) for examples.
@@ -67,16 +67,19 @@ module Attributor
67
67
 
68
68
  require_relative 'attributor/types/container'
69
69
  require_relative 'attributor/types/object'
70
+ require_relative 'attributor/types/bigdecimal'
70
71
  require_relative 'attributor/types/integer'
71
72
  require_relative 'attributor/types/string'
72
73
  require_relative 'attributor/types/model'
73
74
  require_relative 'attributor/types/struct'
74
75
  require_relative 'attributor/types/boolean'
76
+ require_relative 'attributor/types/date'
75
77
  require_relative 'attributor/types/date_time'
78
+ require_relative 'attributor/types/time'
76
79
  require_relative 'attributor/types/float'
77
80
  require_relative 'attributor/types/collection'
78
81
  require_relative 'attributor/types/hash'
79
-
82
+
80
83
 
81
84
  require_relative 'attributor/types/csv'
82
85
  require_relative 'attributor/types/ids'
@@ -85,5 +88,4 @@ module Attributor
85
88
  require_relative 'attributor/types/tempfile'
86
89
  require_relative 'attributor/types/file_upload'
87
90
 
88
-
89
91
  end
@@ -38,11 +38,10 @@ module Attributor
38
38
 
39
39
 
40
40
  def load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
41
- value = type.load(value,context,**options) unless value.nil?
41
+ value = type.load(value,context,**options)
42
42
 
43
- # just in case type.load(value) returned nil, even though value is not nil.
44
43
  if value.nil?
45
- value = self.options[:default] if self.options[:default]
44
+ value = self.options[:default] if self.options.key?(:default)
46
45
  end
47
46
 
48
47
  value
@@ -72,7 +71,7 @@ module Attributor
72
71
  TOP_LEVEL_OPTIONS = [ :description, :values, :default, :example, :required, :required_if ]
73
72
  INTERNAL_OPTIONS = [:dsl_compiler,:dsl_compiler_options] # Options we don't want to expose when describing attributes
74
73
  def describe(shallow=true)
75
- description = { }
74
+ description = { }
76
75
  # Clone the common options
77
76
  TOP_LEVEL_OPTIONS.each do |option_name|
78
77
  description[option_name] = self.options[option_name] if self.options.has_key? option_name
@@ -100,13 +99,13 @@ module Attributor
100
99
  def example(context=nil, parent: nil, values:{})
101
100
  raise ArgumentError, "attribute example cannot take a context of type String" if (context.is_a? ::String )
102
101
  if context
103
- ctx = Attributor.humanize_context(context)
102
+ ctx = Attributor.humanize_context(context)
104
103
  seed, _ = Digest::SHA1.digest(ctx).unpack("QQ")
105
104
  Random.srand(seed)
106
105
  end
107
106
 
108
107
  if self.options.has_key? :example
109
- val = self.options[:example]
108
+ val = self.options[:example]
110
109
  case val
111
110
  when ::String
112
111
  # FIXME: spec this properly to use self.type.native_type
@@ -180,7 +179,7 @@ module Attributor
180
179
 
181
180
  def validate_missing_value(context)
182
181
  raise "INVALID CONTEXT!!! (got: #{context.inspect})" unless context.is_a? Enumerable
183
-
182
+
184
183
  # Missing attribute was required if :required option was set
185
184
  return ["Attribute #{Attributor.humanize_context(context)} is required"] if self.options[:required]
186
185
 
@@ -201,7 +200,7 @@ module Attributor
201
200
  # should never get here if the option validation worked...
202
201
  raise AttributorException.new("unknown type of dependency: #{requirement.inspect} for #{Attributor.humanize_context(context)}")
203
202
  end
204
-
203
+
205
204
  # chop off the last part
206
205
  requirement_context = context[0..-2]
207
206
  requirement_context_string = requirement_context.join(Attributor::SEPARATOR)
@@ -257,7 +256,7 @@ module Attributor
257
256
  raise AttributorException.new("Required_if must be a String, a Hash definition or a Proc") unless definition.is_a?(::String) || definition.is_a?(::Hash) || definition.is_a?(::Proc)
258
257
  raise AttributorException.new("Required_if cannot be specified together with :required") if self.options[:required]
259
258
  when :example
260
- unless definition.is_a?(::Regexp) || definition.is_a?(::String) || definition.is_a?(::Array) || definition.is_a?(::Proc) || definition.nil? || self.type.valid_type?(definition)
259
+ unless definition.is_a?(::Regexp) || definition.is_a?(::String) || definition.is_a?(::Array) || definition.is_a?(::Proc) || definition.nil? || self.type.valid_type?(definition)
261
260
  raise AttributorException.new("Invalid example type (got: #{definition.class.name}). It must always match the type of the attribute (except if passing Regex that is allowed for some types)")
262
261
  end
263
262
  else
@@ -4,9 +4,9 @@ module Attributor
4
4
 
5
5
  class LoadError < AttributorException
6
6
  end
7
-
7
+
8
8
  class IncompatibleTypeError < LoadError
9
-
9
+
10
10
  def initialize(type:, value_type: , context: )
11
11
  super "Type #{type} cannot load values of type #{value_type} while loading #{Attributor.humanize_context(context)}."
12
12
  end
@@ -19,18 +19,18 @@ module Attributor
19
19
  super msg
20
20
  end
21
21
  end
22
-
22
+
23
23
  class DeserializationError < LoadError
24
24
  def initialize( context: , from:, encoding: , value: nil)
25
25
  msg = "Error deserializing a #{from} using #{encoding} while loading #{Attributor.humanize_context(context)}."
26
26
  msg += " Received value #{Attributor.errorize_value(value)}" if value
27
27
  super msg
28
- end
28
+ end
29
29
  end
30
-
30
+
31
31
  class DumpError < AttributorException
32
32
  def initialize( context: , name: , type: , original_exception: )
33
- msg = "Error while dumping attribute #{name} of type #{type} for context #{Attributor.humanize_context(context)} ."
33
+ msg = "Error while dumping attribute #{name} of type #{type} for context #{Attributor.humanize_context(context)}."
34
34
  msg << " Reason: #{original_exception}"
35
35
  super msg
36
36
  end
@@ -7,4 +7,9 @@ class Randgen
7
7
  def self.date
8
8
  return DATE_TIME_EPOCH - rand(800)
9
9
  end
10
+
11
+ def self.time
12
+ return date.to_time
13
+ end
14
+
10
15
  end
@@ -0,0 +1,28 @@
1
+ require 'bigdecimal'
2
+
3
+ module Attributor
4
+
5
+ class BigDecimal
6
+ include Type
7
+
8
+ def self.native_type
9
+ return ::BigDecimal
10
+ end
11
+
12
+ def self.example(context=nil, **options)
13
+ return ::BigDecimal.new("#{/\d{3}/.gen}.#{/\d{3}/.gen}")
14
+ end
15
+
16
+ def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
17
+ return nil if value.nil?
18
+ return value if value.is_a?(self.native_type)
19
+ if value.kind_of?(::Float)
20
+ return BigDecimal(value, 10)
21
+ end
22
+ return BigDecimal(value)
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+
@@ -16,6 +16,8 @@ module Attributor
16
16
  end
17
17
 
18
18
  def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
19
+ return nil if value.nil?
20
+
19
21
  raise CoercionError, context: context, from: value.class, to: self, value: value if value.is_a?(::Float)
20
22
  return false if [ false, 'false', 'FALSE', '0', 0, 'f', 'F' ].include?(value)
21
23
  return true if [ true, 'true', 'TRUE', '1', 1, 't', 'T' ].include?(value)
@@ -41,10 +41,12 @@ module Attributor
41
41
  # @return An Array of native type objects conforming to the specified member_type
42
42
  def self.example(context=nil, options: {})
43
43
  result = []
44
- size = rand(3) + 1
44
+ size = options[:size] || (rand(3) + 1)
45
+ size = [*size].sample if size.is_a?(Range)
46
+
45
47
  context ||= ["Collection-#{result.object_id}"]
46
48
  context = Array(context)
47
-
49
+
48
50
  size.times do |i|
49
51
  subcontext = context + ["at(#{i})"]
50
52
  result << self.member_attribute.example(subcontext)
@@ -64,7 +66,7 @@ module Attributor
64
66
  elsif value.is_a?(::String)
65
67
  loaded_value = decode_string(value,context)
66
68
  else
67
- raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
69
+ raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
68
70
  end
69
71
 
70
72
  return loaded_value.collect { |member| self.member_attribute.load(member,context) }
@@ -82,7 +84,6 @@ module Attributor
82
84
  end
83
85
 
84
86
  def self.describe(shallow=false)
85
- #puts "Collection: #{self.type}"
86
87
  hash = super(shallow)
87
88
  hash[:options] = {} unless hash[:options]
88
89
  hash[:member_attribute] = self.member_attribute.describe
@@ -95,7 +96,7 @@ module Attributor
95
96
  if options.has_key?(:reference) && !member_options.has_key?(:reference)
96
97
  member_options[:reference] = options[:reference]
97
98
  end
98
-
99
+
99
100
  # create the member_attribute, passing in our member_type and whatever constructor_block is.
100
101
  # that in turn will call construct on the type if applicable.
101
102
  @member_attribute = Attributor::Attribute.new self.member_type, member_options, &constructor_block
@@ -6,5 +6,26 @@ module Attributor
6
6
  value.split(',')
7
7
  end
8
8
 
9
+ def self.dump(values, **opts)
10
+ case values
11
+ when ::String
12
+ values
13
+ when ::Array
14
+ values.collect { |value| member_attribute.dump(value,opts).to_s }.join(',')
15
+ else
16
+ context = opts[:context]
17
+ name = opts[:context].last.to_s
18
+ type = values.class.name
19
+ reason = "Attributor::CSV only supports dumping values of type " +
20
+ "Array or String, not #{values.class.name}."
21
+ raise DumpError.new(context: context, name: name, type: type, original_exception: reason)
22
+ end
23
+ end
24
+
25
+ def self.example(context=nil, options: {})
26
+ collection = super(context, options: options.merge(size: (2..4)))
27
+ return collection.join(',')
28
+ end
29
+
9
30
  end
10
31
  end
@@ -0,0 +1,37 @@
1
+ require 'date'
2
+
3
+ module Attributor
4
+
5
+ class Date
6
+ include Type
7
+
8
+ def self.native_type
9
+ return ::Date
10
+ end
11
+
12
+ def self.example(context=nil, options: {})
13
+ return self.load(/[:date:]/.gen, context)
14
+ end
15
+
16
+ def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
17
+ return value if value.is_a?(self.native_type)
18
+ return nil if value.nil?
19
+
20
+ return value.to_date if value.respond_to?(:to_date)
21
+
22
+ case value
23
+ when ::String
24
+ begin
25
+ return ::Date.parse(value)
26
+ rescue ArgumentError => e
27
+ raise Attributor::DeserializationError, context: context, from: value.class, encoding: "Date" , value: value
28
+ end
29
+ else
30
+ raise CoercionError, context: context, from: value.class, to: self, value: value
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+
@@ -19,7 +19,7 @@ module Attributor
19
19
  def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
20
20
  # We assume that if the value is already in the right type, we've decoded it already
21
21
  return value if value.is_a?(self.native_type)
22
- return value.to_datetime if value.is_a?(::Time)
22
+ return value.to_datetime if value.respond_to?(:to_datetime)
23
23
  return nil unless value.is_a?(::String)
24
24
  # TODO: we should be able to convert not only from String but Time...etc
25
25
  # Else, we'll decode it from String.
@@ -119,7 +119,7 @@ module Attributor
119
119
 
120
120
  unless @concrete
121
121
  return self.of(key:self.key_type, value: self.value_type)
122
- .construct(constructor_block,**options)
122
+ .construct(constructor_block,**options)
123
123
  end
124
124
 
125
125
  if options[:case_insensitive_load] && !(self.key_type <= String)
@@ -158,15 +158,10 @@ module Attributor
158
158
 
159
159
 
160
160
  def self.dump(value, **opts)
161
- return nil if value.nil?
162
-
163
- value.each_with_object({}) do |(k,v),hash|
164
- k = key_type.dump(k,opts) if @key_type
165
- v = value_type.dump(v,opts) if @value_type
166
- hash[k] = v
167
- end
161
+ self.load(value).dump(**opts)
168
162
  end
169
163
 
164
+
170
165
  def self.check_option!(name, definition)
171
166
  case name
172
167
  when :reference
@@ -186,9 +181,13 @@ module Attributor
186
181
  end
187
182
 
188
183
 
189
- def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
184
+ def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **options)
190
185
  if value.nil?
191
- return nil
186
+ if recurse
187
+ loaded_value = {}
188
+ else
189
+ return nil
190
+ end
192
191
  elsif value.is_a?(self)
193
192
  return value
194
193
  elsif value.kind_of?(Attributor::Hash)
@@ -201,7 +200,7 @@ module Attributor
201
200
  raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
202
201
  end
203
202
 
204
- return self.from_hash(loaded_value,context) if self.keys.any?
203
+ return self.from_hash(loaded_value,context, recurse: recurse) if self.keys.any?
205
204
  return self.new(loaded_value) if (key_type == Object && value_type == Object)
206
205
 
207
206
  loaded_value.each_with_object(self.new) do| (k, v), obj |
@@ -215,9 +214,41 @@ module Attributor
215
214
  end
216
215
 
217
216
  def generate_subcontext(context, key_name)
218
- self.class.generate_sub_context(context,key_name)
217
+ self.class.generate_subcontext(context,key_name)
218
+ end
219
+
220
+ def get(key, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key))
221
+ key = self.class.key_attribute.load(key, context)
222
+ value = @contents[key]
223
+
224
+ if (attribute = self.class.keys[key])
225
+ return self[key] = attribute.load(value, context)
226
+ end
227
+
228
+ if self.class.options[:case_insensitive_load]
229
+ key = self.class.insensitive_map[key.downcase]
230
+ return self.get(key, context: context)
231
+ end
232
+
233
+ if self.class.options[:allow_extra]
234
+ if self.class.extra_keys.nil?
235
+ return @contents[key] = self.class.value_attribute.load(value, context)
236
+ else
237
+ extra_keys_key = self.class.extra_keys
238
+
239
+ unless @contents.key? extra_keys_key
240
+ extra_keys_value = self.class.keys[extra_keys_key].load({})
241
+ @contents[extra_keys_key] = extra_keys_value
242
+ end
243
+
244
+ return @contents[extra_keys_key].get(key, context: context)
245
+ end
246
+ end
247
+
248
+ raise AttributorException, "Unknown key received: #{key.inspect} for #{Attributor.humanize_context(context)}"
219
249
  end
220
250
 
251
+
221
252
  def set(key, value, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key))
222
253
  key = self.class.key_attribute.load(key, context)
223
254
 
@@ -234,14 +265,21 @@ module Attributor
234
265
  if self.class.extra_keys.nil?
235
266
  return self[key] = self.class.value_attribute.load(value, context)
236
267
  else
237
- return self[self.class.extra_keys].set(key, value, context: context)
268
+ extra_keys_key = self.class.extra_keys
269
+
270
+ unless @contents.key? extra_keys_key
271
+ extra_keys_value = self.class.keys[extra_keys_key].load({})
272
+ @contents[extra_keys_key] = extra_keys_value
273
+ end
274
+
275
+ return self[extra_keys_key].set(key, value, context: context)
238
276
  end
239
277
  end
240
278
 
241
279
  raise AttributorException, "Unknown key received: #{key.inspect} while loading #{Attributor.humanize_context(context)}"
242
280
  end
243
281
 
244
- def self.from_hash(object,context)
282
+ def self.from_hash(object,context, recurse: false)
245
283
  hash = self.new
246
284
 
247
285
  # if the hash definition includes named extra keys, initialize
@@ -265,7 +303,7 @@ module Attributor
265
303
  self.keys.each do |key_name, attribute|
266
304
  next if hash.key?(key_name)
267
305
  sub_context = self.generate_subcontext(context,key_name)
268
- hash[key_name] = attribute.load(nil, sub_context)
306
+ hash[key_name] = attribute.load(nil, sub_context, recurse: recurse)
269
307
  end
270
308
 
271
309
  hash
@@ -374,9 +412,17 @@ module Attributor
374
412
  end
375
413
  end
376
414
 
377
- def dump(*args)
378
- @contents.each_with_object(::Hash.new) do |(k,v), hash|
379
- hash[k] = self.class.value_type.dump(v)
415
+ def dump(**opts)
416
+ @contents.each_with_object({}) do |(k,v),hash|
417
+ k = self.key_attribute.dump(k,opts)
418
+
419
+ if (attribute_for_value = self.class.keys[k])
420
+ v = attribute_for_value.dump(v,opts)
421
+ else
422
+ v = self.value_attribute.dump(v,opts)
423
+ end
424
+
425
+ hash[k] = v
380
426
  end
381
427
  end
382
428