├── .gitignore ├── .rspec ├── .travis.yml ├── Contributors.md ├── Gemfile ├── History.md ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── method_decorators.rb └── method_decorators │ ├── all.rb │ ├── decorator.rb │ ├── decorators.rb │ ├── decorators │ ├── memoize.rb │ ├── precondition.rb │ └── retry.rb │ ├── deprecated.rb │ ├── memoize.rb │ ├── precondition.rb │ ├── retry.rb │ ├── version.rb │ └── within.rb ├── method_decorators.gemspec └── spec ├── decorators ├── deprecated_spec.rb ├── memoize_spec.rb ├── precondition_spec.rb ├── retry_spec.rb └── within_spec.rb ├── method_decorators_spec.rb ├── spec_helper.rb └── support ├── add_n.rb ├── reverse.rb └── stringify.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | .rvmrc 7 | Gemfile.lock 8 | InstalledFiles 9 | _yardoc 10 | coverage 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | -------------------------------------------------------------------------------- /Contributors.md: -------------------------------------------------------------------------------- 1 | Thanks to everyone who's contributed. 2 | 3 | - Micah Frost - @mfrost 4 | - Isaac Sanders - @isaacsanders 5 | - Toby Hsieh - @tobyhs 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in method_decorators.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.9.3 2 | ===== 3 | - Fix double require bugs [@tobyhs] 4 | - Namespace Decorator, Memoize, Precondition, Retry, and Within under MethodDecorators:: [@tobyhs] 5 | 6 | 0.9.2 7 | ===== 8 | - Pass `this` (the receiver of the decorated method) to the decorators. Fixes `Precondition` with multiple preconditions and `Memoize`. 9 | 10 | 0.9.1 11 | ===== 12 | - Added Memoize, Retry, and Precondition [@mfrost] 13 | 14 | 15 | 0.9.0 16 | ===== 17 | - Initial release! 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Michael Fairley 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MethodDecorators 2 | 3 | Python's function decorators for Ruby. 4 | 5 | I probably wouldn't use this in production. 6 | 7 | ## Installation 8 | `gem install method_decorators` 9 | 10 | ## Usage 11 | 12 | ### Using a decorator 13 | Extend MethodDecorators in a class where you want to use them, and then stick `+DecoratorName` before your method declaration to decorate the method. 14 | 15 | ```ruby 16 | require "method_decorators/memoize" 17 | 18 | class MyMath 19 | extend MethodDecorators 20 | 21 | +MethodDecorators::Memoize 22 | def self.fib(n) 23 | if n <= 1 24 | n 25 | else 26 | fib(n - 1) + fib(n - 2) 27 | end 28 | end 29 | end 30 | 31 | puts MyMath.fib(200) 32 | ``` 33 | 34 | You can also decorate with an instance of a decorator, rather than the class. This is useful for configuring specific options for the decorator. 35 | 36 | ```ruby 37 | class ExternalService 38 | extend MethodDecorators 39 | 40 | +MethodDecorators::Retry.new(3) 41 | def request 42 | ... 43 | end 44 | end 45 | ``` 46 | 47 | You can also set multiple decorators for your methods. Each decorator executes within the previously declared decorator. i.e. they are nested, as expected to be. 48 | 49 | ```ruby 50 | class ExternalService 51 | extend MethodDecorators 52 | 53 | +MethodDecorators::Retry.new(3) 54 | +MethodDecorators::Within.new(2.seconds) 55 | def request 56 | ... 57 | end 58 | end 59 | ``` 60 | 61 | ### Included decorators 62 | 63 | Include these with `require 'method_decorators/name_of_decorator'`, or all at once with `require 'method_decorators/all'`. 64 | 65 | - Memoize - caches the result of the method for each arg combination it's called with 66 | - Retry - retries the method up to n (passed in to the constructor) times if the method errors 67 | - Within - times outs if a request doesn't complete within n seconds 68 | - Precondition - raises an error if the precondition (passed as a block) is not met 69 | 70 | ### Defining a decorator 71 | 72 | ```ruby 73 | class Transactional < MethodDecorators::Decorator 74 | def call(wrapped, this, *args, &blk) 75 | ActiveRecord::Base.transaction do 76 | wrapped.call(*args, &blk) 77 | end 78 | end 79 | end 80 | ``` 81 | 82 | ## License 83 | MethodDecorators is available under the MIT license and is freely available for all use, including personal, commercial, and academic. See LICENSE for details. 84 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task :default => :spec 9 | -------------------------------------------------------------------------------- /lib/method_decorators.rb: -------------------------------------------------------------------------------- 1 | require "method_decorators/version" 2 | require "method_decorators/decorator" 3 | 4 | module MethodDecorators 5 | def method_added(name) 6 | super 7 | orig_method = instance_method(name) 8 | 9 | decorators = Decorator.current_decorators 10 | return if decorators.empty? 11 | 12 | if private_method_defined?(name); visibility = :private 13 | elsif protected_method_defined?(name); visibility = :protected 14 | else visibility = :public 15 | end 16 | 17 | define_method(name) do |*args, &blk| 18 | decorated = MethodDecorators.decorate_callable(orig_method.bind(self), decorators) 19 | decorated.call(*args, &blk) 20 | end 21 | 22 | case visibility 23 | when :protected; protected name 24 | when :private; private name 25 | end 26 | end 27 | 28 | def singleton_method_added(name) 29 | super 30 | orig_method = method(name) 31 | 32 | decorators = Decorator.current_decorators 33 | return if decorators.empty? 34 | 35 | MethodDecorators.define_others_singleton_method(self, name) do |*args, &blk| 36 | decorated = MethodDecorators.decorate_callable(orig_method, decorators) 37 | decorated.call(*args, &blk) 38 | end 39 | end 40 | 41 | def self.decorate_callable(orig, decorators) 42 | decorators.reduce(orig) do |callable, decorator| 43 | lambda{ |*a, &b| decorator.call(callable, orig.receiver, *a, &b) } 44 | end 45 | end 46 | 47 | def self.define_others_singleton_method(klass, name, &blk) 48 | if klass.respond_to?(:define_singleton_method) 49 | klass.define_singleton_method(name, &blk) 50 | else 51 | class << klass 52 | self 53 | end.send(:define_method, name, &blk) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/method_decorators/all.rb: -------------------------------------------------------------------------------- 1 | require "method_decorators/memoize" 2 | require "method_decorators/retry" 3 | require "method_decorators/precondition" 4 | require "method_decorators/within" 5 | 6 | -------------------------------------------------------------------------------- /lib/method_decorators/decorator.rb: -------------------------------------------------------------------------------- 1 | module MethodDecorators 2 | class Decorator 3 | @@current_decorators = [] 4 | 5 | def self.current_decorators 6 | decs = @@current_decorators 7 | @@current_decorators = [] 8 | decs 9 | end 10 | 11 | def self.+@ 12 | +new 13 | end 14 | 15 | def +@ 16 | @@current_decorators.unshift(self) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/method_decorators/decorators.rb: -------------------------------------------------------------------------------- 1 | # This is deprecated. Use `require "method_decorators/all"` instead. 2 | Dir[File.dirname(__FILE__) + '/decorators/*.rb'].each {|file| require file } 3 | -------------------------------------------------------------------------------- /lib/method_decorators/decorators/memoize.rb: -------------------------------------------------------------------------------- 1 | require "method_decorators/memoize" 2 | ::Memoize = MethodDecorators::Memoize 3 | -------------------------------------------------------------------------------- /lib/method_decorators/decorators/precondition.rb: -------------------------------------------------------------------------------- 1 | require "method_decorators/precondition" 2 | Precondition = MethodDecorators::Precondition 3 | -------------------------------------------------------------------------------- /lib/method_decorators/decorators/retry.rb: -------------------------------------------------------------------------------- 1 | require "method_decorators/retry" 2 | ::Retry = MethodDecorators::Retry 3 | -------------------------------------------------------------------------------- /lib/method_decorators/deprecated.rb: -------------------------------------------------------------------------------- 1 | require 'method_decorators' 2 | 3 | module MethodDecorators 4 | # Example: 5 | # class MyClass 6 | # extend MethodDecorators 7 | # 8 | # +MethodDecorators::Deprecated 9 | # def deprecated_method 10 | # brand_new_method 11 | # end 12 | # end 13 | # When deprecated_method is called, message 14 | # "MyClass#deprecated_method is deprecated" 15 | # is output. 16 | # 17 | # Another example: 18 | # class MyClass 19 | # extend MethodDecorators 20 | # 21 | # +MethodDecorators::Deprecated.new('deprecated_method will be removed in the future') 22 | # def deprecated_method 23 | # brand_new_method 24 | # end 25 | # end 26 | # Above output given message 27 | # "deprecated_method will be removed in the future" 28 | # 29 | # Custom message example: 30 | # class MyClass 31 | # extend MethodDecorators 32 | # 33 | # +MethodDecorators::Deprecated.new {|class_name, method_name| "#{class_name}##{method_name} will be removed in the future. Use #{class_name}#brand_new_method instead"} 34 | # def deprecated_method 35 | # brand_new_method 36 | # end 37 | # end 38 | # Outputs 39 | # "MyClass#deprecated_method will be removed in the future. Use MyClass#brand_new_method instead" 40 | # As you see, you can use class name as the first argument and method name as the second in the block. 41 | # 42 | # Formatter example: 43 | # class Formatter2 44 | # def call(class_name, method_name) 45 | # "#{class_name}##{method_name} will be removed after the next version. Use #{class_name}#brand_new_method instead" 46 | # end 47 | # end 48 | # class MyClass 49 | # extend MethodDecorators 50 | # 51 | # formatter1 = ->(class_mane, method_name) {"#{class_name}##{method_name} will be removed in the next version. Use #{class_name}#brand_new_method instead"} 52 | # +MethodDecorators::Deprecated.new(formatter1) 53 | # def very_old_method 54 | # brand_new_method 55 | # end 56 | # 57 | # + MethodDecorators::Deprecated.new(Formatter2.new) 58 | # def deprecated_method 59 | # brand_new_method 60 | # end 61 | # end 62 | # Outputs 63 | # "MyClass#deprecated_method will be removed in the future. Use MyClass#brand_new_method instead" 64 | # You can give any object which responds to method "call" like Proc. 65 | class Deprecated < Decorator 66 | DEFAULT_FORMATTER = lambda {|class_name, method_name| "#{class_name}##{method_name} is deprecated"} 67 | def initialize(message=nil, &blk) 68 | @message = message || blk || DEFAULT_FORMATTER 69 | end 70 | 71 | def call(orig, this, *args, &blk) 72 | warn message(this.class, orig.name) 73 | orig.call(*args, &blk) 74 | end 75 | 76 | def message(class_name, method_name) 77 | if @message.respond_to? :call 78 | @message.call(class_name, method_name) 79 | else 80 | @message.to_s 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/method_decorators/memoize.rb: -------------------------------------------------------------------------------- 1 | require "method_decorators" 2 | 3 | module MethodDecorators 4 | class Memoize < Decorator 5 | def initialize 6 | @memo_ivar = "@_memoize_cache#{rand(10**10)}" 7 | end 8 | 9 | def call(orig, this, *args, &blk) 10 | return cache(this)[args] if cache(this).has_key?(args) 11 | cache(this)[args] = orig.call(*args, &blk) 12 | end 13 | 14 | private 15 | def cache(this) 16 | memo_ivar = @memo_ivar 17 | this.instance_eval do 18 | instance_variable_get(memo_ivar) || instance_variable_set(memo_ivar, {}) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/method_decorators/precondition.rb: -------------------------------------------------------------------------------- 1 | require "method_decorators" 2 | 3 | module MethodDecorators 4 | class Precondition < Decorator 5 | def initialize(&blk) 6 | @block = blk 7 | end 8 | 9 | def call(orig, this, *args, &blk) 10 | unless passes?(this, *args) 11 | raise ArgumentError, "failed precondition" 12 | end 13 | orig.call(*args, &blk) 14 | end 15 | 16 | private 17 | 18 | def passes?(context, *args) 19 | context.instance_exec(*args, &@block) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/method_decorators/retry.rb: -------------------------------------------------------------------------------- 1 | require "method_decorators" 2 | 3 | module MethodDecorators 4 | class Retry < Decorator 5 | def initialize(max, options = {}) 6 | @max = max 7 | @options = options 8 | @exceptions = options[:exceptions] || [StandardError] 9 | end 10 | 11 | def call(orig, this, *args, &blk) 12 | attempts = 0 13 | begin 14 | attempts += 1 15 | orig.call(*args, &blk) 16 | rescue *@exceptions 17 | if attempts < @max 18 | sleep(@options[:sleep]) if @options[:sleep] 19 | retry 20 | end 21 | raise 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/method_decorators/version.rb: -------------------------------------------------------------------------------- 1 | module MethodDecorators 2 | VERSION = "0.9.6" 3 | end 4 | -------------------------------------------------------------------------------- /lib/method_decorators/within.rb: -------------------------------------------------------------------------------- 1 | require "method_decorators" 2 | require 'timeout' 3 | 4 | module MethodDecorators 5 | class Within < Decorator 6 | def initialize(timeout, exception_class = nil) 7 | @seconds = timeout 8 | @exception_class = exception_class 9 | end 10 | 11 | def call(orig, this, *args, &blk) 12 | Timeout.timeout(@seconds, @exception_class) do 13 | orig.call(*args, &blk) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /method_decorators.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib_dir = File.expand_path('lib', File.dirname(__FILE__)) 3 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 4 | require 'method_decorators/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.authors = ["Michael Fairley"] 8 | gem.email = ["michaelfairley@gmail.com"] 9 | gem.description = %q{Python's function decorators for Ruby} 10 | gem.summary = %q{Python's function decorators for Ruby} 11 | gem.homepage = "http://github.com/michaelfairley/method_decorators" 12 | 13 | gem.files = `git ls-files`.split($\) 14 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 15 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 16 | gem.name = "method_decorators" 17 | gem.require_paths = ["lib"] 18 | gem.version = MethodDecorators::VERSION 19 | 20 | gem.add_development_dependency "rake" 21 | gem.add_development_dependency "rspec" 22 | end 23 | -------------------------------------------------------------------------------- /spec/decorators/deprecated_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'method_decorators/deprecated' 3 | require 'stringio' 4 | 5 | describe MethodDecorators::Deprecated do 6 | let(:method) { double(:method, :call => 'return value of original method', :name => 'deprecated_method') } 7 | 8 | describe '#call' do 9 | subject { MethodDecorators::Deprecated.new } 10 | 11 | it 'calls original method' do 12 | method.should_receive(:call) 13 | subject.call(method, nil) 14 | end 15 | 16 | it 'warns' do 17 | subject.should_receive(:warn) 18 | subject.call(method, nil) 19 | end 20 | 21 | context 'when string given on initializing' do 22 | subject { MethodDecorators::Deprecated.new('custom message') } 23 | 24 | it 'warns with given string' do 25 | subject.should_receive(:warn).with('custom message') 26 | subject.call(method, nil) 27 | end 28 | end 29 | 30 | context 'when block given on initializing' do 31 | subject { MethodDecorators::Deprecated.new {|klass, method| "#{klass}##{method}"} } 32 | 33 | it 'warns with message formatted by the block' do 34 | subject.should_receive(:warn).with('NilClass#deprecated_method') 35 | subject.call(method, nil) 36 | end 37 | end 38 | 39 | context 'when object which has #call method given on initializing' do 40 | subject { MethodDecorators::Deprecated.new(lambda { |klass, method| "#{klass}##{method}" }) } 41 | 42 | it 'warns with message formatted by the object' do 43 | subject.should_receive(:warn).with('NilClass#deprecated_method') 44 | subject.call(method, nil) 45 | end 46 | end 47 | end 48 | 49 | describe 'acceptance' do 50 | before do 51 | $stderr = StringIO.new 52 | subject.deprecated_method 53 | $stderr.rewind 54 | end 55 | 56 | after do 57 | $stderr = STDERR 58 | end 59 | 60 | let(:klass) { 61 | Class.new Base do 62 | +MethodDecorators::Deprecated 63 | def deprecated_method 64 | 'return value of original method' 65 | end 66 | end 67 | } 68 | subject { klass.new } 69 | 70 | it 'warns' do 71 | expect($stderr.read).to eq("#{klass}#deprecated_method is deprecated\n") 72 | end 73 | 74 | context 'when string given on initializing' do 75 | let(:klass) { 76 | Class.new Base do 77 | +MethodDecorators::Deprecated.new('custom message') 78 | def deprecated_method 79 | 'return value of original method' 80 | end 81 | end 82 | } 83 | 84 | it 'warns with given string' do 85 | expect($stderr.read).to eq("custom message\n") 86 | end 87 | end 88 | 89 | context 'when block given on initializing' do 90 | let(:klass) { 91 | Class.new Base do 92 | +MethodDecorators::Deprecated.new {|class_name, method_name| "#{class_name}##{method_name} is deprecated"} 93 | def deprecated_method 94 | 'return value of original method' 95 | end 96 | end 97 | } 98 | 99 | it 'warns with message formatted by the block' do 100 | expect($stderr.read).to eq("#{klass}#deprecated_method is deprecated\n") 101 | end 102 | end 103 | 104 | context 'when object witch has #call method givn on initializing' do 105 | let(:klass) { 106 | Class.new Base do 107 | +MethodDecorators::Deprecated.new(lambda { |class_name, method_name| "#{class_name}##{method_name} is deprecated" }) 108 | def deprecated_method 109 | 'return value of original method' 110 | end 111 | end 112 | } 113 | 114 | it 'warns with message formatted by the object' do 115 | expect($stderr.read).to eq("#{klass}#deprecated_method is deprecated\n") 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/decorators/memoize_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'method_decorators/memoize' 3 | 4 | describe MethodDecorators::Memoize do 5 | describe "#call" do 6 | let(:method) { double(:method, :call => :calculation) } 7 | let(:this) { Object.new } 8 | subject { MethodDecorators::Memoize.new } 9 | 10 | it "calculates the value the first time the arguments are supplied" do 11 | method.should_receive(:call) 12 | subject.call(method, this, 10) 13 | end 14 | 15 | it "stores the value of the method call" do 16 | method.stub(:call).and_return(:foo, :bar) 17 | subject.call(method, this, 10).should == :foo 18 | subject.call(method, this, 10).should == :foo 19 | end 20 | 21 | it "memoizes the return value and skips the call the second time" do 22 | subject.call(method, this, 10) 23 | method.should_not_receive(:call) 24 | subject.call(method, this, 10) 25 | end 26 | 27 | it "memoizes different values for different arguments" do 28 | method.stub(:call).with(10).and_return(:foo, :bar) 29 | method.stub(:call).with(20).and_return(:bar, :foo) 30 | subject.call(method, this, 10).should == :foo 31 | subject.call(method, this, 10).should == :foo 32 | subject.call(method, this, 20).should == :bar 33 | subject.call(method, this, 20).should == :bar 34 | end 35 | end 36 | 37 | describe "acceptance" do 38 | let(:klass) do 39 | Class.new Base do 40 | +MethodDecorators::Memoize 41 | def count 42 | @count ||= 0 43 | @count += 1 44 | rand 45 | end 46 | 47 | +MethodDecorators::Memoize 48 | def zero 49 | 0 50 | end 51 | 52 | +MethodDecorators::Memoize 53 | def one 54 | 1 55 | end 56 | end 57 | end 58 | subject { klass.new } 59 | 60 | it "memoizes calls to the method" do 61 | x = subject.count 62 | subject.count.should == x 63 | #subject.count.should == 1 64 | #subject.count.should == 1 65 | end 66 | 67 | it "memoizes call to different methods separately" do 68 | subject.zero.should eql 0 69 | subject.one.should eql 1 70 | end 71 | 72 | context "with two instances of the decorated class" do 73 | let(:o1) { subject } 74 | let(:o2) { klass.new } 75 | it "cache does not interact with that of other instances" do 76 | o1.count.should_not == o2.count 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/decorators/precondition_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'method_decorators/precondition' 3 | 4 | describe MethodDecorators::Precondition do 5 | let(:receiver) { double(:receiver) } 6 | let(:method) { double(:method, :call => :secret, :receiver => receiver) } 7 | let(:block) { proc { |arg| true } } 8 | subject { MethodDecorators::Precondition.new(&block) } 9 | 10 | describe "#call" do 11 | it "raises when the precondition fails" do 12 | subject.stub(:passes?){ false } 13 | expect{ subject.call(method, nil) }.to raise_error(ArgumentError) 14 | end 15 | 16 | it "executes the method when authorization succeeds" do 17 | subject.stub(:passes?){ true } 18 | subject.call(method, nil).should == :secret 19 | end 20 | end 21 | 22 | describe "acceptance" do 23 | let(:klass) do 24 | Class.new Base do 25 | def initialize(x) 26 | @x = x 27 | end 28 | 29 | +MethodDecorators::Precondition.new{ |a| a + @x < 10 } 30 | def multiply(a) 31 | a * @x 32 | end 33 | 34 | +MethodDecorators::Precondition.new{ |a| a + @x == 10 } 35 | +MethodDecorators::Precondition.new{ |a| a * @x == 21 } 36 | def concat(a) 37 | "#{@x}#{a}" 38 | end 39 | end 40 | end 41 | subject { klass.new(3) } 42 | 43 | context "with one precondition" do 44 | it "calls the method if the precondition passes" do 45 | subject.multiply(2).should == 6 46 | end 47 | 48 | it "raises if the precondition fails" do 49 | expect{ subject.multiply(8) }.to raise_error(ArgumentError) 50 | end 51 | end 52 | 53 | context "with multiple preconditions" do 54 | it "calls the method if the precondition passes" do 55 | subject.concat(7).should == "37" 56 | end 57 | 58 | it "raises if the precondition fails" do 59 | expect{ subject.concat(8) }.to raise_error(ArgumentError) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/decorators/retry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'method_decorators/retry' 3 | 4 | describe MethodDecorators::Retry do 5 | let(:method) { double(:method, :call => false) } 6 | subject { MethodDecorators::Retry.new(3) } 7 | 8 | describe "#call" do 9 | it "executes the method again if the first time failed " do 10 | method.stub(:call){ raise } 11 | 12 | method.should_receive(:call).exactly(3).times 13 | expect{ subject.call(method, nil) }.to raise_error 14 | end 15 | 16 | it "does not retry the method if it succeeds" do 17 | method.should_receive(:call).once.and_return(3) 18 | subject.call(method, nil).should == 3 19 | end 20 | 21 | context 'when :sleep is given to #initialize' do 22 | subject { MethodDecorators::Retry.new(3, :sleep => 5) } 23 | 24 | it 'sleeps after failures before retrying' do 25 | method.stub(:call) { raise ArgumentError } 26 | subject.should_receive(:sleep).with(5).exactly(2).times 27 | expect { subject.call(method, nil) }.to raise_error(ArgumentError) 28 | end 29 | end 30 | 31 | context 'when :exceptions is given to #initialize' do 32 | subject do 33 | MethodDecorators::Retry.new(3, :exceptions => [ArgumentError]) 34 | end 35 | 36 | context 'and the raised exception matches' do 37 | before do 38 | allow(method).to receive(:call) { raise ArgumentError } 39 | end 40 | 41 | it 'retries' do 42 | expect(method).to receive(:call).exactly(3).times 43 | expect { subject.call(method, nil) }.to raise_error(ArgumentError) 44 | end 45 | end 46 | 47 | context 'and the raised exception does not match' do 48 | before do 49 | allow(method).to receive(:call) { raise IndexError } 50 | end 51 | 52 | it 'does not retry' do 53 | expect(method).to receive(:call).once 54 | expect { subject.call(method, nil) }.to raise_error(IndexError) 55 | end 56 | end 57 | end 58 | end 59 | 60 | describe "acceptance" do 61 | def create_class(options = {}) 62 | Class.new(Base) do 63 | attr_reader :times 64 | 65 | +MethodDecorators::Retry.new(3, options) 66 | def do_it(magic_number) 67 | @times ||= 0 68 | @times += 1 69 | raise if @times == magic_number 70 | @times 71 | end 72 | end 73 | end 74 | 75 | let(:klass) { create_class } 76 | subject { klass.new } 77 | 78 | it 'retries calls to the method' do 79 | subject.do_it(1).should == 2 80 | end 81 | 82 | context 'when :exceptions is given to #initialize' do 83 | let(:klass) { create_class(:exceptions => [ArgumentError]) } 84 | 85 | it 'does not retry if the raised exception does not match' do 86 | expect { subject.do_it(1) }.to raise_error 87 | expect(subject.times).to eq(1) 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/decorators/within_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'method_decorators/within' 3 | 4 | describe MethodDecorators::Within do 5 | let(:method) { double(:method, :call => false) } 6 | subject { MethodDecorators::Within.new(2) } 7 | 8 | describe "#call" do 9 | it "raises when the timeout seconds have elapsed" do 10 | method.stub(:call){ sleep 3 } 11 | expect{ subject.call(method, nil) }.to raise_error(Timeout::Error) 12 | end 13 | 14 | it "does not raise when the method has finished execution before timeout" do 15 | method.stub(:call){ sleep 1 } 16 | expect{ subject.call(method, nil) }.to_not raise_error 17 | end 18 | 19 | context 'when an exception class is given in #initialize' do 20 | subject { MethodDecorators::Within.new(1, ArgumentError) } 21 | 22 | it 'raises the given exception if timeout seconds have elapsed' do 23 | allow(method).to receive(:call) { sleep 2 } 24 | expect { subject.call(method, nil) }.to raise_error(ArgumentError) 25 | end 26 | end 27 | end 28 | 29 | describe "acceptance" do 30 | let(:klass) do 31 | Class.new Base do 32 | +MethodDecorators::Within.new(2) 33 | def do_it(execution_period) 34 | sleep(execution_period) 35 | end 36 | end 37 | end 38 | subject { klass.new } 39 | 40 | context "with longer execution period" do 41 | it "raises if the timeout period has elapsed" do 42 | expect{ subject.do_it(3) }.to raise_error(Timeout::Error) 43 | end 44 | 45 | context 'and given an exception' do 46 | let(:klass) do 47 | Class.new(Base) do 48 | +MethodDecorators::Within.new(1, ArgumentError) 49 | def do_it(execution_period) 50 | sleep(execution_period) 51 | end 52 | end 53 | end 54 | 55 | it 'raises the given exception' do 56 | expect { subject.do_it(2) }.to raise_error(ArgumentError) 57 | end 58 | end 59 | end 60 | 61 | context "with shorter execution period" do 62 | it "finishes within the timeout period" do 63 | expect{ subject.do_it(1) }.to_not raise_error 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/method_decorators_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe MethodDecorators do 4 | subject { klass.new } 5 | 6 | describe "decorating with a simple decorator" do 7 | let(:klass) do 8 | Class.new Base do 9 | +Stringify 10 | def three 11 | 3 12 | end 13 | end 14 | end 15 | 16 | it "works" do 17 | subject.three.should == '3' 18 | end 19 | end 20 | 21 | describe "decorating with a decorator that takes arguments" do 22 | let(:klass) do 23 | Class.new Base do 24 | +AddN.new(1) 25 | def four 26 | 3 27 | end 28 | end 29 | end 30 | 31 | it "works" do 32 | subject.four.should == 4 33 | end 34 | end 35 | 36 | describe "decorating an instance method" do 37 | describe "that is public" do 38 | let(:klass) do 39 | Class.new Base do 40 | +Stringify 41 | def three 42 | 3 43 | end 44 | end 45 | end 46 | 47 | it "works" do 48 | subject.three.should == '3' 49 | end 50 | end 51 | 52 | describe "that is protected" do 53 | let(:klass) do 54 | Class.new Base do 55 | protected 56 | +Stringify 57 | def three 58 | 3 59 | end 60 | end 61 | end 62 | 63 | it "works" do 64 | subject.protected_methods.map(&:to_s).should include 'three' 65 | subject.send(:three).should == '3' 66 | end 67 | end 68 | 69 | describe "that is private" do 70 | let(:klass) do 71 | Class.new Base do 72 | private 73 | +Stringify 74 | def three 75 | 3 76 | end 77 | end 78 | end 79 | 80 | it "works" do 81 | subject.private_methods.map(&:to_s).should include 'three' 82 | subject.send(:three).should == '3' 83 | end 84 | end 85 | 86 | describe "with multiple decorators" do 87 | let(:klass) do 88 | Class.new Base do 89 | +Stringify 90 | +AddN.new(3) 91 | def six 92 | 3 93 | end 94 | end 95 | end 96 | 97 | it "works" do 98 | subject.six.should == '6' 99 | end 100 | end 101 | 102 | describe "that takes args" do 103 | let(:klass) do 104 | Class.new Base do 105 | +Stringify 106 | def sum(a, b) 107 | a + b 108 | end 109 | end 110 | end 111 | 112 | it "works" do 113 | subject.sum(1, 2).should == "3" 114 | end 115 | end 116 | 117 | describe "that takes a block" do 118 | let(:klass) do 119 | Class.new Base do 120 | +Stringify 121 | def double(&blk) 122 | blk.call + blk.call 123 | end 124 | end 125 | end 126 | 127 | it "works" do 128 | subject.double{ 2 }.should == '4' 129 | end 130 | end 131 | 132 | describe "with a decorator that messes with the args" do 133 | let(:klass) do 134 | Class.new Base do 135 | +Reverse 136 | def echo(a, b) 137 | [a, b] 138 | end 139 | end 140 | end 141 | 142 | it "works" do 143 | subject.echo(1, 2).should == [2, 1] 144 | end 145 | end 146 | end 147 | 148 | describe "decorating a singleton method" do 149 | subject { klass } 150 | 151 | describe "that is public" do 152 | let(:klass) do 153 | Class.new Base do 154 | +Stringify 155 | def self.three 156 | 3 157 | end 158 | end 159 | end 160 | 161 | it "works" do 162 | subject.three.should == '3' 163 | end 164 | end 165 | 166 | describe "that is private" do 167 | let(:klass) do 168 | Class.new Base do 169 | +Stringify 170 | def self.three 171 | 3 172 | end 173 | private_class_method :three 174 | end 175 | end 176 | 177 | it "works" do 178 | subject.private_methods.map(&:to_s).should include 'three' 179 | subject.send(:three).should == '3' 180 | end 181 | end 182 | 183 | describe "with multiple decorators" do 184 | let(:klass) do 185 | Class.new Base do 186 | +Stringify 187 | +AddN.new(3) 188 | def self.six 189 | 3 190 | end 191 | end 192 | end 193 | 194 | it "works" do 195 | subject.six.should == '6' 196 | end 197 | end 198 | 199 | describe "that takes args" do 200 | let(:klass) do 201 | Class.new Base do 202 | +Stringify 203 | def self.sum(a, b) 204 | a + b 205 | end 206 | end 207 | end 208 | 209 | it "works" do 210 | subject.sum(1, 2).should == "3" 211 | end 212 | end 213 | 214 | describe "that takes a block" do 215 | let(:klass) do 216 | Class.new Base do 217 | +Stringify 218 | def self.double(&blk) 219 | blk.call + blk.call 220 | end 221 | end 222 | end 223 | 224 | it "works" do 225 | subject.double{ 2 }.should == '4' 226 | end 227 | end 228 | 229 | describe "with a decorator that messes with the args" do 230 | let(:klass) do 231 | Class.new Base do 232 | +Reverse 233 | def self.echo(a, b) 234 | [a, b] 235 | end 236 | end 237 | end 238 | 239 | it "works" do 240 | subject.echo(1, 2).should == [2, 1] 241 | end 242 | end 243 | end 244 | end 245 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'method_decorators' 2 | 3 | require 'support/stringify' 4 | require 'support/add_n' 5 | require 'support/reverse' 6 | 7 | class Base 8 | extend MethodDecorators 9 | end 10 | 11 | # This file was generated by the `rspec --init` command. Conventionally, all 12 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 13 | # Require this file using `require "spec_helper.rb"` to ensure that it is only 14 | # loaded once. 15 | # 16 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 17 | RSpec.configure do |config| 18 | config.treat_symbols_as_metadata_keys_with_true_values = true 19 | config.run_all_when_everything_filtered = true 20 | config.filter_run :focus 21 | end 22 | -------------------------------------------------------------------------------- /spec/support/add_n.rb: -------------------------------------------------------------------------------- 1 | class AddN < MethodDecorators::Decorator 2 | def initialize(n) 3 | @n = n 4 | end 5 | 6 | def call(orig, this, *args, &blk) 7 | orig.call(*args, &blk) + @n 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/reverse.rb: -------------------------------------------------------------------------------- 1 | class Reverse < MethodDecorators::Decorator 2 | def call(orig, this, *args, &blk) 3 | orig.call(*args.reverse, &blk) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/stringify.rb: -------------------------------------------------------------------------------- 1 | class Stringify < MethodDecorators::Decorator 2 | def call(orig, this, *args, &blk) 3 | orig.call(*args, &blk).to_s 4 | end 5 | end 6 | --------------------------------------------------------------------------------