method_man 1.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3f6ff20e7c2d99f044220e1ec5ac2f2797e1d69e
4
- data.tar.gz: 1c3d343db211b89563d90d542ca55571f7da57f6
3
+ metadata.gz: c0ddc3147244cc0dc922a4ebc1aa9512efedfecf
4
+ data.tar.gz: 5139372bb0da36319b9cd7a3338da3dd507e1724
5
5
  SHA512:
6
- metadata.gz: 9ec339e5eb97608a7ef871d8bb2a6481f0e3bdb451de0b887c798ea810133b00afdcdd20398ed084250be4381ab4e36bc3bc7671c6d146e53371fe6e5a822f10
7
- data.tar.gz: 5045fc01e0279d6c99dbe8e5dce37bf612e058f4f6da6edd5768f31ed531565ac610300f03327dbad301b81cbf5168626919f0a9e8053c700e7247620f51e51f
6
+ metadata.gz: 71436d2cf1e9b82acf277477ae6009d1e6d05288f93a848cb8055dfa8b6169c27fdc615ed5ca3ab710b4241f45f658aa0ec5989e2f42380f7b1df92893453a21
7
+ data.tar.gz: 7cf527f78e56ed58fa9e64f7e0149f006fc398fbb81309f4315fe790288075ed07cd600c623f987daafdffc12dbb8fb127bc5c320a9b0bd83075d218b2eb45d7
data/.rubocop.yml ADDED
@@ -0,0 +1,30 @@
1
+ AllCops:
2
+ Exclude:
3
+ - spec/*
4
+ TargetRubyVersion: 2.4
5
+
6
+ # We don't care about method length, since we check method cyclomatic
7
+ # complexity.
8
+ Metrics/MethodLength:
9
+ Enabled: false
10
+ Metrics/ClassLength:
11
+ Enabled: false
12
+ Metrics/ModuleLength:
13
+ Enabled: false
14
+
15
+ # Trailing commas make for clearer diffs because the last line won't appear
16
+ # to have been changed, as it would if it lacked a comma and had one added.
17
+ Style/TrailingCommaInArguments:
18
+ EnforcedStyleForMultiline: comma
19
+ Style/TrailingCommaInLiteral:
20
+ EnforcedStyleForMultiline: comma
21
+
22
+ # Cop supports --auto-correct.
23
+ # Configuration parameters: PreferredDelimiters.
24
+ Style/PercentLiteralDelimiters:
25
+ PreferredDelimiters:
26
+ # Using `[]` for string arrays instead of `()`, since normal arrays are
27
+ # indicated with `[]` not `()`.
28
+ '%w': '[]'
29
+ '%W': '[]'
30
+ '%i': '[]'
data/.ruby-version CHANGED
@@ -1 +1,2 @@
1
- 2.2.1
1
+ 2.4.0
2
+
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Change log
2
+
3
+ ## 2.0.0
4
+ Convert MethodObject to use inheritance and a class method for dynamic setup of class internals
5
+ - Enables code editors to find declaration of MethodObject
6
+ - Allows constants to be nested whereas previous implementation was based on a Struct which could not contain constants.
7
+
8
+ ## 2.1.0
9
+ Allow automatic delegation inspired by Golang's embedding.
data/Gemfile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  source 'https://rubygems.org'
2
3
 
3
4
  # Specify your gem's dependencies in method_man.gemspec
data/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  Defines a MethodObject class which facilitates basic setup for Kent Beck's "[method object](http://c2.com/cgi/wiki?MethodObject)".
4
4
 
5
- The MethodObject class is largely based on a Ruby Struct, but with a few additional features:
6
-
7
5
  * Facilitates basic method object pattern setup. You only need to supply an instance `call` method.
8
6
  * Accepts a list of arguments which are mapped to required keyword arguments.
9
7
  * Disallows calling `new` on the resulting MethodObject class instance.
@@ -22,13 +20,13 @@ gem 'method_man', require: 'method_object'
22
20
  ```ruby
23
21
  require 'method_object'
24
22
 
25
- MakeArbitraryArray = MethodObject.new(:first_name, :last_name, :message) do
23
+ class MakeArbitraryArray < MethodObject
24
+ attrs(:first_name, :last_name, :message)
25
+
26
26
  def call
27
27
  [fullname, message, 42]
28
28
  end
29
29
 
30
- private
31
-
32
30
  def fullname
33
31
  "#{first_name} #{last_name}"
34
32
  end
@@ -39,5 +37,30 @@ gem 'method_man', require: 'method_object'
39
37
  last_name: 'Smith',
40
38
  message: 'Hi',
41
39
  )
42
- => ["John Smith", 'Hi', 42]
40
+ => ['John Smith', 'Hi', 42]
41
+ ```
42
+
43
+ Also allows automatic delegation inspired by Go's
44
+ [delegation](https://nathany.com/good/).
45
+
46
+
47
+ ```ruby
48
+ require 'method_object'
49
+
50
+ class MakeArbitraryArray < MethodObject
51
+ attrs(:company)
52
+
53
+ def call
54
+ [
55
+ company.name,
56
+ name, # Automatic delegation since company has a `name` method
57
+ company_name, # Automatic delegation with prefix
58
+ ]
59
+ end
60
+ end
61
+
62
+ company = Company.new(name: 'Tyrell Corporation')
63
+
64
+ MakeArbitraryArray.call(company: company)
65
+ => ['Tyrell Corporation', 'Tyrell Corporation', 'Tyrell Corporation']
43
66
  ```
data/Rakefile CHANGED
@@ -1 +1,2 @@
1
- require "bundler/gem_tasks"
1
+ # frozen_string_literal: true
2
+ require 'bundler/gem_tasks'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class MethodObject
2
- VERSION = '1.0.0'
3
+ VERSION = '2.1.0'
3
4
  end
data/lib/method_object.rb CHANGED
@@ -1,42 +1,170 @@
1
+ # frozen_string_literal: true
1
2
  require 'method_object/version'
2
3
 
4
+ # See gemspec for description
3
5
  class MethodObject
4
- def self.new(*args, &block)
5
- super(*args, block).call
6
- end
6
+ class AmbigousMethodError < NameError; end
7
+
8
+ class << self
9
+ def attrs(*attributes)
10
+ @attributes = attributes
11
+ Setup.call(attributes: attributes, subclass: self)
12
+ end
7
13
 
8
- def initialize(*required_keyword_args, block)
9
- @required_keyword_args = required_keyword_args
10
- @block = block
14
+ def call(**args)
15
+ new(args).call
16
+ end
17
+
18
+ attr_reader(:attributes)
19
+
20
+ private(:new)
11
21
  end
12
22
 
23
+ def initialize(_); end
24
+
13
25
  def call
14
- code = self.code
15
- block = @block
26
+ raise NotImplementedError, 'Define the call method'
27
+ end
16
28
 
17
- Struct.new(*@required_keyword_args) do
18
- private_class_method :new
19
- eval(code)
20
- def call
21
- fail NotImplementedError, "Please define the call method"
22
- end
23
- class_eval(&block)
29
+ def method_missing(name, *args, &block)
30
+ candidates = candidates_for_method_missing(name)
31
+ case candidates.length
32
+ when 0
33
+ super
34
+ when 1
35
+ define_and_call_new_method(candidates.first)
36
+ else
37
+ handle_ambiguous_missing_method(candidates, name)
24
38
  end
25
39
  end
26
40
 
27
- def code
28
- <<-CODE
29
- def self.call(#{required_keyword_args_string})
30
- new(#{ordered_args_string}).call
41
+ def respond_to_missing?(name)
42
+ candidates_for_method_missing(name).length == 1
43
+ end
44
+
45
+ def candidates_for_method_missing(method_name)
46
+ potential_candidates =
47
+ self.class.attributes.map do |attribute|
48
+ PotentialDelegator.new(attribute, send(attribute), method_name)
49
+ end +
50
+ self.class.attributes.map do |attribute|
51
+ PotentialDelegatorWithPrefix.new(
52
+ attribute,
53
+ send(attribute),
54
+ method_name,
55
+ )
31
56
  end
32
- CODE
57
+ potential_candidates.select(&:candidate?)
33
58
  end
34
59
 
35
- def required_keyword_args_string
36
- @required_keyword_args.map { |arg| "#{arg}:" }.join(',')
60
+ def define_and_call_new_method(candidate)
61
+ self.class.class_eval(
62
+ <<-RUBY
63
+ def #{candidate.delegated_method}
64
+ #{candidate.attribute}.#{candidate.method_to_call_on_delegate}
65
+ end
66
+ RUBY
67
+ )
68
+ send(candidate.delegated_method)
37
69
  end
38
70
 
39
- def ordered_args_string
40
- @required_keyword_args.join(',')
71
+ def handle_ambiguous_missing_method(candidates, method_name)
72
+ raise(
73
+ AmbigousMethodError,
74
+ "#{method_name} is ambiguous: " +
75
+ candidates
76
+ .map do |candidate|
77
+ "#{candidate.attribute}.#{candidate.method_to_call_on_delegate}"
78
+ end
79
+ .join(', '),
80
+ )
81
+ end
82
+
83
+ # Represents a possible match of the form:
84
+ # some_method => my_attribute.some_method
85
+ PotentialDelegator = Struct.new(:attribute, :object, :delegated_method) do
86
+ def candidate?
87
+ object.respond_to?(delegated_method)
88
+ end
89
+
90
+ def call
91
+ object.public_send(delegated_method)
92
+ end
93
+
94
+ alias_method :method_to_call_on_delegate, :delegated_method
95
+ end
96
+
97
+ # Represents a possible match of the form:
98
+ # my_attribute_some_method => my_attribute.some_method
99
+ PotentialDelegatorWithPrefix =
100
+ Struct.new(:attribute, :object, :delegated_method) do
101
+ def candidate?
102
+ name_matches? && object.respond_to?(method_to_call_on_delegate)
103
+ end
104
+
105
+ def call
106
+ object.public_send(method_to_call_on_delegate)
107
+ end
108
+
109
+ def method_to_call_on_delegate
110
+ delegated_method.to_s.sub(prefix, '')
111
+ end
112
+
113
+ private
114
+
115
+ def name_matches?
116
+ delegated_method.to_s.start_with?(prefix)
117
+ end
118
+
119
+ def prefix
120
+ "#{attribute}_"
121
+ end
122
+ end
123
+
124
+ # Dynamically defines custom attr_readers and initializer
125
+ class Setup < SimpleDelegator
126
+ def self.call(attributes:, subclass:)
127
+ new(attributes, subclass).call
128
+ end
129
+
130
+ attr_accessor(:attributes)
131
+
132
+ def initialize(attributes, subclass)
133
+ self.attributes = attributes
134
+ super(subclass)
135
+ end
136
+
137
+ def call
138
+ define_attr_readers
139
+ define_initializer
140
+ end
141
+
142
+ private
143
+
144
+ def define_attr_readers
145
+ __getobj__.send(:attr_reader, *attributes)
146
+ end
147
+
148
+ def attr_accessor(attribute)
149
+ super
150
+ end
151
+
152
+ def define_initializer
153
+ class_eval(
154
+ <<-RUBY
155
+ def initialize(#{required_keyword_args_string})
156
+ #{assignments}
157
+ end
158
+ RUBY
159
+ )
160
+ end
161
+
162
+ def required_keyword_args_string
163
+ attributes.map { |arg| "#{arg}:" }.join(',')
164
+ end
165
+
166
+ def assignments
167
+ attributes.map { |attribute| "@#{attribute} = #{attribute}\n" }.join
168
+ end
41
169
  end
42
170
  end
data/method_man.gemspec CHANGED
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # frozen_string_literal: true
2
3
  lib = File.expand_path('../lib', __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'method_object/version'
@@ -7,16 +8,18 @@ Gem::Specification.new do |spec|
7
8
  spec.name = 'method_man'
8
9
  spec.version = MethodObject::VERSION
9
10
  spec.authors = ['Clay Shentrup']
10
- spec.email = %w(cshentrup@gmail.com)
11
- spec.summary = %q{Provides a MethodObject class which implements Kent Beck's "method object" pattern.}
12
- spec.description = %q{Provides a MethodObject class which implements Kent Beck's "method object" pattern.}
11
+ spec.email = %w[cshentrup@gmail.com]
12
+ spec.summary = 'Provides a MethodObject class which implements Kent' +
13
+ %q(Beck's "method object" pattern.)
14
+ spec.description = 'Provides a MethodObject class which implements Kent' +
15
+ %q(Beck's "method object" pattern.)
13
16
  spec.homepage = 'https://github.com/brokenladder/method_man'
14
17
  spec.license = 'MIT'
15
18
 
16
- spec.files = `git ls-files`.split($/)
19
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
20
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
21
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = %w(lib)
22
+ spec.require_paths = %w[lib]
20
23
 
21
24
  spec.required_ruby_version = '>= 2.1'
22
25
 
@@ -1,38 +1,185 @@
1
+ # frozen_string_literal: true
1
2
  require 'method_object'
2
3
 
3
4
  describe MethodObject do
4
- subject do
5
- described_class.new(:value_one, :value_two) do
6
- def call
7
- value_one + value_two
5
+ it 'makes new a private class method' do
6
+ expect { subject.new }.to raise_error(NoMethodError)
7
+ end
8
+
9
+ context 'without attrs' do
10
+ subject do
11
+ Class.new(described_class) do
12
+ def call
13
+ true
14
+ end
8
15
  end
9
16
  end
10
- end
11
17
 
12
- let(:value_one) { 1 }
13
- let(:value_two) { 2 }
14
- let(:actual_result) do
15
- subject.call(value_one: value_one, value_two: value_two)
18
+ specify { expect(subject.call).to be(true) }
16
19
  end
17
20
 
18
- it 'works' do
19
- expect(actual_result).to eq 3
20
- end
21
+ context 'with attrs' do
22
+ subject do
23
+ Class.new(described_class) do
24
+ attrs(:company, :user)
21
25
 
22
- it 'uses required keyword arguments' do
23
- expect { subject.call }.to raise_error ArgumentError
24
- end
26
+ @sent_messages = []
25
27
 
26
- it 'makes new a private class method' do
27
- expect { subject.new }.to raise_error NoMethodError
28
+ def self.sent_messages
29
+ @sent_messages
30
+ end
31
+
32
+ def call
33
+ {
34
+ address: address,
35
+ respond_to_address: respond_to_missing?(:address),
36
+ company_address: company_address,
37
+ respond_to_company_address: respond_to_missing?(:company_address),
38
+ company: company,
39
+ respond_to_name: respond_to_missing?(:name),
40
+ company_name: company_name,
41
+ respond_to_company_name: respond_to_missing?(:company_name),
42
+ user: user,
43
+ user_name: user_name,
44
+ respond_to_user_name: respond_to_missing?(:user_name),
45
+ respond_to_missing: respond_to_missing?(:undefined_method),
46
+ }
47
+ end
48
+ end
49
+ end
50
+
51
+ let(:company) do
52
+ double('company', address: company_address, name: company_name)
53
+ end
54
+ let(:company_address) { '101 Minitru Lane' }
55
+ let(:company_name) { 'Periscope Data' }
56
+ let(:user) { double('user', name: user_name) }
57
+ let(:user_name) { 'Woody' }
58
+
59
+ let(:result) { subject.call(company: company, user: user) }
60
+
61
+ specify do
62
+ expect(result).to eq(
63
+ address: company_address,
64
+ respond_to_address: true,
65
+ company_address: company_address,
66
+ respond_to_company_address: true,
67
+ company: company,
68
+ respond_to_name: false,
69
+ company_name: company_name,
70
+ respond_to_company_name: true,
71
+ user: user,
72
+ user_name: user_name,
73
+ respond_to_user_name: true,
74
+ respond_to_missing: false,
75
+ )
76
+ end
77
+
78
+ it 'uses required keyword arguments' do
79
+ expect { subject.call }.to raise_error(ArgumentError)
80
+ end
81
+
82
+ context 'with ambiguous method call' do
83
+ subject do
84
+ Class.new(described_class) do
85
+ attrs(:company, :user)
86
+
87
+ def call
88
+ name
89
+ end
90
+ end
91
+ end
92
+
93
+ specify do
94
+ expect { result }.to raise_error(
95
+ MethodObject::AmbigousMethodError,
96
+ a_string_including('company.name, user.name'),
97
+ )
98
+ end
99
+ end
100
+
101
+ context 'ambigous method call due to delegation' do
102
+ subject do
103
+ Class.new(described_class) do
104
+ attrs(:company, :user)
105
+
106
+ def call
107
+ company_address
108
+ end
109
+ end
110
+ end
111
+
112
+ let(:user) { double('user', company_address: nil) }
113
+
114
+ specify do
115
+ expect { result }.to raise_error(
116
+ MethodObject::AmbigousMethodError,
117
+ a_string_including('user.company_address, company.address'),
118
+ )
119
+ end
120
+ end
121
+
122
+ describe 'respecting method privacy' do
123
+ let(:subject) do
124
+ Class.new(described_class) do
125
+ attrs(:diary)
126
+
127
+ def call
128
+ diary_contents
129
+ end
130
+ end
131
+ end
132
+ let(:diary) do
133
+ Module.new do
134
+ def self.contents; end
135
+
136
+ private_class_method(:contents)
137
+ end
138
+ end
139
+
140
+ specify do
141
+ expect { subject.call(diary: diary) }.to raise_error(StandardError)
142
+ end
143
+ end
144
+
145
+ describe '"memoizes" method calls' do
146
+ subject do
147
+ Class.new(described_class) do
148
+ attrs(:company)
149
+ @sent_messages = []
150
+
151
+ def self.sent_messages
152
+ @sent_messages
153
+ end
154
+
155
+ def call
156
+ [name, name]
157
+ end
158
+
159
+ # Ensure it defines resolved methods
160
+ def method_missing(method, *_args)
161
+ if self.class.sent_messages.include?(method)
162
+ raise 'method not memoized'
163
+ end
164
+ self.class.sent_messages << method
165
+ super
166
+ end
167
+ end
168
+ end
169
+
170
+ specify do
171
+ expect(subject.call(company: company)).to eq([company_name, company_name])
172
+ end
173
+ end
28
174
  end
29
175
 
30
176
  context 'without a provided instance call method' do
31
- subject { described_class.new(:value_one) {} }
177
+ subject do
178
+ Class.new(described_class) { attrs(:user_1_age) }
179
+ end
32
180
 
33
- it 'raises an error' do
34
- expect { subject.call(value_one: value_one) }
35
- .to raise_error NotImplementedError
181
+ specify do
182
+ expect { subject.call(user_1_age: user_1_age) }.to raise_error(NameError)
36
183
  end
37
184
  end
38
185
  end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # This file was generated by the `rspec --init` command. Conventionally, all
2
3
  # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
4
  # Require this file using `require "spec_helper"` to ensure that it is only
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: method_man
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clay Shentrup
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-05 00:00:00.000000000 Z
11
+ date: 2017-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,7 +52,7 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.2'
55
- description: Provides a MethodObject class which implements Kent Beck's "method object"
55
+ description: Provides a MethodObject class which implements KentBeck's "method object"
56
56
  pattern.
57
57
  email:
58
58
  - cshentrup@gmail.com
@@ -62,7 +62,9 @@ extra_rdoc_files: []
62
62
  files:
63
63
  - ".gitignore"
64
64
  - ".rspec"
65
+ - ".rubocop.yml"
65
66
  - ".ruby-version"
67
+ - CHANGELOG.md
66
68
  - Gemfile
67
69
  - LICENSE
68
70
  - LICENSE.txt
@@ -93,10 +95,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
95
  version: '0'
94
96
  requirements: []
95
97
  rubyforge_project:
96
- rubygems_version: 2.4.5
98
+ rubygems_version: 2.6.8
97
99
  signing_key:
98
100
  specification_version: 4
99
- summary: Provides a MethodObject class which implements Kent Beck's "method object"
101
+ summary: Provides a MethodObject class which implements KentBeck's "method object"
100
102
  pattern.
101
103
  test_files:
102
104
  - spec/method_man_spec.rb