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.
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