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