dry-system 0.5.0

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