fabrique 0.0.0 → 0.0.1
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 +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
|
+
[](http://badge.fury.io/rb/fabrique) [](https://travis-ci.org/starjuice/fabrique) [](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"
|