fabrique 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -0
- data/Gemfile +5 -0
- data/Guardfile +9 -0
- data/README.md +152 -3
- data/Rakefile +11 -3
- data/config/cucumber.yml +2 -0
- data/constructors +70 -0
- data/docs/autowiring.yml +23 -0
- data/docs/multiple_providers.rb +104 -0
- data/fabrique.gemspec +3 -0
- data/features/bean_factory.feature +295 -0
- data/features/plugin_registry.feature +79 -0
- data/features/step_definitions/bean_factory_steps.rb +73 -0
- data/features/step_definitions/plugin_registry_steps.rb +207 -0
- data/features/support/byebug.rb +4 -0
- data/lib/fabrique/argument_adaptor/keyword.rb +19 -0
- data/lib/fabrique/argument_adaptor/positional.rb +76 -0
- data/lib/fabrique/bean_definition.rb +46 -0
- data/lib/fabrique/bean_definition_registry.rb +43 -0
- data/lib/fabrique/bean_factory.rb +78 -0
- data/lib/fabrique/bean_reference.rb +13 -0
- data/lib/fabrique/construction/as_is.rb +16 -0
- data/lib/fabrique/construction/builder_method.rb +21 -0
- data/lib/fabrique/construction/default.rb +17 -0
- data/lib/fabrique/construction/keyword_argument.rb +16 -0
- data/lib/fabrique/construction/positional_argument.rb +40 -0
- data/lib/fabrique/construction/properties_hash.rb +19 -0
- data/lib/fabrique/constructor/identity.rb +10 -0
- data/lib/fabrique/cyclic_bean_dependency_error.rb +6 -0
- data/lib/fabrique/plugin_registry.rb +56 -0
- data/lib/fabrique/test/fixtures/constructors.rb +81 -0
- data/lib/fabrique/test/fixtures/modules.rb +35 -0
- data/lib/fabrique/test/fixtures/opengl.rb +37 -0
- data/lib/fabrique/test/fixtures/repository.rb +139 -0
- data/lib/fabrique/test.rb +8 -0
- data/lib/fabrique/version.rb +1 -1
- data/lib/fabrique/yaml_bean_factory.rb +42 -0
- data/lib/fabrique.rb +4 -2
- data/spec/fabrique/argument_adaptor/keyword_spec.rb +50 -0
- data/spec/fabrique/argument_adaptor/positional_spec.rb +166 -0
- data/spec/fabrique/construction/as_is_spec.rb +23 -0
- data/spec/fabrique/construction/builder_method_spec.rb +29 -0
- data/spec/fabrique/construction/default_spec.rb +19 -0
- data/spec/fabrique/construction/positional_argument_spec.rb +61 -0
- data/spec/fabrique/construction/properties_hash_spec.rb +36 -0
- data/spec/fabrique/constructor/identity_spec.rb +4 -0
- data/spec/fabrique/plugin_registry_spec.rb +78 -0
- data/spec/fabrique_spec.rb +0 -4
- metadata +72 -4
@@ -0,0 +1,139 @@
|
|
1
|
+
module Fabrique
|
2
|
+
|
3
|
+
module Test
|
4
|
+
|
5
|
+
module Fixtures
|
6
|
+
|
7
|
+
module Repository
|
8
|
+
|
9
|
+
class Customer
|
10
|
+
|
11
|
+
attr_reader :id, :name, :date_of_birth
|
12
|
+
|
13
|
+
def initialize(id: nil, name: nil, date_of_birth: nil)
|
14
|
+
@id, @name, @date_of_birth = id, name, date_of_birth
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class CustomerRepository
|
20
|
+
|
21
|
+
# Exposed for testing
|
22
|
+
attr_reader :store, :data_mapper
|
23
|
+
|
24
|
+
def initialize(store, data_mapper)
|
25
|
+
@store, @data_mapper = store, data_mapper
|
26
|
+
end
|
27
|
+
|
28
|
+
def persist(entity)
|
29
|
+
id = @store.save(:customer, @data_mapper.to_dto(entity))
|
30
|
+
if !id.nil?
|
31
|
+
entity.id = id
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def locate(entity_id)
|
36
|
+
@data_mapper.from_dto(@store.find(:customer, entity_id))
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
class ProductRepository
|
42
|
+
|
43
|
+
# Exposed for testing
|
44
|
+
attr_reader :store, :data_mapper
|
45
|
+
|
46
|
+
def initialize(store: nil, data_mapper: nil)
|
47
|
+
@store, @data_mapper = store, data_mapper
|
48
|
+
end
|
49
|
+
|
50
|
+
def persist(entity)
|
51
|
+
id = @store.save(:customer, @data_mapper.to_dto(entity))
|
52
|
+
if !id.nil?
|
53
|
+
entity.id = id
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def search(filter)
|
58
|
+
@data_mapper.from_dto(@store.search(:customer, filter))
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
class MysqlConnection
|
64
|
+
|
65
|
+
def initialize(*args)
|
66
|
+
# ...
|
67
|
+
end
|
68
|
+
|
69
|
+
def method_missing(method_sym, *arguments)
|
70
|
+
42
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
class MysqlStore
|
76
|
+
|
77
|
+
def initialize(host: 'localhost', port: 3306, username: nil, password: nil)
|
78
|
+
@connection = MysqlConnection.new(host, port, username, password)
|
79
|
+
end
|
80
|
+
|
81
|
+
def find(table, id)
|
82
|
+
@connection.find(table, "WHERE customer_id = ?", [id])
|
83
|
+
end
|
84
|
+
|
85
|
+
def search(table, filter)
|
86
|
+
clauses, bindings = [], []
|
87
|
+
filter.each do |k, v|
|
88
|
+
clauses << k
|
89
|
+
bindings << v
|
90
|
+
end
|
91
|
+
@connection.select(table, clauses.join(" AND "), bindings)
|
92
|
+
end
|
93
|
+
|
94
|
+
def save(table, record)
|
95
|
+
@connection.update_or_insert(table, record)
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
require "date"
|
101
|
+
class CustomerDataMapper
|
102
|
+
|
103
|
+
def to_dto(entity)
|
104
|
+
{
|
105
|
+
id: entity.id,
|
106
|
+
name: entity.name,
|
107
|
+
dob: entity.date_of_birth.iso8601
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def from_dto(dto)
|
112
|
+
Customer.new(id: dto[:id], name: dto[:name], date_of_birth: Date.parse(dto[:dob]))
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
class ProductDataMapper
|
118
|
+
|
119
|
+
def to_dto(entity)
|
120
|
+
{
|
121
|
+
id: entity.id,
|
122
|
+
name: entity.name,
|
123
|
+
price: entity.price,
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
def from_dto(dto)
|
128
|
+
Product.new(id: dto[:id], name: dto[:name], price: dto[:price])
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
data/lib/fabrique/version.rb
CHANGED
@@ -0,0 +1,42 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require_relative "bean_definition_registry"
|
3
|
+
require_relative "bean_definition"
|
4
|
+
require_relative "bean_factory"
|
5
|
+
require_relative "bean_reference"
|
6
|
+
|
7
|
+
module Fabrique
|
8
|
+
|
9
|
+
YAML.add_domain_type("starjuice.net,2015-03-13", "beans") do |type, value|
|
10
|
+
BeanDefinitionRegistry.new(value)
|
11
|
+
end
|
12
|
+
|
13
|
+
YAML.add_domain_type("starjuice.net,2015-03-13", "bean") do |type, value|
|
14
|
+
BeanDefinition.new(value)
|
15
|
+
end
|
16
|
+
|
17
|
+
YAML.add_domain_type("starjuice.net,2015-03-13", "bean/ref") do |type, value|
|
18
|
+
BeanReference.new(value)
|
19
|
+
end
|
20
|
+
|
21
|
+
class YamlBeanFactory < BeanFactory
|
22
|
+
|
23
|
+
def initialize(pathname)
|
24
|
+
data = YAML.load_file(pathname)
|
25
|
+
if data.respond_to?(:keys) and data["beans"]
|
26
|
+
beans = data["beans"]
|
27
|
+
else
|
28
|
+
raise "YAML contains no top-level beans node"
|
29
|
+
end
|
30
|
+
|
31
|
+
if beans.is_a?(BeanDefinitionRegistry)
|
32
|
+
super(beans)
|
33
|
+
elsif beans.is_a?(Array)
|
34
|
+
super(BeanDefinitionRegistry.new(beans))
|
35
|
+
else
|
36
|
+
raise "YAML top-level beans node must be an Array or a #{BeanDefinitionRegistry}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/lib/fabrique.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
1
|
+
Dir[File.join(File.dirname(__FILE__), "fabrique", "*.rb")].each { |f| require_relative f }
|
2
|
+
Dir[File.join(File.dirname(__FILE__), "fabrique", "construction", "*.rb")].each { |f| require_relative f }
|
3
|
+
Dir[File.join(File.dirname(__FILE__), "fabrique", "constructor", "*.rb")].each { |f| require_relative f }
|
4
|
+
Dir[File.join(File.dirname(__FILE__), "fabrique", "argument_adaptor", "*.rb")].each { |f| require_relative f }
|
2
5
|
|
3
6
|
module Fabrique
|
4
|
-
# Your code goes here...
|
5
7
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "fabrique/test"
|
3
|
+
|
4
|
+
describe Fabrique::ArgumentAdaptor::Keyword do
|
5
|
+
|
6
|
+
let(:properties) { {size: "small", color: "red", shape: "dot"} }
|
7
|
+
|
8
|
+
describe "#adapt(*properties)" do
|
9
|
+
|
10
|
+
context "when called without properties" do
|
11
|
+
|
12
|
+
it "returns an empty array" do
|
13
|
+
expect(subject.adapt).to eql []
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
context "when called with empty properties" do
|
19
|
+
|
20
|
+
it "returns an empty hash in an empty array" do
|
21
|
+
expect(subject.adapt({})).to eql [{}]
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when called with properties" do
|
27
|
+
|
28
|
+
it "returns a hash containing the properties" do
|
29
|
+
expect(subject.adapt(properties)).to eql [properties]
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
it "supports properties hash constructors" do
|
35
|
+
object = Fabrique::Test::Fixtures::Constructors::ClassWithPropertiesHashConstructor.new(*subject.adapt(properties))
|
36
|
+
expect(object.size).to eql properties[:size]
|
37
|
+
expect(object.color).to eql properties[:color]
|
38
|
+
expect(object.shape).to eql properties[:shape]
|
39
|
+
end
|
40
|
+
|
41
|
+
it "supports keyword argument constructors" do
|
42
|
+
object = Fabrique::Test::Fixtures::Constructors::ClassWithKeywordArgumentConstructor.new(*subject.adapt(properties))
|
43
|
+
expect(object.size).to eql properties[:size]
|
44
|
+
expect(object.color).to eql properties[:color]
|
45
|
+
expect(object.shape).to eql properties[:shape]
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Fabrique::ArgumentAdaptor::Positional do
|
4
|
+
|
5
|
+
describe "::new(*argument_specifieres)" do
|
6
|
+
|
7
|
+
it "takes symbols as required argument names" do
|
8
|
+
expect { described_class.new(:arg_name1, :arg_name2, :arg_name3) }.to_not raise_error
|
9
|
+
end
|
10
|
+
|
11
|
+
it "takes one-element arrays as optional argument names with no default value" do
|
12
|
+
expect { described_class.new(:arg_name1, :arg_name2, [:arg_name3]) }.to_not raise_error
|
13
|
+
end
|
14
|
+
|
15
|
+
it "takes two-element arrays as optional argument names with a default value" do
|
16
|
+
expect { described_class.new(:arg_name1, [:arg_name2], [:arg_name3, "default arg3"]) }.to_not raise_error
|
17
|
+
end
|
18
|
+
|
19
|
+
it "allows no argument names (useful for a default constructor)" do
|
20
|
+
expect { described_class.new }.to_not raise_error
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when passed arguments that are not symbols or optional argument specifier arrays" do
|
24
|
+
|
25
|
+
it "raises an ArgumentError" do
|
26
|
+
expect { described_class.new(:arg_name1, "arg_name2") }.to raise_error(ArgumentError, /invalid argument specifier/)
|
27
|
+
expect { described_class.new(:arg_name1, []) }.to raise_error(ArgumentError, /invalid argument specifier/)
|
28
|
+
expect { described_class.new(:arg_name1, ["arg_name2"]) }.to raise_error(ArgumentError, /invalid argument specifier/)
|
29
|
+
expect { described_class.new(:arg_name1, [:arg_name2, "value", "nonsense"]) }.to raise_error(ArgumentError, /invalid argument specifier/)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#adapt(*properties)" do
|
37
|
+
|
38
|
+
context "when initialized with no required argument specifiers" do
|
39
|
+
context "when called without properties" do
|
40
|
+
it "returns an empty array" do
|
41
|
+
expect(subject.adapt).to eql []
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "when called with empty properties" do
|
46
|
+
it "returns an empty array" do
|
47
|
+
expect(subject.adapt({})).to eql []
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when called with properties" do
|
52
|
+
it "returns an empty array" do
|
53
|
+
expect(subject.adapt(size: "small", color: "red", shape: "dot")).to eql []
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "when initialized with only required argument names" do
|
59
|
+
subject { described_class.new(:size, :color, :shape) }
|
60
|
+
|
61
|
+
context "when called with a property for each argument name" do
|
62
|
+
it "returns an array of the property value of each argument name in the order specified to new()" do
|
63
|
+
expect(subject.adapt(shape: "dot", size: "small", color: "red")).to eql ["small", "red", "dot"]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "when called with at least one property missing for an argument name" do
|
68
|
+
it "raises an ArgumentError" do
|
69
|
+
expect { subject.adapt(size: "small", color: "red") }.to raise_error(ArgumentError, /required argument \w+ missing from properties/)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "when called with extraneous properties" do
|
74
|
+
it "ignores them, returning an array of the property value of each argument name in the order specified to new()" do
|
75
|
+
expect(subject.adapt(size: "small", color: "red", shape: "dot", status: "on")).to eql ["small", "red", "dot"]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "when initialized with only optional argument names with default values" do
|
81
|
+
subject { described_class.new([:size, "default size"], [:color, "default color"], [:shape, "default shape"]) }
|
82
|
+
|
83
|
+
context "when called with a property for each argument name" do
|
84
|
+
it "returns an array of the property value of each argument name in the order specified to new()" do
|
85
|
+
expect(subject.adapt(shape: "dot", size: "small", color: "red")).to eql ["small", "red", "dot"]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "when called with at least one property missing for an argument name" do
|
90
|
+
it "returns an array of the provided property values and default values, in the order specified to new()" do
|
91
|
+
expect(subject.adapt(size: "small", color: "red")).to eql ["small", "red", "default shape"]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "when called with extraneous properties" do
|
96
|
+
it "ignores them" do
|
97
|
+
expect(subject.adapt(size: "small", color: "red", status: "on")).to eql ["small", "red", "default shape"]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
context "when initialized with optional argument names with no default values, followed by required argument names" do
|
104
|
+
subject { described_class.new([:size], [:color], :shape) }
|
105
|
+
|
106
|
+
context "when called with a property for each argument name" do
|
107
|
+
it "returns an array of the property value of each argument name in the order specified to new()" do
|
108
|
+
expect(subject.adapt(shape: "dot", size: "small", color: "red")).to eql ["small", "red", "dot"]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "when called with at least one property missing for an optional argument" do
|
113
|
+
it "raises an ArgumentError (because the caller would be surprised by Ruby filling optional arguments from left to right)" do
|
114
|
+
expect {subject.adapt(color: "red", shape: "dot") }.to raise_error(ArgumentError, /optional argument size \(with no default\) missing from properties/)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context "when called with at least one property missing for a required argument" do
|
119
|
+
it "raises an ArgumentError" do
|
120
|
+
expect { subject.adapt(size: "small", color: "red") }.to raise_error(ArgumentError, /required argument \w+ missing from properties/)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context "when initialized with optional argument names with nil as the default value, followed by required argument names" do
|
126
|
+
subject { described_class.new([:size, nil], [:color, nil], :shape) }
|
127
|
+
|
128
|
+
context "when called with a property for each argument name" do
|
129
|
+
it "returns an array of the property value of each argument name in the order specified to new()" do
|
130
|
+
expect(subject.adapt(shape: "dot", size: "small", color: "red")).to eql ["small", "red", "dot"]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context "when called with at least one property missing for an optional argument" do
|
135
|
+
it "returns an array of the provided property values and default values, in the order specified to new()" do
|
136
|
+
expect(subject.adapt(color: "red", shape: "dot")).to eql [nil, "red", "dot"]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context "when called with at least one property missing for a required argument" do
|
141
|
+
it "raises an ArgumentError" do
|
142
|
+
expect { subject.adapt(size: "small", color: "red") }.to raise_error(ArgumentError, /required argument \w+ missing from properties/)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
it "supports default constructors" do
|
148
|
+
klass = Fabrique::Test::Fixtures::Constructors::ClassWithDefaultConstructor
|
149
|
+
object = klass.new(*subject.adapt())
|
150
|
+
expect(object.size).to eql klass::DEFAULT_SIZE
|
151
|
+
expect(object.color).to eql klass::DEFAULT_COLOR
|
152
|
+
expect(object.shape).to eql klass::DEFAULT_SHAPE
|
153
|
+
end
|
154
|
+
|
155
|
+
it "supports positional argument constructors" do
|
156
|
+
klass = Fabrique::Test::Fixtures::Constructors::ClassWithPositionalArgumentConstructor
|
157
|
+
subject = described_class.new(:size, :color, :shape)
|
158
|
+
object = klass.new(*subject.adapt(size: "small", color: "red", shape: "dot"))
|
159
|
+
expect(object.size).to eql "small"
|
160
|
+
expect(object.color).to eql "red"
|
161
|
+
expect(object.shape).to eql "dot"
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Fabrique::Construction::AsIs do
|
4
|
+
|
5
|
+
describe "call(type, properties = nil)" do
|
6
|
+
|
7
|
+
it "applies identity to the type" do
|
8
|
+
constructed = subject.call(type = Object.new)
|
9
|
+
expect(constructed.object_id).to eql type.object_id
|
10
|
+
end
|
11
|
+
|
12
|
+
it "accepts optional properties to support construction interface" do
|
13
|
+
constructed = subject.call(type = Object.new, {})
|
14
|
+
expect(constructed.object_id).to eql type.object_id
|
15
|
+
end
|
16
|
+
|
17
|
+
it "raises an ArgumentError if properties is specified and not empty" do
|
18
|
+
expect { subject.call(Object.new, {some: "properties"}) }.to raise_error(ArgumentError, /unexpected properties/)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "fabrique/test"
|
3
|
+
|
4
|
+
describe Fabrique::Construction::BuilderMethod do
|
5
|
+
|
6
|
+
describe "call(type, properties = nil)" do
|
7
|
+
|
8
|
+
context "when initialized with a builder method name and builder runner block" do
|
9
|
+
|
10
|
+
subject do
|
11
|
+
described_class.new(:build) do |builder, properties|
|
12
|
+
builder.size = properties[:size]
|
13
|
+
builder.color = properties[:color]
|
14
|
+
builder.shape = properties[:shape]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it "calls the builder method on the type and yields the builder and the specified properties to the builder runner block" do
|
19
|
+
o = subject.call(Fabrique::Test::Fixtures::Constructors::ClassWithBuilderMethod, size: "huge", color: "black", shape: "hole")
|
20
|
+
expect(o.size).to eql "huge"
|
21
|
+
expect(o.color).to eql "black"
|
22
|
+
expect(o.shape).to eql "hole"
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Fabrique::Construction::Default do
|
4
|
+
|
5
|
+
describe "call(type, properties = nil)" do
|
6
|
+
|
7
|
+
it "calls type.new() without arguments" do
|
8
|
+
constructed = subject.call(type = Object)
|
9
|
+
expect(constructed.class).to eql type
|
10
|
+
end
|
11
|
+
|
12
|
+
it "accepts optional properties to support construction interface" do
|
13
|
+
constructed = subject.call(type = Object, {})
|
14
|
+
expect(constructed.class).to eql type
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
class PositionalArgumentFixture
|
4
|
+
attr_reader :color, :shape, :size
|
5
|
+
|
6
|
+
DEFAULT_ATTRS = {color: "red", shape: "dot", size: "small"} unless defined?(DEFAULT_ATTRS)
|
7
|
+
|
8
|
+
def initialize(color = DEFAULT_ATTRS[:color], shape = DEFAULT_ATTRS[:shape], size = DEFAULT_ATTRS[:size])
|
9
|
+
@color, @shape, @size = color, shape, size
|
10
|
+
end
|
11
|
+
|
12
|
+
def attrs
|
13
|
+
{color: @color, shape: @shape, size: @size}
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
describe Fabrique::Construction::PositionalArgument do
|
19
|
+
|
20
|
+
let(:type) { PositionalArgumentFixture }
|
21
|
+
|
22
|
+
describe "call(type, properties = nil)" do
|
23
|
+
|
24
|
+
it "applies positional argument construction to the type, in the order they were provided to new()" do
|
25
|
+
subject = described_class.new(:color, :shape, :size)
|
26
|
+
constructed = subject.call(type, {size: "tiny", color: "purple", shape: "dot"})
|
27
|
+
expect(constructed.attrs).to eql({color: "purple", shape: "dot", size: "tiny"})
|
28
|
+
end
|
29
|
+
|
30
|
+
it "calls type.new() if no arguments were specified to new()" do
|
31
|
+
subject = described_class.new()
|
32
|
+
type = spy('type')
|
33
|
+
subject.call(type, {size: "tiny", color: "purple", shape: "dot"})
|
34
|
+
expect(type).to have_received(:new).with(no_args)
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when one or more optional arguments were specified to new()" do
|
38
|
+
|
39
|
+
subject = described_class.new(:color, [:shape, :size])
|
40
|
+
|
41
|
+
it "passes optional arguments provided in properties" do
|
42
|
+
type = spy('type')
|
43
|
+
subject.call(type, {color: "purple", shape: "dot", size: "tiny"})
|
44
|
+
expect(type).to have_received(:new).with("purple", "dot", "tiny")
|
45
|
+
end
|
46
|
+
|
47
|
+
it "discards optional arguments if they are not present in the properties" do
|
48
|
+
type = spy('type')
|
49
|
+
subject.call(type, {size: "tiny", color: "purple"})
|
50
|
+
expect(type).to have_received(:new).with("purple", "tiny")
|
51
|
+
end
|
52
|
+
|
53
|
+
it "raises an ArgumentError if required arguments are not present in the properties" do
|
54
|
+
expect { subject.call(Object, shape: "dot", size: "tiny") }.to raise_error(ArgumentError, /required argument color missing/)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
class PropertiesHashFixture
|
4
|
+
attr_reader :color, :shape, :size
|
5
|
+
|
6
|
+
DEFAULT_ATTRS = {color: "red", shape: "dot", size: "small"} unless defined?(DEFAULT_ATTRS)
|
7
|
+
|
8
|
+
def initialize(properties = DEFAULT_ATTRS)
|
9
|
+
@color, @shape, @size = properties[:color], properties[:shape], properties[:size]
|
10
|
+
end
|
11
|
+
|
12
|
+
def attrs
|
13
|
+
{color: @color, shape: @shape, size: @size}
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
describe Fabrique::Construction::PropertiesHash do
|
19
|
+
|
20
|
+
describe "call(type, properties = nil)" do
|
21
|
+
|
22
|
+
let(:type) { PropertiesHashFixture }
|
23
|
+
|
24
|
+
it "calls type.new() if properties is not provided" do
|
25
|
+
o = subject.call(type)
|
26
|
+
expect(o.attrs).to eql type::DEFAULT_ATTRS
|
27
|
+
end
|
28
|
+
|
29
|
+
it "calls type.new(properties) if properties is provided" do
|
30
|
+
o = subject.call(type, properties = {color: "green", shape: "patch", size: "large"})
|
31
|
+
expect(o.attrs).to eql properties
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Fabrique::PluginRegistry do
|
4
|
+
|
5
|
+
subject { described_class.new("Test Plugin Factory") }
|
6
|
+
let(:constructor) { spy("constructor") }
|
7
|
+
|
8
|
+
describe "#register(id, type, constructor)" do
|
9
|
+
|
10
|
+
it "applies the strategy pattern to construction" do
|
11
|
+
subject.register(:my_plugin, type = Class.new, constructor)
|
12
|
+
subject.acquire(:my_plugin, {some: "properties"})
|
13
|
+
expect(constructor).to have_received(:call).with(type, {some: "properties"})
|
14
|
+
end
|
15
|
+
|
16
|
+
it "returns true on success (to avoid leaking registration internals)" do
|
17
|
+
expect(subject.register(:my_plugin, Class.new, constructor)).to be true
|
18
|
+
end
|
19
|
+
|
20
|
+
context "when the unique identity has already been registered" do
|
21
|
+
|
22
|
+
let(:existing_type) { Object.new }
|
23
|
+
before(:each) { subject.register(:existing, existing_type, constructor) }
|
24
|
+
|
25
|
+
it "raises an ArgumentError" do
|
26
|
+
expect {
|
27
|
+
subject.register(:existing, Object.new, double("constructor").as_null_object)
|
28
|
+
}.to raise_error(ArgumentError, /#{existing_type} already registered/)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "leaves the original registration intact" do
|
32
|
+
begin
|
33
|
+
subject.register(:existing, Object.new, double("constructor").as_null_object)
|
34
|
+
rescue
|
35
|
+
end
|
36
|
+
subject.acquire(:existing)
|
37
|
+
expect(constructor).to have_received(:call).with(existing_type)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#acquire(id, properties = {})" do
|
45
|
+
|
46
|
+
let(:type) { Object }
|
47
|
+
before(:each) { subject.register(:my_plugin, type, constructor) }
|
48
|
+
|
49
|
+
it "applies the registered constructor to the registered type with the given properties" do
|
50
|
+
subject.acquire(:my_plugin, properties = {some: "properties"})
|
51
|
+
expect(constructor).to have_received(:call).with(type, properties)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "applies the registered constructor to the registered type only if no properties are given" do
|
55
|
+
subject.acquire(:my_plugin)
|
56
|
+
expect(constructor).to have_received(:call).with(type)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "returns the constructor call's return value" do
|
60
|
+
allow(constructor).to receive(:call).and_return(plugin = type.new)
|
61
|
+
expect(subject.acquire(:my_plugin)).to be plugin
|
62
|
+
end
|
63
|
+
|
64
|
+
context "when the unique identity has not yet been registered" do
|
65
|
+
|
66
|
+
before(:each) { subject.send(:unregister, :my_plugin) }
|
67
|
+
|
68
|
+
it "raises an ArgumentError" do
|
69
|
+
expect {
|
70
|
+
subject.acquire(:my_plugin)
|
71
|
+
}.to raise_error(ArgumentError, /not registered/)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|