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