decoratable 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: