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,85 @@
1
+ RSpec.describe Dry::System::Container, '.finalize' do
2
+ subject(:system) { Test::App }
3
+
4
+ let(:db) { spy(:db) }
5
+
6
+ before do
7
+ Test.const_set(:DB, db)
8
+
9
+ module Test
10
+ class App < Dry::System::Container
11
+ configure do |config|
12
+ config.root = SPEC_ROOT.join('fixtures/test')
13
+ end
14
+
15
+ finalize(:db) do
16
+ register(:db, Test::DB)
17
+
18
+ init do
19
+ db.establish_connection
20
+ end
21
+
22
+ start do
23
+ db.load
24
+ end
25
+
26
+ stop do
27
+ db.close_connection
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ describe '#init' do
35
+ it 'calls init function' do
36
+ system.booter.(:db).init
37
+ expect(db).to have_received(:establish_connection)
38
+ end
39
+ end
40
+
41
+ describe '#start' do
42
+ it 'calls start function' do
43
+ system.booter.(:db).start
44
+ expect(db).to have_received(:load)
45
+ end
46
+ end
47
+
48
+ describe '#stop' do
49
+ it 'calls stop function' do
50
+ system.booter.(:db).stop
51
+ expect(db).to have_received(:close_connection)
52
+ end
53
+ end
54
+
55
+ specify 'boot triggers init' do
56
+ system.booter.boot(:db)
57
+
58
+ expect(db).to have_received(:establish_connection)
59
+ expect(db).to_not have_received(:load)
60
+ end
61
+
62
+ specify 'boot! triggers init + start' do
63
+ system.booter.boot!(:db)
64
+
65
+ expect(db).to have_received(:establish_connection)
66
+ expect(db).to have_received(:load)
67
+ end
68
+
69
+ specify 'booter returns cached lifecycle objects' do
70
+ expect(system.booter.(:db)).to be(system.booter.(:db))
71
+ end
72
+
73
+ specify 'lifecycle triggers are called only once' do
74
+ system.booter.boot!(:db)
75
+ system.booter.boot!(:db)
76
+
77
+ system.booter.boot(:db)
78
+ system.booter.boot(:db)
79
+
80
+ expect(db).to have_received(:establish_connection).exactly(1)
81
+ expect(db).to have_received(:load).exactly(1)
82
+
83
+ expect(system.booter.(:db).statuses).to eql(%i[init start])
84
+ end
85
+ end
@@ -0,0 +1,70 @@
1
+ require 'dry/system/container'
2
+
3
+ RSpec.describe Dry::System::Container, '.import' do
4
+ subject(:app) { Class.new(Dry::System::Container) }
5
+
6
+ let(:db) do
7
+ Class.new(Dry::System::Container) do
8
+ register(:users, %w(jane joe))
9
+ end
10
+ end
11
+
12
+ shared_examples_for 'an extended container' do
13
+ it 'imports one container into another' do
14
+ expect(app.key?('persistence.users')).to be(false)
15
+
16
+ app.finalize!
17
+
18
+ expect(app['persistence.users']).to eql(%w(jane joe))
19
+ end
20
+ end
21
+
22
+ context 'when a container has a name' do
23
+ before do
24
+ db.configure { |c| c.name = :persistence }
25
+ app.import(db)
26
+ end
27
+
28
+ it_behaves_like 'an extended container'
29
+ end
30
+
31
+ context 'when container does not have a name' do
32
+ before do
33
+ app.import(persistence: db)
34
+ end
35
+
36
+ it_behaves_like 'an extended container'
37
+ end
38
+
39
+ describe 'import module' do
40
+ it 'loads system when it was not loaded in the imported container yet' do
41
+ class Test::Other < Dry::System::Container
42
+ configure do |config|
43
+ config.root = SPEC_ROOT.join('fixtures/import_test').realpath
44
+ end
45
+
46
+ load_paths!('lib')
47
+ end
48
+
49
+ class Test::Container < Dry::System::Container
50
+ configure do |config|
51
+ config.root = SPEC_ROOT.join('fixtures/test').realpath
52
+ end
53
+
54
+ load_paths!('lib')
55
+
56
+ import other: Test::Other
57
+ end
58
+
59
+ module Test
60
+ Import = Container.injector
61
+ end
62
+
63
+ class Test::Foo
64
+ include Test::Import['other.test.bar']
65
+ end
66
+
67
+ expect(Test::Foo.new.bar).to be_instance_of(Test::Bar)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,29 @@
1
+ require "dry/system/container"
2
+
3
+ RSpec.describe Dry::System::Container, ".injector" do
4
+ context "injector_options provided" do
5
+ it "builds an injector with the provided options" do
6
+ Test::Foo = Class.new
7
+
8
+ Test::Container = Class.new(Dry::System::Container) do
9
+ register "foo", Test::Foo.new
10
+ end
11
+
12
+ Test::Inject = Test::Container.injector(strategies: {
13
+ default: Dry::AutoInject::Strategies::Args,
14
+ australian: Dry::AutoInject::Strategies::Args
15
+ })
16
+
17
+ injected_class = Class.new do
18
+ include Test::Inject.australian["foo"]
19
+ end
20
+
21
+ obj = injected_class.new
22
+ expect(obj.foo).to be_a Test::Foo
23
+
24
+ another = Object.new
25
+ obj = injected_class.new(another)
26
+ expect(obj.foo).to eq another
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,165 @@
1
+ require 'dry/system/container'
2
+
3
+ RSpec.describe Dry::System::Container do
4
+ subject(:container) { Test::Container }
5
+
6
+ context 'with default core dir' do
7
+ before do
8
+ class Test::Container < Dry::System::Container
9
+ configure do |config|
10
+ config.root = SPEC_ROOT.join('fixtures/test').realpath
11
+ end
12
+
13
+ load_paths!('lib')
14
+ end
15
+
16
+ module Test
17
+ Import = Container.injector
18
+ end
19
+ end
20
+
21
+ describe '.require' do
22
+ it 'requires a single file' do
23
+ container.require(Pathname('lib/test/models'))
24
+
25
+ expect(Test.const_defined?(:Models)).to be(true)
26
+ end
27
+
28
+ it 'requires many files when glob pattern is passed' do
29
+ container.require(Pathname('lib/test/models/*.rb'))
30
+
31
+ expect(Test::Models.const_defined?(:User)).to be(true)
32
+ expect(Test::Models.const_defined?(:Book)).to be(true)
33
+ end
34
+ end
35
+
36
+ describe '.require_component' do
37
+ it 'requires component file' do
38
+ component = container.component('test/foo')
39
+ required = false
40
+ container.require_component(component) do
41
+ required = true
42
+ end
43
+ expect(required).to be(true)
44
+ end
45
+
46
+ it 'raises when file does not exist' do
47
+ component = container.component('test/missing')
48
+ expect { container.require_component(component) }.to raise_error(
49
+ Dry::System::FileNotFoundError, /test\.missing/
50
+ )
51
+ end
52
+ end
53
+
54
+ describe '.load_component' do
55
+ it 'loads and registers systems from configured load paths' do
56
+ container.load_component('test.foo')
57
+
58
+ expect(Test.const_defined?(:Foo)).to be(true)
59
+ expect(Test.const_defined?(:Dep)).to be(true)
60
+
61
+ expect(Test::Foo.new.dep).to be_instance_of(Test::Dep)
62
+ end
63
+
64
+ it "raises an error if a system's file can't be found" do
65
+ expect { container.load_component('test.missing') }.to raise_error(
66
+ Dry::System::ComponentLoadError, /test\.missing/
67
+ )
68
+ end
69
+
70
+ it "is a no op if a matching system is already registered" do
71
+ container.register "test.no_matching_file", Object.new
72
+
73
+ expect { container.load_component("test.no_matching_file") }.not_to raise_error
74
+ end
75
+ end
76
+ end
77
+
78
+ describe '.boot' do
79
+ before do
80
+ class Test::Container < Dry::System::Container
81
+ configure do |config|
82
+ config.root = SPEC_ROOT.join('fixtures/lazytest').realpath
83
+ end
84
+
85
+ load_paths!('lib')
86
+ end
87
+ end
88
+
89
+ it 'lazy-boot a given system' do
90
+ container.boot(:bar)
91
+
92
+ expect(Test.const_defined?(:Bar)).to be(true)
93
+ expect(container.key?('test.bar')).to be(false)
94
+ end
95
+ end
96
+
97
+ describe '.boot!' do
98
+ shared_examples_for 'a booted system' do
99
+ it 'boots a given system and finalizes it' do
100
+ container.boot!(:bar)
101
+
102
+ expect(Test.const_defined?(:Bar)).to be(true)
103
+ expect(container['test.bar']).to eql('I was finalized')
104
+ end
105
+
106
+ it 'expects a symbol identifier matching file name' do
107
+ expect {
108
+ container.boot!('bar')
109
+ }.to raise_error(ArgumentError, 'component identifier "bar" must be a symbol')
110
+ end
111
+
112
+ it 'expects identifier to point to an existing boot file' do
113
+ expect {
114
+ container.boot!(:foo)
115
+ }.to raise_error(
116
+ ArgumentError,
117
+ 'component identifier +foo+ is invalid or boot file is missing'
118
+ )
119
+ end
120
+ end
121
+
122
+ context 'with the default core dir' do
123
+ it_behaves_like 'a booted system' do
124
+ before do
125
+ class Test::Container < Dry::System::Container
126
+ configure do |config|
127
+ config.root = SPEC_ROOT.join('fixtures/test').realpath
128
+ end
129
+
130
+ load_paths!('lib')
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ context 'with a custom core dir' do
137
+ it_behaves_like 'a booted system' do
138
+ before do
139
+ class Test::Container < Dry::System::Container
140
+ configure do |config|
141
+ config.root = SPEC_ROOT.join('fixtures/other').realpath
142
+ config.system_dir = 'config'
143
+ end
144
+
145
+ load_paths!('lib')
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ it 'passes container to the finalizer block' do
152
+ class Test::Container < Dry::System::Container
153
+ configure { |c| c.name = :awesome }
154
+
155
+ finalize(:foo) do |container|
156
+ register(:w00t, container.config.name)
157
+ end
158
+ end
159
+
160
+ Test::Container.booter.(:foo)
161
+
162
+ expect(Test::Container[:w00t]).to be(:awesome)
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,72 @@
1
+ RSpec.describe Dry::System::Injector do
2
+ before do
3
+ class Test::Container < Dry::System::Container
4
+ configure do |config|
5
+ config.root = SPEC_ROOT.join('fixtures/test').realpath
6
+ end
7
+
8
+ load_paths! 'lib'
9
+ end
10
+
11
+ Test::Inject = Test::Container.injector
12
+ end
13
+
14
+ it 'supports args injection by default' do
15
+ obj = Class.new do
16
+ include Test::Inject['test.dep']
17
+ end.new
18
+
19
+ expect(obj.dep).to be_a Test::Dep
20
+ end
21
+
22
+ it 'supports args injection with explicit method' do
23
+ obj = Class.new do
24
+ include Test::Inject.args['test.dep']
25
+ end.new
26
+
27
+ expect(obj.dep).to be_a Test::Dep
28
+ end
29
+
30
+ it 'supports hash injection' do
31
+ obj = Class.new do
32
+ include Test::Inject.hash['test.dep']
33
+ end.new
34
+
35
+ expect(obj.dep).to be_a Test::Dep
36
+ end
37
+
38
+ it 'support kwargs injection' do
39
+ obj = Class.new do
40
+ include Test::Inject.kwargs['test.dep']
41
+ end.new
42
+
43
+ expect(obj.dep).to be_a Test::Dep
44
+ end
45
+
46
+ it 'allows injection strategies to be swapped' do
47
+ obj = Class.new do
48
+ include Test::Inject.kwargs.hash['test.dep']
49
+ end.new
50
+
51
+ expect(obj.dep).to be_a Test::Dep
52
+ end
53
+
54
+ it 'supports aliases' do
55
+ obj = Class.new do
56
+ include Test::Inject['test.dep', foo: 'test.dep']
57
+ end.new
58
+
59
+ expect(obj.dep).to be_a Test::Dep
60
+ expect(obj.foo).to be_a Test::Dep
61
+ end
62
+
63
+ context 'singleton' do
64
+ it 'supports injection' do
65
+ obj = Class.new do
66
+ include Test::Inject[foo: 'test.singleton_dep']
67
+ end.new
68
+
69
+ expect(obj.foo).to be_a Test::SingletonDep
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,64 @@
1
+ require 'dry/system/loader'
2
+ require 'singleton'
3
+
4
+ RSpec.describe Dry::System::Loader, '#call' do
5
+ shared_examples_for 'object loader' do
6
+ let(:instance) { loader.call }
7
+
8
+ context 'not singleton' do
9
+ it 'returns a new instance of the constant' do
10
+ expect(instance).to be_instance_of(constant)
11
+ expect(instance).not_to be(loader.call)
12
+ end
13
+ end
14
+
15
+ context 'singleton' do
16
+ before { constant.send(:include, Singleton) }
17
+
18
+ it 'returns singleton instance' do
19
+ expect(instance).to be(constant.instance)
20
+ end
21
+ end
22
+ end
23
+
24
+ context 'with a singular name' do
25
+ subject(:loader) { Dry::System::Loader.new('test/bar') }
26
+
27
+ let(:constant) { Test::Bar }
28
+
29
+ before do
30
+ module Test;class Bar;end;end
31
+ end
32
+
33
+ it_behaves_like 'object loader'
34
+ end
35
+
36
+ context 'with a plural name' do
37
+ subject(:loader) { Dry::System::Loader.new('test/bars') }
38
+
39
+ let(:constant) { Test::Bars }
40
+
41
+ before do
42
+ module Test;class Bars;end;end
43
+ end
44
+
45
+ it_behaves_like 'object loader'
46
+ end
47
+
48
+ context 'with a constructor accepting args' do
49
+ subject(:loader) { Dry::System::Loader.new('test/bar') }
50
+
51
+ before do
52
+ module Test
53
+ Bar = Struct.new(:one, :two)
54
+ end
55
+ end
56
+
57
+ it 'passes args to the constructor' do
58
+ instance = loader.call(1, 2)
59
+
60
+ expect(instance.one).to be(1)
61
+ expect(instance.two).to be(2)
62
+ end
63
+ end
64
+ end