├── .rspec ├── Gemfile ├── lib ├── encase │ ├── version.rb │ ├── factory_item.rb │ ├── singleton_item.rb │ ├── object_item.rb │ ├── encaseable.rb │ ├── container_item_factory.rb │ ├── container_item.rb │ └── container.rb └── encase.rb ├── .travis.yml ├── .gitignore ├── CHANGELOG.md ├── portkey.json ├── spec ├── spec_helper.rb └── lib │ └── encase │ ├── singleton_item_spec.rb │ ├── object_item_spec.rb │ ├── factory_item_spec.rb │ ├── encaseable_spec.rb │ ├── container_item_factory_spec.rb │ ├── container_item_spec.rb │ └── container_spec.rb ├── Rakefile ├── LICENSE.txt ├── encase.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /lib/encase/version.rb: -------------------------------------------------------------------------------- 1 | module Encase 2 | VERSION = "0.1.2" 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.2 4 | - 1.9.3 5 | - 2.0.0 6 | - 2.1.0 7 | -------------------------------------------------------------------------------- /lib/encase.rb: -------------------------------------------------------------------------------- 1 | require 'encase/version' 2 | require 'encase/encaseable' 3 | 4 | module Encase 5 | def self.included(base) 6 | base.extend Encaseable 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/encase/factory_item.rb: -------------------------------------------------------------------------------- 1 | module Encase 2 | require_relative 'container_item' 3 | 4 | class FactoryItem < ContainerItem 5 | def fetch 6 | reified_value.new 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.1.1 4 | 5 | * Fixes `needs` lookup where parent classes `needs` were not 6 | visible to child classes. 7 | * Adds support chaining to DSL. 8 | 9 | ### 0.1.0 10 | 11 | * Initial Release 12 | 13 | -------------------------------------------------------------------------------- /portkey.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib/encase/*.rb": { 3 | "type": "container", 4 | "test": "spec/lib/encase/%s_spec.rb" 5 | }, 6 | "spec/lib/encase/*_spec.rb": { 7 | "type": "spec", 8 | "alternate": "lib/encase/%s.rb" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << '../lib' 2 | 3 | RSpec.configure do |config| 4 | config.treat_symbols_as_metadata_keys_with_true_values = true 5 | config.run_all_when_everything_filtered = true 6 | config.filter_run :focus 7 | config.order = 'random' 8 | end 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'bundler/gem_tasks' 3 | require 'rspec/core/rake_task' 4 | 5 | desc 'Run rspec' 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | desc 'Default task :test' 9 | task :default => :test 10 | 11 | desc 'Run tests' 12 | task :test => :spec 13 | -------------------------------------------------------------------------------- /lib/encase/singleton_item.rb: -------------------------------------------------------------------------------- 1 | module Encase 2 | require_relative 'factory_item' 3 | 4 | class SingletonItem < FactoryItem 5 | attr_accessor :singleton 6 | 7 | def fetch 8 | self.singleton = super unless self.singleton 9 | self.singleton 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/encase/object_item.rb: -------------------------------------------------------------------------------- 1 | module Encase 2 | require_relative 'container_item' 3 | 4 | class ObjectItem < ContainerItem 5 | attr_accessor :injected 6 | 7 | def inject(object, origin = nil) 8 | if self.injected 9 | false 10 | else 11 | self.injected = super(object, origin) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/encase/encaseable.rb: -------------------------------------------------------------------------------- 1 | module Encase 2 | module Encaseable 3 | def needs(*deps) 4 | if @needs_to_inject 5 | @needs_to_inject.concat(deps) 6 | else 7 | @needs_to_inject = deps 8 | attr_accessor :container 9 | end 10 | 11 | attr_accessor *deps 12 | end 13 | 14 | def needs_to_inject 15 | @needs_to_inject 16 | end 17 | 18 | def needs_to_inject=(value) 19 | @needs_to_inject = value 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/encase/container_item_factory.rb: -------------------------------------------------------------------------------- 1 | module Encase 2 | require_relative 'object_item' 3 | require_relative 'factory_item' 4 | require_relative 'singleton_item' 5 | 6 | class ContainerItemFactory 7 | def self.build(type, container) 8 | container_item_for(type).new(container) 9 | end 10 | 11 | def self.container_item_for(type) 12 | case type 13 | when 'object' then ObjectItem 14 | when 'factory' then FactoryItem 15 | when 'singleton' then SingletonItem 16 | else 17 | ContainerItem 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/lib/encase/singleton_item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'encase/singleton_item' 3 | 4 | module Encase 5 | class MySingletonClass 6 | end 7 | 8 | describe SingletonItem do 9 | let(:container) { 10 | c = double() 11 | c.stub(:inject).with(anything()) { true } 12 | c 13 | } 14 | 15 | let(:singleton_item) { 16 | s = SingletonItem.new(container) 17 | s.store(:lorem, MySingletonClass) 18 | s 19 | } 20 | 21 | it 'returns the same object on every fetch' do 22 | singleton_item.reify() 23 | 24 | instance1 = singleton_item.fetch() 25 | instance2 = singleton_item.fetch() 26 | 27 | expect(instance1).to eq(instance2) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/lib/encase/object_item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'encase/object_item' 3 | 4 | module Encase 5 | describe ObjectItem do 6 | let(:container) { 7 | c = double() 8 | c.stub(:inject).with(anything()) { true } 9 | c 10 | } 11 | 12 | let(:object_item) { 13 | o = ObjectItem.new(container) 14 | o.store(:lorem, 'lorem') 15 | o 16 | } 17 | 18 | it 'applies injection if not injected' do 19 | expect(object_item.inject('lorem')).to be_true 20 | expect(object_item.injected).to be_true 21 | end 22 | 23 | it 'does not apply injection if already injected' do 24 | object_item.inject('lorem') 25 | 26 | expect(object_item.inject('lorem')).to be_false 27 | expect(object_item.injected).to be_true 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/lib/encase/factory_item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'encase/factory_item' 3 | 4 | module Encase 5 | class MyClass 6 | end 7 | 8 | describe FactoryItem do 9 | let(:container) { 10 | c = double() 11 | c.stub(:inject).with(anything()) { true } 12 | c 13 | } 14 | 15 | let(:factory_item) { 16 | f = FactoryItem.new(container) 17 | f.store(:lorem, MyClass) 18 | f 19 | } 20 | 21 | it 'creates a new instance on every fetch' do 22 | factory_item.reify() 23 | 24 | instance1 = factory_item.fetch() 25 | instance2 = factory_item.fetch() 26 | 27 | expect(instance1).to_not eq(instance2) 28 | end 29 | 30 | it 'applies injection on created instance' do 31 | container.stub(:inject) do |to_inject| 32 | expect(to_inject).to be_a(MyClass) 33 | true 34 | end 35 | 36 | factory_item.instance 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/lib/encase/encaseable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'encase' 3 | 4 | class EmptyBox 5 | end 6 | 7 | class Box 8 | include Encase 9 | 10 | needs :apples, :mangoes 11 | needs :oranges 12 | end 13 | 14 | describe 'Encaseable' do 15 | let(:box) { Box.new } 16 | let(:empty_box) { EmptyBox.new } 17 | 18 | it 'creates accessors if needs are specified' do 19 | expect(Box).to respond_to(:needs_to_inject) 20 | expect(box).to respond_to(:container) 21 | expect(box).to respond_to(:apples) 22 | expect(box).to respond_to(:mangoes) 23 | expect(box).to respond_to(:oranges) 24 | end 25 | 26 | it 'does not create accessors if needs are not specified' do 27 | expect(EmptyBox).to_not respond_to(:needs_to_inject) 28 | expect(empty_box).to_not respond_to(:container) 29 | expect(empty_box).to_not respond_to(:apples) 30 | expect(empty_box).to_not respond_to(:mangoes) 31 | expect(empty_box).to_not respond_to(:oranges) 32 | end 33 | 34 | it 'stores provided dependencies for later lookup' do 35 | expect(box.class.needs_to_inject).to match_array([:apples, :mangoes, :oranges]) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Darshan Sawardekar 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /encase.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'encase/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "encase" 8 | spec.version = Encase::VERSION 9 | spec.authors = ["Darshan Sawardekar"] 10 | spec.email = ["darshan@sawardekar.org"] 11 | spec.summary = %q{Lightweight IOC Container for ruby.} 12 | spec.description = %q{Encase is a library for using dependency injection within ruby applications. It provides a lightweight IOC Container that manages dependencies between your classes. It was written to assist with wiring of domain objects outside Rails applications to enable faster test suites.} 13 | spec.homepage = "https://github.com/dsawardekar/encase" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.5" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "rspec", "~> 2.14.1" 24 | end 25 | -------------------------------------------------------------------------------- /lib/encase/container_item.rb: -------------------------------------------------------------------------------- 1 | module Encase 2 | class ContainerItem 3 | attr_accessor :container 4 | attr_accessor :key 5 | attr_accessor :value 6 | attr_accessor :reified_value 7 | 8 | def initialize(container) 9 | self.container = container 10 | end 11 | 12 | def store(key, value) 13 | self.key = key 14 | self.value = value 15 | end 16 | 17 | def inject(object, origin = nil) 18 | if origin.nil? 19 | container = self.container 20 | else 21 | container = origin 22 | end 23 | 24 | container.inject(object) 25 | end 26 | 27 | def reify 28 | return false if reified? 29 | 30 | if value.is_a? Proc 31 | if value.arity == 1 32 | self.reified_value = value.call(container) 33 | else 34 | self.reified_value = value.call 35 | end 36 | else 37 | self.reified_value = value 38 | end 39 | 40 | true 41 | end 42 | 43 | def reified? 44 | !self.reified_value.nil? 45 | end 46 | 47 | # returns just the reified value by default 48 | def fetch 49 | reified_value 50 | end 51 | 52 | # public api 53 | def instance(origin = nil) 54 | reify unless reified? 55 | object = fetch 56 | inject(object, origin) 57 | 58 | object 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/lib/encase/container_item_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'encase/container_item_factory' 3 | require 'encase/container_item' 4 | require 'encase/object_item' 5 | require 'encase/factory_item' 6 | require 'encase/singleton_item' 7 | 8 | module Encase 9 | shared_examples "a container_item_factory" do 10 | it 'finds container_item\'s constructor' do 11 | expect( 12 | ContainerItemFactory.container_item_for(type) 13 | ).to eq(type_constructor) 14 | end 15 | 16 | it 'builds container_item' do 17 | expect( 18 | ContainerItemFactory.build(type, container) 19 | ).to be_a(type_constructor) 20 | end 21 | end 22 | 23 | describe ContainerItemFactory do 24 | let(:container) { double() } 25 | 26 | context 'with object type' do 27 | let(:type) { 'object' } 28 | let(:type_constructor) { ObjectItem } 29 | 30 | it_behaves_like 'a container_item_factory' 31 | end 32 | 33 | context 'with factory type' do 34 | let(:type) { 'factory' } 35 | let(:type_constructor) { FactoryItem } 36 | 37 | it_behaves_like 'a container_item_factory' 38 | end 39 | 40 | context 'with singleton type' do 41 | let(:type) { 'singleton' } 42 | let(:type_constructor) { SingletonItem } 43 | 44 | it_behaves_like 'a container_item_factory' 45 | end 46 | end 47 | end 48 | 49 | 50 | -------------------------------------------------------------------------------- /lib/encase/container.rb: -------------------------------------------------------------------------------- 1 | require 'encase/container_item_factory' 2 | 3 | module Encase 4 | class Container 5 | attr_accessor :parent 6 | 7 | def initialize(parent = nil) 8 | @parent = parent 9 | @items = {} 10 | end 11 | 12 | def contains?(key) 13 | @items.key?(key) 14 | end 15 | 16 | def inject(object) 17 | klass = object.class 18 | if klass.respond_to?(:needs_to_inject) 19 | needs_to_inject = find_needs_to_inject(klass) 20 | needs_to_inject.each do |need| 21 | object.instance_variable_set( 22 | "@#{need}", lookup(need) 23 | ) 24 | end 25 | 26 | object.container = self if object.respond_to?(:container) 27 | object.on_inject() if object.respond_to?(:on_inject) 28 | 29 | true 30 | else 31 | false 32 | end 33 | end 34 | 35 | def find_needs_to_inject(klass) 36 | needs = [] 37 | klass.ancestors.each do |ancestor| 38 | if ancestor.respond_to?(:needs_to_inject) && !ancestor.needs_to_inject.nil? 39 | needs.concat(ancestor.needs_to_inject) 40 | end 41 | end 42 | 43 | needs 44 | end 45 | 46 | def register(type, key, value, block) 47 | item = ContainerItemFactory.build(type, self) 48 | item.store(key, value || block) 49 | @items[key] = item 50 | self 51 | end 52 | 53 | def unregister(key) 54 | @items.delete(key) 55 | self 56 | end 57 | 58 | def clear 59 | @items.clear 60 | self 61 | end 62 | 63 | def object(key, value = nil, &block) 64 | register('object', key, value, block) 65 | self 66 | end 67 | 68 | def factory(key, value = nil, &block) 69 | register('factory', key, value, block) 70 | self 71 | end 72 | 73 | def singleton(key, value = nil, &block) 74 | register('singleton', key, value, block) 75 | self 76 | end 77 | 78 | def lookup(key, origin = nil) 79 | if contains?(key) 80 | item = @items[key] 81 | item.instance(origin) 82 | elsif !parent.nil? 83 | origin = self if origin.nil? 84 | parent.lookup(key, origin) 85 | else 86 | raise KeyError.new("Key:#{key} not found in container.") 87 | end 88 | end 89 | 90 | def configure(&block) 91 | instance_exec(&block) 92 | self 93 | end 94 | 95 | def child 96 | Container.new(self) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/lib/encase/container_item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'encase/container_item' 3 | 4 | module Encase 5 | shared_examples 'a container_item' do 6 | it 'stores container' do 7 | expect(container_item.container).to eq(container) 8 | end 9 | 10 | it 'stores item key - value pair' do 11 | expect(container_item.key).to eq(container_item_key) 12 | expect(container_item.value).to eq(container_item_value) 13 | end 14 | 15 | it 'reifies value' do 16 | container_item.reify() 17 | expect(container_item.reified_value).to eq(container_item_value_reified) 18 | end 19 | 20 | it 'does not reify if already reified' do 21 | container_item.reify() 22 | expect(container_item.reified?).to be_true 23 | 24 | expect(container_item.reify()).to be_false 25 | expect(container_item.reified?).to be_true 26 | end 27 | 28 | it 'applies injection using container' do 29 | container_item.reify() 30 | expect(container_item.inject(container_item_value_reified)).to be_true 31 | end 32 | 33 | it 'applies injection before returning instance' do 34 | container.stub(:inject) do |value| 35 | expect(value).to eq(container_item_value_reified) 36 | end 37 | 38 | container_item.instance() 39 | end 40 | 41 | it 'returns instance for item' do 42 | expect(container_item.instance).to eq(container_item_value_reified) 43 | end 44 | end 45 | 46 | describe ContainerItem do 47 | let(:container) do 48 | c = double() 49 | c.stub(:inject).with(anything()) { true } 50 | c 51 | end 52 | 53 | let(:container_item_key) { :lorem } 54 | let(:container_item_value) { 'lorem' } 55 | let(:container_item_value_reified) { 'lorem' } 56 | let(:container_item) { 57 | c = ContainerItem.new(container) 58 | c.store(container_item_key, container_item_value) 59 | c 60 | } 61 | 62 | context 'without proc' do 63 | it_behaves_like "a container_item" 64 | end 65 | 66 | context 'with proc' do 67 | context 'without argument' do 68 | let(:container_item_value) { 69 | Proc.new do 70 | 'lorem' 71 | end 72 | } 73 | 74 | it_behaves_like 'a container_item' 75 | end 76 | 77 | context 'with argument' do 78 | let(:container_item_value) { 79 | Proc.new do |c| 80 | expect(c).to eq(container) 81 | 'lorem' 82 | end 83 | } 84 | 85 | it_behaves_like 'a container_item' 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/lib/encase/container_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'encase/container' 3 | require 'encase' 4 | 5 | module Encase 6 | class Lorem 7 | end 8 | 9 | class MyBox 10 | include Encase 11 | 12 | needs :apples, :mangoes 13 | needs :oranges 14 | end 15 | 16 | describe Container do 17 | let(:container) { 18 | Container.new 19 | } 20 | 21 | it 'can store objects' do 22 | container.object(:lorem, 'lorem') 23 | expect(container.lookup(:lorem)).to eq('lorem') 24 | end 25 | 26 | it 'can store factories' do 27 | container.factory(:lorem, Lorem) 28 | expect(container.lookup(:lorem)).to be_a(Lorem) 29 | end 30 | 31 | it 'can store singletons' do 32 | container.singleton(:lorem, Lorem) 33 | instance1 = container.lookup(:lorem) 34 | instance2 = container.lookup(:lorem) 35 | 36 | expect(instance1).to eq(instance2) 37 | end 38 | 39 | it 'can be configured with a DSL' do 40 | container.configure do 41 | object :lorem, 'lorem' 42 | factory :lipsum, Lorem 43 | singleton :dolor, Lorem 44 | end 45 | 46 | expect(container.lookup(:lorem)).to eq('lorem') 47 | expect(container.lookup(:lipsum)).to be_a(Lorem) 48 | 49 | instance1 = container.lookup(:dolor) 50 | instance2 = container.lookup(:dolor) 51 | 52 | expect(instance1).to eq(instance2) 53 | end 54 | 55 | it 'raises a KeyError for unknown keys' do 56 | expect { 57 | container.lookup(:unknown_key) 58 | }.to raise_error(KeyError) 59 | end 60 | 61 | it 'can create nested containers' do 62 | child = container.child() 63 | grand_child = child.child() 64 | 65 | expect(grand_child.parent).to eq(child) 66 | expect(child.parent).to eq(container) 67 | expect(container.parent).to be_nil 68 | end 69 | 70 | it 'can lookup values from nested containers' do 71 | child = container.child() 72 | grand_child = child.child() 73 | 74 | container.object(:lorem, 'in_container') 75 | child.object(:lorem, 'in_child') 76 | grand_child.object(:lorem, 'in_grand_child') 77 | 78 | expect(grand_child.lookup(:lorem)).to eq('in_grand_child') 79 | 80 | grand_child.unregister(:lorem) 81 | expect(grand_child.lookup(:lorem)).to eq('in_child') 82 | 83 | child.unregister(:lorem) 84 | expect(grand_child.lookup(:lorem)).to eq('in_container') 85 | end 86 | 87 | it 'can clear the container store' do 88 | container.object(:lorem, 'lorem') 89 | container.object(:ipsum, 'ipsum') 90 | container.object(:dolor, 'dolor') 91 | 92 | container.clear 93 | 94 | expect(container.contains?(:lorem)).to be_false 95 | expect(container.contains?(:ipsum)).to be_false 96 | expect(container.contains?(:dolor)).to be_false 97 | end 98 | 99 | it 'can inject dependencies' do 100 | container.configure do 101 | object :apples, 10 102 | object :mangoes, 20 103 | object :oranges, 30 104 | factory :box, MyBox 105 | end 106 | 107 | box = container.lookup(:box) 108 | expect(box.apples).to eq(10) 109 | expect(box.mangoes).to eq(20) 110 | expect(box.oranges).to eq(30) 111 | end 112 | 113 | it 'can inject dependencies lazily' do 114 | container.configure do 115 | object :apples, -> { 10 } 116 | object :mangoes, -> { 20 } 117 | object :oranges, -> { 30 } 118 | factory :box, -> { MyBox } 119 | end 120 | 121 | box = container.lookup(:box) 122 | expect(box.apples).to eq(10) 123 | expect(box.mangoes).to eq(20) 124 | expect(box.oranges).to eq(30) 125 | end 126 | 127 | it 'assigns container on injection' do 128 | class DummyObjectToBeInjected 129 | include Encase 130 | needs :lorem 131 | end 132 | 133 | container.object(:lorem, 'lorem') 134 | container.factory(:dummy, DummyObjectToBeInjected) 135 | 136 | dummy = container.lookup(:dummy) 137 | expect(dummy.container).to eq(container) 138 | end 139 | 140 | it 'call on_inject hook after injection' do 141 | class DummyWithOnInject 142 | include Encase 143 | needs :lorem 144 | attr_accessor :injected 145 | 146 | def on_inject 147 | self.injected = true 148 | end 149 | end 150 | 151 | container.object(:lorem, 'lorem') 152 | container.factory(:dummy, DummyWithOnInject) 153 | 154 | dummy = container.lookup(:dummy) 155 | expect(dummy.injected).to be_true 156 | end 157 | 158 | it 'can chain calls' do 159 | c = container.object(:a, 'foo') 160 | c.object(:b, 'bar') 161 | c.factory(:c, Object) 162 | c = c.singleton(:d, Object) 163 | 164 | expect(c).to eq(container) 165 | end 166 | 167 | context 'Performance', :benchmark => true do 168 | require 'benchmark' 169 | 170 | it 'can store 100s of dependencies quickly' do 171 | runtime = Benchmark.realtime do 172 | container.configure do 173 | 10000.times do |i| 174 | item = "item#{i}" 175 | object item.to_sym, item 176 | end 177 | end 178 | end 179 | 180 | expect(runtime).to be < 0.1 181 | end 182 | 183 | it 'can lookup 100s of dependencies quickly' do 184 | class ClassWithManyDeps 185 | include Encase 186 | 187 | 100.times do |i| 188 | needs "item#{i}".to_sym 189 | end 190 | end 191 | 192 | container.configure do 193 | 100.times do |i| 194 | item = "item#{i}" 195 | object item.to_sym, item 196 | end 197 | 198 | factory :lorem, ClassWithManyDeps 199 | end 200 | 201 | runtime = Benchmark.realtime do 202 | 100.times do |i| 203 | container.lookup(:lorem) 204 | end 205 | end 206 | 207 | expect(runtime).to be < 0.1 208 | end 209 | end 210 | 211 | context 'Needs with class inheritance' do 212 | it 'can recognize needs in parent class' do 213 | class MyGrandParent 214 | include Encase 215 | needs :a 216 | end 217 | 218 | class MyParent < MyGrandParent 219 | include Encase 220 | needs :b 221 | end 222 | 223 | class MyChild < MyParent 224 | include Encase 225 | needs :c 226 | end 227 | 228 | class MyGrandChild < MyChild 229 | end 230 | 231 | container.configure do 232 | object :a, 'A' 233 | object :b, 'B' 234 | object :c, 'C' 235 | 236 | factory :me, MyGrandChild 237 | end 238 | 239 | me = container.lookup(:me) 240 | expect(me.a).to eq('A') 241 | expect(me.b).to eq('B') 242 | expect(me.c).to eq('C') 243 | end 244 | end 245 | 246 | context 'Nested Container with lazy resolution' do 247 | it 'can resolve logger lazily issue #4' do 248 | class Logger 249 | end 250 | 251 | class MagicLogger 252 | end 253 | 254 | class House 255 | include Encase 256 | needs :logger 257 | end 258 | 259 | container = Container.new 260 | container.configure do 261 | object :logger, Logger.new 262 | factory :house, House 263 | end 264 | 265 | child = container.child 266 | child.configure do 267 | object :logger, MagicLogger.new 268 | end 269 | 270 | house = child.lookup(:house) 271 | expect(house.logger).to be_a(MagicLogger) 272 | end 273 | end 274 | end 275 | end 276 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Encase [![Build Status][1]][2] [![Code Climate][3]][4] [![Dependency Status][5]][6] 2 | 3 | ### Lightweight IOC Container for ruby. 4 | 5 | Encase is a library for using dependency injection within ruby 6 | applications. It provides a lightweight IOC Container that manages 7 | dependencies between your classes. It was written to assist with wiring 8 | of domain objects outside Rails applications to enable faster test 9 | suites. 10 | 11 | # Features 12 | 13 | * Stores objects, factories, and singletons 14 | * Declarative syntax for specifying dependencies 15 | * Simple DSL for configuring the container 16 | * Support for nested containers 17 | * Support for lazy initialization 18 | 19 | ## Usage 20 | 21 | Consider a `Worker` class that has a dependency on a `Logger`. We would 22 | like this dependency to be available to the worker when it is created. 23 | 24 | First we create a container object and declare the dependencies. 25 | 26 | ```ruby 27 | require 'encase/container' 28 | 29 | container = Container.new 30 | container.configure do 31 | object :logger, Logger.new 32 | factory :worker, Worker 33 | end 34 | ``` 35 | 36 | Then we declare the `logger` dependency in the Worker class by including 37 | the `Encase` module into it and using the `needs` declaration. 38 | 39 | ```ruby 40 | require 'encase' 41 | 42 | class Worker 43 | include Encase 44 | 45 | needs :logger 46 | end 47 | ``` 48 | 49 | That's it! Now we can create a new `Worker` by looking up the `:worker` 50 | key on the Container. 51 | 52 | ```ruby 53 | my_worker = container.lookup('worker') 54 | my_worker.logger.is_a?(Logger) # true 55 | ``` 56 | 57 | ## Container Configuration 58 | 59 | Containers are configured using a mini DSL in a `configure` method. 60 | Container items are stored with ruby `symbols` and are used to later lookup the object. 61 | 62 | ```ruby 63 | container.configure do 64 | # declarations go here 65 | end 66 | ``` 67 | 68 | The declarations supported are listed below. 69 | 70 | ### Object 71 | 72 | Objects are pre existing entities that have already been created in your 73 | system. They are stored and returned as is without any modification. To 74 | store objects use the `object` declaration. 75 | 76 | ```ruby 77 | container.configure do 78 | object :key, value 79 | end 80 | ``` 81 | 82 | ### Factory 83 | 84 | A Factory can be stored with the container to create instances of a 85 | class with it's dependencies auto-injected. On every lookup a new 86 | instance of that class will be returned. 87 | 88 | ```ruby 89 | container.configure do 90 | factory :key, TheClass 91 | end 92 | ``` 93 | 94 | ### Singleton 95 | 96 | A Singleton is similar to a Factory. However it caches the instance 97 | created on the first lookup and returns that instance on every 98 | subsequent lookups. 99 | 100 | ```ruby 101 | container.configure do 102 | singleton :key, TheSingletonClass 103 | end 104 | ``` 105 | 106 | ## Declaring Dependencies 107 | 108 | To specify dependencies of a class, you use the `needs` declaration. It 109 | takes an array of symbols corresponding to the keys of the dependencies 110 | stored in the container. Multiple `needs` declarations are also supported. 111 | 112 | ```ruby 113 | require 'encase' 114 | 115 | class Worker 116 | include Encase 117 | 118 | needs :a, :b, :c 119 | needs :one 120 | needs :two 121 | needs :three 122 | end 123 | ``` 124 | 125 | ## Lazy Initialization 126 | 127 | Encase allows storage of dependencies lazily. This can be useful if the 128 | dependencies aren't ready at the time of container configuration. But 129 | will ready before lookup. 130 | 131 | Lazy initialization is done by passing a `block` to the DSL declaration 132 | instead of a value. Here `:key`'s block will be evaluated before the 133 | first lookup. 134 | 135 | ```ruby 136 | container.configure do 137 | object :key do 138 | value 139 | end 140 | end 141 | ``` 142 | 143 | The block can optionally take an argument equal to the container object 144 | itself. You can use this to conditionally resolve the value based on 145 | other objects in the container or elsewhere. 146 | 147 | ```ruby 148 | container.configure do 149 | object :key do |c| 150 | value 151 | end 152 | end 153 | ``` 154 | 155 | ## Nested Containers 156 | 157 | Containers can also be nested within other containers. This allows grouping 158 | dependencies within different contexts of your application. When looking 159 | up keys, parent containers are queried when a key is not found in a 160 | child container. 161 | 162 | ```ruby 163 | parent_container = Container.new 164 | parent_container.configure do 165 | object :logger, Logger.new 166 | factory :worker, Worker 167 | end 168 | 169 | child_container = parent_container.child 170 | child_container.configure do 171 | factory :worker, CustomWorker 172 | end 173 | 174 | child_container.lookup(:logger) # from parent_container 175 | child_container.lookup(:worker) # CustomWorker 176 | ``` 177 | 178 | Here the `child_container` will use `CustomWorker` for resolving 179 | `:worker`. While the `:logger` will be looked up from the 180 | `parent_container` 181 | 182 | ## Accessors 183 | 184 | Encase creates `accessor` methods corresponding to each declared `need` 185 | in the class. A `container` accessor is also injected into the class for 186 | looking up other dependencies at runtime. 187 | 188 | ```ruby 189 | class Worker 190 | include Encase 191 | 192 | needs :one, :two, :three 193 | end 194 | 195 | worker = container.lookup('worker') 196 | worker.one 197 | worker.two 198 | worker.three 199 | worker.container 200 | ``` 201 | 202 | ## Lifecycle 203 | 204 | An `on_inject` event hook is provided to container items after their 205 | dependencies are injected. Any post injection initialization can be 206 | carried out here. 207 | 208 | ```ruby 209 | class Worker 210 | include Encase 211 | 212 | needs :logger 213 | 214 | def on_inject 215 | logger.log('Worker is ready') 216 | end 217 | end 218 | ``` 219 | 220 | ## Testing 221 | 222 | Encase significantly simplifies testability of objects. In the earlier 223 | example in order to test that the logger is indeed called by the worker 224 | we can register the worker as a `mock`. Then verify that this mock was called. 225 | 226 | ```ruby 227 | describe Worker do 228 | let(:container) { 229 | container = Container.new 230 | container.configure do 231 | factory :worker, Worker 232 | end 233 | 234 | container 235 | } 236 | 237 | it 'logs message to the logger' do 238 | logger = double() 239 | logger.should_receive(:log).with(anything()) 240 | 241 | container.object logger, double(:log => true) 242 | 243 | worker = container.lookup(:worker) 244 | worker.start() 245 | end 246 | end 247 | ``` 248 | 249 | Using the Encase created `accessor` methods it is also possible to 250 | manually assign the dependencies. Below `logger` is directly assigned to 251 | the `worker` without requiring a container. 252 | 253 | ```ruby 254 | it 'logs message to the logger' do 255 | logger = double() 256 | logger.should_receive(:log).with(anything()) 257 | 258 | worker = Worker.new 259 | worker.logger = logger 260 | worker.start() 261 | end 262 | ``` 263 | 264 | ## Installation 265 | 266 | Add this line to your application's Gemfile: 267 | 268 | gem 'encase' 269 | 270 | And then execute: 271 | 272 | $ bundle install 273 | 274 | # System Requirements 275 | 276 | Encase has been tested to work on these platforms. 277 | 278 | * ruby 1.9.2 279 | * ruby 1.9.3 280 | * ruby 2.0.0 281 | * ruby 2.1.0 282 | 283 | ## Contributing 284 | 285 | See contributing guidelines for Portkey. 286 | 287 | [1]: https://travis-ci.org/dsawardekar/encase.png 288 | [2]: https://travis-ci.org/dsawardekar/encase 289 | [3]: https://codeclimate.com/github/dsawardekar/encase.png 290 | [4]: https://codeclimate.com/github/dsawardekar/encase 291 | [5]: https://gemnasium.com/dsawardekar/encase.png 292 | [6]: https://gemnasium.com/dsawardekar/encase 293 | --------------------------------------------------------------------------------