attrocity 0.0.1 → 0.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 (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