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
@@ -0,0 +1,76 @@
|
|
1
|
+
module Fabrique
|
2
|
+
|
3
|
+
module ArgumentAdaptor
|
4
|
+
|
5
|
+
# TODO Initialize with the name of the class we're adapting arguments for, for use in error messages
|
6
|
+
class Positional
|
7
|
+
|
8
|
+
def initialize(*argument_specifiers)
|
9
|
+
@positional_arguments = argument_specifiers.map { |spec| PositionalArgument.create(spec) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def adapt(properties = {})
|
13
|
+
@positional_arguments.map { |argument| argument.pick(properties) }
|
14
|
+
end
|
15
|
+
|
16
|
+
class PositionalArgument
|
17
|
+
|
18
|
+
class Required
|
19
|
+
def initialize(arg)
|
20
|
+
@arg = arg
|
21
|
+
end
|
22
|
+
|
23
|
+
def pick(properties)
|
24
|
+
pick_or_do(properties) do
|
25
|
+
raise ArgumentError, "required argument #{@arg} missing from properties"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def pick_or_do(properties, &block)
|
32
|
+
if properties.include?(@arg)
|
33
|
+
properties[@arg]
|
34
|
+
else
|
35
|
+
block.call
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Optional < Required
|
41
|
+
def pick(properties)
|
42
|
+
pick_or_do(properties) do
|
43
|
+
raise ArgumentError, "optional argument #{@arg} (with no default) missing from properties"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Default < Required
|
49
|
+
def initialize(arg, default)
|
50
|
+
@arg, @default = arg, default
|
51
|
+
end
|
52
|
+
|
53
|
+
def pick(properties)
|
54
|
+
pick_or_do(properties) { @default }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.create(specifier)
|
59
|
+
if specifier.is_a?(Symbol)
|
60
|
+
Required.new(specifier)
|
61
|
+
elsif specifier.is_a?(Array) and specifier.size == 1 and specifier[0].is_a?(Symbol)
|
62
|
+
Optional.new(*specifier)
|
63
|
+
elsif specifier.is_a?(Array) and specifier.size == 2 and specifier[0].is_a?(Symbol)
|
64
|
+
Default.new(*specifier)
|
65
|
+
else
|
66
|
+
raise ArgumentError.new("invalid argument specifier #{specifier}")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Fabrique
|
2
|
+
|
3
|
+
class BeanDefinition
|
4
|
+
attr_reader :constructor_args, :factory_method, :id, :properties, :type
|
5
|
+
|
6
|
+
def initialize(attrs = {})
|
7
|
+
@id = attrs["id"]
|
8
|
+
type_name = attrs["class"]
|
9
|
+
@type = type_name.is_a?(Module) ? @type_name : Module.const_get(type_name)
|
10
|
+
@constructor_args = attrs["constructor_args"] || []
|
11
|
+
@constructor_args = keywordify(@constructor_args) if @constructor_args.is_a?(Hash)
|
12
|
+
@properties = attrs["properties"] || {}
|
13
|
+
@scope = attrs["scope"] || "singleton"
|
14
|
+
@factory_method = attrs["factory_method"] || "new"
|
15
|
+
end
|
16
|
+
|
17
|
+
def dependencies
|
18
|
+
(dependencies_of(@constructor_args) + dependencies_of(@properties)).uniq
|
19
|
+
end
|
20
|
+
|
21
|
+
def singleton?
|
22
|
+
@scope == "singleton"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def keywordify(args)
|
28
|
+
args.inject({}) { |m, (k, v)| k = k.intern rescue k; m[k.intern] = v; m }
|
29
|
+
end
|
30
|
+
|
31
|
+
def dependencies_of(data, acc = [])
|
32
|
+
if data.is_a?(Hash)
|
33
|
+
dependencies_of(data.values, acc)
|
34
|
+
elsif data.is_a?(Array)
|
35
|
+
data.each do |o|
|
36
|
+
dependencies_of(o, acc)
|
37
|
+
end
|
38
|
+
elsif data.is_a?(BeanReference)
|
39
|
+
acc << data
|
40
|
+
end
|
41
|
+
acc
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "tsort"
|
2
|
+
require_relative "bean_definition"
|
3
|
+
require_relative "cyclic_bean_dependency_error"
|
4
|
+
|
5
|
+
module Fabrique
|
6
|
+
|
7
|
+
class BeanDefinitionRegistry
|
8
|
+
include TSort
|
9
|
+
|
10
|
+
def initialize(definitions)
|
11
|
+
@defs = definitions.map { |d| d.is_a?(BeanDefinition) ? d : BeanDefinition.new(d) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_definition(bean_name)
|
15
|
+
@defs.detect { |d| d.id == bean_name }
|
16
|
+
end
|
17
|
+
|
18
|
+
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
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def tsort_each_child(node, &block)
|
29
|
+
defn = get_definition(node)
|
30
|
+
deps = defn.dependencies
|
31
|
+
deps.map { |dep| dep.bean }.each(&block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def tsort_each_node
|
35
|
+
@defs.each do |dep|
|
36
|
+
yield dep.id
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "thread"
|
2
|
+
|
3
|
+
module Fabrique
|
4
|
+
|
5
|
+
class BeanFactory
|
6
|
+
attr_reader :registry, :singletons
|
7
|
+
|
8
|
+
def initialize(registry)
|
9
|
+
@registry = registry
|
10
|
+
@registry.validate!
|
11
|
+
@singletons = {}
|
12
|
+
@semaphore = Mutex.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_bean(bean_name)
|
16
|
+
@semaphore.synchronize do
|
17
|
+
get_bean_unsynchronized(bean_name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def get_bean_unsynchronized(bean_name)
|
24
|
+
defn = @registry.get_definition(bean_name)
|
25
|
+
|
26
|
+
if defn.factory_method == "itself"
|
27
|
+
# Support RUBY_VERSION < 2.2.0 (missing Kernel#itself)
|
28
|
+
return defn.type
|
29
|
+
end
|
30
|
+
|
31
|
+
if defn.singleton? and singleton = @singletons[bean_name]
|
32
|
+
return singleton
|
33
|
+
end
|
34
|
+
|
35
|
+
bean = constructor_injection(defn)
|
36
|
+
property_injection(bean, defn)
|
37
|
+
if defn.singleton?
|
38
|
+
@singletons[bean_name] = bean
|
39
|
+
end
|
40
|
+
bean
|
41
|
+
end
|
42
|
+
|
43
|
+
def constructor_injection(defn)
|
44
|
+
args = resolve_bean_references(defn.constructor_args)
|
45
|
+
if args.respond_to?(:keys)
|
46
|
+
bean = defn.type.send(defn.factory_method, args)
|
47
|
+
else
|
48
|
+
bean = defn.type.send(defn.factory_method, *args)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def property_injection(bean, defn)
|
53
|
+
bean.tap do |b|
|
54
|
+
defn.properties.each do |k, v|
|
55
|
+
b.send("#{k}=", resolve_bean_references(v))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def resolve_bean_references(data)
|
61
|
+
if data.is_a?(Hash)
|
62
|
+
data.inject({}) do |memo, (k, v)|
|
63
|
+
memo[k] = resolve_bean_references(v)
|
64
|
+
memo
|
65
|
+
end
|
66
|
+
elsif data.is_a?(Array)
|
67
|
+
data.inject([]) do |acc, v|
|
68
|
+
acc << resolve_bean_references(v)
|
69
|
+
end
|
70
|
+
elsif data.is_a?(BeanReference)
|
71
|
+
get_bean_unsynchronized(data.bean)
|
72
|
+
else
|
73
|
+
data
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Fabrique
|
2
|
+
|
3
|
+
module Construction
|
4
|
+
|
5
|
+
class AsIs
|
6
|
+
|
7
|
+
def call(type, properties = nil)
|
8
|
+
raise ArgumentError.new("unexpected properties for as-is construction") unless (properties.nil? or properties.empty?)
|
9
|
+
type
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Fabrique
|
2
|
+
|
3
|
+
module Construction
|
4
|
+
|
5
|
+
class BuilderMethod
|
6
|
+
|
7
|
+
def initialize(builder_method_name, &block)
|
8
|
+
@builder_method_name, @builder_runner = builder_method_name, block
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(type, properties = {})
|
12
|
+
type.send(@builder_method_name) do |builder|
|
13
|
+
@builder_runner.call(builder, properties)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Fabrique
|
2
|
+
|
3
|
+
module Construction
|
4
|
+
|
5
|
+
class PositionalArgument
|
6
|
+
|
7
|
+
def initialize(*argument_names)
|
8
|
+
@argument_names = argument_names
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(type, properties = nil)
|
12
|
+
if properties.nil?
|
13
|
+
type.new
|
14
|
+
else
|
15
|
+
type.new(*get_args(properties))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def get_args(properties)
|
22
|
+
@argument_names.inject([]) do |arguments, arg|
|
23
|
+
if arg.is_a?(Array)
|
24
|
+
arg.each do |optional_arg|
|
25
|
+
arguments << properties[optional_arg] if properties.include?(optional_arg)
|
26
|
+
end
|
27
|
+
elsif properties.include?(arg)
|
28
|
+
arguments << properties[arg]
|
29
|
+
else
|
30
|
+
raise ArgumentError, "required argument #{arg} missing from properties"
|
31
|
+
end
|
32
|
+
arguments
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Fabrique
|
2
|
+
|
3
|
+
class PluginRegistry
|
4
|
+
|
5
|
+
def initialize(name)
|
6
|
+
@name = name
|
7
|
+
@registrations = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def register(id, type, constructor)
|
11
|
+
if existing = find_registration(id)
|
12
|
+
raise ArgumentError, "could not register #{type} as #{id} in #{@name}: #{existing.type} already registered as #{id}"
|
13
|
+
end
|
14
|
+
@registrations << Registration.new(id, type, constructor)
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def acquire(id, properties = nil)
|
19
|
+
if registration = find_registration(id)
|
20
|
+
registration.call_constructor(properties)
|
21
|
+
else
|
22
|
+
raise ArgumentError, "#{id} not registered in #{@name}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def find_registration(id)
|
29
|
+
@registrations.detect { |r| r.id == id }
|
30
|
+
end
|
31
|
+
|
32
|
+
def unregister(id)
|
33
|
+
@registrations.delete(find_registration(id))
|
34
|
+
end
|
35
|
+
|
36
|
+
class Registration
|
37
|
+
|
38
|
+
attr_reader :id, :type, :constructor
|
39
|
+
|
40
|
+
def initialize(id, type, constructor)
|
41
|
+
@id, @type, @constructor = id, type, constructor
|
42
|
+
end
|
43
|
+
|
44
|
+
def call_constructor(properties = nil)
|
45
|
+
# TODO Push conditional into construction helpers
|
46
|
+
if properties.nil?
|
47
|
+
@constructor.call(@type)
|
48
|
+
else
|
49
|
+
@constructor.call(@type, properties)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Fabrique
|
2
|
+
|
3
|
+
module Test
|
4
|
+
|
5
|
+
module Fixtures
|
6
|
+
|
7
|
+
module Constructors
|
8
|
+
|
9
|
+
class ClassWithProperties
|
10
|
+
|
11
|
+
DEFAULT_SIZE = "default size" unless defined?(DEFAULT_SIZE)
|
12
|
+
DEFAULT_COLOR = "default color" unless defined?(DEFAULT_COLOR)
|
13
|
+
DEFAULT_SHAPE = "default shape" unless defined?(DEFAULT_SHAPE)
|
14
|
+
|
15
|
+
attr_accessor :size, :color, :shape
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class ClassWithDefaultConstructor < ClassWithProperties
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@size, @color, @shape = DEFAULT_SIZE, DEFAULT_COLOR, DEFAULT_SHAPE
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
OtherClassWithDefaultConstructor = Class.new(ClassWithDefaultConstructor)
|
28
|
+
|
29
|
+
class ClassWithPropertiesHashConstructor < ClassWithProperties
|
30
|
+
|
31
|
+
def initialize(properties)
|
32
|
+
@size, @color, @shape = properties[:size], properties[:color], properties[:shape]
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
class ClassWithPositionalArgumentConstructor < ClassWithProperties
|
38
|
+
|
39
|
+
def initialize(size, color, shape)
|
40
|
+
@size, @color, @shape = size, color, shape
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
class ClassWithKeywordArgumentConstructor < ClassWithProperties
|
46
|
+
|
47
|
+
def initialize(size: DEFAULT_SIZE, color: DEFAULT_COLOR, shape: DEFAULT_SHAPE)
|
48
|
+
@size, @color, @shape = size, color, shape
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
class ClassWithBuilderMethod < ClassWithProperties
|
54
|
+
|
55
|
+
private_class_method :new
|
56
|
+
|
57
|
+
def initialize(builder)
|
58
|
+
@size, @color, @shape = builder.size, builder.color, builder.shape
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.build
|
62
|
+
builder = Builder.new
|
63
|
+
if block_given?
|
64
|
+
yield builder
|
65
|
+
end
|
66
|
+
new(builder)
|
67
|
+
end
|
68
|
+
|
69
|
+
class Builder
|
70
|
+
attr_accessor :size, :color, :shape
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Fabrique
|
2
|
+
|
3
|
+
module Test
|
4
|
+
|
5
|
+
module Fixtures
|
6
|
+
|
7
|
+
module Modules
|
8
|
+
|
9
|
+
module ModuleWithStaticMethods
|
10
|
+
|
11
|
+
DEFAULT_SIZE = "module size" unless defined?(DEFAULT_SIZE)
|
12
|
+
DEFAULT_COLOR = "module color" unless defined?(DEFAULT_COLOR)
|
13
|
+
DEFAULT_SHAPE = "module shape" unless defined?(DEFAULT_SHAPE)
|
14
|
+
|
15
|
+
def self.size
|
16
|
+
DEFAULT_SIZE
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.color
|
20
|
+
DEFAULT_COLOR
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.shape
|
24
|
+
DEFAULT_SHAPE
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Fabrique
|
2
|
+
|
3
|
+
module Test
|
4
|
+
|
5
|
+
module Fixtures
|
6
|
+
|
7
|
+
module OpenGL
|
8
|
+
|
9
|
+
class Object
|
10
|
+
|
11
|
+
attr_reader :shader, :mesh, :scale
|
12
|
+
|
13
|
+
def initialize(shader, physical = {})
|
14
|
+
@shader = shader
|
15
|
+
@mesh = physical[:mesh]
|
16
|
+
@scale = physical[:scale]
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class Mesh
|
22
|
+
|
23
|
+
attr_reader :vectors
|
24
|
+
|
25
|
+
def initialize(vectors = [])
|
26
|
+
@vectors = vectors
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|