attributor 2.2.1 → 2.3.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 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