fabrique 0.3.1 → 1.0.0

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -2
  3. data/Gemfile +4 -0
  4. data/README.md +2 -155
  5. data/fabrique.gemspec +3 -3
  6. data/features/bean_factory.feature +194 -3
  7. data/features/step_definitions/bean_factory_steps.rb +41 -3
  8. data/features/support/fabrique.rb +1 -0
  9. data/fixtures/local_only/.gitignore +9 -0
  10. data/fixtures/local_only/Gemfile +4 -0
  11. data/fixtures/local_only/README.md +36 -0
  12. data/fixtures/local_only/Rakefile +2 -0
  13. data/fixtures/local_only/bin/console +14 -0
  14. data/fixtures/local_only/bin/setup +8 -0
  15. data/fixtures/local_only/lib/local_only/version.rb +3 -0
  16. data/fixtures/local_only/lib/local_only.rb +7 -0
  17. data/fixtures/local_only/local_only.gemspec +31 -0
  18. data/fixtures/local_only-0.1.0.gem +0 -0
  19. data/fixtures/sample/.gitignore +9 -0
  20. data/fixtures/sample/Gemfile +4 -0
  21. data/fixtures/sample/README.md +36 -0
  22. data/fixtures/sample/Rakefile +2 -0
  23. data/fixtures/sample/bin/console +14 -0
  24. data/fixtures/sample/bin/setup +8 -0
  25. data/fixtures/sample/lib/sample/version.rb +3 -0
  26. data/fixtures/sample/lib/sample.rb +9 -0
  27. data/fixtures/sample/sample.gemspec +23 -0
  28. data/lib/fabrique/bean_definition.rb +4 -4
  29. data/lib/fabrique/bean_definition_registry.rb +11 -5
  30. data/lib/fabrique/bean_factory.rb +34 -13
  31. data/lib/fabrique/bean_property_reference.rb +9 -1
  32. data/lib/fabrique/data_bean.rb +62 -0
  33. data/lib/fabrique/gem_definition.rb +15 -0
  34. data/lib/fabrique/gem_dependency_error.rb +6 -0
  35. data/lib/fabrique/gem_loader.rb +25 -0
  36. data/lib/fabrique/test/fixtures/constructors.rb +12 -1
  37. data/lib/fabrique/version.rb +1 -1
  38. data/lib/fabrique.rb +0 -3
  39. data/spec/fabrique/data_bean_spec.rb +129 -0
  40. metadata +32 -38
  41. data/features/plugin_registry.feature +0 -79
  42. data/features/step_definitions/plugin_registry_steps.rb +0 -207
  43. data/lib/fabrique/argument_adaptor/keyword.rb +0 -19
  44. data/lib/fabrique/argument_adaptor/positional.rb +0 -76
  45. data/lib/fabrique/construction/as_is.rb +0 -16
  46. data/lib/fabrique/construction/builder_method.rb +0 -21
  47. data/lib/fabrique/construction/default.rb +0 -17
  48. data/lib/fabrique/construction/keyword_argument.rb +0 -16
  49. data/lib/fabrique/construction/positional_argument.rb +0 -40
  50. data/lib/fabrique/construction/properties_hash.rb +0 -19
  51. data/lib/fabrique/constructor/identity.rb +0 -10
  52. data/lib/fabrique/plugin_registry.rb +0 -56
  53. data/spec/fabrique/argument_adaptor/keyword_spec.rb +0 -50
  54. data/spec/fabrique/argument_adaptor/positional_spec.rb +0 -166
  55. data/spec/fabrique/construction/as_is_spec.rb +0 -23
  56. data/spec/fabrique/construction/builder_method_spec.rb +0 -29
  57. data/spec/fabrique/construction/default_spec.rb +0 -19
  58. data/spec/fabrique/construction/positional_argument_spec.rb +0 -61
  59. data/spec/fabrique/construction/properties_hash_spec.rb +0 -36
  60. data/spec/fabrique/constructor/identity_spec.rb +0 -4
  61. data/spec/fabrique/plugin_registry_spec.rb +0 -78
@@ -0,0 +1,36 @@
1
+ # Sample
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/sample`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'sample'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install sample
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/sample.
36
+
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sample"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,3 @@
1
+ module Sample
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,9 @@
1
+ require "sample/version"
2
+
3
+ module Sample
4
+ # Your code goes here...
5
+
6
+ def self.version
7
+ Sample::VERSION
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sample/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sample"
8
+ spec.version = Sample::VERSION
9
+ spec.authors = ["Sheldon Hearn"]
10
+ spec.email = ["sheldonh@starjuice.net"]
11
+
12
+ spec.summary = %q{Sample gem}
13
+ spec.description = %q{Sample gem for testing software that uses gems}
14
+ spec.homepage = "https://github.com/starjuice/fabrique"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.12"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ end
@@ -1,12 +1,12 @@
1
1
  module Fabrique
2
2
 
3
3
  class BeanDefinition
4
- attr_reader :constructor_args, :factory_method, :id, :properties, :type
4
+ attr_reader :constructor_args, :factory_method, :gem, :id, :properties, :type
5
5
 
6
6
  def initialize(attrs = {})
7
7
  @id = attrs["id"]
8
- type_name = attrs["class"]
9
- @type = type_name.is_a?(Module) ? type_name : Module.const_get(type_name)
8
+ @type = attrs["class"]
9
+ @gem = GemDefinition.new(attrs["gem"]) if attrs["gem"]
10
10
  @constructor_args = attrs["constructor_args"] || []
11
11
  @constructor_args = keywordify(@constructor_args) if @constructor_args.is_a?(Hash)
12
12
  @properties = attrs["properties"] || {}
@@ -15,7 +15,7 @@ module Fabrique
15
15
  end
16
16
 
17
17
  def dependencies
18
- (accumulate_dependencies(@constructor_args) + accumulate_dependencies(@properties)).uniq
18
+ (accumulate_dependencies(@type) + accumulate_dependencies(@constructor_args) + accumulate_dependencies(@properties)).uniq
19
19
  end
20
20
 
21
21
  def singleton?
@@ -15,12 +15,18 @@ module Fabrique
15
15
  @defs.detect { |d| d.id == bean_name }
16
16
  end
17
17
 
18
+ def get_definitions
19
+ @defs
20
+ end
21
+
22
+ def get_gem_definitions
23
+ @defs.collect(&:gem).compact
24
+ end
25
+
18
26
  def validate!
19
- begin
20
- tsort
21
- rescue TSort::Cyclic => e
22
- raise CyclicBeanDependencyError.new(e.message.gsub(/topological sort failed/, "cyclic bean dependency error"))
23
- end
27
+ tsort
28
+ rescue TSort::Cyclic => e
29
+ raise CyclicBeanDependencyError.new(e.message.gsub(/topological sort failed/, "cyclic bean dependency error"))
24
30
  end
25
31
 
26
32
  private
@@ -8,6 +8,7 @@ module Fabrique
8
8
  def initialize(registry)
9
9
  @registry = registry
10
10
  @registry.validate!
11
+ @gem_loader = GemLoader.new(@registry.get_gem_definitions)
11
12
  @singletons = {}
12
13
  @semaphore = Mutex.new
13
14
  end
@@ -18,6 +19,16 @@ module Fabrique
18
19
  end
19
20
  end
20
21
 
22
+ def load_gem_dependencies
23
+ @gem_loader.load_gems
24
+ end
25
+
26
+ def to_h
27
+ @semaphore.synchronize do
28
+ @registry.get_definitions.map { |defn| [defn.id, get_bean_unsynchronized(defn.id)] }.to_h
29
+ end
30
+ end
31
+
21
32
  private
22
33
 
23
34
  def get_bean_unsynchronized(bean_name)
@@ -27,38 +38,47 @@ module Fabrique
27
38
  return singleton
28
39
  end
29
40
 
30
- get_bean_by_definition(defn).tap do |bean|
31
- if defn.singleton?
32
- @singletons[bean_name] = bean
33
- end
41
+ bean = get_bean_by_definition(defn)
42
+ if defn.singleton?
43
+ @singletons[bean_name] = bean
34
44
  end
45
+ bean
35
46
  end
36
47
 
37
48
  def get_bean_by_definition(defn)
38
49
  if defn.factory_method == "itself"
39
- # Support RUBY_VERSION < 2.2.0 (missing Kernel#itself)
40
- return defn.type
50
+ return get_factory(defn)
41
51
  end
42
52
 
43
53
  bean = constructor_injection(defn)
44
54
  property_injection(bean, defn)
45
55
  end
46
56
 
57
+ def get_factory(defn)
58
+ if defn.type.is_a?(BeanReference)
59
+ get_bean_unsynchronized(defn.type.bean)
60
+ elsif defn.type.is_a?(Module)
61
+ defn.type
62
+ else
63
+ Module.const_get(defn.type)
64
+ end
65
+ end
66
+
47
67
  def constructor_injection(defn)
48
68
  args = resolve_bean_references(defn.constructor_args)
69
+ factory = get_factory(defn)
49
70
  if args.respond_to?(:keys)
50
- bean = defn.type.send(defn.factory_method, args)
71
+ bean = factory.send(defn.factory_method, args)
51
72
  else
52
- bean = defn.type.send(defn.factory_method, *args)
73
+ bean = factory.send(defn.factory_method, *args)
53
74
  end
54
75
  end
55
76
 
56
77
  def property_injection(bean, defn)
57
- bean.tap do |b|
58
- defn.properties.each do |k, v|
59
- b.send("#{k}=", resolve_bean_references(v))
60
- end
78
+ defn.properties.each do |k, v|
79
+ bean.send("#{k}=", resolve_bean_references(v))
61
80
  end
81
+ bean
62
82
  end
63
83
 
64
84
  def resolve_bean_references(data)
@@ -76,11 +96,12 @@ module Fabrique
76
96
  elsif data.is_a?(BeanReference)
77
97
  get_bean_unsynchronized(data.bean)
78
98
  elsif data.is_a?(BeanPropertyReference)
79
- get_bean_unsynchronized(data.bean).send(data.property)
99
+ data.resolve(get_bean_unsynchronized(data.bean))
80
100
  else
81
101
  data
82
102
  end
83
103
  end
104
+
84
105
  end
85
106
 
86
107
  end
@@ -4,8 +4,16 @@ module Fabrique
4
4
  attr :bean, :property
5
5
 
6
6
  def initialize(bean_property)
7
- @bean, @property = bean_property.split('.')
7
+ chain = bean_property.split('.')
8
+ @bean = chain.first
9
+ @property_chain = chain.drop(1)
10
+ @property = @property_chain.join('.')
8
11
  end
12
+
13
+ def resolve(bean)
14
+ @property_chain.inject(bean) { |acc, property| acc.send(property) }
15
+ end
16
+
9
17
  end
10
18
 
11
19
  end
@@ -0,0 +1,62 @@
1
+ # TODO underscore method names to reduce collision
2
+
3
+ module Fabrique
4
+
5
+ class DataBean < BasicObject
6
+
7
+ def initialize(hash, name = nil)
8
+ @hash = hash
9
+ @name = name
10
+ end
11
+
12
+ def method_missing(sym, *args)
13
+ key = include?(sym)
14
+
15
+ raise_no_method_error(sym) if key.nil?
16
+ raise_argument_error(args) unless args.empty?
17
+
18
+ glide(sym, fetch(sym))
19
+ end
20
+
21
+ alias_method :send, :method_missing
22
+
23
+ def to_s
24
+ @name or ::Kernel.sprintf("0x%014x", __id__)
25
+ end
26
+
27
+ private
28
+
29
+ def glide(sym, v)
30
+ v.is_a?(::Hash) ? ::Fabrique::DataBean.new(v, "#{to_s}.#{sym}") : v
31
+ end
32
+
33
+ # TODO investigate possible optimization with Ruby 2.3 frozen strings
34
+ def fetch(sym)
35
+ if @hash.include?(sym)
36
+ @hash[sym]
37
+ elsif @hash.include?(sym.to_s)
38
+ @hash[sym.to_s]
39
+ else
40
+ raise_no_method_error(sym)
41
+ end
42
+ end
43
+
44
+ def include?(sym)
45
+ if @hash.include?(sym)
46
+ sym
47
+ elsif @hash.include?(sym.to_s)
48
+ sym.to_s
49
+ end
50
+ end
51
+
52
+ def raise_no_method_error(sym)
53
+ ::Kernel.raise ::NoMethodError.new("undefined method `#{sym}' for #<Fabrique::DataBean:#{to_s}>")
54
+ end
55
+
56
+ def raise_argument_error(args)
57
+ ::Kernel.raise ::ArgumentError.new("wrong number of arguments (given #{args.size}, expected 0)")
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,15 @@
1
+ module Fabrique
2
+
3
+ class GemDefinition
4
+
5
+ attr_reader :dependency, :required_as
6
+
7
+ def initialize(defn)
8
+ @dependency = Gem::Dependency.new(defn["name"], defn["version"] || Gem::Requirement.default)
9
+ @required_as = defn["require"] || defn["name"]
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+
@@ -0,0 +1,6 @@
1
+ module Fabrique
2
+
3
+ class GemDependencyError < RuntimeError
4
+ end
5
+
6
+ end
@@ -0,0 +1,25 @@
1
+ module Fabrique
2
+
3
+ class GemLoader
4
+
5
+ def initialize(gem_definitions)
6
+ @gem_defs = gem_definitions
7
+ deps = @gem_defs.collect(&:dependency).reject { |x| not x.matching_specs.empty? }
8
+ @gem_set = Gem::RequestSet.new(*deps)
9
+ end
10
+
11
+ def load_gems
12
+ require "rubygems/dependency_installer"
13
+ @gem_set.resolve
14
+ specs = @gem_set.install(Gem::DependencyInstaller::DEFAULT_OPTIONS.merge(document: []))
15
+ specs.each do |spec|
16
+ spec.activate
17
+ end
18
+ @gem_defs.collect(&:required_as).each { |x| require x }
19
+ rescue Gem::DependencyError => e
20
+ raise GemDependencyError.new(e.message)
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -6,13 +6,24 @@ module Fabrique
6
6
 
7
7
  module Constructors
8
8
 
9
+ class FactoryWithCreateMethod
10
+
11
+ def initialize(*args)
12
+ end
13
+
14
+ def create
15
+ ClassWithPositionalArgumentConstructor.new("factory size", "factory color", "factory shape")
16
+ end
17
+
18
+ end
19
+
9
20
  class ClassWithProperties
10
21
 
11
22
  DEFAULT_SIZE = "default size" unless defined?(DEFAULT_SIZE)
12
23
  DEFAULT_COLOR = "default color" unless defined?(DEFAULT_COLOR)
13
24
  DEFAULT_SHAPE = "default shape" unless defined?(DEFAULT_SHAPE)
14
25
 
15
- attr_accessor :size, :color, :shape
26
+ attr_accessor :size, :color, :shape, :object
16
27
 
17
28
  end
18
29
 
@@ -1,3 +1,3 @@
1
1
  module Fabrique
2
- VERSION = "0.3.1"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/fabrique.rb CHANGED
@@ -1,7 +1,4 @@
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 }
5
2
 
6
3
  module Fabrique
7
4
  end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Fabrique::DataBean do
6
+
7
+ context "over a string-keyed hash" do
8
+
9
+ let(:wrapped) do
10
+ {
11
+ "string" => "string value",
12
+ "number" => 42,
13
+ "array" => %w[element1 element2 element3].freeze,
14
+ "hash" => {"key" => "value"}.freeze
15
+ }.freeze
16
+ end
17
+
18
+ subject { described_class.new(wrapped) }
19
+
20
+ it "provides direct method access to the values of top-level scalar keys" do
21
+ expect(subject.string).to eql wrapped["string"]
22
+ expect(subject.number).to eql wrapped["number"]
23
+ expect(subject.array).to eql wrapped["array"]
24
+ end
25
+
26
+ it "provides daisy-chained method access to the values of nested hash keys" do
27
+ expect(subject.hash.key).to eql wrapped["hash"]["key"]
28
+ end
29
+
30
+ it "raises NoMethodError for access to non-existent top-level keys" do
31
+ expect { subject.nosuchmethod }.to raise_error(NoMethodError, /undefined method `nosuchmethod' for /)
32
+ end
33
+
34
+ it "raises NoMethodError for access to non-existent nested keys" do
35
+ expect { subject.hash.nosuchnestedmethod }.to raise_error(NoMethodError, /undefined method `nosuchnestedmethod' for/)
36
+ end
37
+
38
+ end
39
+
40
+ context "over a symbol-keyed hash" do
41
+
42
+ let(:wrapped) do
43
+ {
44
+ string: "string value",
45
+ number: 42,
46
+ array: %w[element1 element2 element3].freeze,
47
+ hash: {key: "value"}.freeze
48
+ }.freeze
49
+ end
50
+
51
+ subject { described_class.new(wrapped) }
52
+
53
+ it "provides direct method access to the values of top-level scalar keys" do
54
+ expect(subject.string).to eql wrapped[:string]
55
+ expect(subject.number).to eql wrapped[:number]
56
+ expect(subject.array).to eql wrapped[:array]
57
+ end
58
+
59
+ it "provides daisy-chained method access to the values of nested hash keys" do
60
+ expect(subject.hash.key).to eql wrapped[:hash][:key]
61
+ end
62
+
63
+ it "raises NoMethodError for access to non-existent top-level keys" do
64
+ expect { subject.nosuchmethod }.to raise_error(NoMethodError, /undefined method `nosuchmethod' for /)
65
+ end
66
+
67
+ it "raises NoMethodError for access to non-existent nested keys" do
68
+ expect { subject.hash.nosuchnestedmethod }.to raise_error(NoMethodError, /undefined method `nosuchnestedmethod' for/)
69
+ end
70
+
71
+ end
72
+
73
+ context "regardless of wrapped hash" do
74
+
75
+ let(:wrapped) do
76
+ {
77
+ "meaning_of_life" => 42,
78
+ "object_id" => "The best object ever!",
79
+ "hash" => "A breakfast food type",
80
+ "top" => {middle: {"bottom" => {key: "value"}}}
81
+ }
82
+ end
83
+
84
+ subject { described_class.new(wrapped) }
85
+
86
+ it "goes out of its way to avoid method name collision" do
87
+ expect(subject.object_id).to eql wrapped["object_id"]
88
+ expect(subject.hash).to eql wrapped["hash"]
89
+ end
90
+
91
+ it "raises ArgumentError on known method access with arguments" do
92
+ expect { subject.meaning_of_life(42) }.to raise_error(ArgumentError, /wrong number of arguments \(given 1, expected 0\)/)
93
+ end
94
+
95
+ it "raises NoMethodError on unknown method access with arguments" do
96
+ expect { subject.nosuchmethod(42) }.to raise_error(NoMethodError, /undefined method `nosuchmethod' for /)
97
+ end
98
+
99
+ context "without a name" do
100
+
101
+ subject { described_class.new(wrapped) }
102
+
103
+ it "includes its string representation in NoMethodError messages" do
104
+ expect { subject.nosuchmethod }.to raise_error(/ for #<#{described_class}:#{subject.to_s}>/)
105
+ end
106
+
107
+ it "includes its string representation and nested keys daisy-chained in NoMethodError messages" do
108
+ expect { subject.top.middle.bottom.nosuchnestedmethod }.to raise_error(/ for #<#{described_class}:#{subject.to_s}.top.middle.bottom>/)
109
+ end
110
+
111
+ end
112
+
113
+ context "with a name" do
114
+
115
+ subject { described_class.new(wrapped, "config") }
116
+
117
+ it "includes its name in NoMethodError messages" do
118
+ expect { subject.nosuchmethod }.to raise_error(NoMethodError, / for #<#{described_class}:config>/)
119
+ end
120
+
121
+ it "includes its name and nested keys daisy-chained in NoMethodError messages" do
122
+ expect { subject.top.middle.bottom.nosuchnestedmethod }.to raise_error(/ for #<#{described_class}:config.top.middle.bottom>/)
123
+ end
124
+
125
+ end
126
+
127
+ end
128
+
129
+ end