├── .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 · Supported by Cado Labs · [![Gem Version](https://badge.fury.io/rb/smart_container.png)](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 | Supported by Cado Labs 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 | Supported by Cado Labs 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 | --------------------------------------------------------------------------------