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,15 @@
1
+ Test::Container.namespace(:test) do |container|
2
+ container.finalize(:bar) do
3
+ init do
4
+ module Test
5
+ module Bar
6
+ # I shall be booted
7
+ end
8
+ end
9
+ end
10
+
11
+ start do
12
+ container.register(:bar, 'I was finalized')
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module Namespaced
2
+ class Bar
3
+ include Test::Container.injector["foo"]
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module Namespaced
2
+ class Foo
3
+ end
4
+ 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,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
@@ -0,0 +1,2 @@
1
+ test:
2
+ foo: 'bar'
@@ -0,0 +1,2 @@
1
+ test:
2
+ bar: 'baz'
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'singleton'
2
+
3
+ module Test
4
+ class SingletonDep
5
+ include Singleton
6
+ end
7
+ end
File without changes
@@ -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,7 @@
1
+ Test::Container.finalize(:client) do |container|
2
+ use :logger
3
+
4
+ Client = Struct.new(:logger)
5
+
6
+ container.register(:client, Client.new(logger))
7
+ end
@@ -0,0 +1 @@
1
+ # this is just for Container.finalize spec, actual finalization code is in the test
@@ -0,0 +1,5 @@
1
+ Test::Container.finalize(:logger) do |container|
2
+ require 'logger'
3
+
4
+ container.register(:logger, Logger.new(container.root.join('log/test.log')))
5
+ end
@@ -0,0 +1,10 @@
1
+ Test::Umbrella.namespace(:db) do |container|
2
+ module Db
3
+ class Repo
4
+ end
5
+ end
6
+
7
+ container.finalize(:db) do
8
+ container.register(:repo, Db::Repo.new)
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ RSpec.describe 'boot files' do
2
+ subject(:system) { Test::Container }
3
+
4
+ before do
5
+ class Test::Container < Dry::System::Container
6
+ configure do |config|
7
+ config.root = SPEC_ROOT.join('fixtures/test').realpath
8
+ end
9
+ end
10
+ end
11
+
12
+ it 'auto-boots dependency of a bootable component' do
13
+ system.boot!(:client)
14
+
15
+ expect(system[:client]).to be_a(Client)
16
+ expect(system[:client].logger).to be_a(Logger)
17
+ end
18
+ end
@@ -0,0 +1,63 @@
1
+ RSpec.describe 'Lazy-booting external deps' do
2
+ before do
3
+ module Test
4
+ class Umbrella < Dry::System::Container
5
+ configure do |config|
6
+ config.name = :core
7
+ config.root = SPEC_ROOT.join('fixtures/umbrella').realpath
8
+ end
9
+ end
10
+
11
+ class App < Dry::System::Container
12
+ configure do |config|
13
+ config.name = :main
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ shared_examples_for 'lazy booted dependency' do
20
+ it 'lazy boots an external dep provided by top-level container' do
21
+ expect(user_repo.repo).to be_instance_of(Db::Repo)
22
+ end
23
+
24
+ it 'loads an external dep during finalization' do
25
+ system.finalize!
26
+ expect(user_repo.repo).to be_instance_of(Db::Repo)
27
+ end
28
+ end
29
+
30
+ context 'when top-level container provides the dependency' do
31
+ let(:user_repo) do
32
+ Class.new { include Test::Import['db.repo'] }.new
33
+ end
34
+
35
+ let(:system) { Test::Umbrella }
36
+
37
+ before do
38
+ module Test
39
+ Umbrella.import(App)
40
+ Import = Umbrella.injector
41
+ end
42
+ end
43
+
44
+ it_behaves_like 'lazy booted dependency'
45
+ end
46
+
47
+ context 'when top-level container provides the dependency through import' do
48
+ let(:user_repo) do
49
+ Class.new { include Test::Import['core.db.repo'] }.new
50
+ end
51
+
52
+ let(:system) { Test::App }
53
+
54
+ before do
55
+ module Test
56
+ App.import(Umbrella)
57
+ Import = App.injector
58
+ end
59
+ end
60
+
61
+ it_behaves_like 'lazy booted dependency'
62
+ end
63
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+
3
+ if RUBY_ENGINE == 'ruby' && RUBY_VERSION >= '2.3'
4
+ require 'codeclimate-test-reporter'
5
+ CodeClimate::TestReporter.start
6
+ end
7
+
8
+ begin
9
+ require 'byebug'
10
+ rescue LoadError; end
11
+
12
+ SPEC_ROOT = Pathname(__FILE__).dirname
13
+
14
+ Dir[SPEC_ROOT.join('support/*.rb').to_s].each { |f| require f }
15
+ Dir[SPEC_ROOT.join('shared/*.rb').to_s].each { |f| require f }
16
+
17
+ require 'dry/system/container'
18
+
19
+ module TestNamespace
20
+ def remove_constants
21
+ constants.each do |name|
22
+ remove_const(name)
23
+ end
24
+ end
25
+ end
26
+
27
+ RSpec.configure do |config|
28
+ config.disable_monkey_patching!
29
+
30
+ config.before do
31
+ @load_paths = $LOAD_PATH.dup
32
+ @loaded_features = $LOADED_FEATURES.dup
33
+ Object.const_set(:Test, Module.new { |m| m.extend(TestNamespace) })
34
+ end
35
+
36
+ config.after do
37
+ ($LOAD_PATH - @load_paths).each do |path|
38
+ $LOAD_PATH.delete(path)
39
+ end
40
+ ($LOADED_FEATURES - @loaded_features).each do |file|
41
+ $LOADED_FEATURES.delete(file)
42
+ end
43
+
44
+ Test.remove_constants
45
+ Object.send(:remove_const, :Test)
46
+ end
47
+ end
@@ -0,0 +1,116 @@
1
+ require 'dry/system/component'
2
+
3
+ RSpec.describe Dry::System::Component do
4
+ subject(:component) { Dry::System::Component.new(name) }
5
+
6
+ describe '.new' do
7
+ it 'caches components' do
8
+ create = -> {
9
+ Dry::System::Component.new('foo.bar', namespace: 'foo')
10
+ }
11
+
12
+ expect(create.()).to be(create.())
13
+ end
14
+
15
+ it 'raises when namespace includes a separator' do
16
+ expect { Dry::System::Component.new('foo.bar.baz', namespace: 'foo.bar') }
17
+ .to raise_error(Dry::System::InvalidNamespaceError, /foo\.bar/)
18
+ end
19
+
20
+ it 'raises when identifier/path has duplicated keys' do
21
+ expect { Dry::System::Component.new('foo.bar.foo', namespace: 'foo') }
22
+ .to raise_error(Dry::System::InvalidComponentError, /foo\.bar\.foo/)
23
+ end
24
+ end
25
+
26
+ context 'when name is a symbol' do
27
+ let(:name) { :foo }
28
+
29
+ describe '#identifier' do
30
+ it 'returns qualified identifier' do
31
+ expect(component.identifier).to eql('foo')
32
+ end
33
+ end
34
+
35
+ describe '#namespace' do
36
+ it 'returns configured namespace' do
37
+ expect(component.namespace).to be(nil)
38
+ end
39
+ end
40
+
41
+ describe '#root_key' do
42
+ it 'returns component key' do
43
+ expect(component.root_key).to be(:foo)
44
+ end
45
+ end
46
+
47
+ describe '#instance' do
48
+ it 'builds an instance' do
49
+ class Foo; end
50
+ expect(component.instance).to be_instance_of(Foo)
51
+ Object.send(:remove_const, :Foo)
52
+ end
53
+ end
54
+ end
55
+
56
+ shared_examples_for 'a valid component' do
57
+ describe '#identifier' do
58
+ it 'returns qualified identifier' do
59
+ expect(component.identifier).to eql('test.foo')
60
+ end
61
+ end
62
+
63
+ describe '#file' do
64
+ it 'returns relative path to the component file' do
65
+ expect(component.file).to eql('test/foo.rb')
66
+ end
67
+ end
68
+
69
+ describe '#namespace' do
70
+ it 'returns configured namespace' do
71
+ expect(component.namespace).to be(nil)
72
+ end
73
+ end
74
+
75
+ describe '#namespaced' do
76
+ it 'returns a namespaced component' do
77
+ namespaced = component.namespaced(:test)
78
+
79
+ expect(namespaced.identifier).to eql('foo')
80
+ expect(namespaced.path).to eql('test/foo')
81
+ expect(namespaced.file).to eql('test/foo.rb')
82
+ end
83
+ end
84
+
85
+ describe '#root_key' do
86
+ it 'returns component key' do
87
+ expect(component.root_key).to be(:test)
88
+ end
89
+ end
90
+
91
+ describe '#instance' do
92
+ it 'builds an instance' do
93
+ module Test; class Foo; end; end
94
+ expect(component.instance).to be_instance_of(Test::Foo)
95
+ end
96
+ end
97
+ end
98
+
99
+ context 'when name is a path' do
100
+ let(:name) { 'test/foo' }
101
+
102
+ it_behaves_like 'a valid component'
103
+ end
104
+
105
+ context 'when name is a qualified string identifier' do
106
+ let(:name) { 'test.foo' }
107
+
108
+ it_behaves_like 'a valid component'
109
+ end
110
+
111
+ context 'when name is a qualified symbol identifier' do
112
+ let(:name) { :'test.foo' }
113
+
114
+ it_behaves_like 'a valid component'
115
+ end
116
+ end
@@ -0,0 +1,85 @@
1
+ require 'dry/system/container'
2
+
3
+ RSpec.describe Dry::System::Container, '.auto_register!' do
4
+ context 'standard loader' do
5
+ before do
6
+ class Test::Container < Dry::System::Container
7
+ configure do |config|
8
+ config.root = SPEC_ROOT.join('fixtures').realpath
9
+ end
10
+
11
+ load_paths!('components')
12
+ auto_register!('components')
13
+ end
14
+ end
15
+
16
+ it { expect(Test::Container['foo']).to be_an_instance_of(Foo) }
17
+ it { expect(Test::Container['bar']).to be_an_instance_of(Bar) }
18
+ it { expect(Test::Container['bar.baz']).to be_an_instance_of(Bar::Baz) }
19
+ end
20
+
21
+ context 'with custom registration block' do
22
+ before do
23
+ class Test::Container < Dry::System::Container
24
+ configure do |config|
25
+ config.root = SPEC_ROOT.join('fixtures').realpath
26
+ end
27
+
28
+ load_paths!('components')
29
+ end
30
+ end
31
+
32
+ it 'yields found components' do
33
+ Test::Container.auto_register!('components') do |component|
34
+ component.identifier
35
+ end
36
+
37
+ expect(Test::Container['foo']).to eql('foo')
38
+ expect(Test::Container['bar']).to eql('bar')
39
+ expect(Test::Container['bar.baz']).to eql('bar.baz')
40
+ end
41
+ end
42
+
43
+ context 'standard loader with a default namespace configured' do
44
+ before do
45
+ class Test::Container < Dry::System::Container
46
+ configure do |config|
47
+ config.root = SPEC_ROOT.join('fixtures').realpath
48
+ config.default_namespace = 'namespaced'
49
+ end
50
+
51
+ load_paths!('namespaced_components')
52
+ auto_register!('namespaced_components')
53
+ end
54
+ end
55
+
56
+ specify { expect(Test::Container['bar']).to be_a(Namespaced::Bar) }
57
+ specify { expect(Test::Container['bar'].foo).to be_a(Namespaced::Foo) }
58
+ specify { expect(Test::Container['foo']).to be_a(Namespaced::Foo) }
59
+ end
60
+
61
+ context 'with a custom loader' do
62
+ before do
63
+ class Test::Loader < Dry::System::Loader
64
+ def call(*args)
65
+ constant.respond_to?(:call) ? constant : constant.new(*args)
66
+ end
67
+ end
68
+
69
+ class Test::Container < Dry::System::Container
70
+ configure do |config|
71
+ config.root = SPEC_ROOT.join('fixtures').realpath
72
+ config.loader = ::Test::Loader
73
+ end
74
+
75
+ load_paths!('components')
76
+ auto_register!('components')
77
+ end
78
+ end
79
+
80
+ it { expect(Test::Container['foo']).to be_an_instance_of(Foo) }
81
+ it { expect(Test::Container['bar']).to eq(Bar) }
82
+ it { expect(Test::Container['bar'].call).to eq("Welcome to my Moe's Tavern!") }
83
+ it { expect(Test::Container['bar.baz']).to be_an_instance_of(Bar::Baz) }
84
+ end
85
+ end