composition 1.0.0.beta1
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 +7 -0
- data/lib/composition.rb +9 -0
- data/lib/composition/base.rb +7 -0
- data/lib/composition/builders/compose.rb +33 -0
- data/lib/composition/builders/composed_from.rb +39 -0
- data/lib/composition/compositions/compose.rb +84 -0
- data/lib/composition/compositions/composed_from.rb +72 -0
- data/lib/composition/compositions/composition.rb +25 -0
- data/lib/composition/macros/compose.rb +49 -0
- data/lib/composition/macros/composed_from.rb +49 -0
- data/lib/composition/reflection.rb +19 -0
- data/lib/composition/version.rb +3 -0
- data/spec/macros/compose_spec.rb +251 -0
- data/spec/rails_helper.rb +7 -0
- data/spec/spec_helper.rb +42 -0
- data/spec/support/apps/rails4_2.rb +50 -0
- data/spec/support/apps/rails5_0.rb +48 -0
- data/spec/support/model_macros.rb +29 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 87b706985a087f9b5576f1cd7d4b59e21ce83636
|
4
|
+
data.tar.gz: 620c33e2f2c1bdb8502334bcd63768a79caa3aa6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f72b8800a0ee152194ee3e57aef74c9e076e11edb4526bcc6a370ac8e61a990a31585a25a556c2ece4029f472d54fd17cbe7313f69381cafadb87110d94ddbb3
|
7
|
+
data.tar.gz: 8979cd300b8dc32f81efb27aadd9fdee6e186b4671185ff94ee722578b69581f2e7242c77adc4ac0b99c47960f48b7e2a65824d42a623a174dfbaeddd113d24f
|
data/lib/composition.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'composition/reflection'
|
2
|
+
require 'composition/macros/compose'
|
3
|
+
require 'composition/macros/composed_from'
|
4
|
+
require 'composition/compositions/composition'
|
5
|
+
require 'composition/compositions/compose'
|
6
|
+
require 'composition/compositions/composed_from'
|
7
|
+
require 'composition/builders/compose'
|
8
|
+
require 'composition/builders/composed_from'
|
9
|
+
require 'composition/base'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Composition
|
2
|
+
module Builders
|
3
|
+
class Compose
|
4
|
+
attr_reader :object
|
5
|
+
delegate :_composition_reflections, to: :object
|
6
|
+
|
7
|
+
def initialize(object)
|
8
|
+
@object = object
|
9
|
+
end
|
10
|
+
|
11
|
+
def def_composition_methods
|
12
|
+
_composition_reflections.each_value do |composition|
|
13
|
+
def_composition_getter(composition)
|
14
|
+
def_composition_setter(composition)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def def_composition_getter(composition)
|
21
|
+
define_method(composition.name) { composition.getter(self) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def def_composition_setter(composition)
|
25
|
+
define_method("#{composition.name}=") { |setter_value| composition.setter(self, setter_value) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def define_method(method_name, &block)
|
29
|
+
@object.class.send(:define_method, method_name, &block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Composition
|
2
|
+
module Builders
|
3
|
+
class ComposedFrom
|
4
|
+
attr_reader :object
|
5
|
+
delegate :_composition_reflections, to: :object
|
6
|
+
|
7
|
+
def initialize(object)
|
8
|
+
@object = object
|
9
|
+
end
|
10
|
+
|
11
|
+
# TODO: add documentation
|
12
|
+
def def_composition_setters
|
13
|
+
_composition_reflections.each_value do |composition|
|
14
|
+
composition.aliases.each do |attr|
|
15
|
+
def_attr_reader(attr)
|
16
|
+
define_method("#{attr}=") { |setter_value| composition.setter(self, attr, setter_value) }
|
17
|
+
define_method(:attributes) { composition.attributes(self) }
|
18
|
+
define_method(:to_h) { composition.attributes(self) }
|
19
|
+
end
|
20
|
+
def_attr_accessor(composition.name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def define_method(method_name, &block)
|
27
|
+
@object.class.send(:define_method, method_name, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def def_attr_accessor(*attr)
|
31
|
+
@object.class.send(:attr_accessor, *attr)
|
32
|
+
end
|
33
|
+
|
34
|
+
def def_attr_reader(*attr)
|
35
|
+
@object.class.send(:attr_reader, *attr)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Composition
|
2
|
+
module Compositions
|
3
|
+
class Compose < ::Composition::Compositions::Composition
|
4
|
+
|
5
|
+
# For a composition defined like:
|
6
|
+
#
|
7
|
+
# class User < ActiveRecord::Base
|
8
|
+
# compose :credit_card,
|
9
|
+
# mapping: {
|
10
|
+
# credit_card_name: :name,
|
11
|
+
# credit_card_brand: :brand
|
12
|
+
# }
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# The getter method will be in charge of implementing @user.credit_card.
|
16
|
+
#
|
17
|
+
# It is responsible for instantiating a new CreditCard object with the attributes
|
18
|
+
# from the de-normalized columns in User, and then return it.
|
19
|
+
def getter(ar)
|
20
|
+
attributes = attributes(ar)
|
21
|
+
klass.new(attributes.merge(composed_from.name => ar)).tap(&:valid?) unless all_blank?(attributes)
|
22
|
+
end
|
23
|
+
|
24
|
+
# For a composition defined like:
|
25
|
+
#
|
26
|
+
# class User < ActiveRecord::Base
|
27
|
+
# compose :credit_card,
|
28
|
+
# mapping: {
|
29
|
+
# credit_card_name: :name,
|
30
|
+
# credit_card_brand: :brand
|
31
|
+
# }
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# The setter method will be in charge of implementing @user.credit_card=.
|
35
|
+
#
|
36
|
+
# setter_value can be either a credit_card instance or a hash of attributes
|
37
|
+
# and the setter will only set the @credit_card attributes that are included
|
38
|
+
# in the hash. This means that if a credit_card attribute is not given in the hash
|
39
|
+
# then we'll set it with the value from the @user de-normalized column. The reason
|
40
|
+
# behind this is to imitate how ActiveRecord assign_attributes method works.
|
41
|
+
def setter(ar, setter_value)
|
42
|
+
nil_columns(ar) and return if setter_value.nil?
|
43
|
+
attributes = setter_value.to_h.with_indifferent_access
|
44
|
+
|
45
|
+
mapping.each do |actual_column, composed_alias|
|
46
|
+
ar.send("#{actual_column}=", attributes[composed_alias]) if attributes.key?(composed_alias)
|
47
|
+
end
|
48
|
+
|
49
|
+
setter_value
|
50
|
+
end
|
51
|
+
|
52
|
+
def mapping
|
53
|
+
@options[:mapping]
|
54
|
+
end
|
55
|
+
|
56
|
+
def actual_column_for(aliased_attribute)
|
57
|
+
mapping.key(aliased_attribute)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Returns the hash of attributes for instantiating the composition defined for a given
|
63
|
+
# class.
|
64
|
+
def attributes(ar)
|
65
|
+
mapping.each_with_object({}) do |(actual_column, composed_alias), memo|
|
66
|
+
memo[composed_alias] = ar.send(actual_column)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def all_blank?(attributes = {})
|
71
|
+
attributes.all? { |_, value| value.blank? }
|
72
|
+
end
|
73
|
+
|
74
|
+
def nil_columns(ar)
|
75
|
+
mapping.each { |actual_column, _| ar.send("#{actual_column}=", nil) }
|
76
|
+
end
|
77
|
+
|
78
|
+
# TODO: Add descriptive error if find returns nil. "composed_from is missing"
|
79
|
+
def composed_from
|
80
|
+
klass._composition_reflections.find { |_, composition| composition.class_name == inverse_of }.last
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Composition
|
2
|
+
module Compositions
|
3
|
+
class ComposedFrom < ::Composition::Compositions::Composition
|
4
|
+
delegate :mapping, to: :inverse_of_composition
|
5
|
+
|
6
|
+
# For a composition defined like:
|
7
|
+
#
|
8
|
+
# class User < ActiveRecord::Base
|
9
|
+
# compose :credit_card,
|
10
|
+
# mapping: {
|
11
|
+
# credit_card_name: :name,
|
12
|
+
# credit_card_brand: :brand
|
13
|
+
# }
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# class CreditCard < Composition::Base
|
17
|
+
# composed_from :user
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# The setter method will be in charge of implementing @credit_card.name= and @credit_card.brand=.
|
21
|
+
#
|
22
|
+
# If calling @credit_card.name= it will take care of updating the @name instance variable in
|
23
|
+
# @credit_card, but also will take care of keeping @user.credit_card_name in sync with it.
|
24
|
+
def setter(obj, setter_attr, setter_value)
|
25
|
+
set_instance_variable(obj, setter_attr, setter_value)
|
26
|
+
set_parent_attribute(obj, setter_attr, setter_value)
|
27
|
+
setter_value
|
28
|
+
end
|
29
|
+
|
30
|
+
#TODO: Add documentation
|
31
|
+
def attributes(obj)
|
32
|
+
aliases.each_with_object({}) do |attr, memo|
|
33
|
+
value = obj.send(attr)
|
34
|
+
if value.respond_to?(:attributes)
|
35
|
+
memo[attr] = value.send(:attributes)
|
36
|
+
else
|
37
|
+
memo[attr] = value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def aliases
|
43
|
+
mapping.values
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def set_instance_variable(obj, setter_attr, setter_value)
|
49
|
+
obj.instance_variable_set("@#{setter_attr}", setter_value)
|
50
|
+
end
|
51
|
+
|
52
|
+
def set_parent_attribute(obj, setter_attr, setter_value)
|
53
|
+
parent = parent_for(obj)
|
54
|
+
|
55
|
+
if parent
|
56
|
+
parent_composition = parent._composition_reflections[inverse_of]
|
57
|
+
parent.send("#{parent_composition.actual_column_for(setter_attr)}=", setter_value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def inverse_of_composition
|
62
|
+
klass._composition_reflections[inverse_of]
|
63
|
+
end
|
64
|
+
|
65
|
+
# A composition class can have more than one reference, but only one parent should be not nil
|
66
|
+
# at the same time.
|
67
|
+
def parent_for(obj)
|
68
|
+
obj._composition_reflections.map { |_, composition| obj.send(composition.name).presence }.compact.first
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Composition
|
2
|
+
module Compositions
|
3
|
+
class Composition
|
4
|
+
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(name, options = {})
|
8
|
+
@name = name
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def class_name
|
13
|
+
@options[:class_name]
|
14
|
+
end
|
15
|
+
|
16
|
+
def klass
|
17
|
+
class_name.constantize
|
18
|
+
end
|
19
|
+
|
20
|
+
def inverse_of
|
21
|
+
@options[:inverse_of]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Composition
|
2
|
+
module Macros
|
3
|
+
module Compose
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def method_missing(method_name, *args, &block)
|
7
|
+
if match_composition?(method_name)
|
8
|
+
Composition::Builders::Compose.new(self).def_composition_methods
|
9
|
+
send(method_name, *args, &block)
|
10
|
+
else
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def respond_to?(method_name, include_private = false)
|
16
|
+
if match_composition?(method_name)
|
17
|
+
Composition::Builders::Compose.new(self).def_composition_methods
|
18
|
+
true
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def match_composition?(method_id)
|
27
|
+
composition_name = method_id.to_s.gsub(/=$/, '')
|
28
|
+
_composition_reflections.any? { |_, composition| composition_name == composition.name.to_s }
|
29
|
+
end
|
30
|
+
|
31
|
+
class_methods do
|
32
|
+
def compose(*args)
|
33
|
+
composed_attribute = args.shift
|
34
|
+
options = args.last || {}
|
35
|
+
options = {
|
36
|
+
composed_attribute: composed_attribute,
|
37
|
+
mapping: options[:mapping],
|
38
|
+
class_name: options[:class_name] || composed_attribute.to_s.camelize,
|
39
|
+
inverse_of: options[:inverse_of] || model_name.name
|
40
|
+
}
|
41
|
+
composition = Compositions::Compose.new(options[:composed_attribute], options)
|
42
|
+
add_composition_reflection(self, options[:class_name], composition)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
ActiveRecord::Base.send(:include, Composition::Macros::Compose)
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Composition
|
2
|
+
module Macros
|
3
|
+
module ComposedFrom
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def method_missing(method_name, *args, &block)
|
7
|
+
if match_attribute?(method_name)
|
8
|
+
Composition::Builders::ComposedFrom.new(self).def_composition_setters
|
9
|
+
send(method_name, *args, &block)
|
10
|
+
else
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def respond_to?(method_name, include_private = false)
|
16
|
+
if match_attribute?(method_name)
|
17
|
+
Composition::Builders::ComposedFrom.new(self).def_composition_setters
|
18
|
+
true
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def match_attribute?(method_id)
|
27
|
+
if method_id.to_s.match(/=$/)
|
28
|
+
attribute = method_id.to_s.gsub(/=$/, '')
|
29
|
+
reflection = _composition_reflections.first.try(:last)
|
30
|
+
reflection.aliases.include?(attribute.to_sym)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class_methods do
|
35
|
+
def composed_from(*args)
|
36
|
+
composed_from = args.shift
|
37
|
+
options = args.last || {}
|
38
|
+
options = {
|
39
|
+
composed_from: composed_from,
|
40
|
+
class_name: options[:class_name] || composed_from.to_s.camelize,
|
41
|
+
inverse_of: options[:inverse_of] || model_name.name
|
42
|
+
}
|
43
|
+
composition = Compositions::ComposedFrom.new(options[:composed_from], options)
|
44
|
+
add_composition_reflection(self, options[:inverse_of], composition)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Composition
|
2
|
+
module Reflection
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :_composition_reflections, instance_writer: false
|
7
|
+
self._composition_reflections = {}.with_indifferent_access
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def add_composition_reflection(obj, name, reflection)
|
12
|
+
new_reflection = { name => reflection }.with_indifferent_access
|
13
|
+
obj._composition_reflections = obj._composition_reflections.merge(new_reflection)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ActiveRecord::Base.send(:include, Composition::Reflection)
|
@@ -0,0 +1,251 @@
|
|
1
|
+
describe Composition::Macros::Compose do
|
2
|
+
|
3
|
+
describe 'compose getter' do
|
4
|
+
context 'when there is at least 1 value in the composed object' do
|
5
|
+
let(:user) { User.new(credit_card_name: 'Jon Snow', credit_card_brand: 'Visa') }
|
6
|
+
|
7
|
+
before do
|
8
|
+
create_table(:users) do |t|
|
9
|
+
t.string :credit_card_name
|
10
|
+
t.string :credit_card_brand
|
11
|
+
end
|
12
|
+
|
13
|
+
spawn_model(:User) do
|
14
|
+
compose :credit_card,
|
15
|
+
mapping: {
|
16
|
+
credit_card_name: :name,
|
17
|
+
credit_card_brand: :brand
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
spawn_composition(:CreditCard) do
|
22
|
+
composed_from :user
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it { expect(user.credit_card).to be_an_instance_of(CreditCard) }
|
27
|
+
it { expect(user.credit_card.name).to eq 'Jon Snow' }
|
28
|
+
it { expect(user.credit_card.brand).to eq 'Visa' }
|
29
|
+
it { expect(user.credit_card.user).to eq user }
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'when every attribute is nil' do
|
33
|
+
let(:user) { User.new(credit_card_name: nil, credit_card_brand: nil) }
|
34
|
+
|
35
|
+
before do
|
36
|
+
create_table(:users) do |t|
|
37
|
+
t.string :credit_card_name
|
38
|
+
t.string :credit_card_brand
|
39
|
+
end
|
40
|
+
|
41
|
+
spawn_model(:User) do
|
42
|
+
compose :credit_card,
|
43
|
+
mapping: {
|
44
|
+
credit_card_name: :name,
|
45
|
+
credit_card_brand: :brand
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
spawn_composition(:CreditCard) do
|
50
|
+
composed_from :user
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it { expect(user.credit_card).to be_nil }
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when using class_name' do
|
58
|
+
let(:user) { AdminUser.new(credit_card_name: 'Jon Snow', credit_card_brand: 'Visa') }
|
59
|
+
|
60
|
+
before do
|
61
|
+
create_table(:admin_users) do |t|
|
62
|
+
t.string :credit_card_name
|
63
|
+
t.string :credit_card_brand
|
64
|
+
end
|
65
|
+
|
66
|
+
spawn_model(:AdminUser) do
|
67
|
+
compose :credit_card,
|
68
|
+
mapping: {
|
69
|
+
credit_card_name: :name,
|
70
|
+
credit_card_brand: :brand
|
71
|
+
}, class_name: 'CCard'
|
72
|
+
end
|
73
|
+
|
74
|
+
spawn_composition(:CCard) do
|
75
|
+
composed_from :user, class_name: 'AdminUser'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it { expect(user.credit_card).to be_an_instance_of(CCard) }
|
80
|
+
it { expect(user.credit_card.name).to eq 'Jon Snow' }
|
81
|
+
it { expect(user.credit_card.brand).to eq 'Visa' }
|
82
|
+
it { expect(user.credit_card.user).to eq user }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'compose setter' do
|
87
|
+
let(:user) { User.new(credit_card_name: 'Jon Snow', credit_card_brand: 'Visa') }
|
88
|
+
|
89
|
+
before do
|
90
|
+
create_table(:users) do |t|
|
91
|
+
t.string :credit_card_name
|
92
|
+
t.string :credit_card_brand
|
93
|
+
end
|
94
|
+
|
95
|
+
spawn_model(:User) do
|
96
|
+
compose :credit_card,
|
97
|
+
mapping: {
|
98
|
+
credit_card_name: :name,
|
99
|
+
credit_card_brand: :brand
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
spawn_composition(:CreditCard) do
|
104
|
+
composed_from :user
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'when setting attributes separately' do
|
109
|
+
before do
|
110
|
+
user.credit_card.name = 'Arya Stark'
|
111
|
+
user.credit_card.brand = 'MasterCard'
|
112
|
+
end
|
113
|
+
|
114
|
+
it { expect(user.credit_card.name).to eq 'Arya Stark' }
|
115
|
+
it { expect(user.credit_card_name).to eq 'Arya Stark' }
|
116
|
+
it { expect(user.credit_card.brand).to eq 'MasterCard' }
|
117
|
+
it { expect(user.credit_card_brand).to eq 'MasterCard' }
|
118
|
+
|
119
|
+
context 'and saving' do
|
120
|
+
before { user.save! && user.reload }
|
121
|
+
it { expect(user.credit_card.name).to eq 'Arya Stark' }
|
122
|
+
it { expect(user.credit_card_name).to eq 'Arya Stark' }
|
123
|
+
it { expect(user.credit_card.brand).to eq 'MasterCard' }
|
124
|
+
it { expect(user.credit_card_brand).to eq 'MasterCard' }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'when setting attributes through assign_attributes' do
|
129
|
+
before do
|
130
|
+
user.update_attributes(credit_card: { name: 'Arya Stark', brand: 'MasterCard' })
|
131
|
+
end
|
132
|
+
|
133
|
+
it { expect(user.credit_card.name).to eq 'Arya Stark' }
|
134
|
+
it { expect(user.credit_card_name).to eq 'Arya Stark' }
|
135
|
+
it { expect(user.credit_card.brand).to eq 'MasterCard' }
|
136
|
+
it { expect(user.credit_card_brand).to eq 'MasterCard' }
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'when setting attributes (partially) through assign_attributes' do
|
140
|
+
before do
|
141
|
+
user.update_attributes(credit_card: { brand: 'MasterCard' })
|
142
|
+
end
|
143
|
+
|
144
|
+
it { expect(user.credit_card.name).to eq 'Jon Snow' }
|
145
|
+
it { expect(user.credit_card_name).to eq 'Jon Snow' }
|
146
|
+
it { expect(user.credit_card.brand).to eq 'MasterCard' }
|
147
|
+
it { expect(user.credit_card_brand).to eq 'MasterCard' }
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'when setting attributes using a new object' do
|
151
|
+
before do
|
152
|
+
user.credit_card = CreditCard.new(name: 'Arya Stark', brand: 'MasterCard')
|
153
|
+
end
|
154
|
+
|
155
|
+
it { expect(user.credit_card.name).to eq 'Arya Stark' }
|
156
|
+
it { expect(user.credit_card_name).to eq 'Arya Stark' }
|
157
|
+
it { expect(user.credit_card.brand).to eq 'MasterCard' }
|
158
|
+
it { expect(user.credit_card_brand).to eq 'MasterCard' }
|
159
|
+
end
|
160
|
+
|
161
|
+
context 'when setting attribute through the base class' do
|
162
|
+
before do
|
163
|
+
user.credit_card_name = 'Arya Stark'
|
164
|
+
user.credit_card_brand = 'MasterCard'
|
165
|
+
end
|
166
|
+
|
167
|
+
it { expect(user.credit_card.name).to eq 'Arya Stark' }
|
168
|
+
it { expect(user.credit_card_name).to eq 'Arya Stark' }
|
169
|
+
it { expect(user.credit_card.brand).to eq 'MasterCard' }
|
170
|
+
it { expect(user.credit_card_brand).to eq 'MasterCard' }
|
171
|
+
end
|
172
|
+
|
173
|
+
context 'when setting to nil using =' do
|
174
|
+
before { user.credit_card = nil }
|
175
|
+
|
176
|
+
it { expect(user.credit_card).to be_nil }
|
177
|
+
it { expect(user.credit_card_name).to be_nil }
|
178
|
+
it { expect(user.credit_card_brand).to be_nil }
|
179
|
+
end
|
180
|
+
|
181
|
+
context 'when setting to nil using {}' do
|
182
|
+
before { user.update_attributes(credit_card: nil) }
|
183
|
+
|
184
|
+
it { expect(user.credit_card).to be_nil }
|
185
|
+
it { expect(user.credit_card_name).to be_nil }
|
186
|
+
it { expect(user.credit_card_brand).to be_nil }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
describe 'validations' do
|
191
|
+
let(:user) { User.new(credit_card_name: 'Jon Snow', credit_card_brand: 'Visa') }
|
192
|
+
|
193
|
+
before do
|
194
|
+
create_table(:users) do |t|
|
195
|
+
t.string :credit_card_name
|
196
|
+
t.string :credit_card_brand
|
197
|
+
end
|
198
|
+
|
199
|
+
spawn_model(:User) do
|
200
|
+
compose :credit_card,
|
201
|
+
mapping: {
|
202
|
+
credit_card_name: :name,
|
203
|
+
credit_card_brand: :brand
|
204
|
+
}
|
205
|
+
end
|
206
|
+
|
207
|
+
spawn_composition(:CreditCard) do
|
208
|
+
composed_from :user
|
209
|
+
|
210
|
+
validates :name, presence: true
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
context 'when credit_card is valid' do
|
215
|
+
it { expect(user.credit_card).to be_valid }
|
216
|
+
end
|
217
|
+
|
218
|
+
context 'when credit_card is not valid' do
|
219
|
+
before { user.credit_card.name = '' }
|
220
|
+
|
221
|
+
it { expect(user.credit_card).not_to be_valid }
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
describe '#attributes' do
|
226
|
+
let(:user) { User.new(credit_card_name: 'Jon Snow', credit_card_brand: 'Visa') }
|
227
|
+
|
228
|
+
before do
|
229
|
+
create_table(:users) do |t|
|
230
|
+
t.string :credit_card_name
|
231
|
+
t.string :credit_card_brand
|
232
|
+
end
|
233
|
+
|
234
|
+
spawn_model(:User) do
|
235
|
+
compose :credit_card,
|
236
|
+
mapping: {
|
237
|
+
credit_card_name: :name,
|
238
|
+
credit_card_brand: :brand
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
spawn_composition(:CreditCard) do
|
243
|
+
composed_from :user
|
244
|
+
|
245
|
+
validates :name, presence: true
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
it { expect(user.credit_card.attributes).to eq(name: 'Jon Snow', brand: 'Visa') }
|
250
|
+
end
|
251
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
ENV['RAILS_ENV'] = 'test'
|
2
|
+
ENV['DATABASE_URL'] = 'sqlite3://localhost/tmp/composition_test'
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'rails'
|
6
|
+
case Rails.version
|
7
|
+
when '4.2.7.1'
|
8
|
+
require 'support/apps/rails4_2'
|
9
|
+
when '5.0.2'
|
10
|
+
require 'support/apps/rails5_0'
|
11
|
+
end
|
12
|
+
require 'support/model_macros'
|
13
|
+
require 'composition'
|
14
|
+
|
15
|
+
RSpec.configure do |config|
|
16
|
+
config.expect_with :rspec do |expectations|
|
17
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
18
|
+
end
|
19
|
+
|
20
|
+
config.mock_with :rspec do |mocks|
|
21
|
+
mocks.verify_partial_doubles = true
|
22
|
+
end
|
23
|
+
|
24
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
25
|
+
|
26
|
+
config.include Composition::Testing::ModelMacros
|
27
|
+
|
28
|
+
config.before :each do
|
29
|
+
@spawned_models = []
|
30
|
+
@spawned_compositions = []
|
31
|
+
end
|
32
|
+
|
33
|
+
config.after :each do
|
34
|
+
@spawned_models.each do |model|
|
35
|
+
Object.instance_eval { remove_const model } if Object.const_defined?(model)
|
36
|
+
end
|
37
|
+
|
38
|
+
@spawned_compositions.each do |model|
|
39
|
+
Object.instance_eval { remove_const model } if Object.const_defined?(model)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rails/all'
|
2
|
+
|
3
|
+
module Rails42
|
4
|
+
class Application < Rails::Application
|
5
|
+
config.active_record.raise_in_transactional_callbacks = true
|
6
|
+
# Settings specified here will take precedence over those in config/application.rb.
|
7
|
+
|
8
|
+
# In the development environment your application's code is reloaded on
|
9
|
+
# every request. This slows down response time but is perfect for development
|
10
|
+
# since you don't have to restart the web server when you make code changes.
|
11
|
+
config.cache_classes = false
|
12
|
+
|
13
|
+
# Do not eager load code on boot.
|
14
|
+
config.eager_load = false
|
15
|
+
|
16
|
+
# Show full error reports and disable caching.
|
17
|
+
config.consider_all_requests_local = true
|
18
|
+
config.action_controller.perform_caching = false
|
19
|
+
|
20
|
+
# Don't care if the mailer can't send.
|
21
|
+
config.action_mailer.raise_delivery_errors = false
|
22
|
+
|
23
|
+
# Print deprecation notices to the Rails logger.
|
24
|
+
config.active_support.deprecation = :log
|
25
|
+
|
26
|
+
# Raise an error on page load if there are pending migrations.
|
27
|
+
config.active_record.migration_error = :page_load
|
28
|
+
|
29
|
+
# Debug mode disables concatenation and preprocessing of assets.
|
30
|
+
# This option may cause significant delays in view rendering with a large
|
31
|
+
# number of complex assets.
|
32
|
+
config.assets.debug = true
|
33
|
+
|
34
|
+
# Asset digests allow you to set far-future HTTP expiration dates on all assets,
|
35
|
+
# yet still be able to expire them through the digest params.
|
36
|
+
config.assets.digest = true
|
37
|
+
|
38
|
+
# Adds additional error checking when serving assets at runtime.
|
39
|
+
# Checks for improperly declared sprockets dependencies.
|
40
|
+
# Raises helpful error messages.
|
41
|
+
config.assets.raise_runtime_errors = true
|
42
|
+
|
43
|
+
# Raises error for missing translations
|
44
|
+
# config.action_view.raise_on_missing_translations = true
|
45
|
+
|
46
|
+
config.secret_key_base = '49837489qkuweoiuoqwehisuakshdjksadhaisdy78o34y138974xyqp9rmye8yrpiokeuioqwzyoiuxftoyqiuxrhm3iou1hrzmjk'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Rails.application.initialize!
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rails/all'
|
2
|
+
|
3
|
+
module Rails50
|
4
|
+
class Application < Rails::Application
|
5
|
+
# The test environment is used exclusively to run your application's
|
6
|
+
# test suite. You never need to work with it otherwise. Remember that
|
7
|
+
# your test database is "scratch space" for the test suite and is wiped
|
8
|
+
# and recreated between test runs. Don't rely on the data there!
|
9
|
+
config.cache_classes = true
|
10
|
+
|
11
|
+
# Do not eager load code on boot. This avoids loading your whole application
|
12
|
+
# just for the purpose of running a single test. If you are using a tool that
|
13
|
+
# preloads Rails for running tests, you may have to set it to true.
|
14
|
+
config.eager_load = false
|
15
|
+
|
16
|
+
# Configure public file server for tests with Cache-Control for performance.
|
17
|
+
config.public_file_server.enabled = true
|
18
|
+
config.public_file_server.headers = {
|
19
|
+
'Cache-Control' => 'public, max-age=3600'
|
20
|
+
}
|
21
|
+
|
22
|
+
# Show full error reports and disable caching.
|
23
|
+
config.consider_all_requests_local = true
|
24
|
+
config.action_controller.perform_caching = false
|
25
|
+
|
26
|
+
# Raise exceptions instead of rendering exception templates.
|
27
|
+
config.action_dispatch.show_exceptions = false
|
28
|
+
|
29
|
+
# Disable request forgery protection in test environment.
|
30
|
+
config.action_controller.allow_forgery_protection = false
|
31
|
+
config.action_mailer.perform_caching = false
|
32
|
+
|
33
|
+
# Tell Action Mailer not to deliver emails to the real world.
|
34
|
+
# The :test delivery method accumulates sent emails in the
|
35
|
+
# ActionMailer::Base.deliveries array.
|
36
|
+
config.action_mailer.delivery_method = :test
|
37
|
+
|
38
|
+
# Print deprecation notices to the stderr.
|
39
|
+
config.active_support.deprecation = :stderr
|
40
|
+
|
41
|
+
# Raises error for missing translations
|
42
|
+
# config.action_view.raise_on_missing_translations = true
|
43
|
+
|
44
|
+
config.secret_key_base = '49837489qkuweoiuoqwehisuakshdjksadhaisdy78o34y138974xyqp9rmye8yrpiokeuioqwzyoiuxftoyqiuxrhm3iou1hrzmjk'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Rails.application.initialize!
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Composition
|
2
|
+
module Testing
|
3
|
+
module ModelMacros
|
4
|
+
def spawn_model(klass, &block)
|
5
|
+
Object.instance_eval { remove_const klass } if Object.const_defined?(klass)
|
6
|
+
Object.const_set klass, Class.new(ActiveRecord::Base)
|
7
|
+
Object.const_get(klass).class_eval(&block) if block_given?
|
8
|
+
@spawned_models << klass.to_sym
|
9
|
+
end
|
10
|
+
|
11
|
+
def spawn_composition(klass, &block)
|
12
|
+
Object.instance_eval { remove_const klass } if Object.const_defined?(klass)
|
13
|
+
Object.const_set klass, Class.new(Composition::Base)
|
14
|
+
Object.const_get(klass).class_eval(&block) if block_given?
|
15
|
+
@spawned_compositions << klass.to_sym
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_table(table_name)
|
19
|
+
ActiveRecord::Migration.suppress_messages do
|
20
|
+
ActiveRecord::Schema.define do
|
21
|
+
create_table table_name, force: true do |t|
|
22
|
+
yield(t)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: composition
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.beta1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Marcelo Casiraghi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-03-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: appraisal
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rails
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sqlite3
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: Composition for ActiveRecord models
|
126
|
+
email: marcelo@paragon-labs.com
|
127
|
+
executables: []
|
128
|
+
extensions: []
|
129
|
+
extra_rdoc_files: []
|
130
|
+
files:
|
131
|
+
- lib/composition.rb
|
132
|
+
- lib/composition/base.rb
|
133
|
+
- lib/composition/builders/compose.rb
|
134
|
+
- lib/composition/builders/composed_from.rb
|
135
|
+
- lib/composition/compositions/compose.rb
|
136
|
+
- lib/composition/compositions/composed_from.rb
|
137
|
+
- lib/composition/compositions/composition.rb
|
138
|
+
- lib/composition/macros/compose.rb
|
139
|
+
- lib/composition/macros/composed_from.rb
|
140
|
+
- lib/composition/reflection.rb
|
141
|
+
- lib/composition/version.rb
|
142
|
+
- spec/macros/compose_spec.rb
|
143
|
+
- spec/rails_helper.rb
|
144
|
+
- spec/spec_helper.rb
|
145
|
+
- spec/support/apps/rails4_2.rb
|
146
|
+
- spec/support/apps/rails5_0.rb
|
147
|
+
- spec/support/model_macros.rb
|
148
|
+
homepage: https://github.com/marceloeloelo/composition
|
149
|
+
licenses:
|
150
|
+
- MIT
|
151
|
+
metadata: {}
|
152
|
+
post_install_message:
|
153
|
+
rdoc_options: []
|
154
|
+
require_paths:
|
155
|
+
- lib
|
156
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
157
|
+
requirements:
|
158
|
+
- - ">="
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '0'
|
161
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">"
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: 1.3.1
|
166
|
+
requirements: []
|
167
|
+
rubyforge_project:
|
168
|
+
rubygems_version: 2.6.10
|
169
|
+
signing_key:
|
170
|
+
specification_version: 4
|
171
|
+
summary: Composition for ActiveRecord models
|
172
|
+
test_files:
|
173
|
+
- spec/macros/compose_spec.rb
|
174
|
+
- spec/rails_helper.rb
|
175
|
+
- spec/spec_helper.rb
|
176
|
+
- spec/support/apps/rails4_2.rb
|
177
|
+
- spec/support/apps/rails5_0.rb
|
178
|
+
- spec/support/model_macros.rb
|