dry-system 0.5.0

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 (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +39 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +34 -0
  5. data/.rubocop_todo.yml +26 -0
  6. data/.travis.yml +26 -0
  7. data/.yardopts +5 -0
  8. data/CHANGELOG.md +158 -0
  9. data/Gemfile +9 -0
  10. data/LICENSE +20 -0
  11. data/README.md +23 -0
  12. data/Rakefile +12 -0
  13. data/dry-system.gemspec +30 -0
  14. data/examples/standalone/Gemfile +5 -0
  15. data/examples/standalone/lib/user_repo.rb +5 -0
  16. data/examples/standalone/run.rb +7 -0
  17. data/examples/standalone/system/boot/persistence.rb +13 -0
  18. data/examples/standalone/system/container.rb +9 -0
  19. data/examples/standalone/system/import.rb +3 -0
  20. data/lib/dry-system.rb +1 -0
  21. data/lib/dry/system.rb +4 -0
  22. data/lib/dry/system/auto_registrar.rb +80 -0
  23. data/lib/dry/system/booter.rb +101 -0
  24. data/lib/dry/system/component.rb +167 -0
  25. data/lib/dry/system/constants.rb +9 -0
  26. data/lib/dry/system/container.rb +500 -0
  27. data/lib/dry/system/errors.rb +62 -0
  28. data/lib/dry/system/importer.rb +53 -0
  29. data/lib/dry/system/injector.rb +68 -0
  30. data/lib/dry/system/lifecycle.rb +104 -0
  31. data/lib/dry/system/loader.rb +69 -0
  32. data/lib/dry/system/version.rb +5 -0
  33. data/spec/fixtures/components/bar.rb +5 -0
  34. data/spec/fixtures/components/bar/baz.rb +4 -0
  35. data/spec/fixtures/components/foo.rb +2 -0
  36. data/spec/fixtures/import_test/config/application.yml +2 -0
  37. data/spec/fixtures/import_test/lib/test/bar.rb +4 -0
  38. data/spec/fixtures/import_test/lib/test/foo.rb +5 -0
  39. data/spec/fixtures/import_test/system/boot/bar.rb +11 -0
  40. data/spec/fixtures/lazytest/config/application.yml +2 -0
  41. data/spec/fixtures/lazytest/lib/test/dep.rb +4 -0
  42. data/spec/fixtures/lazytest/lib/test/foo.rb +5 -0
  43. data/spec/fixtures/lazytest/lib/test/models.rb +4 -0
  44. data/spec/fixtures/lazytest/lib/test/models/book.rb +6 -0
  45. data/spec/fixtures/lazytest/lib/test/models/user.rb +6 -0
  46. data/spec/fixtures/lazytest/system/boot/bar.rb +15 -0
  47. data/spec/fixtures/namespaced_components/namespaced/bar.rb +5 -0
  48. data/spec/fixtures/namespaced_components/namespaced/foo.rb +4 -0
  49. data/spec/fixtures/other/config/boot/bar.rb +11 -0
  50. data/spec/fixtures/other/lib/test/dep.rb +4 -0
  51. data/spec/fixtures/other/lib/test/foo.rb +5 -0
  52. data/spec/fixtures/other/lib/test/models.rb +4 -0
  53. data/spec/fixtures/other/lib/test/models/book.rb +6 -0
  54. data/spec/fixtures/other/lib/test/models/user.rb +6 -0
  55. data/spec/fixtures/test/config/application.yml +2 -0
  56. data/spec/fixtures/test/config/subapp.yml +2 -0
  57. data/spec/fixtures/test/lib/test/dep.rb +4 -0
  58. data/spec/fixtures/test/lib/test/foo.rb +5 -0
  59. data/spec/fixtures/test/lib/test/models.rb +4 -0
  60. data/spec/fixtures/test/lib/test/models/book.rb +6 -0
  61. data/spec/fixtures/test/lib/test/models/user.rb +6 -0
  62. data/spec/fixtures/test/lib/test/singleton_dep.rb +7 -0
  63. data/spec/fixtures/test/log/.gitkeep +0 -0
  64. data/spec/fixtures/test/system/boot/bar.rb +11 -0
  65. data/spec/fixtures/test/system/boot/client.rb +7 -0
  66. data/spec/fixtures/test/system/boot/db.rb +1 -0
  67. data/spec/fixtures/test/system/boot/logger.rb +5 -0
  68. data/spec/fixtures/umbrella/system/boot/db.rb +10 -0
  69. data/spec/integration/boot_spec.rb +18 -0
  70. data/spec/integration/import_spec.rb +63 -0
  71. data/spec/spec_helper.rb +47 -0
  72. data/spec/unit/component_spec.rb +116 -0
  73. data/spec/unit/container/auto_register_spec.rb +85 -0
  74. data/spec/unit/container/finalize_spec.rb +85 -0
  75. data/spec/unit/container/import_spec.rb +70 -0
  76. data/spec/unit/container/injector_spec.rb +29 -0
  77. data/spec/unit/container_spec.rb +165 -0
  78. data/spec/unit/injector_spec.rb +72 -0
  79. data/spec/unit/loader_spec.rb +64 -0
  80. metadata +295 -0
@@ -0,0 +1,62 @@
1
+ module Dry
2
+ module System
3
+ # Error raised when the container tries to load a component with missing
4
+ # file
5
+ #
6
+ # @api public
7
+ FileNotFoundError = Class.new(StandardError) do
8
+ def initialize(component)
9
+ super("could not resolve require file for #{component.identifier}")
10
+ end
11
+ end
12
+
13
+ # Error raised when a resolved component couldn't be found
14
+ #
15
+ # @api public
16
+ ComponentLoadError = Class.new(StandardError) do
17
+ def initialize(component)
18
+ super("could not load component #{component.inspect}")
19
+ end
20
+ end
21
+
22
+ # Error raised when invalid namespace name was provided
23
+ #
24
+ # @api public
25
+ InvalidNamespaceError = Class.new(StandardError) do
26
+ def initialize(ns)
27
+ super("Namespace #{ns} cannot include a separator")
28
+ end
29
+ end
30
+
31
+ # Error raised when resolved component couldn't be loaded
32
+ #
33
+ # @api public
34
+ InvalidComponentError = Class.new(ArgumentError) do
35
+ def initialize(name, reason = nil)
36
+ super(
37
+ "Tried to create an invalid #{name.inspect} component - #{reason}"
38
+ )
39
+ end
40
+ end
41
+
42
+ # Error raised when component's identifier is not valid
43
+ #
44
+ # @api public
45
+ InvalidComponentIdentifierError = Class.new(ArgumentError) do
46
+ def initialize(name)
47
+ super(
48
+ "component identifier +#{name}+ is invalid or boot file is missing"
49
+ )
50
+ end
51
+ end
52
+
53
+ # Error raised when component's identifier for booting is not a symbol
54
+ #
55
+ # @api public
56
+ InvalidComponentIdentifierTypeError = Class.new(ArgumentError) do
57
+ def initialize(name)
58
+ super("component identifier #{name.inspect} must be a symbol")
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,53 @@
1
+ module Dry
2
+ module System
3
+ # Default importer implementation
4
+ #
5
+ # This is currently configured by default for every System::Container.
6
+ # Importer objects are responsible for importing components from one
7
+ # container to another. This is used in cases where an application is split
8
+ # into multiple sub-systems.
9
+ #
10
+ # @api private
11
+ class Importer
12
+ attr_reader :container
13
+
14
+ attr_reader :separator
15
+
16
+ attr_reader :registry
17
+
18
+ # @api private
19
+ def initialize(container)
20
+ @container = container
21
+ @separator = container.config.namespace_separator
22
+ @registry = {}
23
+ end
24
+
25
+ # @api private
26
+ def finalize!
27
+ registry.each do |name, container|
28
+ call(name, container.finalize!)
29
+ end
30
+ end
31
+
32
+ # @api private
33
+ def [](name)
34
+ registry.fetch(name)
35
+ end
36
+
37
+ # @api private
38
+ def key?(name)
39
+ registry.key?(name)
40
+ end
41
+
42
+ # @api private
43
+ def call(ns, other)
44
+ container.merge(other, namespace: ns)
45
+ end
46
+
47
+ # @api private
48
+ def register(other)
49
+ registry.update(other)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,68 @@
1
+ require "dry-auto_inject"
2
+
3
+ module Dry
4
+ module System
5
+ # Injection mixin builder
6
+ #
7
+ # Injector objects are created by containers and can be used to automatically
8
+ # define object constructors where depedencies will be injected in.
9
+ #
10
+ # Main purpose of this object is to provide injection along with lazy-loading
11
+ # of components on demand. This gives us a way to load components in
12
+ # isolation from the rest of the system.
13
+ #
14
+ # @see [Container.injector]
15
+ #
16
+ # @api public
17
+ class Injector < BasicObject
18
+ # @api private
19
+ attr_reader :container
20
+
21
+ # @api private
22
+ attr_reader :options
23
+
24
+ # @api private
25
+ attr_reader :injector
26
+
27
+ # @api private
28
+ def initialize(container, options: {}, strategy: :default)
29
+ @container = container
30
+ @options = options
31
+ @injector = ::Dry::AutoInject(container, options).__send__(strategy)
32
+ end
33
+
34
+ # Create injection mixin for specified dependencies
35
+ #
36
+ # @example
37
+ # require 'system/import'
38
+ #
39
+ # class UserRepo
40
+ # include Import['persistence.db']
41
+ # end
42
+ #
43
+ # @param [Array<String>] *deps Keys under which dependencies are registered
44
+ #
45
+ # @return [Dry::AutoInject::Injector]
46
+ #
47
+ # @api public
48
+ def [](*deps)
49
+ load_components(*deps)
50
+ injector[*deps]
51
+ end
52
+
53
+ private
54
+
55
+ # @api private
56
+ def method_missing(name, *args, &block)
57
+ ::Dry::System::Injector.new(container, options: options, strategy: name)
58
+ end
59
+
60
+ # @api private
61
+ def load_components(*keys, **aliases)
62
+ (keys + aliases.values).each do |key|
63
+ container.load_component(key)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,104 @@
1
+ require 'concurrent/map'
2
+
3
+ module Dry
4
+ module System
5
+ # Lifecycle booting DSL
6
+ #
7
+ # Lifecycle objects are used in the boot files where you can register custom
8
+ # init/start/stop triggers
9
+ #
10
+ # @see [Container.finalize]
11
+ #
12
+ # @api private
13
+ class Lifecycle < BasicObject
14
+ attr_reader :container
15
+
16
+ attr_reader :init
17
+
18
+ attr_reader :start
19
+
20
+ attr_reader :stop
21
+
22
+ attr_reader :statuses
23
+
24
+ attr_reader :triggers
25
+
26
+ # @api private
27
+ def self.new(container, &block)
28
+ cache.fetch_or_store([container, block].hash) do
29
+ super
30
+ end
31
+ end
32
+
33
+ # @api private
34
+ def self.cache
35
+ @cache ||= ::Concurrent::Map.new
36
+ end
37
+
38
+ # @api private
39
+ def initialize(container, &block)
40
+ @container = container
41
+ @statuses = []
42
+ @triggers = {}
43
+ instance_exec(container, &block)
44
+ end
45
+
46
+ # @api private
47
+ def call(*triggers)
48
+ triggers.each do |trigger|
49
+ unless statuses.include?(trigger)
50
+ __send__(trigger)
51
+ statuses << trigger
52
+ end
53
+ end
54
+ end
55
+
56
+ # @api private
57
+ def init(&block)
58
+ trigger!(:init, &block)
59
+ end
60
+
61
+ # @api private
62
+ def start(&block)
63
+ trigger!(:start, &block)
64
+ end
65
+
66
+ # @api private
67
+ def stop(&block)
68
+ trigger!(:stop, &block)
69
+ end
70
+
71
+ # @api private
72
+ def use(*names)
73
+ names.each do |name|
74
+ container.boot!(name)
75
+ end
76
+ end
77
+
78
+ # @api private
79
+ def register(*args, &block)
80
+ container.register(*args, &block)
81
+ end
82
+
83
+ private
84
+
85
+ # @api private
86
+ def trigger!(name, &block)
87
+ if triggers.key?(name)
88
+ triggers[name].()
89
+ elsif block
90
+ triggers[name] = block
91
+ end
92
+ end
93
+
94
+ # @api private
95
+ def method_missing(meth, *args, &block)
96
+ if container.key?(meth)
97
+ container[meth]
98
+ elsif ::Kernel.respond_to?(meth)
99
+ ::Kernel.public_send(meth, *args, &block)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,69 @@
1
+ require 'inflecto'
2
+
3
+ module Dry
4
+ module System
5
+ # Default component loader implementation
6
+ #
7
+ # This class is configured by default for every System::Container. You can
8
+ # provide your own and use it in your containers too.
9
+ #
10
+ # @example
11
+ # class MyLoader < Dry::System::Loader
12
+ # def call(*args)
13
+ # constant.build(*args)
14
+ # end
15
+ # end
16
+ #
17
+ # class MyApp < Dry::System::Container
18
+ # configure do |config|
19
+ # # ...
20
+ # config.loader MyLoader
21
+ # end
22
+ # end
23
+ #
24
+ # @api public
25
+ class Loader
26
+ # @!attribute [r] path
27
+ # @return [String] Path to component's file
28
+ attr_reader :path
29
+
30
+ # @api private
31
+ def initialize(path)
32
+ @path = path
33
+ end
34
+
35
+ # Returns component's instance
36
+ #
37
+ # Provided optional args are passed to object's constructor
38
+ #
39
+ # @param [Array] *args Optional constructor args
40
+ #
41
+ # @return [Object]
42
+ #
43
+ # @api public
44
+ def call(*args)
45
+ if singleton?(constant)
46
+ constant.instance(*args)
47
+ else
48
+ constant.new(*args)
49
+ end
50
+ end
51
+
52
+ # Return component's class constant
53
+ #
54
+ # @return [Class]
55
+ #
56
+ # @api public
57
+ def constant
58
+ Inflecto.constantize(Inflecto.camelize(path))
59
+ end
60
+
61
+ private
62
+
63
+ # @api private
64
+ def singleton?(constant)
65
+ constant.respond_to?(:instance) && !constant.respond_to?(:new)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,5 @@
1
+ module Dry
2
+ module System
3
+ VERSION = '0.5.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Bar
2
+ def self.call
3
+ "Welcome to my Moe's Tavern!"
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ class Bar
2
+ class Baz
3
+ end
4
+ end
@@ -0,0 +1,2 @@
1
+ class Foo
2
+ end
@@ -0,0 +1,2 @@
1
+ test:
2
+ foo: 'bar'
@@ -0,0 +1,4 @@
1
+ module Test
2
+ class Bar
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ module Test
2
+ class Foo
3
+ include Import['test.bar']
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ Test::Container.namespace(:test) do |container|
2
+ module Test
3
+ module Bar
4
+ # I shall be booted
5
+ end
6
+ end
7
+
8
+ container.finalize(:bar) do
9
+ container.register(:bar, 'I was finalized')
10
+ end
11
+ end
@@ -0,0 +1,2 @@
1
+ test:
2
+ foo: 'bar'
@@ -0,0 +1,4 @@
1
+ module Test
2
+ class Dep
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ module Test
2
+ class Foo
3
+ include Import['test.dep']
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module Test
2
+ module Models
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Test
2
+ module Models
3
+ class Book
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Test
2
+ module Models
3
+ class User
4
+ end
5
+ end
6
+ end