attributor 2.1.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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/CHANGELOG.md +52 -0
  6. data/Gemfile +3 -0
  7. data/Guardfile +12 -0
  8. data/LICENSE +22 -0
  9. data/README.md +62 -0
  10. data/Rakefile +28 -0
  11. data/attributor.gemspec +40 -0
  12. data/lib/attributor.rb +89 -0
  13. data/lib/attributor/attribute.rb +271 -0
  14. data/lib/attributor/attribute_resolver.rb +116 -0
  15. data/lib/attributor/dsl_compiler.rb +106 -0
  16. data/lib/attributor/exceptions.rb +38 -0
  17. data/lib/attributor/extensions/randexp.rb +10 -0
  18. data/lib/attributor/type.rb +117 -0
  19. data/lib/attributor/types/boolean.rb +26 -0
  20. data/lib/attributor/types/collection.rb +135 -0
  21. data/lib/attributor/types/container.rb +42 -0
  22. data/lib/attributor/types/csv.rb +10 -0
  23. data/lib/attributor/types/date_time.rb +36 -0
  24. data/lib/attributor/types/file_upload.rb +11 -0
  25. data/lib/attributor/types/float.rb +27 -0
  26. data/lib/attributor/types/hash.rb +337 -0
  27. data/lib/attributor/types/ids.rb +26 -0
  28. data/lib/attributor/types/integer.rb +63 -0
  29. data/lib/attributor/types/model.rb +316 -0
  30. data/lib/attributor/types/object.rb +19 -0
  31. data/lib/attributor/types/string.rb +25 -0
  32. data/lib/attributor/types/struct.rb +50 -0
  33. data/lib/attributor/types/tempfile.rb +36 -0
  34. data/lib/attributor/version.rb +3 -0
  35. data/spec/attribute_resolver_spec.rb +227 -0
  36. data/spec/attribute_spec.rb +597 -0
  37. data/spec/attributor_spec.rb +25 -0
  38. data/spec/dsl_compiler_spec.rb +130 -0
  39. data/spec/spec_helper.rb +30 -0
  40. data/spec/support/models.rb +81 -0
  41. data/spec/support/types.rb +21 -0
  42. data/spec/type_spec.rb +134 -0
  43. data/spec/types/boolean_spec.rb +85 -0
  44. data/spec/types/collection_spec.rb +286 -0
  45. data/spec/types/container_spec.rb +49 -0
  46. data/spec/types/csv_spec.rb +17 -0
  47. data/spec/types/date_time_spec.rb +90 -0
  48. data/spec/types/file_upload_spec.rb +6 -0
  49. data/spec/types/float_spec.rb +78 -0
  50. data/spec/types/hash_spec.rb +372 -0
  51. data/spec/types/ids_spec.rb +32 -0
  52. data/spec/types/integer_spec.rb +151 -0
  53. data/spec/types/model_spec.rb +401 -0
  54. data/spec/types/string_spec.rb +55 -0
  55. data/spec/types/struct_spec.rb +189 -0
  56. data/spec/types/tempfile_spec.rb +6 -0
  57. metadata +348 -0
@@ -0,0 +1,25 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+
4
+ describe Attributor do
5
+ context '.resolve_type' do
6
+ context 'given valid types' do
7
+ {
8
+ ::Integer => Attributor::Integer,
9
+ Integer => Attributor::Integer,
10
+ Attributor::Integer => Attributor::Integer,
11
+ ::Attributor::Integer => Attributor::Integer,
12
+ ::Attributor::DateTime => Attributor::DateTime,
13
+ # FIXME: Boolean doesn't exist in Ruby, thus this causes and error
14
+ # https://github.com/rightscale/attributor/issues/25
15
+ #Boolean => Attributor::Boolean,
16
+ Attributor::Boolean => Attributor::Boolean,
17
+ Attributor::Struct => Attributor::Struct
18
+ }.each do |type, expected_type|
19
+ it "resolves #{type} as #{expected_type}" do
20
+ Attributor.resolve_type(type).should == expected_type
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,130 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+
4
+ describe Attributor::DSLCompiler do
5
+
6
+ let(:target) { double("model", attributes: {}) }
7
+
8
+ let(:dsl_compiler_options) { {} }
9
+ subject(:dsl_compiler) { Attributor::DSLCompiler.new(target, dsl_compiler_options) }
10
+
11
+ let(:attribute_name) { :name }
12
+ let(:type) { Attributor::String }
13
+
14
+ let!(:reference_attributes) { Turducken.attributes }
15
+ let(:reference_type) { reference_attribute.type }
16
+ let(:reference_attribute_options) { reference_attribute.options }
17
+ let(:reference_attribute) {reference_attributes[attribute_name] }
18
+
19
+ context '#attribute' do
20
+ let(:attribute_options) { {} }
21
+
22
+ let(:expected_options) { attribute_options }
23
+ let(:expected_type) { type }
24
+
25
+ context 'when not not given a block for a sub-definition' do
26
+ context 'without a reference' do
27
+ it 'raises an error for a missing type' do
28
+ expect {
29
+ dsl_compiler.attribute(attribute_name)
30
+ }.to raise_error(/type for attribute/)
31
+
32
+ end
33
+
34
+ it 'creates an attribute given a name and type' do
35
+ Attributor::Attribute.should_receive(:new).with(expected_type, expected_options)
36
+ dsl_compiler.attribute(attribute_name, type)
37
+ end
38
+
39
+
40
+ it 'creates an attribute given a name, type, and options' do
41
+ Attributor::Attribute.should_receive(:new).with(expected_type, expected_options)
42
+ dsl_compiler.attribute(attribute_name, type, attribute_options)
43
+ end
44
+
45
+ end
46
+
47
+
48
+ context 'with a reference' do
49
+ let(:dsl_compiler_options) { {:reference => Turducken} }
50
+
51
+ context 'with no options' do
52
+ let(:expected_options) { reference_attribute_options }
53
+
54
+ it 'creates an attribute with the inherited type' do
55
+ Attributor::Attribute.should_receive(:new).with(expected_type, expected_options)
56
+ dsl_compiler.attribute(attribute_name)
57
+ end
58
+ end
59
+
60
+ context 'with options' do
61
+ let(:attribute_options) { {:description => "some new description", :required => true} }
62
+ let(:expected_options) { reference_attribute_options.merge(attribute_options) }
63
+
64
+ before do
65
+ Attributor::Attribute.should_receive(:new).with(expected_type, expected_options)
66
+ end
67
+
68
+ it 'creates an attribute with the inherited type and merged options' do
69
+ dsl_compiler.attribute(attribute_name, attribute_options)
70
+ end
71
+
72
+ it 'accepts explicit nil type' do
73
+ dsl_compiler.attribute(attribute_name, nil, attribute_options)
74
+ end
75
+
76
+ end
77
+
78
+ context 'for a referenced Model attribute' do
79
+ let(:attribute_name) { :turkey }
80
+ let(:expected_type) { Turkey }
81
+ let(:expected_options) { reference_attribute_options.merge(attribute_options) }
82
+
83
+ it 'creates an attribute with the inherited type' do
84
+ Attributor::Attribute.should_receive(:new).with(expected_type, expected_options)
85
+ dsl_compiler.attribute(attribute_name)
86
+ end
87
+ end
88
+
89
+ end
90
+
91
+ end
92
+
93
+
94
+ context 'when given a block for sub-attributes' do
95
+ let(:attribute_block) { Proc.new { } }
96
+ let(:attribute_name) { :turkey }
97
+ let(:type) { Attributor::Struct }
98
+ let(:expected_type) { Attributor::Struct }
99
+
100
+ context 'without a reference' do
101
+ it 'defaults type to Struct' do
102
+ Attributor::Attribute.should_receive(:new).with(expected_type, expected_options)
103
+ dsl_compiler.attribute(attribute_name, &attribute_block)
104
+ end
105
+ end
106
+
107
+ context 'with a reference' do
108
+ let(:dsl_compiler_options) { {:reference => Turducken} }
109
+ let(:expected_options) do
110
+ attribute_options.merge(:reference => reference_type)
111
+ end
112
+
113
+ it 'is unhappy from somewhere else if you do not specify a type' do
114
+ expect {
115
+ dsl_compiler.attribute(attribute_name, attribute_options, &attribute_block)
116
+ }.to raise_error(/does not support anonymous generation/)
117
+ end
118
+
119
+ it 'passes the correct reference to the created attribute' do
120
+ Attributor::Attribute.should_receive(:new).with(expected_type, expected_options)
121
+ dsl_compiler.attribute(attribute_name, type, attribute_options, &attribute_block)
122
+ end
123
+
124
+ end
125
+
126
+ end
127
+
128
+ end
129
+
130
+ end
@@ -0,0 +1,30 @@
1
+ Encoding.default_external = Encoding::UTF_8
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+
6
+ # Configure simplecov gem (must be here at top of file)
7
+ require 'simplecov'
8
+ SimpleCov.start do
9
+ add_filter 'spec' # Don't include RSpec stuff
10
+ add_group 'Types', 'lib/attributor/types'
11
+ end
12
+
13
+ require 'rspec'
14
+ require 'attributor'
15
+
16
+ require 'pry'
17
+
18
+ # Requires supporting files with custom matchers and macros, etc,
19
+ # in ./support/ and its subdirectories.
20
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
21
+
22
+ RSpec.configure do |config|
23
+
24
+ config.around(:each) do |example|
25
+ Attributor::AttributeResolver.current = Attributor::AttributeResolver.new
26
+ example.run
27
+ Attributor::AttributeResolver.current = nil
28
+ end
29
+
30
+ end
@@ -0,0 +1,81 @@
1
+ class Chicken < Attributor::Model
2
+ attributes(:identity => :email) do
3
+ attribute :name, Attributor::String, example: /[:first_name:]/
4
+ attribute :age, Attributor::Integer, :default => 1, :min => 0, :max => 120, :description => "The age of the chicken"
5
+ attribute :email, Attributor::String, :example => /[:email:]/, :regexp => /@/, :description => "The email address of the chicken"
6
+ attribute :angry, Attributor::Boolean, :example => "true", :description => "Angry bird?"
7
+ attribute :weight, Attributor::Float, :example => /\d{1,2}\.\d/, :description => "The weight of the chicken"
8
+ end
9
+ end
10
+
11
+
12
+ class Duck < Attributor::Model
13
+ attributes do
14
+ attribute :age, Attributor::Integer, :required_if => {"name" => "Daffy" }
15
+ attribute :name, Attributor::String
16
+ attribute :email, Attributor::String, :required_if => "name"
17
+ attribute :angry, Attributor::Boolean, :default => true, :example => /true|false/, :description => "Angry bird?"
18
+ attribute :weight, Attributor::Float, :example => /\d{1,2}\.\d/, :description => "The weight of the duck"
19
+ end
20
+ end
21
+
22
+
23
+ class Turkey < Attributor::Model
24
+ attributes do
25
+ attribute :age, Integer, :default => 1, :min => 0, :max => 120, :description => "The age of the turkey"
26
+ attribute :name, String , :description => "name of the turkey", :example => /[:name:]/ #, :default => "Providencia Zboncak"
27
+ attribute :email, String, :example => /[:email:]/, :regexp => /@/, :description => "The email address of the turkey"
28
+ attribute :weight, Attributor::Float, :example => /\d{1,2}\.\d/, :max => 86.7, :description => "The weight of the turkey"
29
+ end
30
+ end
31
+
32
+
33
+
34
+ class Turducken < Attributor::Model
35
+ attributes do
36
+ attribute :name, String, :description => "Turducken name", :example => /[:name:]/
37
+ attribute :chicken, Chicken
38
+ attribute :duck, Duck
39
+ attribute :turkey, Turkey, :description => "The turkey"
40
+ end
41
+ end
42
+
43
+
44
+ # http://en.wikipedia.org/wiki/Cormorant
45
+ class Cormorant < Attributor::Model
46
+ attributes do
47
+ # This will be a collection of arbitrary Ruby Objects
48
+ attribute :fish, Attributor::Collection, :description => "All kinds of fish for feeding the babies"
49
+
50
+ # This will be a collection of Cormorants (note, this relationship is circular)
51
+ attribute :neighbors, Attributor::Collection.of(Cormorant), :description => "Neighbor cormorants"
52
+
53
+ # This will be a collection of instances of an anonymous Struct class, each having two well-defined attributes
54
+
55
+ attribute :babies, Attributor::Collection.of(Attributor::Struct), :description => "All the babies", :member_options => {:identity => 'name'} do
56
+ attribute :name, Attributor::String, :example => /[:name]/, :description => "The name of the baby cormorant"
57
+ attribute :months, Attributor::Integer, :default => 0, :min => 0, :description => "The age in months of the baby cormorant"
58
+ attribute :weight, Attributor::Float, :example => /\d{1,2}\.\d{3}/, :description => "The weight in kg of the baby cormorant"
59
+ end
60
+
61
+ end
62
+ end
63
+
64
+
65
+ class Person < Attributor::Model
66
+ attributes do
67
+ attribute :name, String, example: /[:first_name:]/
68
+ attribute :title, String, values: %w{Mr Mrs Ms Dr}
69
+ attribute :okay, Attributor::Boolean, values: [true]
70
+ attribute :address, Address, example: proc { |person, context| Address.example(context, person: person) }
71
+ end
72
+ end
73
+
74
+
75
+ class Address < Attributor::Model
76
+ attributes do
77
+ attribute :name, String, example: /\w+/
78
+ attribute :state, String, values: %w{OR CA}
79
+ attribute :person, Person, example: proc { |address, context| Person.example(context, address: address) }
80
+ end
81
+ end
@@ -0,0 +1,21 @@
1
+ class AttributeType
2
+ include Attributor::Type
3
+ def self.native_type
4
+ ::String
5
+ end
6
+ end
7
+
8
+
9
+ class IntegerAttributeType
10
+ include Attributor::Type
11
+ def self.native_type
12
+ ::Integer
13
+ end
14
+
15
+ def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
16
+ value.to_i
17
+ end
18
+
19
+ end
20
+
21
+
data/spec/type_spec.rb ADDED
@@ -0,0 +1,134 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+
4
+ describe Attributor::Type do
5
+
6
+
7
+ subject(:test_type) { AttributeType }
8
+
9
+ let(:attribute_options) { Hash.new }
10
+ let(:attribute_attributes) { Hash.new }
11
+
12
+ let(:attribute) do
13
+ double "attribute",
14
+ :options => attribute_options,
15
+ :attributes => attribute_attributes
16
+ end
17
+
18
+
19
+ its(:native_type) { should be(::String) }
20
+
21
+
22
+ context 'load' do
23
+ let(:value) { nil }
24
+ let(:context) { nil }
25
+
26
+ context "when given a nil value" do
27
+ it 'always successfully returns it (i.e., you can always load nil)' do
28
+ test_type.load(value).should be(value)
29
+ end
30
+ end
31
+
32
+ context "when given a value that is of native_type" do
33
+ let(:value) { "one" }
34
+ it 'returns the value' do
35
+ test_type.load(value).should be(value)
36
+ end
37
+ end
38
+
39
+ context "when given a value that is not of native_type" do
40
+ let(:value) { 1 }
41
+ let(:context) { ['top','sub'] }
42
+
43
+ it 'raises an exception' do
44
+ expect { test_type.load(value,context) }.to raise_error( Attributor::IncompatibleTypeError, /AttributeType cannot load values of type Fixnum.*while loading top.sub/)
45
+ end
46
+
47
+
48
+ end
49
+
50
+ end
51
+
52
+
53
+ context 'validate' do
54
+ let(:context) { ['some_attribute'] }
55
+
56
+ let(:attribute_options) { {} }
57
+
58
+ let(:attribute) { double("some_attribute", :options => attribute_options)}
59
+ subject(:errors) { test_type.validate(value, context, attribute) }
60
+
61
+ context 'min and max' do
62
+ let(:min) { 10 }
63
+ let(:max) { 100}
64
+
65
+ let(:attribute_options) { {:min => min, :max => max} }
66
+
67
+
68
+
69
+ context "with a value <= min" do
70
+ let(:value) { 1 }
71
+
72
+ it { should_not be_empty }
73
+ it 'returns the correct error message' do
74
+ errors.first.should =~ /value \(#{value}\) is smaller than the allowed min/
75
+ end
76
+ end
77
+
78
+ context "with a value >= max" do
79
+ let(:value) { 1000 }
80
+ it { should_not be_empty }
81
+ it 'returns the correct error message' do
82
+ errors.first.should =~ /value \(#{value}\) is larger than the allowed max/
83
+ end
84
+
85
+ end
86
+
87
+ context 'with a value within the range' do
88
+ let(:value) { 50 }
89
+ it { should be_empty }
90
+ end
91
+
92
+
93
+ end
94
+
95
+
96
+
97
+ context 'regexp' do
98
+ let(:regexp) { /dog/ }
99
+ let(:attribute_options) { {:regexp => regexp} }
100
+
101
+ context 'with a value that matches' do
102
+ let(:value) { 'bulldog' }
103
+
104
+ it { should be_empty }
105
+ end
106
+
107
+
108
+ context 'with a value that does not match' do
109
+ let(:value) { 'chicken' }
110
+ it { should_not be_empty}
111
+ it 'returns the correct error message' do
112
+ errors.first.should =~ /value \(#{value}\) does not match regexp/
113
+ end
114
+ end
115
+
116
+ end
117
+
118
+
119
+ end
120
+
121
+
122
+ context 'example' do
123
+
124
+ end
125
+
126
+ context 'describe' do
127
+ subject(:description) { test_type.describe }
128
+ it 'outputs the type name' do
129
+ description[:name].should == test_type.name
130
+ end
131
+
132
+ end
133
+
134
+ end
@@ -0,0 +1,85 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
2
+
3
+ describe Attributor::Boolean do
4
+
5
+ subject(:type) { Attributor::Boolean }
6
+
7
+ context '.valid_type?' do
8
+
9
+ context 'for incoming Boolean values' do
10
+
11
+ [false, true].each do |value|
12
+
13
+ it "returns true for #{value.inspect}" do
14
+ type.valid_type?(value).should be_true
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+
21
+ context 'for incoming non-Boolean values' do
22
+
23
+ ['false', 2, 1.0, Class, Object.new].each do |value|
24
+
25
+ it "returns false for #{value.inspect}" do
26
+ type.valid_type?(value).should be_false
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+
35
+ context '.example' do
36
+ it "should return a valid Boolean" do
37
+ [true, false].should include type.example
38
+ end
39
+ end
40
+
41
+ context '.load' do
42
+
43
+ context 'for incoming Boolean false values' do
44
+
45
+ [false, 'false', 'FALSE', '0', 0, 'f', 'F'].each do |value|
46
+
47
+ it "returns false for #{value.inspect}" do
48
+ type.load(value).should be(false)
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+
55
+ context 'for incoming Boolean false values' do
56
+
57
+ [true, 'true', 'TRUE', '1', 1, 't', 'T'].each do |value|
58
+
59
+ it "returns true for #{value.inspect}" do
60
+ type.load(value).should be(true)
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+
67
+ context 'that are not valid Booleans' do
68
+ let(:context){ ['root','subattr'] }
69
+ ['string', 2, 1.0, Class, Object.new].each do |value|
70
+
71
+ it "raises Attributor::CoercionError for #{value.inspect}" do
72
+ expect {
73
+ type.load(value,context)
74
+ }.to raise_error(Attributor::CoercionError, /Error coercing from .+ to Attributor::Boolean.* #{context.join('.')}/)
75
+ end
76
+
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+
85
+