fabrique 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -0
  3. data/Gemfile +5 -0
  4. data/Guardfile +9 -0
  5. data/README.md +152 -3
  6. data/Rakefile +11 -3
  7. data/config/cucumber.yml +2 -0
  8. data/constructors +70 -0
  9. data/docs/autowiring.yml +23 -0
  10. data/docs/multiple_providers.rb +104 -0
  11. data/fabrique.gemspec +3 -0
  12. data/features/bean_factory.feature +295 -0
  13. data/features/plugin_registry.feature +79 -0
  14. data/features/step_definitions/bean_factory_steps.rb +73 -0
  15. data/features/step_definitions/plugin_registry_steps.rb +207 -0
  16. data/features/support/byebug.rb +4 -0
  17. data/lib/fabrique/argument_adaptor/keyword.rb +19 -0
  18. data/lib/fabrique/argument_adaptor/positional.rb +76 -0
  19. data/lib/fabrique/bean_definition.rb +46 -0
  20. data/lib/fabrique/bean_definition_registry.rb +43 -0
  21. data/lib/fabrique/bean_factory.rb +78 -0
  22. data/lib/fabrique/bean_reference.rb +13 -0
  23. data/lib/fabrique/construction/as_is.rb +16 -0
  24. data/lib/fabrique/construction/builder_method.rb +21 -0
  25. data/lib/fabrique/construction/default.rb +17 -0
  26. data/lib/fabrique/construction/keyword_argument.rb +16 -0
  27. data/lib/fabrique/construction/positional_argument.rb +40 -0
  28. data/lib/fabrique/construction/properties_hash.rb +19 -0
  29. data/lib/fabrique/constructor/identity.rb +10 -0
  30. data/lib/fabrique/cyclic_bean_dependency_error.rb +6 -0
  31. data/lib/fabrique/plugin_registry.rb +56 -0
  32. data/lib/fabrique/test/fixtures/constructors.rb +81 -0
  33. data/lib/fabrique/test/fixtures/modules.rb +35 -0
  34. data/lib/fabrique/test/fixtures/opengl.rb +37 -0
  35. data/lib/fabrique/test/fixtures/repository.rb +139 -0
  36. data/lib/fabrique/test.rb +8 -0
  37. data/lib/fabrique/version.rb +1 -1
  38. data/lib/fabrique/yaml_bean_factory.rb +42 -0
  39. data/lib/fabrique.rb +4 -2
  40. data/spec/fabrique/argument_adaptor/keyword_spec.rb +50 -0
  41. data/spec/fabrique/argument_adaptor/positional_spec.rb +166 -0
  42. data/spec/fabrique/construction/as_is_spec.rb +23 -0
  43. data/spec/fabrique/construction/builder_method_spec.rb +29 -0
  44. data/spec/fabrique/construction/default_spec.rb +19 -0
  45. data/spec/fabrique/construction/positional_argument_spec.rb +61 -0
  46. data/spec/fabrique/construction/properties_hash_spec.rb +36 -0
  47. data/spec/fabrique/constructor/identity_spec.rb +4 -0
  48. data/spec/fabrique/plugin_registry_spec.rb +78 -0
  49. data/spec/fabrique_spec.rb +0 -4
  50. metadata +72 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4daf727bd88f8eb4d46c304c084da967dbb73a0c
4
- data.tar.gz: 2bb4305c3529851cf01d62c8a31ec7e0999df500
3
+ metadata.gz: 8683ae625b53e11ecf2abd341320e42a060906fc
4
+ data.tar.gz: 34b2ae0dc2fb5aa5b8436ab1b275e19f388a9484
5
5
  SHA512:
6
- metadata.gz: f39eca63286eca501f0b5a0521f782e9573105508e2e28bc9f85bcdfa78586858e4114088c893fb98228ad46ed771c653d15c3a7fc589c283513e29fff77dcd1
7
- data.tar.gz: b3dda7ca00ff149cd4d76ce07b725b9fc0603882d21fd5ccd6f3c5ae44beed65e2b85e0d8edffa5d82c5d97abf36e730912b76ee481e99582d38b4ba17b67a37
6
+ metadata.gz: ebaa94d196dde76335b23b3d5b78212d9d71d41cced7c950479561c8ea5a0ea85c59c069dfa1478dbe34a3a17bf1b61ade882d2c357b2d183c05ad03836d30c4
7
+ data.tar.gz: 326909d817f1a2ba15e8363fe21e74e5c5419ce199306b5bda2609eb3a34bc485120661c2895b99100abd37c43271ddc03fd9773dad12a68f55378ca604a356a
data/.travis.yml CHANGED
@@ -1,3 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 2.0.0
3
4
  - 2.1.5
5
+ - 2.2.1
6
+ bundler_args: --without devtools
data/Gemfile CHANGED
@@ -2,3 +2,8 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in fabrique.gemspec
4
4
  gemspec
5
+
6
+ group :devtools do
7
+ gem "guard-cucumber", "~> 1.5"
8
+ gem "byebug", "~> 3.5"
9
+ end
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
- TODO: Write a gem description
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
- TODO: Write usage instructions here
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/[my-github-username]/fabrique/fork )
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
- RSpec::Core::RakeTask.new(:spec)
3
+ begin
4
+ require "rspec/core/rake_task"
5
+ require "cucumber"
6
+ require "cucumber/rake/task"
5
7
 
6
- task :default => :spec
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
@@ -0,0 +1,2 @@
1
+ ---
2
+ default: --no-source
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
@@ -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"