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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8683ae625b53e11ecf2abd341320e42a060906fc
|
4
|
+
data.tar.gz: 34b2ae0dc2fb5aa5b8436ab1b275e19f388a9484
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ebaa94d196dde76335b23b3d5b78212d9d71d41cced7c950479561c8ea5a0ea85c59c069dfa1478dbe34a3a17bf1b61ade882d2c357b2d183c05ad03836d30c4
|
7
|
+
data.tar.gz: 326909d817f1a2ba15e8363fe21e74e5c5419ce199306b5bda2609eb3a34bc485120661c2895b99100abd37c43271ddc03fd9773dad12a68f55378ca604a356a
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/Guardfile
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
guard "cucumber" do
|
2
|
+
watch(%r{^lib/.+\.rb$})
|
3
|
+
watch(%r{^features/.+\.feature$})
|
4
|
+
watch(%r{^features/support/.+$}) { "features" }
|
5
|
+
|
6
|
+
watch(%r{^features/step_definitions/(.+)_steps\.rb$}) do |m|
|
7
|
+
Dir[File.join("**/#{m[1]}.feature")][0] || "features"
|
8
|
+
end
|
9
|
+
end
|
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
[![Gem Version](https://badge.fury.io/rb/fabrique.svg)](http://badge.fury.io/rb/fabrique) [![Build Status](https://travis-ci.org/starjuice/fabrique.svg?branch=master)](https://travis-ci.org/starjuice/fabrique) [![Dependency Status](https://gemnasium.com/starjuice/fabrique.svg)](https://gemnasium.com/starjuice/fabrique)
|
2
|
+
|
1
3
|
# Fabrique
|
2
4
|
|
3
|
-
|
5
|
+
Factory support library for adapting existing modules for injection as dependencies.
|
4
6
|
|
5
7
|
## Installation
|
6
8
|
|
@@ -20,11 +22,158 @@ Or install it yourself as:
|
|
20
22
|
|
21
23
|
## Usage
|
22
24
|
|
23
|
-
|
25
|
+
Under construction; hard hat required!
|
26
|
+
|
27
|
+
## Puzzling
|
28
|
+
|
29
|
+
However plugin factories are composed, the process of constructing a plugin
|
30
|
+
will be:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
Properties -> PropertyValidator -> ArgumentAdaptor -> Constructor => plugin`
|
34
|
+
```
|
35
|
+
|
36
|
+
A global function that takes the plugin registration as the composite would
|
37
|
+
then look like this:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
def fabricate(registry, plugin_identity, properties)
|
41
|
+
registration = registry.find(plugin_identity)
|
42
|
+
property_validator = registration.property_validator
|
43
|
+
argument_adaptor = registration.argument_adaptor
|
44
|
+
constructor = registration.constructor
|
45
|
+
plugin_template = registration.template # Currently called the type
|
46
|
+
|
47
|
+
if property_validator.valid?(properties)
|
48
|
+
arguments = argument_adaptor.adapt(properties)
|
49
|
+
plugin = constructor.construct(plugin_template, arguments)
|
50
|
+
return plugin
|
51
|
+
else
|
52
|
+
raise
|
53
|
+
end
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
So we might compose thus:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
# API gem does this
|
61
|
+
class Store
|
62
|
+
def initialize(provider)
|
63
|
+
@provider = provider
|
64
|
+
end
|
65
|
+
# API definition
|
66
|
+
end
|
67
|
+
StoreApiFactory = Fabrique::FactoryAdaptor.new(
|
68
|
+
template: Store,
|
69
|
+
constructor: Constructor::Classical.new,
|
70
|
+
argument_adaptor: ArgumentAdaptor::Positional.new(:provider)
|
71
|
+
)
|
72
|
+
StoreProviderFactoryRegistry = Fabrique::Registry.new("Store API Provider Registry")
|
73
|
+
|
74
|
+
# Provider gem does this
|
75
|
+
require "store_api"
|
76
|
+
class S3StoreProvider
|
77
|
+
# API implementation here
|
78
|
+
end
|
79
|
+
S3StoreProviderFactory = Fabrique::FactoryAdaptor.new(
|
80
|
+
template: S3StoreProvider,
|
81
|
+
constructor: Constructor::Classical.new,
|
82
|
+
argument_adaptor: ArgumentAdaptor::Keyword.new,
|
83
|
+
)
|
84
|
+
StoreProviderFactoryRegistry.register(:s3, S3StoreProviderFactory)
|
85
|
+
|
86
|
+
# API consumer does this
|
87
|
+
Bundler.require(:default)
|
88
|
+
provider_factory = StoreProviderFactoryRegistry.find(:s3)
|
89
|
+
provider = provider_factory.create(region: "eu-west-1", bucket: "fabrique")
|
90
|
+
api = StoreApiFactory.create(provider: provider)
|
91
|
+
|
92
|
+
# Now, if the API consumer and the API developer agree that this is too high ceremony...
|
93
|
+
|
94
|
+
# API gem adds this
|
95
|
+
class StoreFactory
|
96
|
+
def self.create(provider_id: DEFAULT_PROVIDER, provider_properties: DEFAULT_PROVIDER_PROPERTIES)
|
97
|
+
provider_factory = StoreProviderFactoryRegistry.find(provider_id)
|
98
|
+
provider = provider_factory.create(provider_properties)
|
99
|
+
StoreApiFactory.create(provider: provider)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# and API consumer just does this
|
104
|
+
Bundler.require(:default)
|
105
|
+
api = StoreFactory.create(provider_id: :s3, provider_properties: {region: "eu-west-1", bucket: "fabrique"})
|
106
|
+
```
|
107
|
+
|
108
|
+
Fabrique might be able to offer an easy way to build the low ceremony "provider
|
109
|
+
API factory". Let's wait and see if high ceremony is really a problem for people.
|
110
|
+
|
111
|
+
### Constructors
|
112
|
+
|
113
|
+
What kinds of construction process do we care about?
|
114
|
+
|
115
|
+
#### Identity
|
116
|
+
|
117
|
+
* Makes no sense to use an ArgumentAdaptor (or, by implication, a
|
118
|
+
PropertyValidator).
|
119
|
+
|
120
|
+
So this is a good pressure to compose everything except Properties
|
121
|
+
into the Constructor, registered by plugin\_identity.
|
122
|
+
|
123
|
+
#### Classical
|
124
|
+
|
125
|
+
* Keywords
|
126
|
+
* Positional
|
127
|
+
* Builder?
|
128
|
+
|
129
|
+
If we say you can mix Classical constructor with Builder ArgumentAdaptor,
|
130
|
+
then the constructor must call a default constructor only (::new()), passing
|
131
|
+
in a block.
|
132
|
+
|
133
|
+
#### Builder
|
134
|
+
|
135
|
+
If we say Builder is a constructor, then it can pass adapted arguments *and*
|
136
|
+
a block to the adapted constructor.
|
137
|
+
|
138
|
+
So, does the world really have constructors that take arguments *and* a
|
139
|
+
builder block?
|
140
|
+
|
141
|
+
#### Lambda?
|
142
|
+
|
143
|
+
This could be used to allow *any* adaptation conceivably supported by the
|
144
|
+
interface of the provider type.
|
145
|
+
|
146
|
+
So, are there adaptations we might want that wouldn't be supported by
|
147
|
+
Identity, Classical and Builder? Yes, surely. But are they *factory*
|
148
|
+
adaptations? Well...
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
class BadlyDesigned
|
152
|
+
def initialize(first_name, last_name)
|
153
|
+
@first_name, @last_name = first_name, last_name
|
154
|
+
end
|
155
|
+
|
156
|
+
def set_title(title)
|
157
|
+
@title = title
|
158
|
+
end
|
159
|
+
|
160
|
+
def address
|
161
|
+
"#{@name} #{@first_name} #{@last_name}"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
lambda_constructor = ->(type, properties) do
|
166
|
+
type.new(properties).tap { |o| o.set_title(properties[:title]) if properties.include?(:title) }
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
But are we in the business of adapting badly designed software for use in
|
171
|
+
plugin factories? Should the scope perhaps be to adapt well designed software
|
172
|
+
for use in plugin factories?
|
24
173
|
|
25
174
|
## Contributing
|
26
175
|
|
27
|
-
1. Fork it ( https://github.com/
|
176
|
+
1. Fork it ( https://github.com/starjuice/fabrique/fork )
|
28
177
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
29
178
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
30
179
|
4. Push to the branch (`git push origin my-new-feature`)
|
data/Rakefile
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
-
require "rspec/core/rake_task"
|
3
2
|
|
4
|
-
|
3
|
+
begin
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
require "cucumber"
|
6
|
+
require "cucumber/rake/task"
|
5
7
|
|
6
|
-
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
9
|
|
10
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
11
|
+
t.cucumber_opts = "features --format pretty"
|
12
|
+
end
|
13
|
+
|
14
|
+
task :default => [:spec, :features]
|
15
|
+
end
|
data/config/cucumber.yml
ADDED
data/constructors
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
Properties -> PropertyValidator -> ArgumentAdaptor -> Constructor => o
|
2
|
+
|
3
|
+
def fabricate(registry, plugin_identity, properties)
|
4
|
+
registration = registry.find(plugin_identity)
|
5
|
+
property_validator = registration.property_validator
|
6
|
+
argument_adaptor = registration.argument_adaptor
|
7
|
+
constructor = registration.constructor
|
8
|
+
plugin_template = registration.template
|
9
|
+
|
10
|
+
if property_validator.valid?(properties)
|
11
|
+
arguments = argument_adaptor.adapt(properties)
|
12
|
+
plugin = constructor.construct(plugin_template, arguments)
|
13
|
+
return plugin
|
14
|
+
else
|
15
|
+
raise
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Identity
|
20
|
+
|
21
|
+
* Makes no sense to use an ArgumentAdaptor (or, by implication, a
|
22
|
+
PropertyValidator).
|
23
|
+
|
24
|
+
So this is a good pressure to compose everything except Properties
|
25
|
+
into the Constructor, registered by plugin_identity.
|
26
|
+
|
27
|
+
Classical
|
28
|
+
|
29
|
+
* Keywords
|
30
|
+
* Positional
|
31
|
+
* Builder?
|
32
|
+
|
33
|
+
If we say you can mix Classical constructor with Builder ArgumentAdaptor,
|
34
|
+
then the constructor must call a default constructor only (::new()), passing
|
35
|
+
in a block.
|
36
|
+
|
37
|
+
Builder
|
38
|
+
|
39
|
+
If we say Builder is a constructor, then it can pass adapted arguments *and*
|
40
|
+
a block to the adapted constructor.
|
41
|
+
|
42
|
+
So, does the world really have constructors that take arguments *and* a
|
43
|
+
builder block?
|
44
|
+
|
45
|
+
Lambda?
|
46
|
+
|
47
|
+
This could be used to allow *any* adaptation conceivably supported by the
|
48
|
+
interface of the provider type.
|
49
|
+
|
50
|
+
So, are there adaptations we might want that wouldn't be supported by
|
51
|
+
Identity, Classical and Builder? Yes, surely. But are they *factory*
|
52
|
+
adaptations? Well...
|
53
|
+
|
54
|
+
class BadlyDesigned
|
55
|
+
def initialize(first_name, last_name)
|
56
|
+
@first_name, @last_name = first_name, last_name
|
57
|
+
end
|
58
|
+
|
59
|
+
def set_title(title)
|
60
|
+
@title = title
|
61
|
+
end
|
62
|
+
|
63
|
+
def address
|
64
|
+
"#{@name} #{@first_name} #{@last_name}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
lambda_constructor = ->(type, properties) do
|
69
|
+
type.new(properties).tap { |o| o.set_title(properties[:title]) if properties.include?(:title) }
|
70
|
+
end
|
data/docs/autowiring.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
---
|
2
|
+
beans:
|
3
|
+
invoice_service:
|
4
|
+
class: InvoiceService
|
5
|
+
method: constructor
|
6
|
+
arguments:
|
7
|
+
- bean:billing_provider
|
8
|
+
- bean:asset_provider
|
9
|
+
- dry_run: true
|
10
|
+
billing_provider:
|
11
|
+
class: FreshBooksBillingProvider
|
12
|
+
method: constructor
|
13
|
+
arguments:
|
14
|
+
- url: https://fb.localdomain/ep
|
15
|
+
license_key: 497bca6b-50192c4c-8384f347-4a3ea754-082a7d70-45e041e2-9cfe07a1-3544d7e5
|
16
|
+
asset_provider:
|
17
|
+
class: AssetPointAssetProvider
|
18
|
+
method: constructor
|
19
|
+
arguments:
|
20
|
+
host: 192.168.22.42
|
21
|
+
port: 443
|
22
|
+
username: user
|
23
|
+
password: secret
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# Stretch the design to support multiple providers
|
2
|
+
|
3
|
+
# The API gem does this
|
4
|
+
class InvoiceService
|
5
|
+
def initialize(billing_provider, asset_provider, options = {})
|
6
|
+
@billing_provider, @asset_provider, @options = billing_provider, asset_provider, options
|
7
|
+
end
|
8
|
+
|
9
|
+
def business_method
|
10
|
+
@asset_provider.get_all_assets.map do |asset|
|
11
|
+
@billing_provider.get_billing_for_asset(asset).tap do |billing|
|
12
|
+
if !@options[:dry_run]
|
13
|
+
@billing_provider.invoice_billing(billing)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
InvoiceServiceApiFactory = Fabrique::FactoryAdaptor::Method.new(
|
20
|
+
template: InvoiceService,
|
21
|
+
method: :new,
|
22
|
+
arguments: Fabrique::ArgumentAdaptor::Positional.new(:billing_provider, :asset_provider, [:properties])
|
23
|
+
)
|
24
|
+
InvoiceServiceProviderFactoryRegistry = Fabrique::Registry.new(name: "Invoice service billing provider registry", index_components: 2)
|
25
|
+
|
26
|
+
# A billing provider gem does this
|
27
|
+
require "invoice_service"
|
28
|
+
class FreshBooksBillingProvider
|
29
|
+
def initialize(options = {})
|
30
|
+
#...
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_billing_for_asset(asset)
|
34
|
+
{
|
35
|
+
asset: asset,
|
36
|
+
#...
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def invoice_billing(billing)
|
41
|
+
#...
|
42
|
+
end
|
43
|
+
end
|
44
|
+
FreshBooksBillingProviderFactory = Fabrique::FactoryAdaptor::Method.new(
|
45
|
+
template: FreshBooksBillingProvider,
|
46
|
+
method: :new,
|
47
|
+
arguments: Fabrique::ArgumentAdaptor::Keyword.new
|
48
|
+
)
|
49
|
+
InvoiceServiceProviderFactoryRegistry.register("billing", "freshbooks", FreshBooksBillingProviderFactory)
|
50
|
+
|
51
|
+
# An asset provider gem does this
|
52
|
+
require "invoice_service"
|
53
|
+
class AssetPointAssetProvider
|
54
|
+
def initialize(host: nil, port: nil, username: nil, password: nil)
|
55
|
+
#...
|
56
|
+
end
|
57
|
+
|
58
|
+
def get_all_assets
|
59
|
+
[
|
60
|
+
#...
|
61
|
+
]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
AssetPointAssetProviderFactory = Fabrique::FactoryAdaptor::Method.new(
|
65
|
+
template: AssetPointAssetProviderFactory,
|
66
|
+
method: :new,
|
67
|
+
arguments: Fabrique::ArgumentAdaptor::Keyword.new(:host, :port, :username, :password)
|
68
|
+
)
|
69
|
+
InvoiceServiceProviderFactoryRegistry.register("asset", "assetpoint", AssetPointAssetProviderFactory)
|
70
|
+
|
71
|
+
# API consumer does this
|
72
|
+
Bundler.require(:default)
|
73
|
+
api = InvoiceServiceApiFactory.create(
|
74
|
+
billing_provider: InvoiceServiceProviderFactoryRegistry.find("billing", config.get_string("billing_provider")).create(
|
75
|
+
config.get_properties("billing_provider_properties")
|
76
|
+
),
|
77
|
+
asset_provider: InvoiceServiceProviderFactoryRegistry.find("asset", config.get_string("asset_provider")).create(
|
78
|
+
config.get_properties("asset_provider_properties")
|
79
|
+
)
|
80
|
+
properties: {dry_run: true}
|
81
|
+
)
|
82
|
+
|
83
|
+
# Now, if the API consumer and the API developer agree that this is too high ceremony...
|
84
|
+
|
85
|
+
# API gem adds this
|
86
|
+
InvoiceServiceFactory = Fabrique::Factory::PluggableApi.new(
|
87
|
+
api_factory: InvoiceServiceApiFactory,
|
88
|
+
providers: {
|
89
|
+
billing_provider: {registry: InvoiceServiceProviderFactoryRegistry, index_prefix: ["billing"]},
|
90
|
+
asset_provider: {registry: InvoiceServiceProviderFactoryRegistry, index_prefix: ["asset"]},
|
91
|
+
}
|
92
|
+
)
|
93
|
+
|
94
|
+
# and API consumer just does this
|
95
|
+
Bundler.require(:default)
|
96
|
+
api = InvoiceServiceFactory.create(
|
97
|
+
billing_provider: config.get_string("billing_provider"),
|
98
|
+
billing_provider_properties: config.get_string("billing_provider_properties"),
|
99
|
+
asset_provider: config.get_string("asset_provider"),
|
100
|
+
asset_provider_properties: config.get_string("asset_provider_properties"),
|
101
|
+
properties: {dry_run: true}
|
102
|
+
)
|
103
|
+
|
104
|
+
# Hmmm. That certainly doesn't pay for itself! Screw it, let's just implement Spring Beans for Ruby.
|
data/fabrique.gemspec
CHANGED
@@ -12,12 +12,15 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.description = %q{Factory support library for adapting existing modules for injection as dependencies}
|
13
13
|
spec.homepage = "https://github.com/starjuice/fabrique"
|
14
14
|
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.0"
|
15
16
|
|
16
17
|
spec.files = `git ls-files -z`.split("\x0")
|
17
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
20
|
spec.require_paths = ["lib"]
|
20
21
|
|
22
|
+
spec.add_dependency "liquid", "~> 3.0"
|
23
|
+
|
21
24
|
spec.add_development_dependency "bundler", "~> 1.7"
|
22
25
|
spec.add_development_dependency "rake", "~> 10.0"
|
23
26
|
spec.add_development_dependency "rspec", "~> 3.2"
|