method_man 1.0.0 → 2.1.0

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.
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