attrocity 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +30 -10
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile.lock +37 -0
  6. data/LICENSE +22 -0
  7. data/README.md +32 -2
  8. data/Rakefile +14 -0
  9. data/TODO.md +36 -0
  10. data/attrocity.gemspec +5 -2
  11. data/lib/attrocity.rb +91 -2
  12. data/lib/attrocity/attributes/attribute.rb +10 -0
  13. data/lib/attrocity/attributes/attribute_methods_builder.rb +47 -0
  14. data/lib/attrocity/attributes/attribute_set.rb +24 -0
  15. data/lib/attrocity/attributes/attribute_template.rb +28 -0
  16. data/lib/attrocity/attributes/attribute_template_set.rb +16 -0
  17. data/lib/attrocity/attributes/attributes_hash.rb +9 -0
  18. data/lib/attrocity/attributes/model_attribute.rb +14 -0
  19. data/lib/attrocity/attributes/model_attribute_set.rb +6 -0
  20. data/lib/attrocity/builders/model_builder.rb +60 -0
  21. data/lib/attrocity/builders/object_extension_builder.rb +18 -0
  22. data/lib/attrocity/coercer_registry.rb +38 -0
  23. data/lib/attrocity/coercers/boolean.rb +12 -0
  24. data/lib/attrocity/coercers/integer.rb +13 -0
  25. data/lib/attrocity/coercers/string.rb +14 -0
  26. data/lib/attrocity/mappers/key_mapper.rb +14 -0
  27. data/lib/attrocity/model.rb +10 -0
  28. data/lib/attrocity/value_extractor.rb +23 -0
  29. data/lib/attrocity/version.rb +1 -1
  30. data/notes.md +253 -0
  31. data/spec/attrocity/attributes/attribute_methods_builder_spec.rb +57 -0
  32. data/spec/attrocity/attributes/attribute_set_spec.rb +24 -0
  33. data/spec/attrocity/attributes/attribute_spec.rb +4 -0
  34. data/spec/attrocity/attributes/attribute_template_set_spec.rb +6 -0
  35. data/spec/attrocity/attributes/attribute_template_spec.rb +25 -0
  36. data/spec/attrocity/attributes/model_attribute_spec.rb +26 -0
  37. data/spec/attrocity/attrocity_spec.rb +83 -0
  38. data/spec/attrocity/coercer_registry_spec.rb +14 -0
  39. data/spec/attrocity/coercers/boolean_spec.rb +32 -0
  40. data/spec/attrocity/coercers/coercer_with_args_spec.rb +9 -0
  41. data/spec/attrocity/coercers/integer_spec.rb +25 -0
  42. data/spec/attrocity/coercers/string_spec.rb +13 -0
  43. data/spec/attrocity/default_value_spec.rb +22 -0
  44. data/spec/attrocity/mappers/key_mapper_spec.rb +22 -0
  45. data/spec/attrocity/mapping_spec.rb +47 -0
  46. data/spec/attrocity/model_spec.rb +34 -0
  47. data/spec/attrocity/object_extension_spec.rb +53 -0
  48. data/spec/attrocity/value_extractor_spec.rb +23 -0
  49. data/spec/spec_helper.rb +93 -0
  50. data/spec/support/examples.rb +79 -0
  51. metadata +99 -8
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ module Attrocity
4
+ describe AttributeMethodsBuilder do
5
+ let(:object) { Object.new }
6
+ let(:attr_name) { :a_string }
7
+ let(:attribute) { Attribute.new(attr_name, 'hello') }
8
+ subject { described_class.new(object, Array(attribute)) }
9
+
10
+ describe '#define_reader' do
11
+ it 'defines a reader method for the attribute' do
12
+ expect {
13
+ subject.define_reader(attribute)
14
+ }.to change { object.respond_to?(attr_name) }.from(false).to(true)
15
+ end
16
+
17
+ it 'returns the attribute value from the defined reader method' do
18
+ subject.define_reader(attribute)
19
+ expect(object.send(attr_name)).to eq('hello')
20
+ end
21
+ end
22
+
23
+ describe '#define_writer' do
24
+ it 'defines a writer method for the attribute' do
25
+ expect {
26
+ subject.define_writer(attribute)
27
+ }.to change { object.respond_to?("#{attr_name}=") }.from(false).to(true)
28
+ end
29
+
30
+ it 'sets the attribute value' do
31
+ subject.define_writer(attribute)
32
+ object.send("#{attr_name}=", 'foobar')
33
+ expect(attribute.value).to eq('foobar')
34
+ end
35
+ end
36
+
37
+ describe '#define_predicate' do
38
+ it 'defines a predicate method for the attribute' do
39
+ expect {
40
+ subject.define_predicate(attribute)
41
+ }.to change { object.respond_to?("#{attr_name}?") }.from(false).to(true)
42
+ end
43
+
44
+ it 'returns true if the attribute has a truthy value' do
45
+ subject.define_predicate(attribute)
46
+ attribute.value = 'xyz'
47
+ expect(object.send("#{attr_name}?")).to be true
48
+ end
49
+
50
+ it 'returns false if the attribute has a falsy value' do
51
+ subject.define_predicate(attribute)
52
+ expect(attribute).to receive(:value).and_return(nil)
53
+ expect(object.send("#{attr_name}?")).to be false
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,24 @@
1
+ module Attrocity
2
+ RSpec.describe AttributeSet do
3
+ let(:attrs) { [Attribute.new(:foo, 1), Attribute.new(:bar, 2) ] }
4
+
5
+ subject(:attribute_set) { described_class.new }
6
+
7
+ describe '#<<' do
8
+ it 'adds an attribute' do
9
+ attr = attrs.first
10
+ subject << attr
11
+ expect(attribute_set.attributes).to include(attr)
12
+ end
13
+
14
+ it 'raises error when non-attribute is added'
15
+ end
16
+
17
+ describe '#to_h' do
18
+ it 'returns hash of instance attribute names and values' do
19
+ attrs.each { |attr| attribute_set << attr }
20
+ expect(attribute_set.to_h).to eq({ foo: 1, bar: 2 })
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,4 @@
1
+ module Attrocity
2
+ RSpec.describe Attribute do
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ require 'attrocity/attributes/attribute_template_set'
2
+
3
+ module Attrocity
4
+ RSpec.describe AttributeTemplateSet do
5
+ end
6
+ end
@@ -0,0 +1,25 @@
1
+ require 'attrocity/attributes/attribute_template'
2
+
3
+ module Attrocity
4
+ RSpec.describe AttributeTemplate do
5
+ describe '#mapper_key_for' do
6
+ it "returns mapper key when attribute name matches" do
7
+ attribute = AttributeTemplate.new(
8
+ :street, Coercers::String.new, KeyMapper.new(:addressline1))
9
+ expect(attribute.mapper_key_for(:street)).to eq(:addressline1)
10
+ end
11
+
12
+ it "returns mapper key when mapper matches" do
13
+ attribute = AttributeTemplate.new(
14
+ :zip, Coercers::String.new, KeyMapper.new(:zipcode))
15
+ expect(attribute.mapper_key_for(:zipcode)).to eq(:zipcode)
16
+ end
17
+
18
+ it 'returns nil when no match' do
19
+ attribute = AttributeTemplate.new(
20
+ :street, Coercers::String.new, KeyMapper.new(:addressline1))
21
+ expect(attribute.mapper_key_for(:nomatch)).to be_nil
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ module Attrocity
2
+ RSpec.describe ModelAttribute do
3
+
4
+ let(:listing) {
5
+ Examples::Listing.new({ 'listingid' => '1234',
6
+ 'addressline1' => '100 Elm Street',
7
+ 'zip' => '27571' })
8
+ }
9
+ let(:address) { listing.address }
10
+
11
+ it 'instantiates composed model object' do
12
+ expect(address.street).to eq('100 Elm Street')
13
+ expect(address.zip).to eq('27571')
14
+ end
15
+
16
+ it 'provides model object' do
17
+ model_attr = ModelAttribute.new(:address, Examples::Address)
18
+ address = model_attr.model(
19
+ { 'addressline1' => '100 Elm Street',
20
+ 'zip' => '27571' })
21
+ expect(address.street).to eq('100 Elm Street')
22
+ expect(address.zip).to eq('27571')
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,83 @@
1
+ require 'attrocity'
2
+ require 'support/examples'
3
+
4
+ module Attrocity
5
+ RSpec.describe 'Attrocity module' do
6
+
7
+ describe '.initialize' do
8
+ it 'accepts a hash' do
9
+ expect {
10
+ Examples::Person.new(age: 29)
11
+ }.not_to raise_error
12
+ end
13
+
14
+ let(:person) { Examples::Person.new(age: 29) }
15
+
16
+ it 'creates a reader for instance-scope attribute set' do
17
+ expect { person.attribute_set }.not_to raise_error
18
+ end
19
+
20
+ it 'creates a reader for attribute' do
21
+ expect(person.respond_to?(:age)).to be true
22
+ end
23
+
24
+ # TODO: Move this spec to another unit?
25
+ it 'returns correct value for reader method' do
26
+ expect(person.age).to eq(29)
27
+ end
28
+ end
29
+
30
+ describe '.attribute' do
31
+ it 'does not raise error with declarative attribute method' do
32
+ expect {
33
+ module Examples
34
+ class SpecialPerson
35
+ include Attrocity.model
36
+ attribute :foo, coercer: :integer
37
+ end
38
+ end
39
+ }.not_to raise_error
40
+ end
41
+
42
+ it 'raises error when missing coercer keyword argument' do
43
+ expect {
44
+ module Examples
45
+ class SpecialDog
46
+ include Attrocity.model
47
+ attribute :name
48
+ end
49
+ end
50
+ }.to raise_error(ArgumentError)
51
+ end
52
+
53
+ it 'raises error when using coercer name that is not on the registry' do
54
+ expect {
55
+ module Examples
56
+ class Person
57
+ include Attrocity.model
58
+ attribute :foo, coercer: :xyz
59
+ end
60
+ end
61
+ }.to raise_error(UnknownCoercerError)
62
+ end
63
+ end
64
+
65
+ describe '#attribute_set' do
66
+ it 'contains expected attributes' do
67
+ pending
68
+ obj = Examples::Person.new(age: 29)
69
+ attribute_names = obj.attribute_set.map(&:name)
70
+ expect(attribute_names).to include(:age)
71
+ end
72
+ end
73
+
74
+ describe '.model_from_mapped_data' do
75
+ it 'returns an instantiated model' do
76
+ data = { street: '1234 Elm', zip: '27517' }
77
+ address = Examples::Address.model_from_mapped_data(data)
78
+ expect(address.street).to eq('1234 Elm')
79
+ expect(address.zip).to eq('27517')
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,14 @@
1
+ require 'attrocity/coercer_registry'
2
+ require 'support/examples'
3
+
4
+ module Attrocity
5
+ RSpec.describe CoercerRegistry do
6
+ it 'adds to the registry' do
7
+ CoercerRegistry.register do
8
+ add :example, Examples::ExampleCoercer
9
+ end
10
+ expect(CoercerRegistry.coercer_for(:example)).
11
+ to eq(Examples::ExampleCoercer)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ require 'attrocity/coercers/boolean'
2
+
3
+ module Attrocity
4
+ RSpec.describe Coercers::Boolean do
5
+
6
+ subject(:coercer) { Coercers::Boolean.new }
7
+
8
+ describe '#coerce' do
9
+ it 'coerces nil to false' do
10
+ expect(coercer.coerce(nil)).to be false
11
+ end
12
+
13
+ it 'coerces false to false' do
14
+ expect(coercer.coerce(false)).to be false
15
+ end
16
+
17
+ it 'coerces true to true' do
18
+ expect(coercer.coerce(true)).to be true
19
+ end
20
+
21
+ it 'coerces 0 to true' do
22
+ expect(coercer.coerce(0)).to be true
23
+ end
24
+
25
+ it 'coerces an object to true' do
26
+ expect(coercer.coerce(Object.new)).to be true
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+
@@ -0,0 +1,9 @@
1
+ module Attrocity
2
+ RSpec.describe 'coercer with args' do
3
+ it 'coerces correctly' do
4
+ model = Examples::DepositRange.new({ depositlow: '0', deposithigh: '999999' })
5
+ expect(model.low).to be_nil
6
+ expect(model.high).to be_nil
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ require 'attrocity/coercers/integer'
2
+
3
+ module Attrocity
4
+ RSpec.describe Coercers::Integer do
5
+ subject(:coercer) { Coercers::Integer.new }
6
+
7
+ describe '#coerce' do
8
+ it 'coerces to an integer' do
9
+ expect(coercer.coerce('1')).to eq(1)
10
+ end
11
+
12
+ describe 'given nil' do
13
+ it 'returns nil' do
14
+ expect(coercer.coerce(nil)).to be_nil
15
+ end
16
+ end
17
+
18
+ describe 'given an empty string' do
19
+ it 'returns nil' do
20
+ expect(coercer.coerce('')).to be_nil
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ require 'attrocity/coercers/string'
2
+
3
+ module Attrocity
4
+ RSpec.describe Coercers::String do
5
+ describe '#coerce' do
6
+ it 'coerces to a string' do
7
+ coercer = Coercers::String.new
8
+ expect(coercer.coerce(1)).to eq('1')
9
+ end
10
+ end
11
+ end
12
+ end
13
+
@@ -0,0 +1,22 @@
1
+ module Attrocity
2
+ RSpec.describe 'Default value' do
3
+ before do
4
+ module Examples
5
+ class DefaultValueExample
6
+ include Attrocity.model
7
+ attribute :bool, coercer: :boolean, default: true
8
+ end
9
+ end
10
+ end
11
+
12
+ it 'defaults to default value' do
13
+ example = Examples::DefaultValueExample.new
14
+ expect(example.bool).to be true
15
+ end
16
+
17
+ it 'overrides default value' do
18
+ example = Examples::DefaultValueExample.new(bool: false)
19
+ expect(example.bool).to be false
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module Attrocity
2
+ RSpec.describe KeyMapper do
3
+ let(:default_value) { nil }
4
+ subject(:mapper) { KeyMapper.new(:foo, default_value) }
5
+
6
+ it 'extracts data via hash key' do
7
+ expect(mapper.call(double, { foo: 'bar' })).to eq('bar')
8
+ end
9
+
10
+ it 'returns default value when key absent' do
11
+ expect(mapper.call(double, { biz: 'baz' })).to eq(default_value)
12
+ end
13
+
14
+ describe 'override default value' do
15
+ let(:default_value) { true }
16
+
17
+ it 'returns overridden value when key is absent' do
18
+ expect(mapper.call(double, { biz: 'baz' })).to eq(default_value)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ require 'support/examples'
2
+ require 'attrocity'
3
+
4
+ module Attrocity
5
+ RSpec.describe "Mapping" do
6
+
7
+ describe 'with default key mapper' do
8
+ let(:data) { { 'listingid' => '1234' } }
9
+ let(:listing) { Examples::Listing.new(data) }
10
+
11
+ it 'maps attribute data to attribute name' do
12
+ expect(listing.id).to eq('1234')
13
+ end
14
+
15
+ it 'does not respond to mapped name' do
16
+ expect { listing.listingid }.to raise_error(NoMethodError)
17
+ end
18
+ end
19
+
20
+ describe 'with proc mapper' do
21
+ before do
22
+ module Examples
23
+ class WithProcMapper
24
+ include Attrocity.model
25
+ attribute :age,
26
+ coercer: :integer,
27
+ from: lambda { |obj, attrs_data| attrs_data['properties']['age'] }
28
+ end
29
+ end
30
+ end
31
+
32
+ it 'maps attribute data to attribute name' do
33
+ object = Examples::WithProcMapper.new({'properties' => { 'age' => 27 } })
34
+ expect(object.age).to eq(27)
35
+ end
36
+
37
+ end
38
+
39
+ describe 'without explicit mapper' do
40
+ it 'maps attribute data to attribute name' do
41
+ person = Examples::Person.new(age: 44)
42
+ expect(person.age).to eq(44)
43
+ end
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ module Attrocity
2
+ RSpec.describe 'Attrocity.model' do
3
+ describe '#model' do
4
+ let(:person) { Examples::Person.new(age: 50) }
5
+ let(:model) { person.model }
6
+
7
+ it 'responds to a reader' do
8
+ expect(model.respond_to?(:age)).to be true
9
+ end
10
+
11
+ it 'does not respond to a writer' do
12
+ expect(model.respond_to?(:age=)).to be false
13
+ end
14
+
15
+ it 'responds to predicate'do
16
+ expect(model.respond_to?(:age?)).to be true
17
+ end
18
+
19
+ it 'has correct value for attribute' do
20
+ expect(model.age).to eq(50)
21
+ end
22
+
23
+ it 'has correct value for predicate' do
24
+ expect(model.age?).to be true
25
+ end
26
+
27
+ it 'includes model attributes' do
28
+ listing = Examples::Listing.new({ id: '1234', zip: '10101' })
29
+ model = listing.model
30
+ expect(model.address).not_to be_nil
31
+ end
32
+ end
33
+ end
34
+ end