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.
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"