fabrique 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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,13 @@
1
+ module Fabrique
2
+
3
+ class BeanReference
4
+
5
+ attr_reader :bean
6
+
7
+ def initialize(bean)
8
+ @bean = bean
9
+ end
10
+
11
+ end
12
+
13
+ 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,17 @@
1
+ require_relative "positional_argument"
2
+
3
+ module Fabrique
4
+
5
+ module Construction
6
+
7
+ class Default < PositionalArgument
8
+
9
+ def initialize
10
+ super()
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,16 @@
1
+ module Fabrique
2
+
3
+ module Construction
4
+
5
+ # TODO Derive from PropertiesHash
6
+ class KeywordArgument
7
+
8
+ def call(type, properties)
9
+ type.new(properties)
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+
16
+ 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,19 @@
1
+ module Fabrique
2
+
3
+ module Construction
4
+
5
+ class PropertiesHash
6
+
7
+ def call(type, properties = nil)
8
+ if properties.nil?
9
+ type.new
10
+ else
11
+ type.new(properties)
12
+ end
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,10 @@
1
+ module Fabrique
2
+
3
+ module Constructor
4
+
5
+ class Identity
6
+ end
7
+
8
+ end
9
+
10
+ end
@@ -0,0 +1,6 @@
1
+ module Fabrique
2
+
3
+ class CyclicBeanDependencyError < RuntimeError
4
+ end
5
+
6
+ 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