├── .gitignore
├── .rspec
├── .rubocop.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── Gemfile.lock
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
├── console
└── setup
├── lib
└── smart_core
│ ├── container.rb
│ └── container
│ ├── definition_dsl.rb
│ ├── definition_dsl
│ ├── command_set.rb
│ ├── commands.rb
│ ├── commands
│ │ ├── base.rb
│ │ ├── definition.rb
│ │ ├── definition
│ │ │ ├── compose.rb
│ │ │ ├── namespace.rb
│ │ │ └── register.rb
│ │ ├── instantiation.rb
│ │ └── instantiation
│ │ │ ├── compose.rb
│ │ │ └── freeze_state.rb
│ └── inheritance.rb
│ ├── dependency_compatability.rb
│ ├── dependency_compatability
│ ├── definition.rb
│ ├── general.rb
│ └── registry.rb
│ ├── dependency_resolver.rb
│ ├── dependency_resolver
│ ├── route.rb
│ └── route
│ │ └── cursor.rb
│ ├── dependency_watcher.rb
│ ├── dependency_watcher
│ └── observer.rb
│ ├── entities.rb
│ ├── entities
│ ├── base.rb
│ ├── dependency.rb
│ ├── dependency_builder.rb
│ ├── memoized_dependency.rb
│ ├── namespace.rb
│ └── namespace_builder.rb
│ ├── errors.rb
│ ├── host.rb
│ ├── key_guard.rb
│ ├── mixin.rb
│ ├── registry.rb
│ ├── registry_builder.rb
│ └── version.rb
├── smart_container.gemspec
└── spec
├── features
├── build_instance_avoding_class_declaration_spec.rb
├── changement_subscription_spec.rb
├── composition_spec.rb
├── definition_and_instantiation_spec.rb
├── dot_notation_aka_fetch_spec.rb
├── frozen_state_spec.rb
├── hash_tree_spec.rb
├── iteration_spec.rb
├── key_extraction_spec.rb
├── key_predicates_spec.rb
├── memoization_spec.rb
├── mixin_spec.rb
└── reload_spec.rb
└── spec_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 | .rspec_status
10 | /.idea
11 | .ruby-version
12 | *.gem
13 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 | --require spec_helper
4 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_gem:
2 | armitage-rubocop:
3 | - lib/rubocop.general.yml
4 | - lib/rubocop.rake.yml
5 | - lib/rubocop.rspec.yml
6 |
7 | AllCops:
8 | TargetRubyVersion: 3.1
9 | NewCops: enable
10 | Include:
11 | - lib/**/*.rb
12 | - spec/**/*.rb
13 | - Gemfile
14 | - Rakefile
15 | - smart_container.gemspec
16 | - bin/console
17 |
18 | Lint/EmptyBlock:
19 | Exclude:
20 | - spec/**/*.rb
21 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | ## [0.11.0] - 2022-10-14
5 | ### Fixed
6 | - Unreachable `SmartCore::Container::ArbitraryLock` (rewritten with `SmartCore::Engine::ReadWriteLock`);
7 |
8 | ## [0.10.0] - 2022-10-14
9 | ### Changed
10 | - Simple `Mutex`-based locks was replaced with `SmartCore::Engine::ReadWriteLock` in order to decrease
11 | context switching during method resolving inside RubyVM (reduced thread lock acquire count);
12 | - Development progress:
13 | - Minimal ruby version - **2.5**;
14 | - Updated development dependencies;
15 | - Updated `smart_engine` dependency (`~> 0.11` -> `~> 0.17`);
16 |
17 |
18 | ## [0.9.0] - 2020-01-17
19 | ### Added
20 | - Support for **Ruby@3**;
21 | - Updated development dependencies;
22 |
23 | ### Changed
24 | - No more TravisCI (todo: migrate to Github Actions);
25 | - Minimal `smart_engine` version: **0.11.0** (in order to support **Ruby@3**);
26 |
27 | ## [0.8.1] - 2020-07-09
28 | ### Changed
29 | - *Core*
30 | - refactored `SmartCore::Container::Entities::NamespaceBuilder` and `SmartCore::Container::Entities::DependencyBuilder`
31 | (from stateful-based logic on instances to stateless-based logic on modules);
32 |
33 | ### Fixed
34 | - Subscription to the nested dependency changement doesn't work
35 | (incomplete nested dependency path in watcher notification);
36 |
37 | ## [0.8.0] - 2020-07-08
38 | ### Added
39 | - An ability to observe dependency re-registrations:
40 | - `#observe(path, &observer) # => observer object` - listen specific dependency path;
41 | - `#unobserve(observer)` - unsubscribe concrete observer object;
42 | - `#clear_observers(path = nil)` - unsubscribe specific listenr or all listeners (`nil` parameter);
43 |
44 | ## [0.7.0] - 2020-06-20
45 | ### Added
46 | - `SmartCore::Container.define {}` - an ability to avoid explicit class definition that allows
47 | to create container instances from an anonymous container class imidietly
48 |
49 | ## [0.6.0] - 2020-01-12
50 | ### Added
51 | - Missing memoization flag `:memoize` for runtime-based dependency registration:
52 | - `memoize: false` by default;
53 | - signature: `SmartCore::Container#register(dependency_name, memoize: false, &dependency)`
54 |
55 | ## [0.5.0] - 2020-01-07
56 | ### Added
57 | - Key predicates (`#key?(key)`, `#dependency?(path, memoized: nil/true/false)`, `#namespace?(path)`);
58 |
59 | ## [0.4.0] - 2020-01-06
60 | ### Added
61 | - `#keys(all_variants: false)` - return a list of dependency keys
62 | (`all_variants: true` is mean "including namespace kaeys");
63 | - `#each_dependency(yield_all: false) { |key, value| }` - iterate over conteiner's dependencies
64 | (`yield_all: true` will include nested containers to iteration process);
65 | ### Fixed
66 | - `SmartCore::Container::ResolvingError` class has incorrect message attribute name;
67 |
68 | ## [0.3.0] - 2020-01-05
69 | ### Changed
70 | - Dependency resolving is not memoized by default (previously: totally memoized 😱);
71 |
72 | ## [0.2.0] - 2020-01-05
73 | ### Changed
74 | - (Private API (`SmartCore::Container::RegistryBuilder`)) improved semantics:
75 | - `build_state` is renamed to `initialise`;
76 | - `build_definitions` is renamed to `define`;
77 | - (Public API) Support for memoized dependencies (all dependencies are memoized by default);
78 |
79 | ## [0.1.0] - 2020-01-02
80 |
81 | - Release :)
82 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at iamdaiver@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: https://contributor-covenant.org
74 | [version]: https://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 | gemspec
5 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | smart_container (0.10.0)
5 | smart_engine (~> 0.17)
6 |
7 | GEM
8 | remote: https://rubygems.org/
9 | specs:
10 | activesupport (7.0.4)
11 | concurrent-ruby (~> 1.0, >= 1.0.2)
12 | i18n (>= 1.6, < 2)
13 | minitest (>= 5.1)
14 | tzinfo (~> 2.0)
15 | armitage-rubocop (1.36.0)
16 | rubocop (= 1.36.0)
17 | rubocop-performance (= 1.15.0)
18 | rubocop-rails (= 2.16.1)
19 | rubocop-rake (= 0.6.0)
20 | rubocop-rspec (= 2.13.2)
21 | ast (2.4.2)
22 | coderay (1.1.3)
23 | concurrent-ruby (1.1.10)
24 | diff-lcs (1.5.0)
25 | docile (1.4.0)
26 | i18n (1.12.0)
27 | concurrent-ruby (~> 1.0)
28 | json (2.6.2)
29 | method_source (1.0.0)
30 | minitest (5.16.3)
31 | parallel (1.22.1)
32 | parser (3.1.2.1)
33 | ast (~> 2.4.1)
34 | pry (0.14.1)
35 | coderay (~> 1.1)
36 | method_source (~> 1.0)
37 | rack (3.0.0)
38 | rainbow (3.1.1)
39 | rake (13.0.6)
40 | regexp_parser (2.6.0)
41 | rexml (3.2.5)
42 | rspec (3.11.0)
43 | rspec-core (~> 3.11.0)
44 | rspec-expectations (~> 3.11.0)
45 | rspec-mocks (~> 3.11.0)
46 | rspec-core (3.11.0)
47 | rspec-support (~> 3.11.0)
48 | rspec-expectations (3.11.1)
49 | diff-lcs (>= 1.2.0, < 2.0)
50 | rspec-support (~> 3.11.0)
51 | rspec-mocks (3.11.1)
52 | diff-lcs (>= 1.2.0, < 2.0)
53 | rspec-support (~> 3.11.0)
54 | rspec-support (3.11.1)
55 | rubocop (1.36.0)
56 | json (~> 2.3)
57 | parallel (~> 1.10)
58 | parser (>= 3.1.2.1)
59 | rainbow (>= 2.2.2, < 4.0)
60 | regexp_parser (>= 1.8, < 3.0)
61 | rexml (>= 3.2.5, < 4.0)
62 | rubocop-ast (>= 1.20.1, < 2.0)
63 | ruby-progressbar (~> 1.7)
64 | unicode-display_width (>= 1.4.0, < 3.0)
65 | rubocop-ast (1.21.0)
66 | parser (>= 3.1.1.0)
67 | rubocop-performance (1.15.0)
68 | rubocop (>= 1.7.0, < 2.0)
69 | rubocop-ast (>= 0.4.0)
70 | rubocop-rails (2.16.1)
71 | activesupport (>= 4.2.0)
72 | rack (>= 1.1)
73 | rubocop (>= 1.33.0, < 2.0)
74 | rubocop-rake (0.6.0)
75 | rubocop (~> 1.0)
76 | rubocop-rspec (2.13.2)
77 | rubocop (~> 1.33)
78 | ruby-progressbar (1.11.0)
79 | simplecov (0.21.2)
80 | docile (~> 1.1)
81 | simplecov-html (~> 0.11)
82 | simplecov_json_formatter (~> 0.1)
83 | simplecov-html (0.12.3)
84 | simplecov_json_formatter (0.1.4)
85 | smart_engine (0.17.0)
86 | tzinfo (2.0.5)
87 | concurrent-ruby (~> 1.0)
88 | unicode-display_width (2.3.0)
89 |
90 | PLATFORMS
91 | arm64-darwin-21
92 | x86_64-darwin-20
93 |
94 | DEPENDENCIES
95 | armitage-rubocop (~> 1.36)
96 | bundler (~> 2.3)
97 | pry (~> 0.14)
98 | rake (~> 13.0)
99 | rspec (~> 3.11)
100 | simplecov (~> 0.21)
101 | smart_container!
102 |
103 | BUNDLED WITH
104 | 2.3.23
105 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020-2024 Rustam Ibragimov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SmartCore::Container ·
· [](https://badge.fury.io/rb/smart_container)
2 |
3 | Thread-safe semanticaly-defined IoC/DI Container with a developer-friendly DSL and API.
4 |
5 | ---
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ---
14 |
15 | ## Installation
16 |
17 | ```ruby
18 | gem 'smart_container'
19 | ```
20 |
21 | ```shell
22 | bundle install
23 | # --- or ---
24 | gem install smart_container
25 | ```
26 |
27 | ```ruby
28 | require 'smart_core/container'
29 | ```
30 |
31 | ---
32 |
33 | ## Table of cotnents
34 |
35 | - [Functionality](#functionality)
36 | - [container class creation](#container-class-creation)
37 | - [mixin](#mixin)
38 | - [container instantiation and dependency resolving](#container-instantiation-and-dependency-resolving)
39 | - [runtime-level dependency/namespace registration](#runtime-level-dependencynamespace-registration)
40 | - [container keys (dependency names)](#container-keys-dependency-names)
41 | - [key predicates](#key-predicates)
42 | - [state freeze](#state-freeze)
43 | - [reloading](#reloading)
44 | - [hash tree](#hash-tree)
45 | - [explicit class definition](#explicit-class-definition)
46 | - [subscribe to dependency changements](#subscribe-to-dependency-changements)
47 | - [Roadmap](#roadmap)
48 |
49 | ---
50 |
51 | ## Functionality
52 |
53 | #### container class creation
54 |
55 | ```ruby
56 | class Container < SmartCore::Container
57 | namespace(:database) do # support for namespaces
58 | register(:resolver, memoize: true) { SomeDatabaseResolver.new } # dependency registration
59 |
60 | namespace(:cache) do # support for nested naespaces
61 | register(:memcached, memoize: true) { MemcachedClient.new }
62 | register(:redis, memoize: true) { RedisClient.new }
63 | end
64 | end
65 |
66 | # root dependencies
67 | register(:logger, memoize: true) { Logger.new(STDOUT) }
68 |
69 | # dependencies are not memoized by default (memoize: false)
70 | register(:random) { rand(1000) }
71 | end
72 | ```
73 |
74 | ---
75 |
76 | #### mixin
77 |
78 | ```ruby
79 | # full documentaiton is coming;
80 |
81 | class Application
82 | include SmartCore::Container::Mixin
83 |
84 | dependencies do
85 | namespace(:database) do
86 | register(:cache) { MemcachedClient.new }
87 | end
88 | end
89 | end
90 |
91 | # access:
92 | Application.container
93 | Application.new.container # NOTE: the same instance as Application.container
94 | ```
95 |
96 | ---
97 |
98 | #### container instantiation and dependency resolving
99 |
100 | ```ruby
101 | container = Container.new # create container instance
102 | ```
103 |
104 | ```ruby
105 | container['database.resolver'] # => #
106 | container['database.cache.redis'] # => #
107 | container['logger'] # => #
108 |
109 | container.resolve('logger') # #resolve(path) is an alias for #[](path)
110 |
111 | # non-memoized dependency
112 | container['random'] # => 352
113 | container['random'] # => 57
114 |
115 | # trying to resolve a namespace as dependency
116 | container['database'] # => SmartCore::Container::ResolvingError
117 |
118 | # but you can fetch any depenendency type (internal containers and values) via #fetch
119 | container.fetch('database') # => SmartCore::Container (nested container)
120 | container.fetch('database.resolver') # => #
121 | ```
122 |
123 | ---
124 |
125 | #### runtime-level dependency/namespace registration
126 |
127 | ```ruby
128 | container.namespace(:api) do
129 | register(:provider) { GoogleProvider } # without memoization
130 | end
131 |
132 | container.register('game_api', memoize: true) { 'overwatch' } # with memoization
133 |
134 | container['api.provider'] # => GoogleProvider
135 | container['game_api'] # => 'overwatch'
136 | ```
137 |
138 | ---
139 |
140 | #### container keys (dependency names):
141 |
142 | ```ruby
143 | # get dependnecy keys (only dependencies)
144 | container.keys
145 | # => result:
146 | [
147 | 'database.resolver',
148 | 'database.cache.memcached',
149 | 'database.cache.redis',
150 | 'logger',
151 | 'random'
152 | ]
153 | ```
154 | ```ruby
155 | # get all keys (namespaces and dependencies)
156 | container.keys(all_variants: true)
157 | # => result:
158 | [
159 | 'database', # namespace
160 | 'database.resolver',
161 | 'database.cache', # namespace
162 | 'database.cache.memcached',
163 | 'database.cache.redis',
164 | 'logger',
165 | 'random'
166 | ]
167 | ```
168 |
169 | ---
170 |
171 | #### key predicates
172 |
173 | - `key?(key)` - has dependency or namespace?
174 | - `namespace?(path)` - has namespace?
175 | - `dependency?(path)` - has dependency?
176 | - `dependency?(path, memoized: true)` - has memoized dependency?
177 | - `dependency?(path, memoized: false)` - has non-memoized dependency?
178 |
179 | ```ruby
180 | container.key?('database') # => true
181 | container.key?('database.cache.memcached') # => true
182 |
183 | container.dependency?('database') # => false
184 | container.dependency?('database.resolver') # => true
185 |
186 | container.namespace?('database') # => true
187 | container.namespace?('database.resolver') # => false
188 |
189 | container.dependency?('database.resolver', memoized: true) # => true
190 | container.dependency?('database.resolver', memoized: false) # => false
191 |
192 | container.dependency?('random', memoized: true) # => false
193 | container.dependency?('random', memoized: false) # => true
194 | ```
195 |
196 | ---
197 |
198 | #### state freeze
199 |
200 | - state freeze (`#freeze!`, `.#frozen?`):
201 |
202 | ```ruby
203 | # documentation is coming;
204 | ```
205 |
206 | ---
207 |
208 | #### reloading
209 |
210 | - reloading (`#reload!`):
211 |
212 | ```ruby
213 | # documentation is coming;
214 | ```
215 |
216 | ---
217 |
218 | #### hash tree
219 |
220 | - hash tree (`#hash_tree`, `#hash_tree(resolve_dependencies: true)`):
221 |
222 | ```ruby
223 | # documentation is coming;
224 | ```
225 |
226 | ---
227 |
228 | #### explicit class definition
229 |
230 | - `SmartCore::Container.define` - avoid explicit class definition (allows to create container instance from an anonymous container class immidietly):
231 |
232 | ```ruby
233 | # - create from empty container class -
234 |
235 | AppContainer = SmartCore::Container.define do
236 | namespace :database do
237 | register(:logger) { Logger.new }
238 | end
239 | end # => an instance of Class
240 |
241 | AppContainer.resolve('database.logger') # => #
242 | AppContainer['database.logger'] # => #
243 | ```
244 |
245 | ```ruby
246 | # - create from another container class with a custom sub-definitions -
247 |
248 | class BasicContainer < SmartCore::Container
249 | namespace(:api) do
250 | register(:client) { Kickbox.new }
251 | end
252 | end
253 |
254 | AppContainer = BasicContainer.define do
255 | register(:db_driver) { Sequel }
256 | end
257 | # --- or ---
258 | AppContainer = SmartCore::Container.define(BasicContainer) do
259 | register(:db_driver) { Sequel }
260 | end
261 |
262 | AppContainer['api.client'] # => # (BasicContainer dependency)
263 | AppContainer['db_driver'] # => Sequel (AppContainer dependency)
264 | ```
265 |
266 | ---
267 |
268 | #### subscribe to dependency changements
269 |
270 | - features and limitations:
271 | - you can subscribe only on container instances (on container instance changements);
272 | - at this moment only the full entity path patterns are supported (pattern-based pathes are not supported yet);
273 | - you can subscribe on namespace changements (when the full namespace is re-registered) and dependency changement (when some dependency has been changed);
274 | - `#observe(path, &observer) => observer` - subscribe a custom block to dependency changement events (your proc will be invoked with `|path, container|` attributes);
275 | - `#unobserve(observer)` - unsubscribe concrete observer from dependency observing (returns `true` (unsubscribed) or `false` (nothing to unsubscribe));
276 | - `#clear_observers(entity_path = nil)` - unsubscribe all observers from concrete path or from all pathes (`nil` parameters);
277 | - aliases:
278 | - `#observe` => `#subscribe`;
279 | - `#unobserve` => `#unsubscribe`;
280 | - `#clear_observers` => `#clear_listeners`;
281 |
282 | ```ruby
283 | container = SmartCore::Container.define do
284 | namespace(:database) do
285 | register(:stats) { 'stat_db' }
286 | end
287 | end
288 | ```
289 |
290 | ```ruby
291 | # observe entity change
292 | entity_observer = container.observe('database.stats') do |dependency_path, container|
293 | puts "changed => '#{container[dependency_path]}'"
294 | end
295 |
296 | # observe namespace change
297 | namespace_observer = container.observe('database') do |namespace_path, container|
298 | puts "changed => '#{namespace_path}'"
299 | end
300 | ```
301 |
302 | ```ruby
303 | container.fetch('database').register('stats') = 'kek' # => invokes entity_observer and outputs "changed! => 'kek'"
304 | container.namespace('database') {} # => invoks namespace_observer and outputs "changed => 'database'"
305 |
306 | container.unobserve(observer) # unsubscribe entity_observer from dependency changement observing;
307 | container.clear_observers # unsubscribe all observers
308 |
309 | container.fetch('database').register('stats') = 'pek' # no one to listen this changement... :)
310 | container.namespace('database') {} # no one to listen this changement... :)
311 | ```
312 |
313 | ---
314 |
315 | ## Roadmap
316 |
317 | - migrate to Github Actions;
318 |
319 | - convinient way to rebind registered dependnecies:
320 |
321 | ```ruby
322 | # PoC
323 |
324 | container['dependency.path'] = 'pek' # simplest instant dependency registration without memoization
325 | # --- or/and ---
326 | container.rebind('dependency.path', memoize: true/false) { 'pek' } # bind with dynamic dependency registration
327 | container.rebind('dependency.path', memoize: true/false, 'pek') # bind with instant dependency registration
328 | ```
329 |
330 | - pattern-based pathes in dependency changement observing;
331 |
332 | ```ruby
333 | container.observe('path.*') { puts 'kek!' } # subscribe to all changements in `path` namespace;
334 | ```
335 |
336 | - support for instant dependency registration:
337 |
338 | ```ruby
339 | # common (dynamic) way:
340 | register('dependency_name') { dependency_value }
341 |
342 | # instant way:
343 | register('dependency_name', dependency_value)
344 | ```
345 |
346 | - support for memoization ignorance during dependency resolving:
347 |
348 | ```ruby
349 | resolve('logger', :allocate) # Draft
350 | ```
351 |
352 | - container composition;
353 |
354 | - support for fallback block in `.resolve` operation (similar to `Hash#fetch` works);
355 |
356 | - inline temporary dependency switch:
357 |
358 | ```ruby
359 | with(logger: Logger.new, db: DB.new) do
360 | # logger is a new logger
361 | # db is a new db
362 | end
363 |
364 | # out of block: logger is an old logger, db is an old db
365 | ```
366 |
367 | ---
368 |
369 | ## Contributing
370 |
371 | - Fork it ( https://github.com/smart-rb/smart_container/fork )
372 | - Create your feature branch (`git checkout -b feature/my-new-feature`)
373 | - Commit your changes (`git commit -am '[feature_context] Add some feature'`)
374 | - Push to the branch (`git push origin feature/my-new-feature`)
375 | - Create new Pull Request
376 |
377 | ## License
378 |
379 | Released under MIT License.
380 |
381 | ## Supporting
382 |
383 |
384 |
385 |
386 |
387 | ## Authors
388 |
389 | [Rustam Ibragimov](https://github.com/0exp)
390 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'bundler/gem_tasks'
4 | require 'rspec/core/rake_task'
5 | require 'rubocop'
6 | require 'rubocop/rake_task'
7 | require 'rubocop-rails'
8 | require 'rubocop-performance'
9 | require 'rubocop-rspec'
10 | require 'rubocop-rake'
11 |
12 | RuboCop::RakeTask.new(:rubocop) do |t|
13 | config_path = File.expand_path(File.join('.rubocop.yml'), __dir__)
14 | t.options = ['--config', config_path]
15 | t.requires << 'rubocop-rspec'
16 | t.requires << 'rubocop-performance'
17 | t.requires << 'rubocop-rake'
18 | end
19 |
20 | RSpec::Core::RakeTask.new(:rspec)
21 |
22 | task default: :rspec
23 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'bundler/setup'
5 | require 'smart_core/container'
6 |
7 | require 'irb'
8 | IRB.start(__FILE__)
9 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/lib/smart_core/container.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'smart_core'
4 |
5 | # @api public
6 | # @since 0.1.0
7 | module SmartCore
8 | # @api public
9 | # @since 0.1.0
10 | # @version 0.10.0
11 | class Container # rubocop:disable Metrics/ClassLength
12 | require_relative 'container/version'
13 | require_relative 'container/errors'
14 | require_relative 'container/key_guard'
15 | require_relative 'container/entities'
16 | require_relative 'container/definition_dsl'
17 | require_relative 'container/dependency_compatability'
18 | require_relative 'container/registry'
19 | require_relative 'container/registry_builder'
20 | require_relative 'container/dependency_resolver'
21 | require_relative 'container/dependency_watcher'
22 | require_relative 'container/host'
23 | require_relative 'container/mixin'
24 |
25 | class << self
26 | # @param initial_container_klass [Class]
27 | # @param container_definitions [Block]
28 | # @return [SmartCore::Container]
29 | #
30 | # @api public
31 | # @since 0.7.0
32 | def define(initial_container_klass = self, &container_definitions)
33 | unless initial_container_klass <= SmartCore::Container
34 | raise(SmartCore::Container::ArgumentError, <<~ERROR_MESSAGE)
35 | Base class should be a type of SmartCore::Container
36 | ERROR_MESSAGE
37 | end
38 |
39 | Class.new(initial_container_klass, &container_definitions).new
40 | end
41 | end
42 |
43 | # @since 0.4.0
44 | include ::Enumerable
45 |
46 | # @since 0.1.0
47 | include DefinitionDSL
48 |
49 | # @return [NilClass]
50 | #
51 | # @api private
52 | # @since 0.8.1
53 | NO_HOST_CONTAINER = nil
54 |
55 | # @return [NilClass]
56 | #
57 | # @api private
58 | # @since 0.8.1
59 | NO_HOST_PATH = nil
60 |
61 | # @return [SmartCore::Container::Registry]
62 | #
63 | # @api private
64 | # @since 0.1.0
65 | attr_reader :registry
66 |
67 | # @return [SmartCore::Container::Host]
68 | #
69 | # @api private
70 | # @since 0.8.1
71 | attr_reader :host
72 |
73 | # @return [SmartCore::Container::DependencyWatcher]
74 | #
75 | # @api private
76 | # @since 0.8.0
77 | attr_reader :watcher
78 |
79 | # @option host_container [SmartCore::Container, NilClass]
80 | # @option host_path [String, NilClass]
81 | # @return [void]
82 | #
83 | # @api public
84 | # @since 0.1.0
85 | # @version 0.10.0
86 | def initialize(host_container: NO_HOST_CONTAINER, host_path: NO_HOST_PATH)
87 | @host = SmartCore::Container::Host.build(host_container, host_path)
88 | build_registry!
89 | @watcher = SmartCore::Container::DependencyWatcher.new(self)
90 | @host_path = host_path
91 | @lock = SmartCore::Engine::ReadWriteLock.new
92 | end
93 |
94 | # @param dependency_name [String, Symbol]
95 | # @param dependency_definition [Block]
96 | # @return [void]
97 | #
98 | # @api public
99 | # @sicne 0.1.0
100 | # @version 0.10.0
101 | def register(
102 | dependency_name,
103 | memoize: SmartCore::Container::Registry::DEFAULT_MEMOIZATION_BEHAVIOR,
104 | &dependency_definition
105 | )
106 |
107 | @lock.write_sync do
108 | registry.register_dependency(dependency_name, memoize: memoize, &dependency_definition)
109 | watcher.notify(dependency_name)
110 | end
111 | end
112 |
113 | # @param namespace_name [String, Symbol]
114 | # @param dependencies_definition [Block]
115 | # @return [void]
116 | #
117 | # @api public
118 | # @since 0.1.0
119 | # @version 0.10.0
120 | def namespace(namespace_name, &dependencies_definition)
121 | @lock.write_sync do
122 | registry.register_namespace(namespace_name, self, &dependencies_definition)
123 | watcher.notify(namespace_name)
124 | end
125 | end
126 |
127 | # @param dependency_path [String, Symbol]
128 | # @return [Any]
129 | #
130 | # @api public
131 | # @since 0.1.0
132 | # @version 0.10.0
133 | def resolve(dependency_path)
134 | @lock.read_sync { DependencyResolver.resolve(self, dependency_path) }
135 | end
136 | alias_method :[], :resolve
137 |
138 | # @param dependency_path [String, Symbol]
139 | # @return [Any]
140 | #
141 | # @api public
142 | # @since 0.1.0
143 | # @version 0.10.0
144 | def fetch(dependency_path)
145 | @lock.read_sync { DependencyResolver.fetch(self, dependency_path) }
146 | end
147 |
148 | # @return [void]
149 | #
150 | # @api public
151 | # @since 0.1.0
152 | # @version 0.10.0
153 | def freeze!
154 | @lock.write_sync { registry.freeze! }
155 | end
156 |
157 | # @return [Boolean]
158 | #
159 | # @api public
160 | # @since 0.1.0
161 | # @version 0.10.0
162 | def frozen?
163 | @lock.read_sync { registry.frozen? }
164 | end
165 |
166 | # @return [void]
167 | #
168 | # @api public
169 | # @since 0.1.0
170 | # @version 0.10.0
171 | def reload!
172 | @lock.write_sync { build_registry! }
173 | end
174 |
175 | # @option all_variants [Boolean]
176 | # @return [Array]
177 | #
178 | # @api public
179 | # @since 0.4.0
180 | # @version 0.10.0
181 | def keys(all_variants: SmartCore::Container::Registry::DEFAULT_KEY_EXTRACTION_BEHAVIOUR)
182 | @lock.read_sync { registry.keys(all_variants: all_variants) }
183 | end
184 |
185 | # @param key [String, Symbol]
186 | # @return [Boolean]
187 | #
188 | # @api public
189 | # @since 0.5.0
190 | # @version 0.10.0
191 | def key?(key)
192 | @lock.read_sync { DependencyResolver.key?(self, key) }
193 | end
194 |
195 | # @param namespace_path [String, Symbol]
196 | # @return [Boolean]
197 | #
198 | # @api public
199 | # @since 0.5.0
200 | # @version 0.10.0
201 | def namespace?(namespace_path)
202 | @lock.read_sync { DependencyResolver.namespace?(self, namespace_path) }
203 | end
204 |
205 | # @param dependency_path [String, Symbol]
206 | # @option memoized [NilClass, Boolean]
207 | # @return [Boolean]
208 | #
209 | # @api public
210 | # @since 0.5.0
211 | # @version 0.10.0
212 | def dependency?(dependency_path, memoized: nil)
213 | @lock.read_sync { DependencyResolver.dependency?(self, dependency_path, memoized: memoized) }
214 | end
215 |
216 | # @option yield_all [Boolean]
217 | # @param block [Block]
218 | # @yield [dependency_name, dependency_value]
219 | # @yield_param dependency_name [String]
220 | # @yield_param dependency_value [Any, SmartCore::Container]
221 | # @return [Enumerable]
222 | #
223 | # @api public
224 | # @since 0.4.0
225 | # @version 0.10.0
226 | def each_dependency(
227 | yield_all: SmartCore::Container::Registry::DEFAULT_ITERATION_YIELD_BEHAVIOUR,
228 | &block
229 | )
230 | @lock.read_sync { registry.each_dependency(yield_all: yield_all, &block) }
231 | end
232 | alias_method :each, :each_dependency
233 | alias_method :each_pair, :each_dependency
234 |
235 | # @option resolve_dependencies [Boolean]
236 | # @return [Hash]
237 | #
238 | # @api public
239 | # @since 0.1.0
240 | # @version 0.10.0
241 | def hash_tree(resolve_dependencies: false)
242 | @lock.read_sync { registry.hash_tree(resolve_dependencies: resolve_dependencies) }
243 | end
244 | alias_method :to_h, :hash_tree
245 | alias_method :to_hash, :hash_tree
246 |
247 | # @param entity_path [String]
248 | # @param observer [Block]
249 | # @yield [entity_path, container]
250 | # @yieldparam entity_path [String]
251 | # @yieldparam container [SmartCore::Container]
252 | # @return [SmartCore::Container::DependencyWatcher::Observer]
253 | #
254 | # @api public
255 | # @since 0.8.0
256 | # @version 0.10.0
257 | def observe(entity_path, &observer) # TODO: support for pattern-based pathes
258 | @lock.write_sync { watcher.watch(entity_path, &observer) }
259 | end
260 | alias_method :subscribe, :observe
261 |
262 | # @param observer [SmartCore::Container::DependencyWatcher::Observer]
263 | # @return [Boolean]
264 | #
265 | # @api public
266 | # @since 0.8.0
267 | # @version 0.10.0
268 | def unobserve(observer)
269 | @lock.write_sync { watcher.unwatch(observer) }
270 | end
271 | alias_method :unsubscribe, :unobserve
272 |
273 | # @param entity_path [String, Symbol, NilClass]
274 | # @return [void]
275 | #
276 | # @api public
277 | # @since 0.8.0
278 | # @version 0.10.0
279 | def clear_observers(entity_path = nil) # TODO: support for pattern-based pathes
280 | @lock.write_sync { watcher.clear_listeners(entity_path) }
281 | end
282 | alias_method :clear_listeners, :clear_observers
283 |
284 | private
285 |
286 | # @return [void]
287 | #
288 | # @api private
289 | # @since 0.1.0
290 | # @version 0.8.1
291 | def build_registry!
292 | @registry = RegistryBuilder.build(self)
293 | end
294 | end
295 | end
296 |
--------------------------------------------------------------------------------
/lib/smart_core/container/definition_dsl.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class SmartCore::Container
4 | # @api private
5 | # @since 0.1.0
6 | module DefinitionDSL
7 | require_relative 'definition_dsl/commands'
8 | require_relative 'definition_dsl/command_set'
9 | require_relative 'definition_dsl/inheritance'
10 |
11 | class << self
12 | # @param base_klass [Class]
13 | # @return [void]
14 | #
15 | # @api private
16 | # @since 0.1.0
17 | def included(base_klass)
18 | base_klass.instance_variable_set(:@__container_definition_commands__, CommandSet.new)
19 | base_klass.instance_variable_set(:@__container_instantiation_commands__, CommandSet.new)
20 | base_klass.instance_variable_set(:@__container_definition_lock__, SmartCore::Engine::ReadWriteLock.new)
21 | base_klass.singleton_class.send(:attr_reader, :__container_definition_commands__)
22 | base_klass.singleton_class.send(:attr_reader, :__container_instantiation_commands__)
23 | base_klass.extend(ClassMethods)
24 | base_klass.singleton_class.prepend(ClassInheritance)
25 | end
26 | end
27 |
28 | # @api private
29 | # @since 0.1.0
30 | module ClassInheritance
31 | # @param child_klass [Class]
32 | # @return [void]
33 | #
34 | # @api private
35 | # @since 0.1.0
36 | def inherited(child_klass)
37 | child_klass.instance_variable_set(:@__container_definition_commands__, CommandSet.new)
38 | child_klass.instance_variable_set(:@__container_instantiation_commands__, CommandSet.new)
39 | child_klass.instance_variable_set(:@__container_definition_lock__, SmartCore::Engine::ReadWriteLock.new)
40 | SmartCore::Container::DefinitionDSL::Inheritance.inherit(base: self, child: child_klass)
41 | child_klass.singleton_class.prepend(ClassInheritance)
42 | super
43 | end
44 | end
45 |
46 | # @api private
47 | # @since 0.1.0
48 | module ClassMethods
49 | # @param namespace_name [String, Symbol]
50 | # @param dependencies_definition [Block]
51 | # @return [void]
52 | #
53 | # @api public
54 | # @since 0.1.0
55 | def namespace(namespace_name, &dependencies_definition)
56 | @__container_definition_lock__.write_sync do
57 | DependencyCompatability::Definition.prevent_dependency_overlap!(self, namespace_name)
58 |
59 | __container_definition_commands__ << Commands::Definition::Namespace.new(
60 | namespace_name, dependencies_definition
61 | )
62 | end
63 | end
64 |
65 | # @param dependency_name [String, Symbol]
66 | # @option memoize [Boolean]
67 | # @param dependency_definition [Block]
68 | # @return [void]
69 | #
70 | # @api public
71 | # @since 0.1.0
72 | # @version 0.3.0
73 | def register(
74 | dependency_name,
75 | memoize: SmartCore::Container::Registry::DEFAULT_MEMOIZATION_BEHAVIOR,
76 | &dependency_definition
77 | )
78 | @__container_definition_lock__.write_sync do
79 | DependencyCompatability::Definition.prevent_namespace_overlap!(self, dependency_name)
80 |
81 | __container_definition_commands__ << Commands::Definition::Register.new(
82 | dependency_name, dependency_definition, memoize
83 | )
84 | end
85 | end
86 |
87 | # @param container_klass [Class]
88 | # @return [void]
89 | #
90 | # @api public
91 | # @since 0.1.0
92 | def compose(container_klass)
93 | @__container_definition_lock__.write_sync do
94 | __container_definition_commands__ << Commands::Definition::Compose.new(
95 | container_klass
96 | )
97 |
98 | __container_instantiation_commands__ << Commands::Instantiation::Compose.new(
99 | container_klass
100 | )
101 | end
102 | end
103 |
104 | # @return [void]
105 | #
106 | # @api public
107 | # @since 0.1.0
108 | def freeze_state!
109 | @__container_definition_lock__.write_sync do
110 | __container_instantiation_commands__ << Commands::Instantiation::FreezeState.new
111 | end
112 | end
113 | end
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/lib/smart_core/container/definition_dsl/command_set.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | # @version 0.11.0
6 | class SmartCore::Container::DefinitionDSL::CommandSet
7 | # @since 0.1.0
8 | include Enumerable
9 |
10 | # @return [Array]
11 | #
12 | # @api private
13 | # @since 0.1.0
14 | attr_reader :commands
15 |
16 | # @return [void]
17 | #
18 | # @api private
19 | # @since 0.1.0
20 | # @version 0.11.0
21 | def initialize
22 | @commands = []
23 | @lock = SmartCore::Engine::ReadWriteLock.new
24 | end
25 |
26 | # @param [SmartCore::Container::DefinitionDSL::Commands::Base]
27 | # @return [void]
28 | #
29 | # @api private
30 | # @since 0.1.0
31 | # @version 0.11.0
32 | def add_command(command)
33 | @lock.write_sync { commands << command }
34 | end
35 | alias_method :<<, :add_command
36 |
37 | # @param block [Block]
38 | # @return [Enumerable]
39 | #
40 | # @api private
41 | # @since 0.1.0
42 | # @version 0.11.0
43 | def each(&block)
44 | @lock.read_sync { block_given? ? commands.each(&block) : commands.each }
45 | end
46 |
47 | # @param command_set [SmartCore::Container::DefinitionDSL::CommandSet]
48 | # @param concat_condition [Block]
49 | # @yield [command]
50 | # @yieldparam command [SmartCore::Container::DefinitionDSL::Commands::Base]
51 | # @return [void]
52 | #
53 | # @api private
54 | # @since 0.1.0
55 | # @version 0.11.0
56 | def concat(command_set, &concat_condition)
57 | @lock.read_sync do
58 | if block_given?
59 | command_set.dup.each { |command| (commands << command) if yield(command) }
60 | else
61 | # :nocov:
62 | commands.concat(command_set.dup.commands) # NOTE: unreachable code at this moment
63 | # :nocov:
64 | end
65 | end
66 | end
67 |
68 | # @return [SmartCore::Container::DefinitionDSL::CommandSet]
69 | #
70 | # @api private
71 | # @since 0.1.0
72 | # @version 0.11.0
73 | def dup
74 | @lock.read_sync do
75 | self.class.new.tap do |duplicate|
76 | commands.each do |command|
77 | duplicate.add_command(command.dup)
78 | end
79 | end
80 | end
81 | end
82 |
83 | private
84 |
85 | # @param block [Block]
86 | # @return [Any]
87 | #
88 | # @api private
89 | # @since 0.1.0
90 | def thread_safe(&block)
91 | @lock.thread_safe(&block)
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/lib/smart_core/container/definition_dsl/commands.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | module SmartCore::Container::DefinitionDSL::Commands
6 | require_relative 'commands/base'
7 | require_relative 'commands/instantiation'
8 | require_relative 'commands/definition'
9 | end
10 |
--------------------------------------------------------------------------------
/lib/smart_core/container/definition_dsl/commands/base.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | class SmartCore::Container::DefinitionDSL::Commands::Base
6 | class << self
7 | # @param identifier [Boolean]
8 | # @return [Boolean]
9 | #
10 | # @api private
11 | # @since 0.19.0
12 | def inheritable=(identifier)
13 | @inheritable = identifier
14 | end
15 |
16 | # @return [Boolean]
17 | #
18 | # @api private
19 | # @since 0.19.0
20 | def inheritable?
21 | @inheritable
22 | end
23 |
24 | # @return [Boolean]
25 | #
26 | # @api private
27 | # @since 0.19.0
28 | def inherited(child_klass)
29 | child_klass.instance_variable_set(:@inheritable, true)
30 | super
31 | end
32 | end
33 |
34 | # @param regsitry [SmartCore::Container::Registry]
35 | # @return [void]
36 | #
37 | # @api private
38 | # @since 0.1.0
39 | def call(registry); end
40 |
41 | # @return [Boolean]
42 | #
43 | # @api private
44 | # @since 0.19.0
45 | def inheritable?
46 | self.class.inheritable?
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/smart_core/container/definition_dsl/commands/definition.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | module SmartCore::Container::DefinitionDSL::Commands::Definition
6 | require_relative 'definition/namespace'
7 | require_relative 'definition/register'
8 | require_relative 'definition/compose'
9 | end
10 |
--------------------------------------------------------------------------------
/lib/smart_core/container/definition_dsl/commands/definition/compose.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmartCore::Container::DefinitionDSL::Commands::Definition
4 | # @api private
5 | # @since 0.1.0
6 | class Compose < SmartCore::Container::DefinitionDSL::Commands::Base
7 | # @since 0.1.0
8 | self.inheritable = true
9 |
10 | # @param container_klass [Class]
11 | # @return [void]
12 | #
13 | # @api private
14 | # @since 0.1.0
15 | def initialize(container_klass)
16 | raise(
17 | SmartCore::ArgumentError,
18 | 'Container class should be a subtype of Quantum::Container'
19 | ) unless container_klass < SmartCore::Container
20 |
21 | @container_klass = container_klass
22 | end
23 |
24 | # @param registry [SmartCore::Container::Registry]
25 | # @return [void]
26 | #
27 | # @api private
28 | # @since 0.1.0
29 | def call(registry)
30 | SmartCore::Container::RegistryBuilder.define(container_klass, registry)
31 | end
32 |
33 | # @return [SmartCore::Container::DefinitionDSL::Commands::Definition::Compose]
34 | #
35 | # @api private
36 | # @since 0.1.0
37 | def dup
38 | self.class.new(container_klass)
39 | end
40 |
41 | private
42 |
43 | # @return [Class]
44 | #
45 | # @api private
46 | # @since 0.1.0
47 | attr_reader :container_klass
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/smart_core/container/definition_dsl/commands/definition/namespace.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmartCore::Container::DefinitionDSL::Commands::Definition
4 | # @api private
5 | # @since 0.1.0
6 | class Namespace < SmartCore::Container::DefinitionDSL::Commands::Base
7 | # @since 0.1.0
8 | self.inheritable = true
9 |
10 | # @return [String]
11 | #
12 | # @api private
13 | # @since 0.1.0
14 | attr_reader :namespace_name
15 |
16 | # @param namespace_name [String, Symbol]
17 | # @param dependencies_definition [Proc]
18 | # @return [void]
19 | #
20 | # @api private
21 | # @since 0.1.0
22 | def initialize(namespace_name, dependencies_definition)
23 | SmartCore::Container::KeyGuard.indifferently_accessable_key(namespace_name).tap do |name|
24 | @namespace_name = name
25 | @dependencies_definition = dependencies_definition
26 | end
27 | end
28 |
29 | # @param registry [SmartCore::Container::Registry]
30 | # @return [void]
31 | #
32 | # @api private
33 | # @since 0.1.0
34 | def call(registry)
35 | registry.register_namespace(namespace_name, &dependencies_definition)
36 | end
37 |
38 | # @return [SmartCore::Container::DefinitionDSL::Commands::Definition::Namespace]
39 | #
40 | # @api private
41 | # @since 0.1.0
42 | def dup
43 | self.class.new(namespace_name, dependencies_definition)
44 | end
45 |
46 | private
47 |
48 | # @return [Proc]
49 | #
50 | # @api private
51 | # @since 0.1.0
52 | attr_reader :dependencies_definition
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/smart_core/container/definition_dsl/commands/definition/register.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmartCore::Container::DefinitionDSL::Commands::Definition
4 | # @api private
5 | # @since 0.1.0
6 | class Register < SmartCore::Container::DefinitionDSL::Commands::Base
7 | # @since 0.1.0
8 | self.inheritable = true
9 |
10 | # @return [String]
11 | #
12 | # @api private
13 | # @since 0.1.0
14 | attr_reader :dependency_name
15 |
16 | # @param dependency_name [String, Symbol]
17 | # @param dependency_definition [Proc]
18 | # @param memoize [Boolean]
19 | # @return [void]
20 | #
21 | # @api private
22 | # @since 0.1.0
23 | # @version 0.2.0
24 | def initialize(dependency_name, dependency_definition, memoize)
25 | SmartCore::Container::KeyGuard.indifferently_accessable_key(dependency_name).tap do |name|
26 | @dependency_name = name
27 | @dependency_definition = dependency_definition
28 | @memoize = memoize
29 | end
30 | end
31 |
32 | # @param registry [SmartCore::Container::Registry]
33 | # @return [void]
34 | #
35 | # @api private
36 | # @since 0.1.0
37 | # @version 0.2.0
38 | def call(registry)
39 | registry.register_dependency(dependency_name, memoize: memoize, &dependency_definition)
40 | end
41 |
42 | # @return [SmartCore::Container::DefinitionDSL::Commands::Definition::Register]
43 | #
44 | # @api private
45 | # @since 0.1.0
46 | # @version 0.2.0
47 | def dup
48 | self.class.new(dependency_name, dependency_definition, memoize)
49 | end
50 |
51 | private
52 |
53 | # @return [Proc]
54 | #
55 | # @api private
56 | # @since 0.1.0
57 | attr_reader :dependency_definition
58 |
59 | # @return [Boolean]
60 | #
61 | # @api private
62 | # @since 0.2.0
63 | attr_reader :memoize
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/lib/smart_core/container/definition_dsl/commands/instantiation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | module SmartCore::Container::DefinitionDSL::Commands::Instantiation
6 | require_relative 'instantiation/compose'
7 | require_relative 'instantiation/freeze_state'
8 | end
9 |
--------------------------------------------------------------------------------
/lib/smart_core/container/definition_dsl/commands/instantiation/compose.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmartCore::Container::DefinitionDSL::Commands::Instantiation
4 | # @api private
5 | # @since 0.1.0
6 | class Compose < SmartCore::Container::DefinitionDSL::Commands::Base
7 | # @since 0.1.0
8 | self.inheritable = true
9 |
10 | # @param container_klass [Class]
11 | # @return [void]
12 | #
13 | # @api private
14 | # @since 0.1.0
15 | def initialize(container_klass)
16 | raise(
17 | SmartCore::ArgumentError,
18 | 'Container class should be a subtype of Quantum::Container'
19 | ) unless container_klass < SmartCore::Container
20 |
21 | @container_klass = container_klass
22 | end
23 |
24 | # @param registry [SmartCore::Container::Registry]
25 | # @return [void]
26 | #
27 | # @api private
28 | # @since 0.1.0
29 | def call(registry)
30 | SmartCore::Container::RegistryBuilder.instantiate(
31 | container_klass, registry, ignored_commands: [
32 | SmartCore::Container::DefinitionDSL::Commands::Instantiation::FreezeState
33 | ]
34 | )
35 | end
36 |
37 | # @return [SmartCore::Container::DefinitionDSL::Commands::Instantiation::Compose]
38 | #
39 | # @api private
40 | # @since 0.1.0
41 | def dup
42 | self.class.new(container_klass)
43 | end
44 |
45 | private
46 |
47 | # @return [Class]
48 | #
49 | # @api private
50 | # @since 0.1.0
51 | attr_reader :container_klass
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/smart_core/container/definition_dsl/commands/instantiation/freeze_state.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmartCore::Container::DefinitionDSL::Commands::Instantiation
4 | # @api private
5 | # @since 0.1.0
6 | class FreezeState < SmartCore::Container::DefinitionDSL::Commands::Base
7 | # @since 0.1.0
8 | self.inheritable = false
9 |
10 | # @param registry [SmartCore::Container::Registry]
11 | # @return [void]
12 | #
13 | # @api private
14 | # @since 0.1.0
15 | def call(registry)
16 | registry.freeze!
17 | end
18 |
19 | # @return [SmartCore::Container::DefinitionDSL::Commands::Instantiation::FreezeState]
20 | #
21 | # @api private
22 | # @since 0.1.0
23 | def dup
24 | self.class.new
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/smart_core/container/definition_dsl/inheritance.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | module SmartCore::Container::DefinitionDSL::Inheritance
6 | class << self
7 | # @option base [Class]
8 | # @option child [Class]
9 | # @return [void]
10 | #
11 | # @api private
12 | # @since 0.1.0
13 | def inherit(base:, child:)
14 | child.__container_definition_commands__.concat(
15 | base.__container_definition_commands__, &:inheritable?
16 | )
17 |
18 | child.__container_instantiation_commands__.concat(
19 | base.__container_instantiation_commands__, &:inheritable?
20 | )
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/smart_core/container/dependency_compatability.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | module SmartCore::Container::DependencyCompatability
6 | require_relative 'dependency_compatability/general'
7 | require_relative 'dependency_compatability/definition'
8 | require_relative 'dependency_compatability/registry'
9 | end
10 |
--------------------------------------------------------------------------------
/lib/smart_core/container/dependency_compatability/definition.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | module SmartCore::Container::DependencyCompatability::Definition
6 | class << self
7 | # @since 0.1.0
8 | include SmartCore::Container::DependencyCompatability::General
9 |
10 | # @param container_klass [Class]
11 | # @param dependency_name [String, Symbol]
12 | # @return [Boolean]
13 | #
14 | # @api private
15 | # @since 0.1.0
16 | def potential_namespace_overlap?(container_klass, dependency_name)
17 | anonymous_container = Class.new(container_klass).new
18 | anonymous_container.register(dependency_name, &(proc {}))
19 | false
20 | rescue SmartCore::Container::DependencyOverNamespaceOverlapError
21 | true
22 | end
23 |
24 | # @param container_klass [Class]
25 | # @param namespace_name [String, Symbol]
26 | # @return [Boolean]
27 | #
28 | # @api private
29 | # @since 0.1.0
30 | def potential_dependency_overlap?(container_klass, namespace_name)
31 | anonymous_container = Class.new(container_klass).new
32 | anonymous_container.namespace(namespace_name, &(proc {}))
33 | false
34 | rescue SmartCore::Container::NamespaceOverDependencyOverlapError
35 | true
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/smart_core/container/dependency_compatability/general.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | module SmartCore::Container::DependencyCompatability::General
6 | # @param context [Class, SmartCore::Container::Registry]
7 | # @param dependency_name [String, Symbol]
8 | # @return [void]
9 | #
10 | # @raise [SmartCore::Container::DependencyOverNamespaceOverlapError]
11 | #
12 | # @api private
13 | # @since 0.1.0
14 | def prevent_namespace_overlap!(context, dependency_name)
15 | raise(
16 | SmartCore::Container::DependencyOverNamespaceOverlapError,
17 | "Trying to overlap already registered '#{dependency_name}' namespace " \
18 | "with '#{dependency_name}' dependency!"
19 | ) if potential_namespace_overlap?(context, dependency_name)
20 | end
21 |
22 | # @param context [Class, SmartCore::Container::Registry]
23 | # @param namespace_name [String, Symbol]
24 | # @return [void]
25 | #
26 | # @raise [SmartCore::Container::NamespaceOverDependencyOverlapError]
27 | #
28 | # @api private
29 | # @since 0.1.0
30 | def prevent_dependency_overlap!(context, namespace_name)
31 | raise(
32 | SmartCore::Container::NamespaceOverDependencyOverlapError,
33 | "Trying to overlap already registered '#{namespace_name}' dependency " \
34 | "with '#{namespace_name}' namespace!"
35 | ) if potential_dependency_overlap?(context, namespace_name)
36 | end
37 |
38 | # @param context [Class, SmartCore::Container::Registry]
39 | # @param dependency_name [String, Symbol]
40 | # @return [Boolean]
41 | #
42 | # @api private
43 | # @since 0.1.0
44 | def potential_namespace_overlap?(context, dependency_name)
45 | # :nocov:
46 | raise NoMethodError
47 | # :nocov:
48 | end
49 |
50 | # @param context [Class, SmartCore::Container::Registry]
51 | # @param namespace_name [String, Symbol]
52 | # @return [Boolean]
53 | #
54 | # @api private
55 | # @since 0.1.0
56 | def potential_dependency_overlap?(context, namespace_name)
57 | # :nocov:
58 | raise NoMethodError
59 | # :nocov:
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/smart_core/container/dependency_compatability/registry.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | module SmartCore::Container::DependencyCompatability::Registry
6 | class << self
7 | # @since 0.1.0
8 | include SmartCore::Container::DependencyCompatability::General
9 |
10 | # @param registry [SmartCore::Container::Registry]
11 | # @param dependency_name [String, Symbol]
12 | # @return [Boolean]
13 | #
14 | # @api private
15 | # @since 0.1.0
16 | def potential_namespace_overlap?(registry, dependency_name)
17 | registry.any? do |(entity_name, entity)|
18 | next unless entity.is_a?(SmartCore::Container::Entities::Namespace)
19 | entity.namespace_name == dependency_name
20 | end
21 | end
22 |
23 | # @param registry [SmartCore::Container::Registry]
24 | # @param namespace_name [String, Symbol]
25 | # @return [Boolean]
26 | #
27 | # @api private
28 | # @since 0.1.0
29 | def potential_dependency_overlap?(registry, namespace_name)
30 | registry.any? do |(entity_name, entity)|
31 | next unless entity.is_a?(SmartCore::Container::Entities::Dependency)
32 | entity.dependency_name == namespace_name
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/smart_core/container/dependency_resolver.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | module SmartCore::Container::DependencyResolver
6 | require_relative 'dependency_resolver/route'
7 |
8 | # @return [String]
9 | #
10 | # @api private
11 | # @since 0.4.0
12 | PATH_PART_SEPARATOR = '.'
13 |
14 | class << self
15 | # @param container [SmartCore::Container]
16 | # @param dependency_path [String, Symbol]
17 | # @return [SmartCore::Container, Any]
18 | #
19 | # @see SmartCore::Container::Registry#resolve
20 | # @see SmartCore::Container::Entities::Namespace#reveal
21 | # @see SmartCore::Container::Entities::Dependency#reveal
22 | #
23 | # @api private
24 | # @since 0.1.0
25 | # @version 0.8.1
26 | def fetch(container, dependency_path)
27 | container.registry.resolve(dependency_path).reveal(container)
28 | end
29 |
30 | # @param container [SmartCore::Container]
31 | # @param key [String, Symbol]
32 | # @return [Boolean]
33 | #
34 | # @api private
35 | # @since 0.5.0
36 | def key?(container, key)
37 | extract(container, key)
38 | true
39 | rescue SmartCore::Container::ResolvingError
40 | false
41 | end
42 |
43 | # @param container [SmartCore::Container]
44 | # @param namespace_path [String, Symbol]
45 | # @return [Boolean]
46 | #
47 | # @api private
48 | # @since 0.5.0
49 | def namespace?(container, namespace_path)
50 | extract(container, namespace_path).is_a?(SmartCore::Container::Entities::Namespace)
51 | rescue SmartCore::Container::ResolvingError
52 | false
53 | end
54 |
55 | # @param container [SmartCore::Container]
56 | # @param dependency_path [String, Symbol]
57 | # @option memoized [NilClass, Boolean]
58 | # @return [Boolean]
59 | #
60 | # @api private
61 | # @since 0.5.0
62 | def dependency?(container, dependency_path, memoized: nil)
63 | entity = extract(container, dependency_path)
64 |
65 | case
66 | when memoized == nil
67 | entity.is_a?(SmartCore::Container::Entities::Dependency)
68 | when !!memoized == true
69 | entity.is_a?(SmartCore::Container::Entities::MemoizedDependency)
70 | when !!memoized == false
71 | entity.is_a?(SmartCore::Container::Entities::Dependency) &&
72 | !entity.is_a?(SmartCore::Container::Entities::MemoizedDependency)
73 | end
74 | rescue SmartCore::Container::ResolvingError
75 | false
76 | end
77 |
78 | # @param container [SmartCore::Container]
79 | # @param dependency_path [String, Symbol]
80 | # @return [SmartCore::Container, Any]
81 | #
82 | # @see SmartCore::Container::Registry#resolve
83 | # @see SmartCore::Container::Entities::Namespace#reveal
84 | # @see SmartCore::Container::Entities::Dependency#reveal
85 | #
86 | # @raise [SmartCore::Container::ResolvingError]
87 | #
88 | # @api private
89 | # @since 0.1.0
90 | # @version 0.8.1
91 | def resolve(container, dependency_path)
92 | entity = container
93 | host_container = container
94 |
95 | Route.build(dependency_path).each do |cursor|
96 | entity = entity.registry.resolve(cursor.current_path)
97 | prevent_ambiguous_resolving!(cursor, entity)
98 | entity = entity.reveal(host_container)
99 | host_container = entity.is_a?(SmartCore::Container) ? entity : nil
100 | end
101 | entity
102 | rescue SmartCore::Container::ResolvingError => error
103 | process_resolving_error(dependency_path, error)
104 | end
105 |
106 | private
107 |
108 | # @param container [SmartCore::Container]
109 | # @param entity_path [String, Symbol]
110 | # @return [SmartCore::Container::Entities::Base]
111 | #
112 | # @api private
113 | # @since 0.5.0
114 | # @version 0.8.1
115 | def extract(container, entity_path)
116 | resolved_entity = container
117 | extracted_entity = container
118 | host_container = container
119 |
120 | Route.build(entity_path).each do |cursor|
121 | resolved_entity = resolved_entity.registry.resolve(cursor.current_path)
122 | extracted_entity = resolved_entity
123 | resolved_entity = resolved_entity.reveal(host_container)
124 | host_container = resolved_entity.is_a?(SmartCore::Container) ? resolved_entity : nil
125 | end
126 |
127 | extracted_entity
128 | end
129 |
130 | # @param cursor [SmartCore::Container::DependencyResolver::Route::Cursor]
131 | # @param entity [SmartCore::Container::Entities::Base]
132 | # @return [void]
133 | #
134 | # @raise [SmartCore::Container::ResolvingError]
135 | #
136 | # @api private
137 | # @since 0.5.0
138 | def prevent_ambiguous_resolving!(cursor, entity)
139 | if cursor.last? && entity.is_a?(SmartCore::Container::Entities::Namespace)
140 | raise(
141 | SmartCore::Container::ResolvingError.new(
142 | 'Trying to resolve a namespace as a dependency',
143 | path_part: cursor.current_path
144 | )
145 | )
146 | end
147 |
148 | if !cursor.last? && entity.is_a?(SmartCore::Container::Entities::Dependency)
149 | raise(
150 | SmartCore::Container::ResolvingError.new(
151 | 'Trying to resolve nonexistent dependency',
152 | path_part: cursor.current_path
153 | )
154 | )
155 | end
156 | end
157 |
158 | # @param dependency_path [String, Symbol]
159 | # @param error [SmartCore::Container::ResolvingError]
160 | # @return [void]
161 | #
162 | # @raise [SmartCore::Container::ResolvingError]
163 | #
164 | # @api private
165 | # @since 0.1.0
166 | def process_resolving_error(dependency_path, error)
167 | full_dependency_path = Route.build_path(error.path_part)
168 | message = "#{error.message} (incorrect path: \"#{full_dependency_path}\")"
169 | raise(SmartCore::Container::ResolvingError.new(message, path_part: full_dependency_path))
170 | end
171 | end
172 | end
173 |
--------------------------------------------------------------------------------
/lib/smart_core/container/dependency_resolver/route.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | # @version 0.4.0
6 | class SmartCore::Container::DependencyResolver::Route
7 | require_relative 'route/cursor'
8 |
9 | # @since 0.1.0
10 | include Enumerable
11 |
12 | class << self
13 | # @param path [String, Symbol]
14 | # @return [SmartCore::Container::DependencyResolver::Route]
15 | #
16 | # @api private
17 | # @since 0.1.0
18 | def build(path)
19 | new(SmartCore::Container::KeyGuard.indifferently_accessable_key(path))
20 | end
21 |
22 | # @return [Array]
23 | #
24 | # @api private
25 | # @since 0.1.0
26 | # @version 0.4.0
27 | def build_path(*path_parts)
28 | path_parts.join(SmartCore::Container::DependencyResolver::PATH_PART_SEPARATOR)
29 | end
30 | end
31 |
32 | # @return [Integer]
33 | #
34 | # @api private
35 | # @since 0.1.0
36 | attr_reader :size
37 |
38 | # @return [String]
39 | #
40 | # @api private
41 | # @since 0.1.0
42 | attr_reader :path
43 |
44 | # @param path [String]
45 | # @return [void]
46 | #
47 | # @api private
48 | # @since 0.1.0
49 | # @version 0.4.0
50 | def initialize(path)
51 | @path = path
52 | @path_parts = path.split(SmartCore::Container::DependencyResolver::PATH_PART_SEPARATOR).freeze
53 | @size = @path_parts.size
54 | end
55 |
56 | # @param block [Block]
57 | # @yield cursor [SmartCore::Container::DependencyResolver::Route::Cursor]
58 | # @return [Enumerable]
59 | #
60 | # @api private
61 | # @since 0.1.0
62 | def each(&block)
63 | enumerator = Enumerator.new do |yielder|
64 | path_parts.each_with_index do |path_part, path_part_index|
65 | cursor = Cursor.new(path_part, path_part_index, self)
66 | yielder.yield(cursor)
67 | end
68 | end
69 |
70 | block_given? ? enumerator.each(&block) : enumerator
71 | end
72 |
73 | private
74 |
75 | # @return [Array]
76 | #
77 | # @api private
78 | # @since 0.1.0
79 | attr_reader :path_parts
80 | end
81 |
--------------------------------------------------------------------------------
/lib/smart_core/container/dependency_resolver/route/cursor.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | class SmartCore::Container::DependencyResolver::Route::Cursor
6 | # @return [String]
7 | #
8 | # @api private
9 | # @since 0.1.0
10 | attr_reader :path_part
11 | alias_method :current_path, :path_part
12 |
13 | # @param path_part [String]
14 | # @param path_part_index [Integer]
15 | # @param route [SmartCore::Container::DependencyResolver::Route]
16 | # @return [void]
17 | #
18 | # @api private
19 | # @since 0.1.0
20 | def initialize(path_part, path_part_index, route)
21 | @path_part = path_part
22 | @path_part_index = path_part_index
23 | @route = route
24 | end
25 |
26 | # @return [Boolean]
27 | #
28 | # @api private
29 | # @since 0.1.0
30 | def last?
31 | route.size <= (path_part_index + 1)
32 | end
33 |
34 | private
35 |
36 | # @return [Integer]
37 | #
38 | # @api private
39 | # @since 0.1.0
40 | attr_reader :path_part_index
41 |
42 | # @return [SmartCore::Container::DependencyResolver::Route]
43 | #
44 | # @api private
45 | # @since 0.1.0
46 | attr_reader :route
47 | end
48 |
--------------------------------------------------------------------------------
/lib/smart_core/container/dependency_watcher.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.8.0
5 | # @version 0.11.0
6 | class SmartCore::Container::DependencyWatcher
7 | require_relative 'dependency_watcher/observer'
8 |
9 | # @param container [SmartCore::Container]
10 | # @return [void]
11 | #
12 | # @api private
13 | # @since 0.8.0
14 | # @version 0.11.0
15 | def initialize(container)
16 | @container = container
17 | @observers = Hash.new { |h, k| h[k] = [] }
18 | @lock = SmartCore::Engine::ReadWriteLock.new
19 | end
20 |
21 | # @param entity_path [String, Symbol]
22 | # @return [void]
23 | #
24 | # @api private
25 | # @since 0.8.0
26 | # @version 0.11.0
27 | def notify(entity_path)
28 | @lock.read_sync { notify_listeners(entity_path) }
29 | end
30 |
31 | # @param entity_path [String, Symbol]
32 | # @param observer [Block]
33 | # @return [SmartCore::Container::DependencyWatcher::Observer]
34 | #
35 | # @api private
36 | # @since 0.8.0
37 | # @version 0.11.0
38 | def watch(entity_path, &observer) # TODO: support for pattern-based pathes
39 | @lock.write_sync { listen(entity_path, observer) }
40 | end
41 |
42 | # @param observer [SmartCore::Container::DependencyWatcher::Observer]
43 | # @return [Boolean]
44 | #
45 | # @api private
46 | # @since 0.8.0
47 | # @version 0.11.0
48 | def unwatch(observer)
49 | @lock.write_sync { remove_listener(observer) }
50 | end
51 |
52 | # @param entity_path [String, Symbol, NilClass]
53 | # @return [void]
54 | #
55 | # @api private
56 | # @since 0.8.0
57 | # @version 0.11.0
58 | def clear_listeners(entity_path = nil) # TODO: support for pattern-based pathes
59 | @lock.write_sync { remove_listeners(entity_path) }
60 | end
61 |
62 | private
63 |
64 | # @return [SmartCore::Container]
65 | #
66 | # @api private
67 | # @since 0.8.0
68 | attr_reader :container
69 |
70 | # @return [Hash]
71 | #
72 | # @api private
73 | # @since 0.8.0
74 | attr_reader :observers
75 |
76 | # @param entity_path [String, Symbol]
77 | # @return [void]
78 | #
79 | # @api private
80 | # @since 0.8.0
81 | # @version 0.8.1
82 | def notify_listeners(entity_path)
83 | entity_path = indifferently_accessable_path(entity_path)
84 | observers.fetch(entity_path).each(&:notify!) if observers.key?(entity_path)
85 | container.host.notify_about_nested_changement(entity_path)
86 | end
87 |
88 | # @param entity_path [String, Symbol]
89 | # @param observer [Proc]
90 | # @return [SmartCore::Container::DependencyWatcher::Observer]
91 | #
92 | # @api private
93 | # @since 0.8.0
94 | def listen(entity_path, observer) # TODO: support for pattern-based pathes
95 | raise(SmartCore::Container::ArgumentError, <<~ERROR_MESSAGE) unless observer.is_a?(Proc)
96 | Observer is missing: you should provide an observer proc object (block).
97 | ERROR_MESSAGE
98 |
99 | entity_path = indifferently_accessable_path(entity_path)
100 | Observer.new(container, entity_path, observer).tap { |obs| observers[entity_path] << obs }
101 | end
102 |
103 | # @param observer [SmartCore::Container::DependencyWatcher::Observer]
104 | # @return [Boolean]
105 | #
106 | # @api private
107 | # @since 0.8.0
108 | def remove_listener(observer)
109 | unless observer.is_a?(SmartCore::Container::DependencyWatcher::Observer)
110 | raise(SmartCore::Container::ArgumentError, <<~ERROR_MESSAGE)
111 | You should provide an observer object for unsubscribion
112 | (an instance of SmartCore::Container::DependencyWatcher::Observer).
113 | ERROR_MESSAGE
114 | end
115 |
116 | unsubscribed = false
117 | observers.each_value do |observer_list|
118 | if observer_list.delete(observer)
119 | unsubscribed = true
120 | break
121 | end
122 | end
123 | unsubscribed
124 | end
125 |
126 | # @param entity_path [String, Symbol]
127 | # @return [void]
128 | #
129 | # @api private
130 | # @since 0.8.0
131 | def remove_listeners(entity_path) # TODO: support for pattern-based pathes
132 | if entity_path == nil
133 | observers.each_value(&:clear)
134 | else
135 | entity_path = indifferently_accessable_path(entity_path)
136 | observers[entity_path].clear if observers.key?(entity_path)
137 | end
138 | end
139 |
140 | # @param entity_path [String, Symbol]
141 | # @return [String]
142 | #
143 | # @api private
144 | # @since 0.8.0
145 | def indifferently_accessable_path(entity_path)
146 | SmartCore::Container::KeyGuard.indifferently_accessable_key(entity_path)
147 | end
148 | end
149 |
--------------------------------------------------------------------------------
/lib/smart_core/container/dependency_watcher/observer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.8.0
5 | class SmartCore::Container::DependencyWatcher::Observer
6 | # @param container [SmartCore::Container]
7 | # @param dependency_path [String]
8 | # @param callback [Proc]
9 | # @return [void]
10 | #
11 | # @api private
12 | # @since 0.8.0
13 | def initialize(container, dependency_path, callback)
14 | @container = container
15 | @dependency_path = dependency_path
16 | @callback = callback
17 | end
18 |
19 | # @return [void]
20 | #
21 | # @api private
22 | # @since 0.8.0
23 | def notify!
24 | callback.call(dependency_path, container)
25 | end
26 |
27 | private
28 |
29 | # @return [SmartCore::Container]
30 | #
31 | # @api private
32 | # @since 0.8.0
33 | attr_reader :container
34 |
35 | # @return [String]
36 | #
37 | # @api private
38 | # @since 0.8.0
39 | attr_reader :dependency_path
40 |
41 | # @return [Proc]
42 | #
43 | # @api private
44 | # @since 0.8.0
45 | attr_reader :callback
46 | end
47 |
--------------------------------------------------------------------------------
/lib/smart_core/container/entities.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | module SmartCore::Container::Entities
6 | require_relative 'entities/base'
7 | require_relative 'entities/dependency'
8 | require_relative 'entities/memoized_dependency'
9 | require_relative 'entities/dependency_builder'
10 | require_relative 'entities/namespace'
11 | require_relative 'entities/namespace_builder'
12 | end
13 |
--------------------------------------------------------------------------------
/lib/smart_core/container/entities/base.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | class SmartCore::Container::Entities::Base
6 | # @return [String]
7 | #
8 | # @api private
9 | # @since 0.1.0
10 | attr_reader :external_name
11 |
12 | # @param external_name [String]
13 | # @return [void]
14 | #
15 | # @api private
16 | # @since 0.1.0
17 | def initialize(external_name)
18 | @external_name = external_name
19 | end
20 |
21 | # @return [Any]
22 | #
23 | # @api private
24 | # @since 0.1.0
25 | def reveal; end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/smart_core/container/entities/dependency.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | class SmartCore::Container::Entities::Dependency < SmartCore::Container::Entities::Base
6 | # @return [String]
7 | #
8 | # @api private
9 | # @since 0.1.0
10 | alias_method :dependency_name, :external_name
11 |
12 | # @param dependency_name [String]
13 | # @param dependency_definition [Proc]
14 | # @return [void]
15 | #
16 | # @api private
17 | # @since 0.1.0
18 | def initialize(dependency_name, dependency_definition)
19 | super(dependency_name)
20 | @dependency_definition = dependency_definition
21 | end
22 |
23 | # @param host_container [SmartCore::Container, NilClass]
24 | # @return [Any]
25 | #
26 | # @api private
27 | # @since 0.1.0
28 | # @version 0.8.1
29 | def reveal(host_container = SmartCore::Container::NO_HOST_CONTAINER)
30 | dependency_definition.call
31 | end
32 |
33 | private
34 |
35 | # @return [Proc]
36 | #
37 | # @api private
38 | # @since 0.1.0
39 | attr_reader :dependency_definition
40 | end
41 |
--------------------------------------------------------------------------------
/lib/smart_core/container/entities/dependency_builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | # @version 0.8.1
6 | module SmartCore::Container::Entities::DependencyBuilder
7 | class << self
8 | # @param dependency_name [String]
9 | # @param dependency_definition [Proc]
10 | # @param memoize [Boolean]
11 | # @return [SmartCore::Container::Entities::Dependency]
12 | #
13 | # @api private
14 | # @since 0.1.0
15 | # @version 0.8.1
16 | def build(dependency_name, dependency_definition, memoize)
17 | if memoize
18 | build_memoized_dependency(dependency_name, dependency_definition)
19 | else
20 | build_original_dependency(dependency_name, dependency_definition)
21 | end
22 | end
23 |
24 | private
25 |
26 | # @param dependency_name [String]
27 | # @param dependency_definition [Proc]
28 | # @return [SmartCore::Container::Entities::Dependency]
29 | #
30 | # @api private
31 | # @since 0.8.1
32 | def build_memoized_dependency(dependency_name, dependency_definition)
33 | SmartCore::Container::Entities::MemoizedDependency.new(dependency_name, dependency_definition)
34 | end
35 |
36 | # @param dependency_name [String]
37 | # @param dependency_definition [Proc]
38 | # @return [SmartCore::Container::Entities::Dependency]
39 | #
40 | # @api private
41 | # @since 0.8.1
42 | def build_original_dependency(dependency_name, dependency_definition)
43 | SmartCore::Container::Entities::Dependency.new(dependency_name, dependency_definition)
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/smart_core/container/entities/memoized_dependency.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmartCore::Container::Entities
4 | # @api private
5 | # @since 0.2.0
6 | # @version 0.10.0
7 | class MemoizedDependency < Dependency
8 | # @param dependency_name [String]
9 | # @param dependency_definition [Proc]
10 | # @return [void]
11 | #
12 | # @api private
13 | # @since 0.2.0
14 | # @version 0.10.0
15 | def initialize(dependency_name, dependency_definition)
16 | super(dependency_name, dependency_definition)
17 | @lock = SmartCore::Engine::ReadWriteLock.new
18 | end
19 |
20 | # @param host_container [SmartCore::Container, NilClass]
21 | # @return [Any]
22 | #
23 | # @api private
24 | # @since 0.2.0
25 | # @version 0.8.1
26 | def reveal(host_container = SmartCore::Container::NO_HOST_CONTAINER)
27 | @lock.read_sync do
28 | unless instance_variable_defined?(:@revealed_dependency)
29 | @revealed_dependency = dependency_definition.call
30 | else
31 | @revealed_dependency
32 | end
33 | end
34 | end
35 |
36 | private
37 |
38 | # @return [Proc]
39 | #
40 | # @api private
41 | # @since 0.2.0
42 | attr_reader :dependency_definition
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/smart_core/container/entities/namespace.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | # @version 0.10.0
6 | class SmartCore::Container::Entities::Namespace < SmartCore::Container::Entities::Base
7 | # @return [String]
8 | #
9 | # @api private
10 | # @since 0.1.0
11 | alias_method :namespace_name, :external_name
12 |
13 | # @return [NilClass, SmartCore::Container]
14 | #
15 | # @api private
16 | # @since 0.8.01
17 | attr_reader :host_container
18 |
19 | # @param namespace_name [String]
20 | # @param host_container [NilClass, SmartCore::Container]
21 | # @return [void]
22 | #
23 | # @api private
24 | # @since 0.1.0
25 | # @version 0.10.0
26 | def initialize(namespace_name, host_container = SmartCore::Container::NO_HOST_CONTAINER)
27 | super(namespace_name)
28 | @container_klass = Class.new(SmartCore::Container)
29 | @container_instance = nil
30 | @host_container = host_container
31 | @lock = SmartCore::Engine::ReadWriteLock.new
32 | end
33 |
34 | # @param runtime_host_container [SmartCore::Container, NilClass]
35 | # @return [SmartCore::Container]
36 | #
37 | # @api private
38 | # @since 0.1.0
39 | # @version 0.10.0
40 | def reveal(runtime_host_container = SmartCore::Container::NO_HOST_CONTAINER)
41 | @lock.read_sync { container_instance(runtime_host_container) }
42 | end
43 |
44 | # @param dependencies_definition [Proc]
45 | # @return [void]
46 | #
47 | # @api private
48 | # @since 0.10.0
49 | def append_definitions(dependencies_definition)
50 | @lock.write_sync { container_klass.instance_eval(&dependencies_definition) }
51 | end
52 |
53 | # @return [void]
54 | #
55 | # @api private
56 | # @since 0.10.0
57 | def freeze!
58 | @lock.write_sync { container_instance.freeze! }
59 | end
60 |
61 | private
62 |
63 | # @return [Class]
64 | #
65 | # @api private
66 | # @since 0.1.0
67 | attr_reader :container_klass
68 |
69 | # @param runtime_host_container [SmartCore::Container, NilClass]
70 | # @return [SmartCore::Container]
71 | #
72 | # @api private
73 | # @since 0.1.0
74 | # @version 0.8.1
75 | def container_instance(runtime_host_container = SmartCore::Container::NO_HOST_CONTAINER)
76 | @host_container ||= runtime_host_container
77 | @container_instance ||= container_klass.new(
78 | host_container: @host_container,
79 | host_path: @host_container && namespace_name
80 | )
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/lib/smart_core/container/entities/namespace_builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | # @version 0.8.1
6 | module SmartCore::Container::Entities
7 | module NamespaceBuilder
8 | class << self
9 | # @param namespace_name [String]
10 | # @param host_container [SmartContainer, NilClass]
11 | # @return [SmartCore::Container::Entities::Namespace]
12 | #
13 | # @api private
14 | # @since 0.1.0
15 | # @version 0.8.1
16 | def build(namespace_name, host_container = SmartCore::Container::NO_HOST_CONTAINER)
17 | SmartCore::Container::Entities::Namespace.new(namespace_name, host_container)
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/smart_core/container/errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class SmartCore::Container
4 | # @api public
5 | # @since 0.1.0
6 | Error = Class.new(SmartCore::Error)
7 |
8 | # @api public
9 | # @since 0.1.0
10 | ArgumentError = Class.new(SmartCore::ArgumentError)
11 |
12 | # @api public
13 | # @since 0.1.0
14 | IncompatibleEntityNameError = Class.new(ArgumentError)
15 |
16 | # @see SmartCore::Container::Registry
17 | #
18 | # @api public
19 | # @since 0.1.0
20 | FrozenRegistryError = Class.new(SmartCore::FrozenError)
21 |
22 | # @api public
23 | # @since 0.1.0
24 | FetchError = Class.new(Error)
25 |
26 | # @see SmartCore::Container::DependencyCompatability::General
27 | # @see SmartCore::Container::DependencyCompatability::Definition
28 | # @see SmartCore::Container::DependencyCompatability::Registry
29 | #
30 | # @api public
31 | # @since 0.1.0
32 | DependencyOverNamespaceOverlapError = Class.new(Error)
33 |
34 | # @see SmartCore::Container::DependencyCompatability::General
35 | # @see SmartCore::Container::DependencyCompatability::Definition
36 | # @see SmartCore::Container::DependencyCompatability::Registry
37 | #
38 | # @api public
39 | # @since 0.1.0
40 | NamespaceOverDependencyOverlapError = Class.new(Error)
41 |
42 | # @see SmartCore::Container::DependencyResolver
43 | # @see SmartCore::Container::Registry
44 | #
45 | # @api public
46 | # @since 0.1.0
47 | class ResolvingError < FetchError
48 | # @return [String]
49 | #
50 | # @api private
51 | # @since 0.1.0
52 | attr_reader :path_part
53 |
54 | # @param message [String]
55 | # @param path_part [String]
56 | # @return [void]
57 | #
58 | # @api private
59 | # @since 0.1.0
60 | # @version 0.4.0
61 | def initialize(message = nil, path_part:)
62 | @path_part = path_part
63 | super(message)
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/smart_core/container/host.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | using SmartCore::Ext::BasicObjectAsObject
4 |
5 | # @api private
6 | # @since 0.8.1
7 | class SmartCore::Container::Host
8 | class << self
9 | # @param container [SmartCore::Container]
10 | # @param path [String]
11 | # @return [SmartCore::Container::Host]
12 | #
13 | # @api private
14 | # @since 0.8.1
15 | # rubocop:disable Metrics/AbcSize, Style/NilComparison
16 | def build(container, path)
17 | if (container.nil? && !path.nil?) || (!container.nil? && path.nil?)
18 | raise(SmartCore::Container::ArgumentError, <<~ERROR_MESSAGE)
19 | Host container requires both host container instance and host container path
20 | (container: #{container.inspect} / path: #{path.inspect})
21 | ERROR_MESSAGE
22 | end
23 |
24 | if (!container.nil? && !path.nil?) &&
25 | (!container.is_a?(SmartCore::Container) || !path.is_a?(String))
26 | raise(SmartCore::Container::ArgumentError, <<~ERROR_MESSAGE)
27 | Host container should be a type of SmartCore::Container
28 | and host path should be a type of String.
29 | ERROR_MESSAGE
30 | end
31 |
32 | new(container, path)
33 | end
34 | # rubocop:enable Metrics/AbcSize, Style/NilComparison
35 | end
36 |
37 | # @return [SmartCore::Container]
38 | #
39 | # @api private
40 | # @since 0.8.1
41 | attr_reader :container
42 |
43 | # @return [String]
44 | #
45 | # @api private
46 | # @since 0.8.1
47 | attr_reader :path
48 |
49 | # @return [Boolean]
50 | #
51 | # @api private
52 | # @since 0.8.1
53 | attr_reader :exists
54 | alias_method :exists?, :exists
55 | alias_method :present?, :exists
56 |
57 | # @param container [SmartCore::Container]
58 | # @param path [String]
59 | # @return [void]
60 | #
61 | # @api private
62 | # @since 0.8.1
63 | def initialize(container, path)
64 | @container = container
65 | @path = path
66 | @exists = !!container
67 | end
68 |
69 | # @param nested_entity_path [String]
70 | # @return [void]
71 | #
72 | # @api private
73 | # @since 0.8.1
74 | def notify_about_nested_changement(nested_entity_path)
75 | return unless exists?
76 | host_path = "#{path}" \
77 | "#{SmartCore::Container::DependencyResolver::PATH_PART_SEPARATOR}" \
78 | "#{nested_entity_path}"
79 | container.watcher.notify(host_path)
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/smart_core/container/key_guard.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api priavate
4 | # @since 0.1.0
5 | module SmartCore::Container::KeyGuard
6 | class << self
7 | # @param key [Symbol, String]
8 | # @return [void]
9 | #
10 | # @raise [SmartCore::Container::IncompatibleEntityNameError]
11 | #
12 | # @api private
13 | # @since 0.1.0
14 | def prevent_incomparabilities!(key)
15 | raise(
16 | SmartCore::Container::IncompatibleEntityNameError,
17 | 'Namespace/Dependency name should be a symbol or a string'
18 | ) unless key.is_a?(String) || key.is_a?(Symbol)
19 | end
20 |
21 | # @param key [Symbol, String]
22 | # @return [String]
23 | #
24 | # @api private
25 | # @since 0.1.0
26 | def indifferently_accessable_key(key)
27 | prevent_incomparabilities!(key)
28 | key.to_s
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/smart_core/container/mixin.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api public
4 | # @since 0.1.0
5 | # @version 0.11.0
6 | module SmartCore::Container::Mixin
7 | class << self
8 | # @param base_klass [Class]
9 | # @return [void]
10 | #
11 | # @api private
12 | # @since 0.1.0
13 | # @version 0.11.0
14 | def included(base_klass)
15 | # rubocop:disable Layout/LineLength
16 | base_klass.instance_variable_set(:@__smart_core_container_access_lock__, SmartCore::Engine::ReadWriteLock.new)
17 | base_klass.instance_variable_set(:@__smart_core_container_klass__, Class.new(SmartCore::Container))
18 | base_klass.instance_variable_set(:@__smart_core_container__, nil)
19 | # rubocop:enable Layout/LineLength
20 |
21 | base_klass.extend(ClassMethods)
22 | base_klass.include(InstanceMethods)
23 | base_klass.singleton_class.prepend(ClassInheritance)
24 | end
25 | end
26 |
27 | # @api private
28 | # @since 0.1.0
29 | # @version 0.11.0
30 | module ClassInheritance
31 | # @param child_klass [CLass]
32 | # @return [void]
33 | #
34 | # @api private
35 | # @since 0.1.0
36 | # @version 0.11.0
37 | def inherited(child_klass)
38 | inherited_container_klass = Class.new(@__smart_core_container_klass__)
39 |
40 | # rubocop:disable Layout/LineLength
41 | child_klass.instance_variable_set(:@__smart_core_container_access_lock__, SmartCore::Engine::ReadWriteLock.new)
42 | child_klass.instance_variable_set(:@__smart_core_container_klass__, inherited_container_klass)
43 | child_klass.instance_variable_set(:@__smart_core_container__, nil)
44 | # rubocop:enable Layout/LineLength
45 | super
46 | end
47 | end
48 |
49 | # @api private
50 | # @since 0.1.0
51 | # @version 0.11.0
52 | module ClassMethods
53 | # @param freeze_state [Boolean]
54 | # @param block [Proc]
55 | # @return [void]
56 | #
57 | # @api public
58 | # @since 0.1.0
59 | # @version 0.11.0
60 | def dependencies(freeze_state: false, &block)
61 | @__smart_core_container_access_lock__.write_sync do
62 | @__smart_core_container_klass__.instance_eval(&block) if block_given?
63 | @__smart_core_container_klass__.instance_eval { freeze_state! } if freeze_state
64 | end
65 | end
66 |
67 | # @return [SmartCore::Container]
68 | #
69 | # @api public
70 | # @since 0.1.0
71 | # @version 0.11.0
72 | def container
73 | @__smart_core_container_access_lock__.read_sync do
74 | @__smart_core_container__ ||= @__smart_core_container_klass__.new
75 | end
76 | end
77 | end
78 |
79 | # @api private
80 | # @since 0.1.0
81 | module InstanceMethods
82 | # @return [SmartCore::Container]
83 | #
84 | # @api public
85 | # @since 0.1.0
86 | def container
87 | self.class.container
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/lib/smart_core/container/registry.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | # @version 0.10.0
6 | # rubocop:disable Metrics/ClassLength
7 | class SmartCore::Container::Registry
8 | # @since 0.1.0
9 | include Enumerable
10 |
11 | # @return [Boolean]
12 | #
13 | # @api private
14 | # @since 0.3.0
15 | DEFAULT_MEMOIZATION_BEHAVIOR = false
16 |
17 | # @return [Boolean]
18 | #
19 | # @api private
20 | # @since 0.4.0
21 | DEFAULT_ITERATION_YIELD_BEHAVIOUR = false
22 |
23 | # @return [Boolean]
24 | #
25 | # @api private
26 | # @since 0.4.0
27 | DEFAULT_KEY_EXTRACTION_BEHAVIOUR = false
28 |
29 | # @return [Hash]
30 | #
31 | # @api private
32 | # @since 0.1.0
33 | attr_reader :registry
34 |
35 | # @return [void]
36 | #
37 | # @api private
38 | # @since 0.1.0
39 | # @version 0.10.0
40 | def initialize
41 | @registry = {}
42 | @lock = SmartCore::Engine::ReadWriteLock.new
43 | end
44 |
45 | # @param entity_path [String, Symbol]
46 | # @return [SmartCore::Container::Entity]
47 | #
48 | # @api private
49 | # @since 0.1.0
50 | # @version 0.10.0
51 | def resolve(entity_path)
52 | @lock.read_sync { fetch_entity(entity_path) }
53 | end
54 |
55 | # @param name [String, Symbol]
56 | # @param memoize [Boolean]
57 | # @param dependency_definition [Block]
58 | # @return [void]
59 | #
60 | # @api private
61 | # @since 0.1.0
62 | # @version 0.10.0
63 | def register_dependency(name, memoize: DEFAULT_MEMOIZATION_BEHAVIOR, &dependency_definition)
64 | @lock.write_sync { add_dependency(name, dependency_definition, memoize) }
65 | end
66 |
67 | # @param name [String, Symbol]
68 | # @param host_container [NilClasss, SmartCore::Container]
69 | # @param dependencies_definition [Block]
70 | # @return [void]
71 | #
72 | # @api private
73 | # @since 0.1.0
74 | # @version 0.10.0
75 | def register_namespace(
76 | name,
77 | host_container = SmartCore::Container::NO_HOST_CONTAINER,
78 | &dependencies_definition
79 | )
80 | @lock.write_sync { add_namespace(name, host_container, dependencies_definition) }
81 | end
82 |
83 | # @return [void]
84 | #
85 | # @api private
86 | # @since 0.1.0
87 | # @version 0.10.0
88 | def freeze!
89 | @lock.write_sync { freeze_state! }
90 | end
91 |
92 | # @return [Boolean]
93 | #
94 | # @api private
95 | # @since 0.1.0
96 | # @version 0.10.0
97 | def frozen?
98 | @lock.read_sync { state_frozen? }
99 | end
100 |
101 | # @param block [Block]
102 | # @return [Enumerable]
103 | #
104 | # @api private
105 | # @since 0.1.0
106 | # @version 0.10.0
107 | def each(&block)
108 | @lock.read_sync { enumerate(&block) }
109 | end
110 |
111 | # @param root_dependency_name [NilClass, String]
112 | # @option yield_all [Boolean]
113 | # @param block [Block]
114 | # @return [Enumerable]
115 | #
116 | # @api private
117 | # @since 0.4.0
118 | # @version 0.10.0
119 | def each_dependency(
120 | root_dependency_name = nil,
121 | yield_all: DEFAULT_ITERATION_YIELD_BEHAVIOUR,
122 | &block
123 | )
124 | @lock.read_sync { iterate(root_dependency_name, yield_all: yield_all, &block) }
125 | end
126 |
127 | # @option all_variants [Boolean]
128 | # @return [Array]
129 | #
130 | # @api private
131 | # @since 0.4.0
132 | # @version 0.10.0
133 | def keys(all_variants: DEFAULT_KEY_EXTRACTION_BEHAVIOUR)
134 | @lock.read_sync { extract_keys(all_variants: all_variants) }
135 | end
136 |
137 | # @return [Hash]
138 | #
139 | # @api private
140 | # @since 0.1.0
141 | # @version 0.10.0
142 | def hash_tree(resolve_dependencies: false)
143 | @lock.read_sync { build_hash_tree(resolve_dependencies: resolve_dependencies) }
144 | end
145 | alias_method :to_h, :hash_tree
146 | alias_method :to_hash, :hash_tree
147 |
148 | private
149 |
150 | # @return [Boolean]
151 | #
152 | # @api private
153 | # @since 0.1.0
154 | def state_frozen?
155 | registry.frozen?
156 | end
157 |
158 | # @return [Hash]
159 | #
160 | # @api private
161 | # @since 0.1.0
162 | def build_hash_tree(resolve_dependencies: false)
163 | {}.tap do |tree|
164 | enumerate do |(entity_name, entity)|
165 | case entity
166 | when SmartCore::Container::Entities::Namespace
167 | tree[entity_name] = entity.reveal.hash_tree(resolve_dependencies: resolve_dependencies)
168 | when SmartCore::Container::Entities::Dependency
169 | tree[entity_name] = resolve_dependencies ? entity.reveal : entity
170 | end
171 | end
172 | end
173 | end
174 |
175 | # @return [void]
176 | #
177 | # @api private
178 | # @since 0.1.0
179 | def freeze_state!
180 | registry.freeze.tap do
181 | enumerate do |(entity_name, entity)|
182 | entity.freeze! if entity.is_a?(SmartCore::Container::Entities::Namespace)
183 | end
184 | end
185 | end
186 |
187 | # @param block
188 | # @return [Enumerable]
189 | #
190 | # @api private
191 | # @since 0.1.0
192 | def enumerate(&block)
193 | block_given? ? registry.each(&block) : registry.each
194 | end
195 |
196 | # @paramm entity_path [String, Symbol]
197 | # @return [SmartCore::Container::Entity]
198 | #
199 | # @api private
200 | # @since 0.1.0
201 | # @version 0.1.0
202 | def fetch_entity(entity_path)
203 | dependency_name = indifferently_accessable_name(entity_path)
204 | registry.fetch(dependency_name)
205 | rescue KeyError
206 | error_message = "Entity with \"#{dependency_name}\" name does not exist"
207 | raise(SmartCore::Container::ResolvingError.new(error_message, path_part: dependency_name))
208 | end
209 |
210 | # @param dependency_name [String, Symbol]
211 | # @param dependency_definition [Proc]
212 | # @param memoize [Boolean]
213 | # @return [SmartCore::Container::Entities::Dependency]
214 | #
215 | # @raise [SmartCore::Container::DependencyOverNamespaceOverlapError]
216 | #
217 | # @api private
218 | # @since 0.1.0
219 | # @version 0.2.0
220 | def add_dependency(dependency_name, dependency_definition, memoize)
221 | if state_frozen?
222 | raise(SmartCore::Container::FrozenRegistryError, 'Can not modify frozen registry!')
223 | end
224 | dependency_name = indifferently_accessable_name(dependency_name)
225 | prevent_namespace_overlap!(dependency_name)
226 |
227 | dependency_entity = SmartCore::Container::Entities::DependencyBuilder.build(
228 | dependency_name, dependency_definition, memoize
229 | )
230 |
231 | dependency_entity.tap { registry[dependency_name] = dependency_entity }
232 | end
233 |
234 | # @param namespace_name [String, Symbol]
235 | # @param host_container [NilClass, SmartCore::Container]
236 | # @param dependencies_definition [Proc]
237 | # @return [SmartCore::Container::Entities::Namespace]
238 | #
239 | # @raise [SmartCore::Container::NamespaceOverDependencyOverlapError]
240 | #
241 | # @api private
242 | # @since 0.1.0
243 | # @version 0.8.1
244 | def add_namespace(namespace_name, host_container, dependencies_definition)
245 | if state_frozen?
246 | raise(SmartCore::Container::FrozenRegistryError, 'Can not modify frozen registry!')
247 | end
248 | namespace_name = indifferently_accessable_name(namespace_name)
249 | dependencies_definition ||= proc {}
250 | prevent_dependency_overlap!(namespace_name)
251 |
252 | namespace_entity = begin
253 | fetch_entity(namespace_name)
254 | rescue SmartCore::Container::FetchError
255 | registry[namespace_name] = SmartCore::Container::Entities::NamespaceBuilder.build(
256 | namespace_name, host_container
257 | )
258 | end
259 | namespace_entity.tap { namespace_entity.append_definitions(dependencies_definition) }
260 | end
261 |
262 | # @param root_dependency_name [String, NilClass]
263 | # @param block [Block]
264 | # @option yield_all [Boolean]
265 | # @yield [dependency_name, dependency]
266 | # @yield_param dependency_name [String]
267 | # @yield_param dependency [Any]
268 | # @return [Enumerable]
269 | #
270 | # @api private
271 | # @since 0.4.0
272 | def iterate(root_dependency_name = nil, yield_all: DEFAULT_ITERATION_YIELD_BEHAVIOUR, &block)
273 | enumerator = Enumerator.new do |yielder|
274 | registry.each_pair do |dependency_name, dependency|
275 | final_dependency_name =
276 | if root_dependency_name
277 | "#{root_dependency_name}" \
278 | "#{SmartCore::Container::DependencyResolver::PATH_PART_SEPARATOR}" \
279 | "#{dependency_name}"
280 | else
281 | dependency_name
282 | end
283 |
284 | case dependency
285 | when SmartCore::Container::Entities::Dependency
286 | yielder.yield(final_dependency_name, dependency.reveal)
287 | when SmartCore::Container::Entities::Namespace
288 | yielder.yield(final_dependency_name, dependency.reveal) if yield_all
289 | dependency.reveal.registry.each_dependency(
290 | final_dependency_name,
291 | yield_all: yield_all,
292 | &block
293 | )
294 | end
295 | end
296 | end
297 |
298 | block_given? ? enumerator.each(&block) : enumerator.each
299 | end
300 |
301 | # @option all_variants [Boolean]
302 | # @return [Array]
303 | #
304 | # @api private
305 | # @since 0.4.0
306 | def extract_keys(all_variants: DEFAULT_KEY_EXTRACTION_BEHAVIOUR)
307 | Set.new.tap do |dependency_names|
308 | iterate(yield_all: all_variants) do |dependency_name, _dependency|
309 | dependency_names << dependency_name
310 | end
311 | end.to_a
312 | end
313 |
314 | # @param name [String, Symbol]
315 | # @return [void]
316 | #
317 | # @see [SmartCore::Container::KeyGuard]
318 | #
319 | # @api private
320 | # @since 0.1.0
321 | def indifferently_accessable_name(name)
322 | SmartCore::Container::KeyGuard.indifferently_accessable_key(name)
323 | end
324 |
325 | # @param dependency_name [String]
326 | # @return [void]
327 | #
328 | # @api private
329 | # @since 0.1.0
330 | def prevent_namespace_overlap!(dependency_name)
331 | SmartCore::Container::DependencyCompatability::Registry.prevent_namespace_overlap!(
332 | self, dependency_name
333 | )
334 | end
335 |
336 | # @param namespace_name [String]
337 | # @return [void]
338 | #
339 | # @api private
340 | # @since 0.1.0
341 | def prevent_dependency_overlap!(namespace_name)
342 | SmartCore::Container::DependencyCompatability::Registry.prevent_dependency_overlap!(
343 | self, namespace_name
344 | )
345 | end
346 | end
347 | # rubocop:enable Metrics/ClassLength
348 |
--------------------------------------------------------------------------------
/lib/smart_core/container/registry_builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.1.0
5 | module SmartCore::Container::RegistryBuilder
6 | # rubocop:disable Layout/LineLength
7 | class << self
8 | # @parma container [SmartCore::Container]
9 | # @option ignored_definition_commands [Array>]
10 | # @option ignored_instantiation_commands [Array>]
11 | # @return [SmartCore::Container::Registry]
12 | #
13 | # @api private
14 | # @since 0.1.0
15 | def build(container, ignored_definition_commands: [], ignored_instantiation_commands: [])
16 | SmartCore::Container::Registry.new.tap do |registry|
17 | define(container.class, registry, ignored_commands: ignored_definition_commands)
18 | instantiate(container.class, registry, ignored_commands: ignored_instantiation_commands)
19 | end
20 | end
21 |
22 | # @param container_klass [Class]
23 | # @param registry [SmartCore::Container::Registry]
24 | # @option ignored_commands [Array>]
25 | # @return [void]
26 | #
27 | # @api private
28 | # @since 0.1.0
29 | def define(container_klass, registry, ignored_commands: [])
30 | container_klass.__container_definition_commands__.each do |command|
31 | next if ignored_commands.include?(command.class)
32 | command.call(registry)
33 | end
34 | end
35 |
36 | # @param container_klass [Class]
37 | # @param registry [SmartCore::Container::Registry]
38 | # @option ignored_commands [Array>]
39 | # @return [void]
40 | #
41 | # @api private
42 | # @since 0.1.0
43 | def instantiate(container_klass, registry, ignored_commands: [])
44 | container_klass.__container_instantiation_commands__.each do |command|
45 | next if ignored_commands.include?(command.class)
46 | command.call(registry)
47 | end
48 | end
49 | end
50 | # rubocop:enable Layout/LineLength
51 | end
52 |
--------------------------------------------------------------------------------
/lib/smart_core/container/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmartCore
4 | class Container # rubocop:disable Style/StaticClass
5 | # @return [String]
6 | #
7 | # @api public
8 | # @since 0.1.0
9 | # @version 0.11.0
10 | VERSION = '0.11.0'
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/smart_container.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'lib/smart_core/container/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.required_ruby_version = Gem::Requirement.new('>= 2.5')
7 |
8 | spec.name = 'smart_container'
9 | spec.version = SmartCore::Container::VERSION
10 | spec.authors = ['Rustam Ibragimov']
11 | spec.email = ['iamdaiver@gmail.com']
12 |
13 | spec.summary = 'IoC/DI Container'
14 | spec.description = 'Thread-safe semanticaly-defined IoC/DI Container'
15 | spec.homepage = 'https://github.com/smart-rb/smart-container'
16 | spec.license = 'MIT'
17 |
18 | spec.metadata['homepage_uri'] = spec.homepage
19 | spec.metadata['source_code_uri'] = 'https://github.com/smart-rb/smart-container'
20 | spec.metadata['changelog_uri'] = 'https://github.com/smart-rb/smart-container/CHANGELOG.md'
21 |
22 | spec.files = Dir.chdir(File.expand_path(__dir__)) do
23 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24 | end
25 |
26 | spec.bindir = 'exe'
27 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28 | spec.require_paths = ['lib']
29 |
30 | spec.add_dependency 'smart_engine', '~> 0.17'
31 |
32 | spec.add_development_dependency 'bundler', '~> 2.3'
33 | spec.add_development_dependency 'rake', '~> 13.0'
34 | spec.add_development_dependency 'rspec', '~> 3.11'
35 | spec.add_development_dependency 'armitage-rubocop', '~> 1.36'
36 | spec.add_development_dependency 'simplecov', '~> 0.21'
37 | spec.add_development_dependency 'pry', '~> 0.14'
38 | end
39 |
--------------------------------------------------------------------------------
/spec/features/build_instance_avoding_class_declaration_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Build instance avoiding class declaration' do
4 | specify 'create container instance avoiding class declaration' do
5 | runtime_container = SmartCore::Container.define do
6 | namespace(:database) do
7 | register(:driver) { 'Sequel' }
8 | end
9 | end
10 |
11 | expect(runtime_container).to be_a(SmartCore::Container)
12 | expect(runtime_container['database.driver']).to eq('Sequel')
13 | end
14 |
15 | specify 'create from another container class with a custom sub-definitions' do
16 | basic_container_klass = Class.new(SmartCore::Container) do
17 | namespace(:database) do
18 | register(:driver) { 'ActiveRecord' }
19 | end
20 | end
21 |
22 | # create via SmartCore::Container API
23 | runtime_container = SmartCore::Container.define(basic_container_klass) do
24 | register(:client) { 'Kickbox' }
25 | end
26 |
27 | expect(runtime_container).to be_a(basic_container_klass)
28 | expect(runtime_container).to be_a(SmartCore::Container)
29 | expect(runtime_container['database.driver']).to eq('ActiveRecord')
30 | expect(runtime_container['client']).to eq('Kickbox')
31 |
32 | # create via class-based api from an existing container class
33 | runtime_container = basic_container_klass.define do
34 | namespace(:database) do
35 | register(:driver) { 'Sequel' }
36 | end
37 | register(:client) { 'KwakBox' }
38 | end
39 |
40 | expect(runtime_container).to be_a(basic_container_klass)
41 | expect(runtime_container).to be_a(SmartCore::Container)
42 | expect(runtime_container['database.driver']).to eq('Sequel')
43 | expect(runtime_container['client']).to eq('KwakBox')
44 |
45 | # usage of the basic .define api on any SmartCore::Container class
46 | another_container_klass = Class.new(SmartCore::Container)
47 | runtime_container = basic_container_klass.define(another_container_klass)
48 | expect(runtime_container).to be_a(another_container_klass)
49 | expect(runtime_container).not_to be_a(basic_container_klass)
50 | end
51 |
52 | specify 'fails on incorrect basic container class parameter' do
53 | basic_container_klass = Class.new(SmartCore::Container)
54 |
55 | expect do # NOTE: try to build from non-container class
56 | SmartCore::Container.define(Class.new) {}
57 | end.to raise_error(SmartCore::Container::ArgumentError)
58 |
59 | expect do # NOTE: try to build from correct container class
60 | basic_container_klass.define {}
61 | end.not_to raise_error
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/spec/features/changement_subscription_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Changement subscription (#observe, #unobserve, #clear_observers)' do
4 | specify 'subscribe (#observe) to dependency changement (namespaces and deps) and listen' do
5 | container = SmartCore::Container.define do
6 | namespace(:storages) do
7 | register(:database) { 'database' }
8 | register(:cache) { 'cache' }
9 | register(:replica) { 'replica' }
10 |
11 | namespace(:creds) do
12 | register(:redis) { 'redis' }
13 | end
14 | end
15 | end
16 |
17 | database_interceptor = []
18 | cache_interceptor = []
19 | namespace_interceptor = []
20 |
21 | container.observe('storages.database') { database_interceptor << 'db_changed!' }
22 | container.observe('storages.cache') { cache_interceptor << 'cache_changed!' }
23 | container.observe('storages.creds') { namespace_interceptor << 'namespace_changed!' }
24 |
25 | expect(database_interceptor).to be_empty
26 | expect(cache_interceptor).to be_empty
27 | expect(namespace_interceptor).to be_empty
28 |
29 | container.fetch(:storages).register(:database) { 'new_database' }
30 |
31 | expect(database_interceptor).to contain_exactly('db_changed!')
32 | expect(cache_interceptor).to be_empty
33 | expect(namespace_interceptor).to be_empty
34 |
35 | container.fetch('storages').register(:cache) { 'new_cache' }
36 | expect(database_interceptor).to contain_exactly('db_changed!')
37 | expect(cache_interceptor).to contain_exactly('cache_changed!')
38 | expect(namespace_interceptor).to be_empty
39 |
40 | container.fetch(:storages).register(:database) { 'new_database' }
41 | expect(database_interceptor).to contain_exactly('db_changed!', 'db_changed!')
42 | expect(cache_interceptor).to contain_exactly('cache_changed!')
43 | expect(namespace_interceptor).to be_empty
44 |
45 | container.fetch(:storages).register(:cache) { 'new_cache' }
46 | expect(database_interceptor).to contain_exactly('db_changed!', 'db_changed!')
47 | expect(cache_interceptor).to contain_exactly('cache_changed!', 'cache_changed!')
48 | expect(namespace_interceptor).to be_empty
49 |
50 | container.fetch(:storages).register(:replica) { 'new_replica' }
51 | expect(database_interceptor).to contain_exactly('db_changed!', 'db_changed!')
52 | expect(cache_interceptor).to contain_exactly('cache_changed!', 'cache_changed!')
53 | expect(namespace_interceptor).to be_empty
54 |
55 | container.fetch(:storages).namespace(:creds) {}
56 | expect(database_interceptor).to contain_exactly('db_changed!', 'db_changed!')
57 | expect(cache_interceptor).to contain_exactly('cache_changed!', 'cache_changed!')
58 | expect(namespace_interceptor).to contain_exactly('namespace_changed!')
59 |
60 | container.namespace(:storages) {}
61 | expect(database_interceptor).to contain_exactly('db_changed!', 'db_changed!')
62 | expect(cache_interceptor).to contain_exactly('cache_changed!', 'cache_changed!')
63 | expect(namespace_interceptor).to contain_exactly('namespace_changed!')
64 | end
65 |
66 | specify 'unsubscription from dependency changement (#unobserve, #clear_observers)' do
67 | container = SmartCore::Container.define do
68 | namespace(:api) do
69 | register(:google) { 'google' }
70 | register(:kickbox) { 'kickbox' }
71 | end
72 | end
73 |
74 | google_interceptor = []
75 | kickbox_interceptor = []
76 | namespace_interceptor = []
77 |
78 | # make sensitive observers
79 | # rubocop:disable Lint/UselessAssignment
80 | google_observer = container.observe('api.google') do |path, cntr|
81 | google_interceptor << "#{path}__#{cntr.object_id}"
82 | end
83 | kickbox_observer = container.observe('api.kickbox') do |path, cntr|
84 | kickbox_interceptor << "#{path}__#{cntr.object_id}"
85 | end
86 | namespace_observer = container.observe('api') do |path, cntr|
87 | namespace_interceptor << "#{path}__#{cntr.object_id}"
88 | end
89 | # rubocop:enable Lint/UselessAssignment
90 |
91 | # register new entities
92 | container.fetch(:api).register('google') { 'new_google' }
93 | container.fetch(:api).register('kickbox') { 'new_kickbox' }
94 |
95 | # received dependency entity change
96 | expect(google_interceptor).to contain_exactly("api.google__#{container.object_id}")
97 | # received dependency entity change
98 | expect(kickbox_interceptor).to contain_exactly("api.kickbox__#{container.object_id}")
99 | expect(namespace_interceptor).to be_empty # namespace entity has not changed
100 |
101 | # unsubscribe first entity observer
102 | expect(container.unobserve(google_observer)).to eq(true) # unsubscribed (true)
103 | expect(container.unobserve(google_observer)).to eq(false) # already unsubscribed (false)
104 |
105 | container.fetch('api').register(:google) { 'another_new_google' }
106 | container.fetch('api').register(:kickbox) { 'another_new_kickbox' }
107 |
108 | # unsubscribed
109 | expect(google_interceptor).to contain_exactly("api.google__#{container.object_id}")
110 | # still subscribed
111 | expect(kickbox_interceptor).to contain_exactly(
112 | "api.kickbox__#{container.object_id}",
113 | "api.kickbox__#{container.object_id}" # new
114 | )
115 | expect(namespace_interceptor).to be_empty # namespace entity has not changed
116 |
117 | # change namespace entity
118 | container.namespace('api') {}
119 |
120 | # nothing changed
121 | expect(google_interceptor).to contain_exactly("api.google__#{container.object_id}")
122 | # nothing changed
123 | expect(kickbox_interceptor).to contain_exactly(
124 | "api.kickbox__#{container.object_id}",
125 | "api.kickbox__#{container.object_id}"
126 | )
127 | # received namespace entity change
128 | expect(namespace_interceptor).to contain_exactly("api__#{container.object_id}")
129 |
130 | # register new entities in new enty namespace
131 | container.fetch(:api).register(:google) { 'another_another_new_google' }
132 | container.fetch(:api).register(:kickbox) { 'another_another_new_kickbox' }
133 |
134 | # unsubscribed
135 | expect(google_interceptor).to contain_exactly("api.google__#{container.object_id}")
136 | # received new entity change
137 | expect(kickbox_interceptor).to contain_exactly(
138 | "api.kickbox__#{container.object_id}",
139 | "api.kickbox__#{container.object_id}",
140 | "api.kickbox__#{container.object_id}" # new
141 | )
142 | # nothing chagned
143 | expect(namespace_interceptor).to contain_exactly("api__#{container.object_id}")
144 |
145 | # unsubscribe all kickbox observers
146 | container.clear_observers('api.kickbox')
147 | # register new entities
148 | container.fetch(:api).register(:google) { 'another_another_new_google' }
149 | container.fetch(:api).register(:kickbox) { 'another_another_new_kickbox' }
150 |
151 | # unsubscribed
152 | expect(google_interceptor).to contain_exactly("api.google__#{container.object_id}")
153 | # unsubscribed
154 | expect(kickbox_interceptor).to contain_exactly(
155 | "api.kickbox__#{container.object_id}",
156 | "api.kickbox__#{container.object_id}",
157 | "api.kickbox__#{container.object_id}"
158 | )
159 | # nothing chagned
160 | expect(namespace_interceptor).to contain_exactly("api__#{container.object_id}")
161 |
162 | # unsubscribe all
163 | container.clear_observers
164 |
165 | container.fetch(:api).register(:google) { 'another_new_google' }
166 | container.fetch(:api).register(:kickbox) { 'another_new_kickbox' }
167 | container.namespace('api') {}
168 |
169 | # unsubscribed
170 | expect(google_interceptor).to contain_exactly("api.google__#{container.object_id}")
171 | # unsubscribed
172 | expect(kickbox_interceptor).to contain_exactly(
173 | "api.kickbox__#{container.object_id}",
174 | "api.kickbox__#{container.object_id}",
175 | "api.kickbox__#{container.object_id}"
176 | )
177 | # unsubscribed
178 | expect(namespace_interceptor).to contain_exactly("api__#{container.object_id}")
179 | end
180 |
181 | specify 'right reaction on incompatible behavior and api usage' do
182 | container = SmartCore::Container.define { namespace('path') {} }
183 |
184 | expect { container.observe(123) }.to raise_error(SmartCore::Container::ArgumentError)
185 | expect { container.observe('path') }.to raise_error(SmartCore::Container::ArgumentError)
186 |
187 | container.observe('path') {}
188 |
189 | expect { container.unobserve(123) }.to raise_error(SmartCore::Container::ArgumentError)
190 | expect { container.clear_observers(123) }.to raise_error(SmartCore::Container::ArgumentError)
191 | end
192 | end
193 |
--------------------------------------------------------------------------------
/spec/features/composition_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Composition (.compose macros)' do
4 | specify 'makes composition possbile :)' do
5 | stub_const('DbContainer', Class.new(SmartCore::Container) do
6 | namespace :database do
7 | register(:adapter) { :pg }
8 | end
9 | end)
10 |
11 | stub_const('ApiContainer', Class.new(SmartCore::Container) do
12 | namespace :client do
13 | register(:proxy) { :proxy }
14 | end
15 | end)
16 |
17 | stub_const('CacheContainer', Class.new(SmartCore::Container) do
18 | namespace :database do
19 | register(:cache) { :cache }
20 | end
21 | end)
22 |
23 | stub_const('CompositionRoot', Class.new(SmartCore::Container) do
24 | compose(DbContainer)
25 | compose(ApiContainer)
26 | compose(CacheContainer)
27 |
28 | namespace(:nested) { compose(DbContainer) }
29 | end)
30 |
31 | root_container = CompositionRoot.new
32 |
33 | expect(root_container.fetch(:database).fetch(:adapter)).to eq(:pg)
34 | expect(root_container.fetch(:client).fetch(:proxy)).to eq(:proxy)
35 | expect(root_container.fetch(:database).fetch(:cache)).to eq(:cache)
36 | expect(root_container.fetch(:nested).fetch(:database).fetch(:adapter)).to eq(:pg)
37 | end
38 |
39 | specify 'ignores frozen state (ignores .freeze_state macros)' do
40 | stub_const('DbContainer', Class.new(SmartCore::Container) do
41 | namespace(:database) { register(:adapter) { :db } }
42 |
43 | freeze_state!
44 | end)
45 |
46 | stub_const('CompositionRoot', Class.new(SmartCore::Container) do
47 | compose(DbContainer)
48 | end)
49 |
50 | root_container = CompositionRoot.new
51 |
52 | expect(root_container.frozen?).to eq(false)
53 | expect(root_container.fetch(:database).frozen?).to eq(false)
54 | end
55 |
56 | specify 'fails on incompatible overlappings (at instantiation step only)' do
57 | stub_const('DbContainer', Class.new(SmartCore::Container) do
58 | namespace(:database) {}
59 | end)
60 | stub_const('AnotherDbContainer', Class.new(SmartCore::Container) do
61 | register(:database) {}
62 | end)
63 |
64 | # NOTE: namespace overlap
65 | stub_const('CompositionRoot', Class.new(SmartCore::Container) do
66 | compose(DbContainer)
67 | compose(AnotherDbContainer)
68 | end)
69 | expect do
70 | CompositionRoot.new
71 | end.to raise_error(SmartCore::Container::DependencyOverNamespaceOverlapError)
72 |
73 | # NOTE: dependency overlap
74 | stub_const('CompositionRoot', Class.new(SmartCore::Container) do
75 | compose(AnotherDbContainer)
76 | compose(DbContainer)
77 | end)
78 | expect do
79 | CompositionRoot.new
80 | end.to raise_error(SmartCore::Container::NamespaceOverDependencyOverlapError)
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/spec/features/definition_and_instantiation_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Definition and instantiation' do
4 | specify 'definition DSL and dependency resolving' do
5 | cache_dependency_stub = Object.new
6 | database_dependency_stub = Object.new
7 | messaging_dependency_stub = Object.new
8 | api_dependency_stub = Object.new
9 | logger_stub = Object.new
10 |
11 | container_klass = Class.new(SmartCore::Container) do
12 | # create dependency namespace
13 | namespace :storages do
14 | register(:cache) { cache_dependency_stub }
15 | register(:database) { database_dependency_stub }
16 | end
17 |
18 | # open existing namespace and register new dependencies
19 | namespace :storages do
20 | register(:messaging) { messaging_dependency_stub }
21 | end
22 |
23 | # create new dependency namespace
24 | namespace :api do
25 | # create nested namespace
26 | namespace :common do
27 | register(:general) { api_dependency_stub }
28 | end
29 | end
30 |
31 | # register dependnecy on the root of dependency tree
32 | register(:logger) { logger_stub }
33 | end
34 |
35 | # create container instance
36 | container = container_klass.new
37 |
38 | expect(container.fetch(:storages).fetch(:cache)).to eq(cache_dependency_stub)
39 | expect(container.fetch(:storages).fetch(:database)).to eq(database_dependency_stub)
40 | expect(container.fetch(:storages).fetch(:messaging)).to eq(messaging_dependency_stub)
41 | expect(container.fetch(:api).fetch(:common).fetch(:general)).to eq(api_dependency_stub)
42 | expect(container.fetch(:logger)).to eq(logger_stub)
43 | end
44 |
45 | specify 'define container as frozen that means it should be freezed after instantiation' do
46 | # NOTE: initially it should be non-frozen
47 | non_frozen_container_klass = Class.new(SmartCore::Container) {}
48 | non_frozen_container = non_frozen_container_klass.new
49 | expect(non_frozen_container.frozen?).to eq(false)
50 |
51 | # NOTE: check freezing macros
52 | frozen_container_klass = Class.new(SmartCore::Container) { freeze_state! }
53 | frozen_container = frozen_container_klass.new
54 | expect(frozen_container.frozen?).to eq(true)
55 | end
56 |
57 | specify 'instance-level namespace/dependency registration/resolving' do
58 | database_dependency_stub = Object.new
59 | logger_dependency_stub = Object.new
60 | api_client_dependency_stub = Object.new
61 | randomizer_dependency_stub = Object.new
62 |
63 | container_klass = Class.new(SmartCore::Container) do
64 | namespace :database do
65 | register(:connection) { database_dependency_stub }
66 | end
67 |
68 | register(:logger) { logger_dependency_stub }
69 | end
70 |
71 | container = container_klass.new
72 |
73 | # register new namespace on instance level
74 | container.namespace(:api) { register(:client) { api_client_dependency_stub } }
75 | # register new dependency on instance level
76 | container.register(:randomizer) { randomizer_dependency_stub }
77 |
78 | # check already existing dependencies
79 | expect(container.fetch(:database).fetch(:connection)).to eq(database_dependency_stub)
80 | expect(container.fetch(:logger)).to eq(logger_dependency_stub)
81 |
82 | # check new registered namespaces and dependencies
83 | expect(container.fetch(:api).fetch(:client)).to eq(api_client_dependency_stub)
84 | expect(container.fetch(:randomizer)).to eq(randomizer_dependency_stub)
85 |
86 | another_container = container_klass.new
87 |
88 | # check that new registered dependnecies does not mutate class-level dependency tree
89 | expect { another_container.fetch(:api) }.to raise_error(
90 | SmartCore::Container::FetchError
91 | )
92 | expect { another_container.fetch(:randomizer) }.to raise_error(
93 | SmartCore::Container::FetchError
94 | )
95 | end
96 |
97 | specify '(definition) namespace and dependency can not overlap each other' do
98 | expect do # NOTE: dependency overlaps existing namespace
99 | Class.new(SmartCore::Container) do
100 | namespace(:database) {}
101 | register(:database) {} # overlap!
102 | end
103 | end.to raise_error(SmartCore::Container::DependencyOverNamespaceOverlapError)
104 |
105 | expect do # NOTE: namespace overlaps existing dependency
106 | Class.new(SmartCore::Container) do
107 | register(:database) {}
108 | namespace(:database) # overlap!
109 | end
110 | end.to raise_error(SmartCore::Container::NamespaceOverDependencyOverlapError)
111 | end
112 |
113 | specify '(instance) namespace and dependency can not overlap each other' do
114 | container = Class.new(SmartCore::Container).new
115 | # NOTE: dependency overlaps existing namespace
116 | container.namespace(:database) {}
117 | expect { container.register(:database) {} }.to raise_error(
118 | SmartCore::Container::DependencyOverNamespaceOverlapError
119 | )
120 |
121 | container = Class.new(SmartCore::Container).new
122 | # NOTE: namespace overlaps existing dependency
123 | container.register(:database) {}
124 | expect { container.namespace(:database) {} }.to raise_error(
125 | SmartCore::Container::NamespaceOverDependencyOverlapError
126 | )
127 | end
128 |
129 | specify 'inherited dependency tree does not affect the parent dependency tree' do
130 | database_adapter_stub = Object.new
131 | database_logger_stub = Object.new
132 | base_api_client_stub = Object.new
133 | child_api_client_stub = Object.new
134 | database_logger_stub = Object.new
135 | queue_adapter_stub = Object.new
136 |
137 | base_container_klass = Class.new(SmartCore::Container) do
138 | namespace(:database) do
139 | register(:adapter) { database_adapter_stub }
140 | end
141 |
142 | register(:api_client) { base_api_client_stub }
143 | end
144 |
145 | child_container_klass = Class.new(SmartCore::Container) do
146 | namespace(:database) do
147 | register(:logger) { database_logger_stub }
148 | end
149 |
150 | register(:api_client) { child_api_client_stub }
151 | register(:queue_adapter) { queue_adapter_stub }
152 | end
153 |
154 | base_container = base_container_klass.new
155 | child_container = child_container_klass.new
156 |
157 | # no affections from child_container_klass
158 | expect { base_container.fetch(:database).fetch(:logger) }.to raise_error(
159 | SmartCore::Container::FetchError
160 | )
161 | expect { base_container.fetch(:queue_adapter) }.to raise_error(
162 | SmartCore::Container::FetchError
163 | )
164 | expect(base_container.fetch(:api_client)).to eq(base_api_client_stub)
165 |
166 | # inherited container has own dependency tree
167 | expect(child_container.fetch(:database).fetch(:logger)).to eq(database_logger_stub)
168 | expect(child_container.fetch(:api_client)).to eq(child_api_client_stub)
169 | expect(child_container.fetch(:queue_adapter)).to eq(queue_adapter_stub)
170 | end
171 |
172 | specify 'dependency/namespace name accepts does not accept non-strings/non-symbols' do
173 | incompatible_name = Object.new
174 |
175 | expect do
176 | Class.new(SmartCore::Container) do
177 | namespace(incompatible_name) {}
178 | end
179 | end.to raise_error(SmartCore::Container::IncompatibleEntityNameError)
180 |
181 | container = Class.new(SmartCore::Container).new
182 |
183 | expect { container.namespace(incompatible_name) {} }.to raise_error(
184 | SmartCore::Container::IncompatibleEntityNameError
185 | )
186 |
187 | expect { container.register(incompatible_name) {} }.to raise_error(
188 | SmartCore::Container::IncompatibleEntityNameError
189 | )
190 |
191 | expect { container.fetch(incompatible_name) }.to raise_error(
192 | SmartCore::Container::IncompatibleEntityNameError
193 | )
194 | end
195 |
196 | describe 'host containers' do
197 | specify 'host containers of nested containers' do
198 | root_container = SmartCore::Container.define do
199 | namespace(:database) do
200 | register(:cache) { 'cache' }
201 |
202 | namespace(:creds) do
203 | register(:cache) { 123 }
204 | end
205 |
206 | namespace(:drivers) do
207 | register(:cache) { 'redis' }
208 | end
209 | end
210 | end
211 |
212 | # NOTE: container host tree:
213 | # -----------------------------
214 | # @ROOT -> $database -> $creds
215 | # -> $drivers
216 | # -----------------------------
217 |
218 | # @ROOT
219 | expect(root_container.host).to be_a(SmartCore::Container::Host)
220 | expect(root_container.host.present?).to eq(false)
221 | expect(root_container.host.exists?).to eq(false)
222 | expect(root_container.host.path).to eq(nil)
223 | expect(root_container.host.container).to eq(nil)
224 |
225 | # @ROOT->$database
226 | db_container = root_container.fetch(:database)
227 | expect(db_container.host).to be_a(SmartCore::Container::Host)
228 | expect(db_container.host.present?).to eq(true)
229 | expect(db_container.host.exists?).to eq(true)
230 | expect(db_container.host.path).to eq('database')
231 | expect(db_container.host.container).to eq(root_container)
232 |
233 | # @ROOT->$database->$creds
234 | creds_container = db_container.fetch(:creds)
235 | expect(creds_container.host).to be_a(SmartCore::Container::Host)
236 | expect(creds_container.host.present?).to eq(true)
237 | expect(creds_container.host.exists?).to eq(true)
238 | expect(creds_container.host.path).to eq('creds')
239 | expect(creds_container.host.container).to eq(db_container)
240 |
241 | # @ROOT->$database->$drivers
242 | drivers_container = db_container.fetch(:drivers)
243 | expect(drivers_container.host).to be_a(SmartCore::Container::Host)
244 | expect(drivers_container.host.present?).to eq(true)
245 | expect(drivers_container.host.exists?).to eq(true)
246 | expect(drivers_container.host.path).to eq('drivers')
247 | expect(drivers_container.host.container).to eq(db_container)
248 | end
249 |
250 | specify 'prevent of incompatible host container creation' \
251 | '(requires both container and path)' do
252 | # correct - OK
253 | expect { SmartCore::Container.new }.not_to raise_error
254 |
255 | # correct - OK
256 | expect do
257 | SmartCore::Container.new(host_container: nil, host_path: nil)
258 | end.not_to raise_error
259 |
260 | # correct - OK
261 | expect do
262 | SmartCore::Container.new(
263 | host_container: (SmartCore::Container.define {}),
264 | host_path: 'sample'
265 | )
266 | end.not_to raise_error
267 |
268 | # incorrect - BAD
269 | expect do
270 | SmartCore::Container.new(
271 | host_container: (SmartCore::Container.define {}),
272 | host_path: nil
273 | )
274 | end.to raise_error(SmartCore::Container::ArgumentError)
275 |
276 | # incorrect - BAD
277 | expect do
278 | SmartCore::Container.new(
279 | host_container: nil, # should be a type of SmartCore::Container
280 | host_path: 'sample'
281 | )
282 | end.to raise_error(SmartCore::Container::ArgumentError)
283 |
284 | # incorrect - BAD
285 | expect do
286 | SmartCore::Container.new(
287 | host_container: (SmartCore::Container.define {}),
288 | host_path: 12_345 # should be a type of string
289 | )
290 | end.to raise_error(SmartCore::Container::ArgumentError)
291 |
292 | # incorrect - BAD
293 | expect do
294 | SmartCore::Container.new(
295 | host_container: 123, # should be a type of SmartCore::Container
296 | host_path: 'sample'
297 | )
298 | end.to raise_error(SmartCore::Container::ArgumentError)
299 | end
300 | end
301 | end
302 |
--------------------------------------------------------------------------------
/spec/features/dot_notation_aka_fetch_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Dot-notation' do
4 | let(:container) do
5 | Class.new(SmartCore::Container) do
6 | namespace :storages do
7 | namespace :cache do
8 | register(:general) { :dalli }
9 | end
10 |
11 | namespace :persistent do
12 | register(:general) { :postgres }
13 | end
14 | end
15 | end.new
16 | end
17 |
18 | specify 'method-based resolving (#resolve)' do
19 | expect(container.resolve('storages.cache.general')).to eq(:dalli)
20 | expect(container.resolve('storages.persistent.general')).to eq(:postgres)
21 | end
22 |
23 | specify 'index-like resolving (#[])' do
24 | expect(container['storages.cache.general']).to eq(:dalli)
25 | expect(container['storages.persistent.general']).to eq(:postgres)
26 | end
27 |
28 | specify 'fails on non-finalized dependency key path' do
29 | # namespace-ended dependency is not a dependency
30 | expect do
31 | container.resolve('storages.cache')
32 | end.to raise_error(SmartCore::Container::ResolvingError)
33 |
34 | expect do
35 | container.resolve('storages.persistent')
36 | end.to raise_error(SmartCore::Container::ResolvingError)
37 | end
38 |
39 | specify 'fails on non-existent dependencies' do
40 | # nonexistent dependency is not a dependency
41 | expect do
42 | container.resolve('fantasy.world')
43 | end.to raise_error(SmartCore::Container::ResolvingError)
44 |
45 | expect do
46 | container['storages.virtual']
47 | end.to raise_error(SmartCore::Container::ResolvingError)
48 |
49 | # trying to resolve a dependency from a dependency :D
50 | expect do
51 | container['storages.cache.general.test']
52 | end.to raise_error(SmartCore::Container::ResolvingError)
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/spec/features/frozen_state_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Frozen state' do
4 | describe 'freeze! macros' do
5 | specify 'freezes all instances' do
6 | container = Class.new(SmartCore::Container) { freeze_state! }.new
7 | expect(container.frozen?).to eq(true)
8 | end
9 |
10 | specify 'is not inharitable' do
11 | container_klass = Class.new(SmartCore::Container) { freeze_state! }
12 | container_sub_klass = Class.new(container_klass)
13 |
14 | container = container_klass.new
15 | expect(container.frozen?).to eq(true)
16 |
17 | sub_container = container_sub_klass.new
18 | expect(sub_container.frozen?).to eq(false)
19 | end
20 | end
21 |
22 | context 'frozen state' do
23 | let(:container) do
24 | Class.new(SmartCore::Container) do
25 | namespace :database do
26 | register(:logger) { :logger }
27 | register(:adapter) { :postgresql }
28 | end
29 |
30 | register(:randomizer) { :randomizer }
31 | end.new
32 | end
33 |
34 | specify 'frozen? predicate' do
35 | expect(container.frozen?).to eq(false)
36 | container.freeze!
37 | expect(container.frozen?).to eq(true)
38 | end
39 |
40 | context 'instance behaviour' do
41 | before { container.freeze! }
42 |
43 | specify 'registration of the new dependency should fail' do
44 | expect { container.register(:logger) { :logger } }.to raise_error(
45 | SmartCore::Container::FrozenRegistryError
46 | )
47 |
48 | expect { container.fetch(:logger) }.to raise_error(
49 | SmartCore::Container::FetchError
50 | )
51 | end
52 |
53 | specify 're-registration of the existing dependency should fail' do
54 | expect { container.register(:randomizer) { :new_randomizer } }.to raise_error(
55 | SmartCore::Container::FrozenRegistryError
56 | )
57 |
58 | expect(container.fetch(:randomizer)).to eq(:randomizer)
59 | end
60 |
61 | specify 'creation of the new namespace should fail' do
62 | expect { container.namespace(:services) {} }.to raise_error(
63 | SmartCore::Container::FrozenRegistryError
64 | )
65 |
66 | expect { container.fetch(:services) }.to raise_error(
67 | SmartCore::Container::FetchError
68 | )
69 | end
70 |
71 | specify 'reopening of the existing namespace should fail' do
72 | expect { container.namespace(:database) {} }.to raise_error(
73 | SmartCore::Container::FrozenRegistryError
74 | )
75 | end
76 |
77 | specify 'registering of new dependencies on the existing namespace should fail' do
78 | expect do
79 | container.namespace(:database) do
80 | register(:service) { :service }
81 | end
82 | end.to raise_error(SmartCore::Container::FrozenRegistryError)
83 |
84 | expect { container.fetch(:database).fetch(:service) }.to raise_error(
85 | SmartCore::Container::FetchError
86 | )
87 | end
88 |
89 | specify 'all nested containers should be frozen too' do
90 | expect do
91 | container.fetch(:database).register(:service) { :service }
92 | end.to raise_error(SmartCore::Container::FrozenRegistryError)
93 |
94 | expect { container.fetch(:database).fetch(:service) }.to raise_error(
95 | SmartCore::Container::FetchError
96 | )
97 | end
98 | end
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/spec/features/hash_tree_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Hash tree (#to_h / #to_hash / #hash_tree)' do
4 | let(:container) do
5 | Class.new(SmartCore::Container) do
6 | namespace(:storages) do
7 | namespace(:adapters) do
8 | register(:database) { :database }
9 | register(:cache) { :cache }
10 | end
11 |
12 | register(:logger) { :storage_logger }
13 | end
14 |
15 | namespace(:queues) do
16 | register(:async, memoize: true) { :sidekiq }
17 | register(:sync, memoize: true) { :in_memory }
18 | end
19 | end.new
20 | end
21 |
22 | shared_examples 'dependency tree representation' do |method_name|
23 | context "(#{method_name}) with dependency resolving (resolve_dependencies: true)" do
24 | specify 'dependency tree is represented as a hash with resolved dependencies' do
25 | expect(container.public_send(method_name, resolve_dependencies: true)).to match(
26 | 'storages' => {
27 | 'adapters' => {
28 | 'database' => :database,
29 | 'cache' => :cache
30 | },
31 | 'logger' => :storage_logger
32 | },
33 | 'queues' => {
34 | 'async' => :sidekiq,
35 | 'sync' => :in_memory
36 | }
37 | )
38 | end
39 | end
40 |
41 | context "(#{method_name}) without dependency resolving (resolve_dependencies: false)" do
42 | specify 'dependency tree is represented as a hash with container entities' do
43 | expect(container.public_send(method_name)).to match(
44 | 'storages' => {
45 | 'adapters' => {
46 | 'database' => an_instance_of(SmartCore::Container::Entities::Dependency),
47 | 'cache' => an_instance_of(SmartCore::Container::Entities::Dependency)
48 | },
49 | 'logger' => an_instance_of(SmartCore::Container::Entities::Dependency)
50 | },
51 | 'queues' => {
52 | 'async' => an_instance_of(SmartCore::Container::Entities::MemoizedDependency),
53 | 'sync' => an_instance_of(SmartCore::Container::Entities::MemoizedDependency)
54 | }
55 | )
56 | end
57 | end
58 | end
59 |
60 | it_behaves_like 'dependency tree representation', :hash_tree
61 | it_behaves_like 'dependency tree representation', :to_h
62 | it_behaves_like 'dependency tree representation', :to_hash
63 | end
64 |
--------------------------------------------------------------------------------
/spec/features/iteration_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Dependency iteration' do
4 | let(:container) do
5 | Class.new(SmartCore::Container) do
6 | namespace(:persistence) do
7 | register(:queue) { :sidekiq }
8 | register(:db) { :postgresql }
9 | end
10 | register(:logger) { :logger }
11 | end.new
12 | end
13 |
14 | specify 'iterate only over ending dependencies (only over dependencies)' do
15 | results = [].tap do |res|
16 | container.each_dependency { |name, value| res << [name, value] }
17 | end
18 |
19 | expect(results).to contain_exactly(
20 | ['persistence.queue', :sidekiq],
21 | ['persistence.db', :postgresql],
22 | ['logger', :logger]
23 | )
24 | end
25 |
26 | specify 'iterate over all dependnecies (over namespaces and dependencies)' do
27 | results = [].tap do |res|
28 | container.each_dependency(yield_all: true) { |name, value| res << [name, value] }
29 | end
30 |
31 | expect(results).to contain_exactly(
32 | ['persistence', be_a(SmartCore::Container)],
33 | ['persistence.queue', :sidekiq],
34 | ['persistence.db', :postgresql],
35 | ['logger', :logger]
36 | )
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/spec/features/key_extraction_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Key extraction' do
4 | let(:container) do
5 | Class.new(SmartCore::Container) do
6 | namespace(:persistence) do
7 | register(:queue) { :sidekiq }
8 | register(:db) { :postgresql }
9 |
10 | namespace(:cache) do
11 | register(:front) { :mongodb }
12 | register(:back) { :memcached }
13 | end
14 | end
15 |
16 | register(:logger) { :logger }
17 |
18 | namespace(:external) do
19 | register(:banking) { :sberbank }
20 | end
21 | end.new
22 | end
23 |
24 | specify 'get dependency keys (only dependencies)' do
25 | expect(container.keys).to contain_exactly(
26 | 'persistence.queue',
27 | 'persistence.db',
28 | 'persistence.cache.front',
29 | 'persistence.cache.back',
30 | 'logger',
31 | 'external.banking'
32 | )
33 | end
34 |
35 | specify 'get all keys (dependencies and namespaces)' do
36 | expect(container.keys(all_variants: true)).to contain_exactly(
37 | 'persistence',
38 | 'persistence.queue',
39 | 'persistence.db',
40 | 'persistence.cache',
41 | 'persistence.cache.front',
42 | 'persistence.cache.back',
43 | 'logger',
44 | 'external',
45 | 'external.banking'
46 | )
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/spec/features/key_predicates_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Key predicates' do
4 | let(:container) do
5 | Class.new(SmartCore::Container) do
6 | namespace(:database) do
7 | register(:resolver, memoize: true) { :resolver }
8 | namespace(:cache) do
9 | register(:memcached, memoize: true) { :memcached }
10 | end
11 | end
12 | register(:logger, memoize: true) { :logger }
13 | register(:random) { rand(1000) }
14 | end.new
15 | end
16 |
17 | specify 'key? - has dependency or namespace?' do
18 | expect(container.key?('database')).to eq(true)
19 | expect(container.key?('database.resolver')).to eq(true)
20 | expect(container.key?('database.cache')).to eq(true)
21 | expect(container.key?('database.cache.memcached')).to eq(true)
22 | expect(container.key?('logger')).to eq(true)
23 | expect(container.key?('random')).to eq(true)
24 |
25 | # non-existent keys => false
26 | expect(container.key?('services')).to eq(false)
27 | expect(container.key?('database.presistence')).to eq(false)
28 | end
29 |
30 | specify 'dependency? - has dependency (any/memoized/non-memoized)' do
31 | # namespace => false
32 | expect(container.dependency?('database')).to eq(false)
33 | expect(container.dependency?('database.cache')).to eq(false)
34 |
35 | # any dependency => true
36 | expect(container.dependency?('database.resolver')).to eq(true)
37 | expect(container.dependency?('database.cache.memcached')).to eq(true)
38 | expect(container.dependency?('logger')).to eq(true)
39 | expect(container.dependency?('random')).to eq(true)
40 |
41 | # memoized dependency
42 | expect(container.dependency?('database.resolver', memoized: true)).to eq(true)
43 | expect(container.dependency?('database.cache.memcached', memoized: true)).to eq(true)
44 | expect(container.dependency?('logger', memoized: true)).to eq(true)
45 | expect(container.dependency?('random', memoized: true)).to eq(false) # NON-memoized => false
46 |
47 | # non-memoized dependency
48 | expect(container.dependency?('database.resolver', memoized: false)).to eq(false)
49 | expect(container.dependency?('database.cache.memcached', memoized: false)).to eq(false)
50 | expect(container.dependency?('logger', memoized: false)).to eq(false)
51 | expect(container.dependency?('random', memoized: false)).to eq(true) # NON-memoized => true
52 |
53 | # non-existent dependencies => false
54 | expect(container.dependency?('database.layer')).to eq(false)
55 | expect(container.dependency?('database.layer', memoized: true)).to eq(false)
56 | expect(container.dependency?('database.layer', memoized: false)).to eq(false)
57 | expect(container.dependency?('services')).to eq(false)
58 | expect(container.dependency?('services', memoized: true)).to eq(false)
59 | expect(container.dependency?('services', memoized: false)).to eq(false)
60 | end
61 |
62 | specify 'namespace? - has namespace?' do
63 | # namespace => true
64 | expect(container.namespace?('database')).to eq(true)
65 | expect(container.namespace?('database.cache')).to eq(true)
66 |
67 | # dependency => false
68 | expect(container.namespace?('database.resolver')).to eq(false)
69 | expect(container.namespace?('database.cache.memcached')).to eq(false)
70 | expect(container.namespace?('logger')).to eq(false)
71 | expect(container.namespace?('random')).to eq(false)
72 |
73 | # nonexistent namespaces => false
74 | expect(container.namespace?('database.services')).to eq(false)
75 | expect(container.namespace?('services')).to eq(false)
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/spec/features/memoization_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Memoization (dependency memoization)' do
4 | specify 'all dependencies are not memoized by default' do
5 | container = Class.new(SmartCore::Container) do
6 | namespace(:deps) do
7 | register(:sidekiq) { Object.new }
8 | end
9 | end.new
10 |
11 | first_resolve = container['deps.sidekiq']
12 | second_resolve = container['deps.sidekiq']
13 |
14 | expect(first_resolve.object_id).not_to eq(second_resolve.object_id)
15 |
16 | # runtime non-memoized registration
17 | container.register('no_memoized') { Object.new }
18 | expect(container['no_memoized'].object_id).not_to eq(container['no_memoized'].object_id)
19 | end
20 |
21 | specify 'explicit memoization boolean flag (memoize or not)' do
22 | container = Class.new(SmartCore::Container) do
23 | namespace(:memoized) do
24 | # explicitly memoized
25 | register(:sidekiq, memoize: true) { Object.new }
26 | end
27 |
28 | namespace(:nonmemoized) do
29 | # explicitly non-memoized
30 | register(:resque, memoize: false) { Object.new }
31 |
32 | # non-memoized by default
33 | register(:sneakers) { Object.new }
34 | end
35 | end.new
36 |
37 | # memoized dependencies
38 | expect(container['memoized.sidekiq']).to eq(container['memoized.sidekiq'])
39 |
40 | # non-memoized dependencies
41 | first_reveal = container['nonmemoized.resque']
42 | second_reveal = container['nonmemoized.resque']
43 | expect(first_reveal.object_id).not_to eq(second_reveal.object_id)
44 |
45 | first_reveal = container['nonmemoized.sneakers']
46 | second_reveal = container['nonmemoized.sneakers']
47 | expect(first_reveal.object_id).not_to eq(second_reveal.object_id)
48 |
49 | # runtime non-memoized registration
50 | container.register('non_memoized') { Object.new }
51 | expect(container['non_memoized'].object_id).not_to eq(container['non_memoized'].object_id)
52 |
53 | container.register('expl_non_memoized', memoize: false) { Object.new }
54 | first_reveal = container['expl_non_memoized']
55 | second_reveal = container['expl_non_memoized']
56 | expect(first_reveal.object_id).not_to eq(second_reveal.object_id)
57 |
58 | # runtime memoized registration
59 | container.register('expl_memoized', memoize: true) { Object.new }
60 | first_reveal = container['expl_memoized']
61 | second_reveal = container['expl_memoized']
62 | expect(first_reveal.object_id).to eq(second_reveal.object_id)
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/spec/features/mixin_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Mixin' do
4 | specify 'provides dependencies definition dsl and container accessor methods' do
5 | app_klass = Class.new do
6 | include SmartCore::Container::Mixin
7 |
8 | dependencies do
9 | namespace 'database' do
10 | register('cache') { :dalli }
11 | register('store') { :pg }
12 | end
13 |
14 | register(:logger) { :app_logger }
15 | end
16 | end
17 |
18 | application = app_klass.new
19 |
20 | expect(application.container).to be_a(SmartCore::Container)
21 | expect(application.container).to eq(app_klass.container)
22 |
23 | expect(application.container.fetch(:database).fetch(:cache)).to eq(:dalli)
24 | expect(application.container.fetch(:database).fetch(:store)).to eq(:pg)
25 | expect(application.container.fetch(:logger)).to eq(:app_logger)
26 | end
27 |
28 | specify "you can freeze container state by DSL's macros attribute" do
29 | application = Class.new do
30 | include SmartCore::Container::Mixin
31 | dependencies {}
32 | end.new
33 |
34 | expect(application.container.frozen?).to eq(false)
35 |
36 | application = Class.new do
37 | include SmartCore::Container::Mixin
38 | dependencies(freeze_state: true) {}
39 | end.new
40 |
41 | expect(application.container.frozen?).to eq(true)
42 |
43 | application = Class.new do
44 | include SmartCore::Container::Mixin
45 | dependencies { freeze_state! }
46 | end
47 |
48 | expect(application.container.frozen?).to eq(true)
49 | end
50 |
51 | specify 'inheritance works as expected :)' do
52 | application = Class.new do
53 | include SmartCore::Container::Mixin
54 | dependencies { register(:lock) { 'lock' } }
55 | end
56 |
57 | sub_application = Class.new(application) do
58 | dependencies { register(:unlock) { 'unlock' } }
59 | end
60 |
61 | app = sub_application.new
62 | expect(app.container['lock']).to eq('lock') # NOTE: inherited dependency
63 | expect(app.container['unlock']).to eq('unlock') # NOTE: own dependency
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/spec/features/reload_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Container reloading' do
4 | specify 'reinstantiates existing container with initial dependencies' do
5 | database_dependency_stub = Object.new
6 | cache_dependency_stub = Object.new
7 | randomizer_dependency_stub = Object.new
8 |
9 | container_klass = Class.new(SmartCore::Container) do
10 | namespace :storages do
11 | register(:database) { database_dependency_stub }
12 | register(:cache) { cache_dependency_stub }
13 | end
14 |
15 | register(:randomizer) { randomizer_dependency_stub }
16 | end
17 |
18 | container = container_klass.new
19 |
20 | # re-register dependnecies
21 | container.register(:randomizer) { :randomizer }
22 | container.namespace(:storages) { register(:database) { :database } }
23 | container.namespace(:storages) { register(:cache) { :cache } }
24 |
25 | # register new dependencies
26 | container.register(:logger) { :logger }
27 | container.namespace(:system) { register(:queue) { :sidekiq } }
28 |
29 | # check that our new dependencies are created
30 | expect(container.fetch(:randomizer)).to eq(:randomizer)
31 | expect(container.fetch(:storages).fetch(:database)).to eq(:database)
32 | expect(container.fetch(:storages).fetch(:cache)).to eq(:cache)
33 | expect(container.fetch(:logger)).to eq(:logger)
34 | expect(container.fetch(:system).fetch(:queue)).to eq(:sidekiq)
35 |
36 | # reload!
37 | container.reload!
38 |
39 | # check that we have old dependencies
40 | expect(container.fetch(:randomizer)).to eq(randomizer_dependency_stub)
41 | expect(container.fetch(:storages).fetch(:database)).to eq(database_dependency_stub)
42 | expect(container.fetch(:storages).fetch(:cache)).to eq(cache_dependency_stub)
43 | # check that we have no new dependencies
44 | expect do
45 | container.fetch(:logger)
46 | end.to raise_error(SmartCore::Container::FetchError)
47 | expect do
48 | container.fetch(:system)
49 | end.to raise_error(SmartCore::Container::FetchError)
50 | end
51 |
52 | specify "new definitions from a container's class will be considered too" do
53 | container_klass = Class.new(SmartCore::Container) do
54 | namespace :storages do
55 | register(:database) { :database }
56 | end
57 | end
58 |
59 | container = container_klass.new
60 |
61 | expect(container.fetch(:storages).fetch(:database)).to eq(:database)
62 | expect do
63 | container.fetch(:storages).fetch(:cache)
64 | end.to raise_error(SmartCore::Container::FetchError)
65 |
66 | # register new dependencies on class-level dependency tree
67 | container_klass.namespace(:storages) { register(:cache) { :cache } }
68 |
69 | # reload existing container and check that new definitions are exist
70 | container.reload!
71 |
72 | expect(container.fetch(:storages).fetch(:database)).to eq(:database)
73 | expect(container.fetch(:storages).fetch(:cache)).to eq(:cache)
74 | end
75 |
76 | specify 'resets frozen state' do
77 | container = Class.new(SmartCore::Container).new
78 |
79 | expect(container.frozen?).to eq(false)
80 | container.freeze!
81 | expect(container.frozen?).to eq(true)
82 | container.reload!
83 | expect(container.frozen?).to eq(false)
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'simplecov'
4 |
5 | SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter
6 | SimpleCov.minimum_coverage(100)
7 | SimpleCov.enable_coverage(:branch)
8 | SimpleCov.enable_coverage(:line)
9 | SimpleCov.primary_coverage(:line)
10 | SimpleCov.add_filter('spec')
11 | SimpleCov.start
12 |
13 | require 'bundler/setup'
14 | require 'smart_core/container'
15 | require 'pry'
16 |
17 | RSpec.configure do |config|
18 | Kernel.srand config.seed
19 | config.disable_monkey_patching!
20 | config.filter_run_when_matching :focus
21 | config.order = :random
22 | config.shared_context_metadata_behavior = :apply_to_host_groups
23 | config.expect_with(:rspec) { |c| c.syntax = :expect }
24 | Thread.abort_on_exception = true
25 | end
26 |
--------------------------------------------------------------------------------