attributor 2.4.0 → 2.5.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: 6e832c8023013ffad5fa5a40e13200e5dcfeb4c4
4
- data.tar.gz: a1d2a7043187872b6cdc424b503b8ce08a5935ab
3
+ metadata.gz: 6b7e1ce2db7e093c7a3073db2cea1d45b861f015
4
+ data.tar.gz: f3623a6d5e3b313b9493cba77a6e7a7b250d1de5
5
5
  SHA512:
6
- metadata.gz: 26f1bd976df8c4ad47ea3e617e2abdcabd7d6d070a82dce44abb2e16ededcae6289ca7e074ba503f9ee7f6f4e79036bb49078509c2812f63ca02d3eab190466e
7
- data.tar.gz: ee7b663e6d9095d2909f355cf0f78e76d0cc5e6c8c0a2b0f44338fed5776a9a36360aba0b81f9a2b7ac532b88c2b8e7c8168187503405ed1b8cff1a82fc36cbd
6
+ metadata.gz: 625ac84d1636b6d21044c69d8be170bae210b28f302c2ed78de5b3130e1d82e760ee65c68a6b9f0c614e06b8a2a4770ab26da92a6b5917d847a1a02a73bdb522
7
+ data.tar.gz: f7c4324913a80c92191c9bc6f46ced3d5f7e9f01758faea751c79254b24e547f68b88ce836c446523cd759e2c35d4ca04778b7c033787878b5f75e88b77d7046
data/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  Attributor Changelog
2
2
  ============================
3
3
 
4
+ next
5
+ ----
6
+
7
+ 2.5.0
8
+ ----
9
+
10
+ * Partial support for defining `:default` values through Procs.
11
+ * Note: this is only "partially" supported the `parent` argument of the Proc will NOT contain the correct attribute parent yet. It will contain a fake class, that will loudly complain about any attempt to use any of its methods.
12
+ * Fixed `Model.example` to properly handle the case when no attributes are defined on the class.
13
+ * `Model#dump` now issues a warning if its contents have keys for attributes not present on the class. The unknown contents are not dumped.
14
+ * `Hash.load` now supports loading any value that responds to `to_hash`.
15
+ * `Time`, `DateTime`, and `Date` now all return ISO 8601 formatted values from `.dump` (via calling `iso8601` on the value).
16
+ * Added `Type.id`, a unique value based on the type's class name.
17
+
4
18
  2.4.0
5
19
  ------
6
20
 
data/attributor.gemspec CHANGED
@@ -8,7 +8,6 @@ Gem::Specification.new do |spec|
8
8
  spec.name = "attributor"
9
9
  spec.version = Attributor::VERSION
10
10
  spec.authors = ["Josep M. Blanquer","Dane Jensen"]
11
- spec.date = "2014-08-15"
12
11
  spec.summary = "A powerful attribute and type management library for Ruby"
13
12
  spec.email = ["blanquer@gmail.com","dane.jensen@gmail.com"]
14
13
 
@@ -2,6 +2,19 @@
2
2
 
3
3
  module Attributor
4
4
 
5
+ class FakeParent < ::BasicObject
6
+
7
+ def method_missing(name, *args)
8
+ ::Kernel.warn "Warning, you have tried to access the '#{name}' method of the 'parent' argument of a Proc-defined :default values." +
9
+ "Those Procs should completely ignore the 'parent' attribute for the moment as it will be set to an " +
10
+ "instance of a useless class (until the framework can provide such functionality)"
11
+ nil
12
+ end
13
+
14
+ def class
15
+ FakeParent
16
+ end
17
+ end
5
18
  # It is the abstract base class to hold an attribute, both a leaf and a container (hash/Array...)
6
19
  # TODO: should this be a mixin since it is an abstract class?
7
20
  class Attribute
@@ -40,8 +53,23 @@ module Attributor
40
53
  def load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
41
54
  value = type.load(value,context,**options)
42
55
 
43
- if value.nil?
44
- value = self.options[:default] if self.options.key?(:default)
56
+ if value.nil? && self.options.has_key?(:default)
57
+ defined_val = self.options[:default]
58
+ val = case defined_val
59
+ when ::Proc
60
+ fake_parent = FakeParent.new
61
+ # TODO: we can only support "context" as a parameter to the proc for now, since we don't have the parent...
62
+ if defined_val.arity == 2
63
+ defined_val.call(fake_parent, context)
64
+ elsif defined_val.arity == 1
65
+ defined_val.call(fake_parent)
66
+ else
67
+ defined_val.call
68
+ end
69
+ else
70
+ defined_val
71
+ end
72
+ value = val #Need to load?
45
73
  end
46
74
 
47
75
  value
@@ -107,9 +107,17 @@ module Attributor
107
107
  # Default describe for simple types...only their name (stripping the base attributor module)
108
108
  def describe(root=false)
109
109
  type_name = self.ancestors.find { |k| k.name && !k.name.empty? }.name
110
- { :name => type_name.gsub( Attributor::MODULE_PREFIX_REGEX, '' ) }
110
+ {
111
+ name: type_name.gsub(Attributor::MODULE_PREFIX_REGEX, ''),
112
+ id: self.id
113
+ }
111
114
  end
112
115
 
116
+ def id
117
+ return nil if self.name.nil?
118
+ self.name.gsub('::'.freeze,'-'.freeze)
119
+ end
120
+
113
121
  end
114
122
  end
115
123
  end
@@ -31,6 +31,10 @@ module Attributor
31
31
  end
32
32
  end
33
33
 
34
+ def self.dump(value,**opts)
35
+ value.iso8601
36
+ end
37
+
34
38
  end
35
39
 
36
40
  end
@@ -5,32 +5,36 @@ require 'date'
5
5
 
6
6
  module Attributor
7
7
 
8
- class DateTime
9
- include Type
8
+ class DateTime
9
+ include Type
10
10
 
11
- def self.native_type
12
- return ::DateTime
13
- end
11
+ def self.native_type
12
+ return ::DateTime
13
+ end
14
14
 
15
- def self.example(context=nil, options: {})
16
- return self.load(/[:date:]/.gen, context)
17
- end
15
+ def self.example(context=nil, options: {})
16
+ return self.load(/[:date:]/.gen, context)
17
+ end
18
18
 
19
- def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
20
- # We assume that if the value is already in the right type, we've decoded it already
21
- return value if value.is_a?(self.native_type)
22
- return value.to_datetime if value.respond_to?(:to_datetime)
23
- return nil unless value.is_a?(::String)
24
- # TODO: we should be able to convert not only from String but Time...etc
25
- # Else, we'll decode it from String.
26
- begin
27
- return ::DateTime.parse(value)
28
- rescue ArgumentError => e
29
- raise Attributor::DeserializationError, context: context, from: value.class, encoding: "DateTime" , value: value
30
- end
19
+ def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
20
+ # We assume that if the value is already in the right type, we've decoded it already
21
+ return value if value.is_a?(self.native_type)
22
+ return value.to_datetime if value.respond_to?(:to_datetime)
23
+ return nil unless value.is_a?(::String)
24
+ # TODO: we should be able to convert not only from String but Time...etc
25
+ # Else, we'll decode it from String.
26
+ begin
27
+ return ::DateTime.parse(value)
28
+ rescue ArgumentError => e
29
+ raise Attributor::DeserializationError, context: context, from: value.class, encoding: "DateTime" , value: value
31
30
  end
31
+ end
32
32
 
33
+ def self.dump(value,**opts)
34
+ value.iso8601
33
35
  end
34
36
 
37
+
35
38
  end
36
39
 
40
+ end
@@ -230,6 +230,8 @@ module Attributor
230
230
  loaded_value = value
231
231
  elsif value.is_a?(::String)
232
232
  loaded_value = decode_json(value,context)
233
+ elsif value.respond_to?(:to_hash)
234
+ loaded_value = value.to_hash
233
235
  else
234
236
  raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
235
237
  end
@@ -92,6 +92,21 @@ module Attributor
92
92
  context + [subname]
93
93
  end
94
94
 
95
+ def self.example(context=nil, **values)
96
+ context ||= ["#{self.name || 'Struct'}-#{rand(10000000)}"]
97
+ context = Array(context)
98
+
99
+ if self.keys.any?
100
+ result = self.new
101
+ result.extend(ExampleMixin)
102
+
103
+ result.lazy_attributes = self.example_contents(context, result, values)
104
+ else
105
+ result = self.new
106
+ end
107
+ result
108
+ end
109
+
95
110
  def initialize(data = nil)
96
111
  if data
97
112
  loaded = self.class.load(data)
@@ -161,6 +176,13 @@ module Attributor
161
176
 
162
177
  self.attributes.each_with_object({}) do |(name, value), result|
163
178
  attribute = self.class.attributes[name]
179
+
180
+ # skip dumping undefined attributes
181
+ unless attribute
182
+ warn "WARNING: Trying to dump unknown attribute: #{name.inspect} with context: #{context.inspect}"
183
+ next
184
+ end
185
+
164
186
  result[name.to_sym] = attribute.dump(value, context: context + [name] )
165
187
  end
166
188
  ensure
@@ -33,7 +33,11 @@ module Attributor
33
33
  end
34
34
  end
35
35
 
36
+ def self.dump(value,**opts)
37
+ value.iso8601
38
+ end
39
+
40
+
36
41
  end
37
42
 
38
43
  end
39
-
@@ -1,3 +1,3 @@
1
1
  module Attributor
2
- VERSION = "2.4.0"
2
+ VERSION = "2.5.0"
3
3
  end
@@ -43,7 +43,7 @@ describe Attributor::Attribute do
43
43
  context 'describe' do
44
44
  let(:attribute_options) { {:required => true, :values => ["one"], :description => "something", :min => 0} }
45
45
  let(:expected) do
46
- h = {:type => {:name => type.name} }
46
+ h = {type: {name: type.name, id: type.id}}
47
47
  common = attribute_options.select{|k,v| Attributor::Attribute::TOP_LEVEL_OPTIONS.include? k }
48
48
  h.merge!( common )
49
49
  h[:options] = {:min => 0 }
@@ -237,23 +237,56 @@ describe Attributor::Attribute do
237
237
  end
238
238
 
239
239
  context 'applying default values' do
240
+ let(:value) { nil }
240
241
  let(:default_value) { "default value" }
241
242
  let(:attribute_options) { {:default => default_value} }
242
243
 
243
244
  subject(:result) { attribute.load(value) }
244
245
 
245
246
  context 'for nil' do
246
- let(:value) { nil }
247
247
  it { should == default_value}
248
248
  end
249
249
 
250
250
  context 'for false' do
251
251
  let(:type) { Attributor::Boolean }
252
252
  let(:default_value) { false }
253
- let(:value) { nil }
254
253
  it { should == default_value}
255
254
 
256
255
  end
256
+
257
+ context 'for a Proc-based default value' do
258
+ let(:context){ ["$"] }
259
+ subject(:result){ attribute.load(value,context) }
260
+
261
+
262
+ context 'with no arguments arguments' do
263
+ let(:default_value) { proc { "no_params" } }
264
+ it { should == default_value.call }
265
+ end
266
+
267
+ context 'with 1 argument (the parent)' do
268
+ let(:default_value) { proc {|parent| "parent is fake: #{parent.class}" } }
269
+ it { should == "parent is fake: Attributor::FakeParent" }
270
+ end
271
+
272
+ context 'with 2 argument (the parent and the contents)' do
273
+ let(:default_value) { proc {|parent,context| "parent is fake: #{parent.class} and context is: #{context}" } }
274
+ it { should == "parent is fake: Attributor::FakeParent and context is: [\"$\"]"}
275
+ end
276
+
277
+ context 'which attempts to use the parent (which is not supported for the moment)' do
278
+ let(:default_value) { proc {|parent| "any parent method should spit out warning: [#{parent.something}]" } }
279
+ it "should output a warning" do
280
+ begin
281
+ old_verbose, $VERBOSE = $VERBOSE, nil
282
+ Kernel.should_receive(:warn).and_call_original
283
+ attribute.load(value,context).should == "any parent method should spit out warning: []"
284
+ ensure
285
+ $VERBOSE = old_verbose
286
+ end
287
+ end
288
+ end
289
+ end
257
290
  end
258
291
 
259
292
  context 'validating a value' do
data/spec/type_spec.rb CHANGED
@@ -3,7 +3,6 @@ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
3
3
 
4
4
  describe Attributor::Type do
5
5
 
6
-
7
6
  subject(:test_type) { AttributeType }
8
7
 
9
8
  let(:attribute_options) { Hash.new }
@@ -17,6 +16,7 @@ describe Attributor::Type do
17
16
 
18
17
 
19
18
  its(:native_type) { should be(::String) }
19
+ its(:id) { should eq('AttributeType')}
20
20
 
21
21
 
22
22
  context 'load' do
@@ -123,12 +123,25 @@ describe Attributor::Type do
123
123
 
124
124
  end
125
125
 
126
+ context 'id' do
127
+ it 'works for built-in types' do
128
+ Attributor::String.id.should eq('Attributor-String')
129
+ end
130
+
131
+ it 'returns nil for anonymous types' do
132
+ type = Class.new(Attributor::Model)
133
+ type.id.should eq(nil)
134
+ end
135
+ end
136
+
126
137
  context 'describe' do
127
138
  subject(:description) { test_type.describe }
128
139
  it 'outputs the type name' do
129
- description[:name].should == test_type.name
140
+ description[:name].should eq(test_type.name)
141
+ end
142
+ it 'outputs the type id' do
143
+ description[:id].should eq(test_type.name)
130
144
  end
131
-
132
145
  end
133
146
 
134
147
  end
@@ -12,6 +12,15 @@ describe Attributor::Date do
12
12
  its(:example) { should be_a(::Date) }
13
13
  end
14
14
 
15
+ context '.dump' do
16
+ let(:example) { type.example}
17
+ subject(:value) { type.dump(example) }
18
+ it 'is formatted correctly' do
19
+ value.should match(/\d{4}-\d{2}-\d{2}T00:00:00\+00:00/)
20
+ end
21
+ end
22
+
23
+
15
24
  context '.load' do
16
25
 
17
26
  it 'returns nil for nil' do
@@ -12,6 +12,15 @@ describe Attributor::DateTime do
12
12
  its(:example) { should be_a(::DateTime) }
13
13
  end
14
14
 
15
+ context '.dump' do
16
+ let(:example) { type.example}
17
+ subject(:value) { type.dump(example) }
18
+ it 'is formatted correctly' do
19
+ value.should match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[\+-]\d{2}:\d{2}/)
20
+ end
21
+ end
22
+
23
+
15
24
  context '.load' do
16
25
 
17
26
  it 'returns nil for nil' do
@@ -343,8 +343,8 @@ describe Attributor::Hash do
343
343
  context 'for hashes with key and value types' do
344
344
  it 'describes the type correctly' do
345
345
  description[:name].should eq('Hash')
346
- description[:key].should eq(type:{name: 'Object'})
347
- description[:value].should eq(type:{name: 'Object'})
346
+ description[:key].should eq(type:{name: 'Object', id: 'Attributor-Object'})
347
+ description[:value].should eq(type:{name: 'Object', id: 'Attributor-Object'})
348
348
  end
349
349
  end
350
350
 
@@ -362,15 +362,15 @@ describe Attributor::Hash do
362
362
 
363
363
  it 'describes the type correctly' do
364
364
  description[:name].should eq('Hash')
365
- description[:key].should eq(type:{name: 'String'})
365
+ description[:key].should eq(type:{name: 'String', id: 'Attributor-String'})
366
366
  description.should_not have_key(:value)
367
367
 
368
368
  keys = description[:keys]
369
369
 
370
- keys['a string'].should eq(type: {name: 'String'} )
371
- keys['1'].should eq(type: {name: 'Integer'}, options: {min: 1, max: 20} )
372
- keys['some_date'].should eq(type: {name: 'DateTime' }) #
373
- keys['defaulted'].should eq(type: {name: 'String'}, default: 'default value')
370
+ keys['a string'].should eq(type: {name: 'String', id: 'Attributor-String'} )
371
+ keys['1'].should eq(type: {name: 'Integer', id: 'Attributor-Integer'}, options: {min: 1, max: 20} )
372
+ keys['some_date'].should eq(type: {name: 'DateTime', id: 'Attributor-DateTime'})
373
+ keys['defaulted'].should eq(type: {name: 'String', id: 'Attributor-String'}, default: 'default value')
374
374
  end
375
375
  end
376
376
  end
@@ -416,6 +416,22 @@ describe Attributor::Model do
416
416
 
417
417
  end
418
418
 
419
+ context 'with no defined attributes' do
420
+ let(:model_class) do
421
+ Class.new(Attributor::Model) do
422
+ attributes do
423
+ end
424
+ end
425
+ end
419
426
 
427
+ subject(:example) { model_class.example }
428
+
429
+ its(:attributes) { should be_empty }
430
+
431
+ it 'dumps as an empty hash' do
432
+ example.dump.should eq({})
433
+ end
434
+
435
+ end
420
436
 
421
437
  end
@@ -12,6 +12,15 @@ describe Attributor::Time do
12
12
  its(:example) { should be_a(::Time) }
13
13
  end
14
14
 
15
+ context '.dump' do
16
+ let(:example) { type.example}
17
+ subject(:value) { type.dump(example) }
18
+ it 'is formatted correctly' do
19
+ value.should match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[\+-]\d{2}:\d{2}/)
20
+ end
21
+ end
22
+
23
+
15
24
  context '.load' do
16
25
 
17
26
  it 'returns nil for nil' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attributor
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep M. Blanquer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-08-15 00:00:00.000000000 Z
12
+ date: 2015-02-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: hashie