attributor 2.4.0 → 2.5.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: 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