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