├── .ruby-gemset ├── .ruby-version ├── .coveralls.yml ├── Rakefile ├── Gemfile ├── lib ├── pinball │ ├── version.rb │ ├── container_item.rb │ ├── class.rb │ └── container.rb └── pinball.rb ├── spec ├── spec_helper.rb └── lib │ └── pinball │ ├── class_spec.rb │ └── container_spec.rb ├── .gitignore ├── .travis.yml ├── pinball.gemspec ├── LICENSE ├── LICENSE.txt └── README.md /.ruby-gemset: -------------------------------------------------------------------------------- 1 | pinball 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.1.2 2 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/pinball/version.rb: -------------------------------------------------------------------------------- 1 | module Pinball 2 | VERSION = '0.1.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | Coveralls.wear! 3 | 4 | 5 | $LOAD_PATH << '../lib' 6 | 7 | RSpec.configure do |config| 8 | config.order = 'random' 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 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.2 4 | - 1.9.3 5 | - 2.0.0 6 | - 2.1.0 7 | - 2.1.1 8 | - 2.1.2 9 | - 2.1.3 10 | - 2.1.4 11 | - 2.1.5 12 | - 2.2.1 13 | - 2.2.2 14 | - 2.2.3 15 | - jruby-19mode 16 | - jruby-9.0.0.0.pre1 17 | script: bundle exec rspec 18 | cache: bundler 19 | -------------------------------------------------------------------------------- /lib/pinball/container_item.rb: -------------------------------------------------------------------------------- 1 | module Pinball 2 | class ContainerItem 3 | attr_accessor :value 4 | 5 | def initialize(value) 6 | @value = value 7 | end 8 | 9 | def fetch(target) 10 | if value.is_a? Proc 11 | target.instance_eval(&value) 12 | elsif value.is_a? Module 13 | value.new 14 | else 15 | value 16 | end 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /lib/pinball.rb: -------------------------------------------------------------------------------- 1 | require 'pinball/version' 2 | require 'pinball/container' 3 | require 'pinball/class' 4 | 5 | module Pinball 6 | module Methods 7 | attr_reader :overridden_dependencies 8 | 9 | def override_dependency(key, value = nil, &block) 10 | @overridden_dependencies[key] = ContainerItem.new(value || block) 11 | Container.instance.inject(self) 12 | self 13 | end 14 | end 15 | 16 | class UnknownDependency < StandardError 17 | end 18 | 19 | class WrongArity < StandardError 20 | end 21 | 22 | attr_reader :dependencies 23 | 24 | def new(*args) 25 | object = allocate 26 | Container.instance.inject(object) 27 | object.instance_variable_set(:@overridden_dependencies, {}) 28 | object.send(:initialize, *args) 29 | object 30 | end 31 | end -------------------------------------------------------------------------------- /lib/pinball/class.rb: -------------------------------------------------------------------------------- 1 | class Class 2 | def inject(*deps) 3 | check_pinball 4 | @dependencies ||= [] 5 | @dependencies.concat(deps).uniq! 6 | end 7 | 8 | def class_inject(*deps) 9 | deps.each do |dep| 10 | define_singleton_method dep do 11 | Pinball::Container.lookup(dep).fetch(self) 12 | end 13 | end 14 | end 15 | 16 | def check_pinball 17 | unless is_a? Pinball 18 | extend Pinball 19 | send(:include, Pinball::Methods) 20 | 21 | public_send(:define_singleton_method, :inherited_with_pinball) do |child| 22 | inherited_without_pinball(child) if respond_to?(:inherited_without_pinball) 23 | child.instance_variable_set :@dependencies, dependencies 24 | child.check_pinball 25 | end 26 | 27 | public_send(:define_singleton_method, :inherited_without_pinball, method(:inherited)) if respond_to?(:inherited) 28 | public_send(:define_singleton_method, :inherited, method(:inherited_with_pinball)) 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /pinball.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'pinball/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'pinball' 8 | spec.version = Pinball::VERSION 9 | spec.authors = ['Gleb Sinyavsky'] 10 | spec.email = ['zhulik.gleb@gmail.com'] 11 | spec.summary = 'Simple dependency injection for ruby' 12 | spec.description = 'Simple and stupid IOC container and dependency injection tool for ruby' 13 | spec.homepage = 'https://github.com/zhulik/pinball' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = [] 18 | spec.test_files = `git ls-files spec`.split($/) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '~> 1.6', '> 1.6' 22 | spec.add_development_dependency 'rake', '~> 10.4' 23 | 24 | spec.add_development_dependency 'coveralls', '~> 0.8' 25 | spec.add_development_dependency 'rspec', '~> 3.3' 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Gleb Sinyavsky 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Gleb Sinyavsky 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 | -------------------------------------------------------------------------------- /lib/pinball/container.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | require_relative 'container_item' 3 | 4 | module Pinball 5 | class Container 6 | include Singleton 7 | 8 | attr_reader :items 9 | 10 | class << self 11 | def configure(&block) 12 | instance.instance_exec(&block) 13 | end 14 | 15 | def lookup(key) 16 | instance.items[key] 17 | end 18 | 19 | def clear 20 | instance.items.clear 21 | end 22 | 23 | def define(key, value = nil, &block) 24 | instance.define(key, value, &block) 25 | end 26 | 27 | def define_singleton(key, klass) 28 | instance.define_singleton(key, klass) 29 | end 30 | 31 | def undefine(key) 32 | instance.undefine(key) 33 | end 34 | end 35 | 36 | def initialize 37 | @items = {} 38 | end 39 | 40 | def define(key, value = nil, &block) 41 | @items[key] = ContainerItem.new(value || block) 42 | end 43 | 44 | def define_singleton(key, klass) 45 | if klass.instance_method(:initialize).arity <= 0 46 | @items[key] = ContainerItem.new(klass.new) 47 | else 48 | raise Pinball::WrongArity.new('Singleton dependency initializer should not have mandatory params') 49 | end 50 | end 51 | 52 | def undefine(key) 53 | @items.delete(key) 54 | end 55 | 56 | def inject(target) 57 | target.class.dependencies.each do |dep| 58 | unless target.respond_to?(dep) 59 | target.define_singleton_method dep do 60 | begin 61 | Container.instance.items.merge(overridden_dependencies)[dep].fetch(self) 62 | rescue NoMethodError 63 | raise Pinball::UnknownDependency.new("Dependency #{dep} is unknown, check your pinball config") 64 | end 65 | end 66 | end 67 | end 68 | 69 | end 70 | end 71 | end -------------------------------------------------------------------------------- /spec/lib/pinball/class_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper.rb' 2 | require 'pinball' 3 | 4 | describe Class do 5 | before do 6 | Pinball::Container.clear 7 | 8 | Pinball::Container.configure do 9 | define :baz, 0 10 | define :bar, 1 11 | define :egg, 2 12 | end 13 | 14 | end 15 | 16 | let!(:foo) { Class.new } 17 | 18 | describe '::inject' do 19 | it 'responds to ::inject method' do 20 | expect(foo.respond_to?(:inject)).to be_truthy 21 | end 22 | 23 | it 'creates @dependencies array' do 24 | foo.inject :baz 25 | expect(foo.instance_variable_get('@dependencies')).to match_array([:baz]) 26 | end 27 | 28 | it 'adds items to @dependencies array' do 29 | foo.inject :baz 30 | foo.inject :bar 31 | expect(foo.instance_variable_get('@dependencies')).to match_array([:baz, :bar]) 32 | end 33 | 34 | it 'doesn\'t duplicate dependencies' do 35 | foo.inject :baz 36 | foo.inject :bar 37 | foo.inject :bar 38 | expect(foo.dependencies).to match_array([:baz, :bar]) 39 | end 40 | 41 | it 'resolves known dependencies' do 42 | foo.inject :baz 43 | foo.inject :bar 44 | foo.inject :egg 45 | 46 | expect(foo.new.baz).to eq(0) 47 | expect(foo.new.bar).to eq(1) 48 | expect(foo.new.egg).to eq(2) 49 | end 50 | 51 | it 'raises exception while resolving unknown dependecies' do 52 | foo.inject :unknown 53 | expect { foo.new.unknown }.to raise_error(Pinball::UnknownDependency) 54 | end 55 | end 56 | 57 | describe '::class_inject' do 58 | it 'defines new method' do 59 | foo.class_inject :baz 60 | expect(foo.respond_to?(:baz)).to be_truthy 61 | end 62 | 63 | it 'injects valid dependency' do 64 | foo.class_inject :baz 65 | expect(foo.baz).to eq(0) 66 | end 67 | end 68 | 69 | describe '::dependencies' do 70 | it 'returns list of dependencies' do 71 | foo.inject :baz 72 | foo.inject :bar 73 | expect(foo.dependencies).to match_array([:baz, :bar]) 74 | end 75 | end 76 | 77 | describe '#override_dependency' do 78 | let!(:foo_instance) { foo.inject :baz ; foo.new } 79 | 80 | subject { foo_instance.override_dependency(:baz, 1) } 81 | 82 | it 'adds overridden dependency' do 83 | subject 84 | expect(foo_instance.overridden_dependencies[:baz]).not_to be_nil 85 | end 86 | 87 | it 'resolves overridden dependency' do 88 | subject 89 | expect(foo_instance.baz).to eq(1) 90 | end 91 | 92 | it 'returns self' do 93 | expect(subject).to eq(foo_instance) 94 | end 95 | end 96 | 97 | context 'for subclass' do 98 | let!(:foo) { Class.new { inject :baz ; inject :bar} } 99 | let!(:fooo) { Class.new(foo) } 100 | let!(:bazz) { Class.new{ inject :egg } } 101 | 102 | let!(:foo_instance) { foo.new } 103 | let!(:fooo_instance) { fooo.new } 104 | 105 | describe '::dependencies' do 106 | it 'returns ancestor\'s dependencies' do 107 | expect(fooo.dependencies).to eq(foo.dependencies) 108 | end 109 | end 110 | 111 | describe 'injection in subclass' do 112 | it 'returns dependency' do 113 | expect(fooo_instance.baz).to eq(foo_instance.baz) 114 | expect(fooo_instance.bar).to eq(foo_instance.bar) 115 | end 116 | 117 | it 'dependencies inherited only for descendants' do 118 | expect(bazz.dependencies).to eq([:egg]) 119 | end 120 | end 121 | end 122 | end -------------------------------------------------------------------------------- /spec/lib/pinball/container_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper.rb' 2 | require 'pinball' 3 | 4 | describe Pinball::Container do 5 | before do 6 | Pinball::Container.clear 7 | end 8 | 9 | describe '::configure' do 10 | it 'allows to add new container item' do 11 | Pinball::Container.configure do 12 | define :baz, 0 13 | end 14 | expect(Pinball::Container.instance.items[:baz]).to be_an_instance_of(Pinball::ContainerItem) 15 | end 16 | end 17 | 18 | describe '::lookup' do 19 | it 'returns container_item' do 20 | Pinball::Container.configure do 21 | define :baz, 0 22 | end 23 | expect(Pinball::Container.lookup(:baz)).to be_an_instance_of(Pinball::ContainerItem) 24 | end 25 | end 26 | 27 | describe '::clear' do 28 | it 'clears all container items' do 29 | Pinball::Container.configure do 30 | define :baz, 0 31 | end 32 | Pinball::Container.clear 33 | expect(Pinball::Container.lookup(:baz)).to be_nil 34 | end 35 | end 36 | 37 | describe '::define' do 38 | it 'adds new container item' do 39 | Pinball::Container.define :baz, 0 40 | expect(Pinball::Container.instance.items[:baz]).to be_an_instance_of(Pinball::ContainerItem) 41 | end 42 | end 43 | 44 | describe '::define_singleton' do 45 | context 'with class without mandatory initializer params' do 46 | it 'adds new container item' do 47 | Pinball::Container.define_singleton :baz, String 48 | expect(Pinball::Container.instance.items[:baz].value).to be_an_instance_of(String) 49 | end 50 | end 51 | 52 | context 'with class with mandatory initialize params' do 53 | it 'raises exception' do 54 | WithMandatory = Class.new do 55 | def initialize(param) 56 | end 57 | end 58 | 59 | expect{Pinball::Container.define_singleton :baz, WithMandatory}.to raise_error(Pinball::WrongArity) 60 | end 61 | end 62 | end 63 | 64 | describe '::undefine' do 65 | context 'with usual dependency' do 66 | it 'removes container item' do 67 | Pinball::Container.define :baz, 0 68 | Pinball::Container.undefine :baz 69 | expect(Pinball::Container.instance.items[:baz]).to be_nil 70 | end 71 | end 72 | 73 | context 'with singleton dependency' do 74 | it 'removes container item' do 75 | Pinball::Container.define_singleton :baz, String 76 | Pinball::Container.undefine :baz 77 | expect(Pinball::Container.instance.items[:baz]).to be_nil 78 | end 79 | end 80 | end 81 | 82 | describe '#define' do 83 | it 'adds new container item' do 84 | Pinball::Container.instance.define :baz, 0 85 | expect(Pinball::Container.instance.items[:baz]).to be_an_instance_of(Pinball::ContainerItem) 86 | end 87 | end 88 | 89 | describe '#undefine' do 90 | it 'removes container item' do 91 | Pinball::Container.instance.define :baz, 0 92 | Pinball::Container.instance.undefine :baz 93 | expect(Pinball::Container.instance.items[:baz]).to be_nil 94 | end 95 | end 96 | 97 | describe '#inject' do 98 | let!(:foo) { Class.new{ inject :baz, :bar, :spam } } 99 | 100 | before do 101 | Pinball::Container.configure do 102 | define :baz, 0 103 | define :bar, Hash 104 | define :spam do 105 | Array.new 106 | end 107 | end 108 | end 109 | 110 | it 'automatically injects dependencies to class' do 111 | expect(foo.new.respond_to?(:baz)).to be_truthy 112 | expect(foo.new.respond_to?(:bar)).to be_truthy 113 | expect(foo.new.respond_to?(:spam)).to be_truthy 114 | end 115 | 116 | it 'injects valid dependencies' do 117 | expect(foo.new.baz).to eq(0) 118 | expect(foo.new.bar).to be_an_instance_of(Hash) 119 | expect(foo.new.spam).to be_an_instance_of(Array) 120 | end 121 | end 122 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Pinball [![Build Status](https://travis-ci.org/zhulik/pinball.svg?branch=master)](https://travis-ci.org/zhulik/pinball) [![Coverage Status](https://img.shields.io/coveralls/zhulik/pinball.svg)](https://coveralls.io/r/zhulik/pinball?branch=master) [![Code Climate](https://codeclimate.com/github/zhulik/pinball.png)](https://codeclimate.com/github/zhulik/pinball) 2 | 3 | ### Simple IOC Container and DI tool for ruby. 4 | 5 | Pinball is a library for using dependency injection within Ruby 6 | applications. It provides a clear IOC Container that manages 7 | dependencies between your classes. 8 | 9 | # Features 10 | 11 | * Stores objects, classes(as factories), blocks and singletons 12 | * Can inject dependencies to classes and instances 13 | * Simple DSL for configuring the container 14 | * Stored block will be call in dependent class instance 15 | * You can describe any context-dependent code in blocks 16 | 17 | ## Usage 18 | 19 | ### Class injection 20 | 21 | Consider a `Service` class that has a dependency on a `Repository`. We would 22 | like this dependency to be available to the service when it is created. 23 | 24 | First we create a container object and declare the dependencies. 25 | 26 | ```ruby 27 | require 'pinball' 28 | 29 | Pinball::Container.configure do 30 | define :repository, Repository 31 | end 32 | ``` 33 | 34 | Then we declare the `repository` dependency in the Service class by 35 | using the `inject` declaration. 36 | 37 | ```ruby 38 | class Service 39 | inject :repository 40 | end 41 | ``` 42 | 43 | Now we can instantiate Service object and `repository` method will 44 | be already accessible in it's constructor! In this case *repository* 45 | method will return the instance of Repository. 46 | **Notice:** each call of `repository` will create new instance of `Repository`. 47 | 48 | ### Object injection 49 | 50 | Also you can inject any already existed object 51 | 52 | ```ruby 53 | require 'pinball' 54 | 55 | Pinball::Container.configure do 56 | define :string, 'any pre-defined string' 57 | end 58 | ``` 59 | 60 | In this case `string` method will return 'any pre-defined string' 61 | 62 | ### Block injection 63 | 64 | The most powerful feature of pinball is block injection. 65 | For example, you have `FirstService` class, that dependent on 66 | `SecondService` class, but for instantiating `SecondService` you need 67 | to pass `@current_user` from `FirstService` to it's constructor: 68 | 69 | ```ruby 70 | class FirstService 71 | inject :second_service 72 | 73 | def initialize(current_user) 74 | @current_user = current_user 75 | end 76 | end 77 | 78 | class SecondService 79 | def initialize(current_user) 80 | @current_user = current_user 81 | end 82 | end 83 | ``` 84 | 85 | Simple defining of `SecondService` dependency will not work here. 86 | So we can define dependency with a block: 87 | 88 | ```ruby 89 | Pinball::Container.configure do 90 | define :second_service do 91 | SecondService.new(@current_user) 92 | end 93 | end 94 | ``` 95 | 96 | This block will be executed it `FirstService` instance context where 97 | `@current_user` will be accessible. 98 | **Notice:** each call of `second_service` will call this block over and over again. 99 | 100 | ### Singletons 101 | 102 | Instead of class injection, singleton injection will not create new objects every time. It will create only one and then 103 | returns it. Perfect for stateless services and other singletons. Modifying of classes is not required. 104 | 105 | ```ruby 106 | Pinball::Container.configure do 107 | define_singleton :second_service, SingletonClass 108 | end 109 | ``` 110 | 111 | ### class_inject 112 | 113 | Sometimes you need to inject dependency to class, when it must be available in 114 | class methods. For this purpose Pinball has `class_inject` declaration: 115 | 116 | ```ruby 117 | class Foo 118 | class_inject :baz 119 | end 120 | 121 | Foo.baz 122 | ``` 123 | 124 | ## Future plans 125 | 126 | * Rails integration 127 | * Dependency lifecycle managing in Rails context 128 | * Smart caching 129 | 130 | ## Contributing 131 | 132 | 1. Fork it 133 | 2. Create your feature branch (`git checkout -b my-new-feature`) 134 | 3. Commit your changes (`git commit -am 'Add some feature'`) 135 | 4. Push to the branch (`git push origin my-new-feature`) 136 | 5. Create new Pull Request 137 | 138 | Inspired by [Encase gem](https://github.com/dsawardekar/encase "Encase gem") 139 | --------------------------------------------------------------------------------