decoratable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6a8eadd3a9f64dd170469d9bf46f9765ce9d411a
4
+ data.tar.gz: e0daa4b811d58ef5b10b867b89743219b7a3ac0e
5
+ SHA512:
6
+ metadata.gz: b81d3a486e381d52d909002417d19c2578c63c3c784e5e2ee32357c7fe807eee8b902a5c4d640e7184d9fc4321de13f08f623a18c34c7decf7db01896b1f4208
7
+ data.tar.gz: c49567ab494c3e5ccd247adc8c0492c4b0186eb7086bbce37a6e579d288ab2b34f9abab76243ad65943d7eb5f2b630aa4c9007480505f0dabae6caf800ba1071
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - 2.2.0
7
+ - ruby-head
8
+ - jruby-head
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,9 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs = ["lib", "test"]
5
+ t.test_files = FileList["test/**/*_test.rb"]
6
+ t.verbose = true
7
+ end
8
+
9
+ task :default => :test
@@ -0,0 +1,20 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "decoratable"
6
+ spec.version = "0.0.1"
7
+ spec.authors = ["ecin"]
8
+ spec.email = ["ecin@copypastel.com"]
9
+ spec.description = "Decorate your methods."
10
+ spec.summary = "Easily define decorations for your methods. Put a bow on it."
11
+ spec.homepage = "https://github.com/ecin/decoratable"
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files`.split($/)
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_development_dependency "pry"
20
+ end
@@ -0,0 +1,159 @@
1
+ require "thread"
2
+
3
+ # Public: provide an easy way to define decorations
4
+ # that add common behaviour to a method.
5
+ #
6
+ # More info on decorations as implemented in Python:
7
+ # http://en.wikipedia.org/wiki/Python_syntax_and_semantics#Decorators
8
+ #
9
+ # Examples
10
+ #
11
+ # module Decorations
12
+ # extend Decoratable
13
+ #
14
+ # def retryable(tries = 1, options = { on: [RuntimeError] })
15
+ # attempts = 0
16
+ #
17
+ # begin
18
+ # yield
19
+ # rescue *options[:on]
20
+ # attempts += 1
21
+ # attempts > tries ? raise : retry
22
+ # end
23
+ # end
24
+ #
25
+ # def measurable(logger = STDOUT)
26
+ # start = Time.now
27
+ # yield
28
+ # ensure
29
+ # original_method = __decorated_method__
30
+ # method_location, line = original_method.source_location
31
+ # marker = "#{original_method.owner}##{original_method.name}[#{method_location}:#{line}]"
32
+ # duration = (Time.now - start).round(2)
33
+ #
34
+ # logger.puts "#{marker} took #{duration}s to run."
35
+ # end
36
+ #
37
+ # def debuggable
38
+ # begin
39
+ # yield
40
+ # rescue => e
41
+ # puts "Caught #{e}!!!"
42
+ # require "debug"
43
+ # end
44
+ # end
45
+ #
46
+ # def memoizable
47
+ # key = :"@#{__decorated_method__.name}_cache"
48
+ #
49
+ # instance_variable_set(key, {}) unless defined?(key)
50
+ # cache = instance_variable_get(key)
51
+ #
52
+ # if cache.key?(__args__)
53
+ # cache[__args__]
54
+ # else
55
+ # cache[__args__] = yield
56
+ # end
57
+ # end
58
+ # end
59
+ #
60
+ # class Client
61
+ # extend Decorations
62
+ #
63
+ # # Let's keep track of how long #get takes to run,
64
+ # # and memoize the return value
65
+ # measurable
66
+ # memoizable
67
+ # def get
68
+ # …
69
+ # end
70
+ #
71
+ # # Rescue and retry any Timeout::Errors, up to 5 times
72
+ # retryable 5, on: [Timeout::Error]
73
+ # def post
74
+ # …
75
+ # end
76
+ # end
77
+ module Decoratable
78
+ @@lock = Mutex.new
79
+
80
+ def self.extended(klass)
81
+ # This #method_added affects all methods defined in the module
82
+ # that extends Decoratable.
83
+ def klass.method_added(decoration_name)
84
+ return unless @@lock.try_lock
85
+
86
+ decoration_method = instance_method(decoration_name)
87
+
88
+ define_method(decoration_name) do |*decorator_args|
89
+ # Wrap method_added to decorate the next method definition.
90
+ self.singleton_class.instance_eval do
91
+ alias_method "method_added_without_#{decoration_name}", :method_added
92
+ end
93
+
94
+ unless method_defined?(:__decorated_method__)
95
+ define_method(:__decorated_method__) { @__decorated_method__ }
96
+ end
97
+
98
+ unless method_defined?(:__args__)
99
+ define_method(:__args__) { @__args__ }
100
+ end
101
+
102
+ unless method_defined?(:__block__)
103
+ define_method(:__block__) { @__block__ }
104
+ end
105
+
106
+ # This method_added will affect the next decorated method.
107
+ define_singleton_method(:method_added) do |method_name|
108
+
109
+ original_method = instance_method(method_name)
110
+
111
+ decoration = Module.new do
112
+ self.singleton_class.instance_eval do
113
+ define_method(:name) do
114
+ "Decoratable::#{decoration_name}(#{method_name})"
115
+ end
116
+
117
+ alias_method :inspect, :name
118
+ alias_method :to_s, :name
119
+ end
120
+
121
+ define_method(method_name) do |*args, &block|
122
+ begin
123
+ # The decoration should have access to the original
124
+ # method it's modifying, along with the method call's
125
+ # arguments.
126
+ @__decorated_method__ = original_method
127
+ @__args__ = args
128
+ @__block__ = block
129
+
130
+ decoration_method.bind(self).call(*decorator_args) do
131
+ super(*args, &block)
132
+ end
133
+ ensure
134
+ @__decorated_method__ = nil
135
+ @__args__ = nil
136
+ @__block__ = nil
137
+ end
138
+ end
139
+ end
140
+
141
+ # Call aspect before "real" method.
142
+ prepend decoration
143
+
144
+ # Call next method_added link in the chain.
145
+ __send__("method_added_without_#{decoration_name}", method_name)
146
+
147
+ # Remove ourselves from method_added chain.
148
+ self.singleton_class.instance_eval do
149
+ alias_method :method_added, "method_added_without_#{decoration_name}"
150
+ remove_method "method_added_without_#{decoration_name}"
151
+ end
152
+ end
153
+ end
154
+ ensure
155
+ @@lock.unlock if @@lock.locked?
156
+ end
157
+ end
158
+
159
+ end
@@ -0,0 +1,26 @@
1
+ require "decoratable"
2
+
3
+ module Countable
4
+ extend Decoratable
5
+
6
+ module Helper
7
+ def define_reader(object, key)
8
+ unless object.respond_to?(key)
9
+ object.define_singleton_method(key) { instance_variable_get(:"@#{key}") }
10
+ end
11
+ end
12
+
13
+ module_function :define_reader
14
+ end
15
+
16
+ def countable
17
+ key = :"#{__decorated_method__.name}_call_count"
18
+ Helper.define_reader(self, key)
19
+
20
+ instance_variable = :"@#{key}"
21
+ count = instance_variable_get(instance_variable).to_i
22
+ instance_variable_set(instance_variable, count + 1)
23
+
24
+ yield
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ require "decoratable"
2
+
3
+ module Debuggable
4
+ extend Decoratable
5
+
6
+ def debuggable(options = { on: [RuntimeError] })
7
+ yield
8
+ rescue *options[:on]
9
+ require "debug"
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ require "decoratable"
2
+
3
+ module Hintable
4
+ extend Decoratable
5
+
6
+ def hintable(*classes, require_block: false)
7
+ argument_names = __decorated_method__.parameters.map { |(_, param)| param }
8
+ checks = __args__.zip(classes, argument_names)
9
+
10
+ # check args for types
11
+ failed_check = checks.find { |arg, klass, _| !arg.is_a?(klass) }
12
+ if failed_check
13
+ raise ArgumentError, "#{failed_check[2]} expected argument of type #{failed_check[1]}, was #{failed_check[0].class} (#{failed_check[0].inspect})"
14
+ end
15
+
16
+ if require_block && __block__.nil?
17
+ raise ArgumentError, "#{__decorated_method__.name} requires a block"
18
+ end
19
+
20
+ yield
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ require "decoratable"
2
+
3
+ module Memoizable
4
+ extend Decoratable
5
+
6
+ def memoizable
7
+ key = :"@#{__decorated_method__.name}_cache"
8
+
9
+ if instance_variable_defined?(key)
10
+ instance_variable_get key
11
+ else
12
+ instance_variable_set key, yield
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ require "decoratable"
2
+
3
+ module Pryable
4
+ extend Decoratable
5
+
6
+ def pryable(options = { on: [RuntimeError] })
7
+ yield
8
+ rescue *options[:on]
9
+ require "pry"
10
+ binding.pry
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ require "decoratable"
2
+
3
+ module Retryable
4
+ extend Decoratable
5
+
6
+ def retryable(tries = 1, options = { on: [RuntimeError] } )
7
+ attempts = 0
8
+
9
+ begin
10
+ yield
11
+ rescue *options[:on]
12
+ attempts += 1
13
+ attempts > tries ? raise : retry
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ require "test_helper"
2
+
3
+ require "decoratable/countable"
4
+
5
+ describe Countable do
6
+
7
+ before do
8
+ klass = Class.new do
9
+ extend Countable
10
+
11
+ countable
12
+ def call; end
13
+ end
14
+
15
+ @object = klass.new
16
+ end
17
+
18
+ it "keeps count of how many times a method is called" do
19
+ 3.times { @object.call }
20
+ @object.call_call_count.must_equal 3
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ require "test_helper"
2
+
3
+ require "decoratable/hintable"
4
+
5
+ describe Hintable do
6
+
7
+ before do
8
+ klass = Class.new do
9
+ extend Hintable
10
+
11
+ hintable(Integer, require_block: true)
12
+ def call(a); end
13
+ end
14
+
15
+ @object = klass.new
16
+ end
17
+
18
+ it "checks argument types" do
19
+ proc { @object.call(:a) }.must_raise ArgumentError, /#{Regexp.escape("a expected argument of type Integer, was Symbol (:a)")}/
20
+ end
21
+
22
+ it "checks for a required block" do
23
+ proc { @object.call(1) }.must_raise ArgumentError, /call requires a block/
24
+ end
25
+
26
+ it "steps out of the way for the correct arguments" do
27
+ @object.call(1) { }
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ require "test_helper"
2
+
3
+ require "decoratable/memoizable"
4
+
5
+ describe Memoizable do
6
+
7
+ before do
8
+ klass = Class.new do
9
+ extend Memoizable
10
+
11
+ memoizable
12
+ def call
13
+ @count ||= 0
14
+ @count += 1
15
+ end
16
+ end
17
+
18
+ @object = klass.new
19
+ end
20
+
21
+ it "memoizes a function's return value" do
22
+ 3.times { @object.call }
23
+
24
+ # If method was memoized, the counter shouldn't have incremented
25
+ @object.call.must_equal 1
26
+ end
27
+
28
+ end
@@ -0,0 +1,47 @@
1
+ require "test_helper"
2
+
3
+ require "decoratable/pryable"
4
+
5
+ describe Pryable do
6
+
7
+ # stub Binding#pry for test; I'm sorry
8
+ class Binding
9
+ @@pry_count = 0
10
+
11
+ def self.pry_count
12
+ @@pry_count
13
+ end
14
+
15
+ def pry
16
+ @@pry_count += 1
17
+ end
18
+ end
19
+
20
+ before do
21
+ klass = Class.new do
22
+ extend Pryable
23
+
24
+ pryable(on: [NoMethodError, ArgumentError])
25
+ def call
26
+ @count ||= 0
27
+
28
+ case @count
29
+ when 0 then raise NoMethodError
30
+ when 1 then raise ArgumentError
31
+ else raise RuntimeError
32
+ end
33
+
34
+ ensure
35
+ @count += 1
36
+ end
37
+ end
38
+
39
+ @object = klass.new
40
+ end
41
+
42
+ it "enters a pry session for the specified exceptions" do
43
+ proc { 3.times { @object.call } }.must_raise RuntimeError
44
+ Binding.pry_count.must_equal 2
45
+ end
46
+
47
+ end
@@ -0,0 +1,33 @@
1
+ require "test_helper"
2
+
3
+ require "decoratable/retryable"
4
+
5
+ describe Retryable do
6
+
7
+ before do
8
+ klass = Class.new do
9
+ extend Retryable
10
+
11
+ retryable(3, on: [RuntimeError, ArgumentError])
12
+ def call
13
+ @count ||= 0
14
+
15
+ case @count
16
+ when 0 then raise RuntimeError
17
+ when 1 then raise ArgumentError
18
+ when 2 then raise NoMethodError
19
+ else raise NameError
20
+ end
21
+ ensure
22
+ @count += 1
23
+ end
24
+ end
25
+
26
+ @object = klass.new
27
+ end
28
+
29
+ it "retries a specific number of times on specific errors" do
30
+ proc { @object.call }.must_raise NoMethodError
31
+ end
32
+
33
+ end
@@ -0,0 +1,150 @@
1
+ require "test_helper"
2
+
3
+ require "decoratable"
4
+ require "decoratable/memoizable"
5
+ require "decoratable/countable"
6
+ require "decoratable/hintable"
7
+
8
+ require "logger"
9
+
10
+ describe Decoratable do
11
+
12
+ # Test-only decorations
13
+ module Decorations
14
+ extend Decoratable
15
+
16
+ include Countable
17
+ include Hintable
18
+ include Memoizable
19
+
20
+ def measurable(logger = Logger.new(STDOUT))
21
+ current = Time.now
22
+ yield
23
+ ensure
24
+ original_method = __decorated_method__
25
+ method_location, line = original_method.source_location
26
+ marker = "#{original_method.owner}##{original_method.name}[#{method_location}:#{line}]"
27
+ duration = (Time.now - current).round(2)
28
+
29
+ logger.info "#{marker} took #{duration}s to run."
30
+ end
31
+
32
+ def auditable
33
+ name = __decorated_method__.name
34
+
35
+ key = :"@audit_log"
36
+ audit_log = instance_variable_get(key) || []
37
+ audit_log << name
38
+ instance_variable_set(key, audit_log)
39
+
40
+ yield
41
+ end
42
+ end
43
+
44
+ it "decorates a method" do
45
+ klass = Class.new do
46
+ extend Decorations
47
+
48
+ countable
49
+ def call; end
50
+ end
51
+
52
+ object = klass.new
53
+ 3.times { object.call }
54
+ object.call_call_count.must_equal 3
55
+ end
56
+
57
+ it "can pass configuration arguments to the decoration" do
58
+ logger = mock_logger
59
+ klass = Class.new do
60
+ extend Decorations
61
+
62
+ measurable(logger)
63
+ def initialize; end
64
+ end
65
+
66
+ klass.new
67
+ logger.verify
68
+ end
69
+
70
+ it "can have multiple decorations on a method" do
71
+ klass = Class.new do
72
+ extend Decorations
73
+
74
+ countable
75
+ memoizable
76
+ def call
77
+ @counter ||= 0
78
+ @counter += 1
79
+ end
80
+ end
81
+
82
+ object = klass.new
83
+ 3.times { object.call }
84
+
85
+ # Check countable() works
86
+ object.call_call_count.must_equal 3
87
+
88
+ # Check memoizable() works
89
+ object.call.must_equal 1
90
+ end
91
+
92
+ it "only decorates the next defined method" do
93
+ klass = Class.new do
94
+ extend Decorations
95
+
96
+ attr_reader :call2_call_count
97
+
98
+ countable
99
+ def call; end
100
+ def call2; end
101
+ end
102
+
103
+ object = klass.new
104
+ 3.times { object.call }
105
+ 3.times { object.call2 }
106
+
107
+ object.call_call_count.must_equal 3
108
+ object.call2_call_count.must_equal nil
109
+ end
110
+
111
+ it "allows access to the original decorated method" do
112
+ klass = Class.new do
113
+ extend Decorations
114
+
115
+ attr_reader :audit_log
116
+
117
+ auditable
118
+ def initialize; end
119
+
120
+ auditable
121
+ def call; end
122
+ end
123
+
124
+ object = klass.new
125
+ object.call
126
+ object.audit_log.must_equal [:initialize, :call]
127
+ end
128
+
129
+ it "gives access to the arguments and blocks of the original method call" do
130
+ klass = Class.new do
131
+ extend Decorations
132
+
133
+ hintable(Integer, String, require_block: true)
134
+ def call(a, b); end
135
+ end
136
+
137
+ object = klass.new
138
+ proc { object.call(:a, 1) }.must_raise ArgumentError, /#{Regexp.escape("a expected argument of type Integer, was Symbol (:a)")}/
139
+ proc { object.call(1, :b) }.must_raise ArgumentError, /#{Regexp.escape("b expected argument of type String, was Symbol (:b)")}/
140
+ proc { object.call(1, "") }.must_raise ArgumentError, /call requires a block/
141
+ end
142
+
143
+ def mock_logger
144
+ logger = Minitest::Mock.new
145
+ logger.expect(:info, nil) do |message|
146
+ message =~ /took \d+\.\d+s to run/
147
+ end
148
+ logger
149
+ end
150
+ end
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require "minitest/mock"
3
+ require "minitest/pride"
4
+ require "pry"
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: decoratable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - ecin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pry
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Decorate your methods.
28
+ email:
29
+ - ecin@copypastel.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".travis.yml"
35
+ - Gemfile
36
+ - Rakefile
37
+ - decoratable.gemspec
38
+ - lib/decoratable.rb
39
+ - lib/decoratable/countable.rb
40
+ - lib/decoratable/debuggable.rb
41
+ - lib/decoratable/hintable.rb
42
+ - lib/decoratable/memoizable.rb
43
+ - lib/decoratable/pryable.rb
44
+ - lib/decoratable/retryable.rb
45
+ - test/decoratable/countable_test.rb
46
+ - test/decoratable/hintable_test.rb
47
+ - test/decoratable/memoizable_test.rb
48
+ - test/decoratable/pryable_test.rb
49
+ - test/decoratable/retryable_test.rb
50
+ - test/decoratable_test.rb
51
+ - test/test_helper.rb
52
+ homepage: https://github.com/ecin/decoratable
53
+ licenses:
54
+ - MIT
55
+ metadata: {}
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 2.4.3
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: Easily define decorations for your methods. Put a bow on it.
76
+ test_files:
77
+ - test/decoratable/countable_test.rb
78
+ - test/decoratable/hintable_test.rb
79
+ - test/decoratable/memoizable_test.rb
80
+ - test/decoratable/pryable_test.rb
81
+ - test/decoratable/retryable_test.rb
82
+ - test/decoratable_test.rb
83
+ - test/test_helper.rb
84
+ has_rdoc: