SmartCore::Container · ·
Thread-safe semanticaly-defined IoC/DI Container with a developer-friendly DSL and API.
Installation
gem 'smart_container'
bundle install
# --- or ---
gem install smart_container
require 'smart_core/container'
Table of cotnents
Functionality
container class creation
class Container < SmartCore::Container
namespace(:database) do # support for namespaces
register(:resolver, memoize: true) { SomeDatabaseResolver.new } # dependency registration
namespace(:cache) do # support for nested naespaces
register(:memcached, memoize: true) { MemcachedClient.new }
register(:redis, memoize: true) { RedisClient.new }
end
end
# root dependencies
register(:logger, memoize: true) { Logger.new(STDOUT) }
# dependencies are not memoized by default (memoize: false)
register(:random) { rand(1000) }
end
mixin
# full documentaiton is coming;
class Application
include SmartCore::Container::Mixin
dependencies do
namespace(:database) do
register(:cache) { MemcachedClient.new }
end
end
end
# access:
Application.container
Application.new.container # NOTE: the same instance as Application.container
container instantiation and dependency resolving
container = Container.new # create container instance
container['database.resolver'] # => #<SomeDatabaseResolver:0x00007f0f0f1d6332>
container['database.cache.redis'] # => #<RedisClient:0x00007f0f0f1d0158>
container['logger'] # => #<Logger:0x00007f5f0f2f0158>
container.resolve('logger') # #resolve(path) is an alias for #[](path)
# non-memoized dependency
container['random'] # => 352
container['random'] # => 57
# trying to resolve a namespace as dependency
container['database'] # => SmartCore::Container::ResolvingError
# but you can fetch any depenendency type (internal containers and values) via #fetch
container.fetch('database') # => SmartCore::Container (nested container)
container.fetch('database.resolver') # => #<SomeDatabaseResolver:0x00007f0f0f1d6332>
runtime-level dependency/namespace registration
container.namespace(:api) do
register(:provider) { GoogleProvider } # without memoization
end
container.register('game_api', memoize: true) { 'overwatch' } # with memoization
container['api.provider'] # => GoogleProvider
container['game_api'] # => 'overwatch'
container keys (dependency names):
# get dependnecy keys (only dependencies)
container.keys
# => result:
[
'database.resolver',
'database.cache.memcached',
'database.cache.redis',
'logger',
'random'
]
# get all keys (namespaces and dependencies)
container.keys(all_variants: true)
# => result:
[
'database', # namespace
'database.resolver',
'database.cache', # namespace
'database.cache.memcached',
'database.cache.redis',
'logger',
'random'
]
key predicates
key?(key)
- has dependency or namespace?namespace?(path)
- has namespace?dependency?(path)
- has dependency?dependency?(path, memoized: true)
- has memoized dependency?dependency?(path, memoized: false)
- has non-memoized dependency?
container.key?('database') # => true
container.key?('database.cache.memcached') # => true
container.dependency?('database') # => false
container.dependency?('database.resolver') # => true
container.namespace?('database') # => true
container.namespace?('database.resolver') # => false
container.dependency?('database.resolver', memoized: true) # => true
container.dependency?('database.resolver', memoized: false) # => false
container.dependency?('random', memoized: true) # => false
container.dependency?('random', memoized: false) # => true
state freeze
- state freeze (
#freeze!
,.#frozen?
):
# documentation is coming;
reloading
- reloading (
#reload!
):
# documentation is coming;
hash tree
- hash tree (
#hash_tree
,#hash_tree(resolve_dependencies: true)
):
# documentation is coming;
explicit class definition
SmartCore::Container.define
- avoid explicit class definition (allows to create container instance from an anonymous container class immidietly):
# - create from empty container class -
AppContainer = SmartCore::Container.define do
namespace :database do
register(:logger) { Logger.new }
end
end # => an instance of Class<SmartCore::Container>
AppContainer.resolve('database.logger') # => #<Logger:0x00007f5f0f2f0158>
AppContainer['database.logger'] # => #<Logger:0x00007f5f0f2f0158>
# - create from another container class with a custom sub-definitions -
class BasicContainer < SmartCore::Container
namespace(:api) do
register(:client) { Kickbox.new }
end
end
AppContainer = BasicContainer.define do
register(:db_driver) { Sequel }
end
# --- or ---
AppContainer = SmartCore::Container.define(BasicContainer) do
register(:db_driver) { Sequel }
end
AppContainer['api.client'] # => #<Kickbox:0x00007f5f0f2f0158> (BasicContainer dependency)
AppContainer['db_driver'] # => Sequel (AppContainer dependency)
subscribe to dependency changements
- features and limitations:
- you can subscribe only on container instances (on container instance changements);
- at this moment only the full entity path patterns are supported (pattern-based pathes are not supported yet);
- you can subscribe on namespace changements (when the full namespace is re-registered) and dependency changement (when some dependency has been changed);
#observe(path, &observer) => observer
- subscribe a custom block to dependency changement events (your proc will be invoked with|path, container|
attributes);#unobserve(observer)
- unsubscribe concrete observer from dependency observing (returnstrue
(unsubscribed) orfalse
(nothing to unsubscribe));#clear_observers(entity_path = nil)
- unsubscribe all observers from concrete path or from all pathes (nil
parameters);
- aliases:
#observe
=>#subscribe
;#unobserve
=>#unsubscribe
;#clear_observers
=>#clear_listeners
;
container = SmartCore::Container.define do
namespace(:database) do
register(:stats) { 'stat_db' }
end
end
# observe entity change
entity_observer = container.observe('database.stats') do |dependency_path, container|
puts "changed => '#{container[dependency_path]}'"
end
# observe namespace change
namespace_observer = container.observe('database') do |namespace_path, container|
puts "changed => '#{namespace_path}'"
end
container.fetch('database').register('stats') = 'kek' # => invokes entity_observer and outputs "changed! => 'kek'"
container.namespace('database') {} # => invoks namespace_observer and outputs "changed => 'database'"
container.unobserve(observer) # unsubscribe entity_observer from dependency changement observing;
container.clear_observers # unsubscribe all observers
container.fetch('database').register('stats') = 'pek' # no one to listen this changement... :)
container.namespace('database') {} # no one to listen this changement... :)
Roadmap
migrate to Github Actions;
convinient way to rebind registered dependnecies:
# PoC
container['dependency.path'] = 'pek' # simplest instant dependency registration without memoization
# --- or/and ---
container.rebind('dependency.path', memoize: true/false) { 'pek' } # bind with dynamic dependency registration
container.rebind('dependency.path', memoize: true/false, 'pek') # bind with instant dependency registration
- pattern-based pathes in dependency changement observing;
container.observe('path.*') { puts 'kek!' } # subscribe to all changements in `path` namespace;
- support for instant dependency registration:
# common (dynamic) way:
register('dependency_name') { dependency_value }
# instant way:
register('dependency_name', dependency_value)
- support for memoization ignorance during dependency resolving:
resolve('logger', :allocate) # Draft
container composition;
support for fallback block in
.resolve
operation (similar toHash#fetch
works);inline temporary dependency switch:
with(logger: Logger.new, db: DB.new) do
# logger is a new logger
# db is a new db
end
# out of block: logger is an old logger, db is an old db
Contributing
- Fork it ( https://github.com/smart-rb/smart_container/fork )
- Create your feature branch (
git checkout -b feature/my-new-feature
) - Commit your changes (
git commit -am '[feature_context] Add some feature'
) - Push to the branch (
git push origin feature/my-new-feature
) - Create new Pull Request
License
Released under MIT License.