SmartCore::Container · Supported by Cado Labs · Gem Version

Thread-safe semanticaly-defined IoC/DI Container with a developer-friendly DSL and API.


Supported by Cado Labs


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 (returns true (unsubscribed) or false (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 to Hash#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.

Supporting

Supported by Cado Labs

Authors

Rustam Ibragimov