dry-system 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +39 -0
- data/.rspec +3 -0
- data/.rubocop.yml +34 -0
- data/.rubocop_todo.yml +26 -0
- data/.travis.yml +26 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +158 -0
- data/Gemfile +9 -0
- data/LICENSE +20 -0
- data/README.md +23 -0
- data/Rakefile +12 -0
- data/dry-system.gemspec +30 -0
- data/examples/standalone/Gemfile +5 -0
- data/examples/standalone/lib/user_repo.rb +5 -0
- data/examples/standalone/run.rb +7 -0
- data/examples/standalone/system/boot/persistence.rb +13 -0
- data/examples/standalone/system/container.rb +9 -0
- data/examples/standalone/system/import.rb +3 -0
- data/lib/dry-system.rb +1 -0
- data/lib/dry/system.rb +4 -0
- data/lib/dry/system/auto_registrar.rb +80 -0
- data/lib/dry/system/booter.rb +101 -0
- data/lib/dry/system/component.rb +167 -0
- data/lib/dry/system/constants.rb +9 -0
- data/lib/dry/system/container.rb +500 -0
- data/lib/dry/system/errors.rb +62 -0
- data/lib/dry/system/importer.rb +53 -0
- data/lib/dry/system/injector.rb +68 -0
- data/lib/dry/system/lifecycle.rb +104 -0
- data/lib/dry/system/loader.rb +69 -0
- data/lib/dry/system/version.rb +5 -0
- data/spec/fixtures/components/bar.rb +5 -0
- data/spec/fixtures/components/bar/baz.rb +4 -0
- data/spec/fixtures/components/foo.rb +2 -0
- data/spec/fixtures/import_test/config/application.yml +2 -0
- data/spec/fixtures/import_test/lib/test/bar.rb +4 -0
- data/spec/fixtures/import_test/lib/test/foo.rb +5 -0
- data/spec/fixtures/import_test/system/boot/bar.rb +11 -0
- data/spec/fixtures/lazytest/config/application.yml +2 -0
- data/spec/fixtures/lazytest/lib/test/dep.rb +4 -0
- data/spec/fixtures/lazytest/lib/test/foo.rb +5 -0
- data/spec/fixtures/lazytest/lib/test/models.rb +4 -0
- data/spec/fixtures/lazytest/lib/test/models/book.rb +6 -0
- data/spec/fixtures/lazytest/lib/test/models/user.rb +6 -0
- data/spec/fixtures/lazytest/system/boot/bar.rb +15 -0
- data/spec/fixtures/namespaced_components/namespaced/bar.rb +5 -0
- data/spec/fixtures/namespaced_components/namespaced/foo.rb +4 -0
- data/spec/fixtures/other/config/boot/bar.rb +11 -0
- data/spec/fixtures/other/lib/test/dep.rb +4 -0
- data/spec/fixtures/other/lib/test/foo.rb +5 -0
- data/spec/fixtures/other/lib/test/models.rb +4 -0
- data/spec/fixtures/other/lib/test/models/book.rb +6 -0
- data/spec/fixtures/other/lib/test/models/user.rb +6 -0
- data/spec/fixtures/test/config/application.yml +2 -0
- data/spec/fixtures/test/config/subapp.yml +2 -0
- data/spec/fixtures/test/lib/test/dep.rb +4 -0
- data/spec/fixtures/test/lib/test/foo.rb +5 -0
- data/spec/fixtures/test/lib/test/models.rb +4 -0
- data/spec/fixtures/test/lib/test/models/book.rb +6 -0
- data/spec/fixtures/test/lib/test/models/user.rb +6 -0
- data/spec/fixtures/test/lib/test/singleton_dep.rb +7 -0
- data/spec/fixtures/test/log/.gitkeep +0 -0
- data/spec/fixtures/test/system/boot/bar.rb +11 -0
- data/spec/fixtures/test/system/boot/client.rb +7 -0
- data/spec/fixtures/test/system/boot/db.rb +1 -0
- data/spec/fixtures/test/system/boot/logger.rb +5 -0
- data/spec/fixtures/umbrella/system/boot/db.rb +10 -0
- data/spec/integration/boot_spec.rb +18 -0
- data/spec/integration/import_spec.rb +63 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/unit/component_spec.rb +116 -0
- data/spec/unit/container/auto_register_spec.rb +85 -0
- data/spec/unit/container/finalize_spec.rb +85 -0
- data/spec/unit/container/import_spec.rb +70 -0
- data/spec/unit/container/injector_spec.rb +29 -0
- data/spec/unit/container_spec.rb +165 -0
- data/spec/unit/injector_spec.rb +72 -0
- data/spec/unit/loader_spec.rb +64 -0
- 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
|