fabrique 0.3.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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