duck_typer 0.2.1 → 0.3.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
  SHA256:
3
- metadata.gz: c357093ac549ba934b47e7923d236062457e52b494588adb8a2fdef41dc3b0a9
4
- data.tar.gz: f900f3ad9409f4a8f0707b30aa88fd34ad6f330437e84fd6b3e8560d36edaaef
3
+ metadata.gz: 8effd9ab824cbdb302917cfce5b6e268d19a25b4320a9e55cfc26b20de81c4e9
4
+ data.tar.gz: 57f892ab39e37d9a791996814fd250ee1b5235d18158458ce8c3f784b2533e7b
5
5
  SHA512:
6
- metadata.gz: 86a751c5a4a61ef768bee2aa753c6c16e6e3b9e58860c2ed3ed71c0bf1dd5cf3668c3b4258caa59dc588d5b15964481cf10bdc3913da6654327654a198321de4
7
- data.tar.gz: c8a249693663d8a70cfb7af3aa8fa58d422ed19bea2626586ad6c93a6dd8ec9c99c86823cc8084c923e2922c7cd51e73413333844469bfdbe57d19b879fedc10
6
+ metadata.gz: 3647e54c41e0a190e2c5cb64bf714b7842cf58e9c1eaeacfab08e08c2eff9aa9e14256753306ed84cddc6819ef658381c3ccc5c552843dfebb057fe6c4c12965
7
+ data.tar.gz: e3f28a6a4423f580541a11fce992fde8a61c75540a8a0cddf4435fa33fa9abe7d13b42cf614540c2b0d91104eb3a7fa0595cb00443d9255dbe92f5fdacce09e2
data/README.md CHANGED
@@ -54,8 +54,8 @@ When interfaces don't match, DuckTyper reports the differing
54
54
  signatures:
55
55
 
56
56
  ```
57
- Expected StripeProcessor and BraintreeProcessor to have compatible
58
- method signatures, but the following signatures do not match:
57
+ Expected StripeProcessor and BraintreeProcessor to implement compatible
58
+ interfaces, but the following method signatures differ:
59
59
 
60
60
  StripeProcessor: charge(amount, currency:)
61
61
  BraintreeProcessor: charge(amount, currency:, description:)
@@ -190,6 +190,33 @@ it_behaves_like "an interface", [StripeProcessor, PaypalProcessor],
190
190
  methods: %i[charge refund]
191
191
  ```
192
192
 
193
+ ## Limitations
194
+
195
+ DuckTyper checks the **structure** of public method signatures
196
+ — the number of parameters, their kinds (required, optional,
197
+ keyword, rest, block), and keyword argument names. It does not
198
+ verify the following, which should be covered by your regular
199
+ test suite:
200
+
201
+ - **Parameter types.** DuckTyper only checks that both methods
202
+ declare an `amount` parameter — not what type of value it
203
+ expects. Two methods with identical signatures may still be
204
+ incompatible if they expect different types.
205
+ - **Return types.** Two methods can have identical signatures
206
+ but return completely different things.
207
+ - **Behavior.** Matching signatures are a necessary but not
208
+ sufficient condition for duck typing to work correctly at
209
+ runtime. DuckTyper catches structural drift, not semantic
210
+ divergence.
211
+
212
+ Some things are intentionally out of scope:
213
+
214
+ - **Private methods and `initialize`.** Private methods are not
215
+ part of a class's public interface — they are implementation
216
+ details and intentionally excluded. The same applies to
217
+ `initialize`: how an object is constructed is not an interface
218
+ concern.
219
+
193
220
  ## Development
194
221
 
195
222
  After checking out the repo, run `bin/setup` to install
@@ -9,22 +9,40 @@ module DuckTyper
9
9
  # sequential placeholders when appropriate, focusing the comparison on
10
10
  # parameter structure rather than naming.
11
11
  class ParamsNormalizer
12
- def self.call(params)
13
- sequential_name = ("a".."z").to_enum
12
+ KEYWORD_TYPES = %i[key keyreq].freeze
14
13
 
15
- params.map do |type, name|
16
- name = next_sequential_param(sequential_name) if %i[req opt rest keyrest block].include?(type)
14
+ class << self
15
+ def call(params)
16
+ sequential_name = ("a".."z").to_enum
17
17
 
18
- [type, name]
18
+ sort_keyword_params(params).map do |type, name|
19
+ if %i[req opt rest keyrest block].include?(type)
20
+ name = next_sequential_param(sequential_name)
21
+ end
22
+
23
+ [type, name]
24
+ end
19
25
  end
20
- end
21
26
 
22
- def self.next_sequential_param(enumerator)
23
- enumerator.next
24
- rescue StopIteration
25
- raise TooManyParametersError, "too many positional parameters, maximum supported is 26"
27
+ private
28
+
29
+ # Keyword argument order is irrelevant to a method's interface —
30
+ # m(a:, b:) and m(b:, a:) are equivalent. Sort keyword params
31
+ # alphabetically so comparison is order-independent.
32
+ def sort_keyword_params(params)
33
+ keywords, non_keywords = params.partition do |type, _|
34
+ KEYWORD_TYPES.include?(type)
35
+ end
36
+
37
+ non_keywords + keywords.sort_by { |_, name| name }
38
+ end
39
+
40
+ def next_sequential_param(enumerator)
41
+ enumerator.next
42
+ rescue StopIteration
43
+ raise TooManyParametersError, "too many positional parameters, maximum supported is 26"
44
+ end
26
45
  end
27
- private_class_method :next_sequential_param
28
46
  end
29
47
  end
30
48
  end
@@ -5,11 +5,11 @@ module DuckTyper
5
5
  class Result
6
6
  attr_reader :left, :right
7
7
 
8
- def initialize(left:, right:, match:, method_signatures:)
8
+ def initialize(left:, right:, match:, diff_message:)
9
9
  @left = left
10
10
  @right = right
11
11
  @match = match
12
- @method_signatures = method_signatures
12
+ @diff_message = diff_message
13
13
  end
14
14
 
15
15
  def match?
@@ -17,11 +17,13 @@ module DuckTyper
17
17
  end
18
18
 
19
19
  def failure_message
20
+ return if match?
21
+
20
22
  <<~MSG
21
- Expected #{@left} and #{@right} to have compatible method \
22
- signatures, but the following signatures do not match:
23
+ Expected #{@left} and #{@right} to implement compatible \
24
+ interfaces, but the following method signatures differ:
23
25
 
24
- #{@method_signatures.call}
26
+ #{@diff_message.call}
25
27
  MSG
26
28
  end
27
29
  end
@@ -20,47 +20,20 @@ module DuckTyper
20
20
  end
21
21
 
22
22
  def call(left, right)
23
- left_params = params_for_comparison(left, ParamsNormalizer)
24
- right_params = params_for_comparison(right, ParamsNormalizer)
25
- diff = (left_params - right_params) + (right_params - left_params)
23
+ diff = calculate_diff(left, right)
24
+ match = -> { diff.empty? }
25
+ diff_message = -> { diff_message(left, right, diff) }
26
26
 
27
- match = -> { match?(left_params, right_params) }
28
- method_signatures = -> { build_method_signatures(left, right, diff) }
29
-
30
- Result.new(left:, right:, match:, method_signatures:)
27
+ Result.new(left:, right:, match:, diff_message:)
31
28
  end
32
29
 
33
30
  private
34
31
 
35
- def match?(left_params, right_params)
36
- diff = (left_params - right_params) + (right_params - left_params)
37
- diff.empty?
38
- end
39
-
40
- def build_method_signatures(left, right, diff)
41
- methods = diff.map(&:first).uniq
42
- left_params = params_for_comparison(left).to_h.slice(*methods)
43
- right_params = params_for_comparison(right).to_h.slice(*methods)
44
-
45
- methods.map do |method_name|
46
- <<~DIFF
47
- #{join_signature(left, method_name, left_params)}
48
- #{join_signature(right, method_name, right_params)}
49
- DIFF
50
- end.join("\n")
51
- end
52
-
53
- def join_signature(object, method_name, params)
54
- inspector = @inspectors[object]
55
- display_name = inspector.display_name_for(method_name)
56
-
57
- signature = if params[method_name]
58
- "#{display_name}(#{params[method_name].join(", ")})"
59
- else
60
- "#{display_name} not defined"
61
- end
32
+ def calculate_diff(left, right)
33
+ left_params = params_for_comparison(left, ParamsNormalizer)
34
+ right_params = params_for_comparison(right, ParamsNormalizer)
62
35
 
63
- "#{object}: #{signature}"
36
+ (left_params - right_params) + (right_params - left_params)
64
37
  end
65
38
 
66
39
  def method_params(inspector, method_name, object)
@@ -84,11 +57,38 @@ module DuckTyper
84
57
  when :req then name.to_s
85
58
  when :opt then "#{name} = :opt"
86
59
  when :rest then "*#{name}"
60
+ when :nokey then "**nil"
87
61
  end
88
62
  end
89
63
 
90
64
  [method_name, args]
91
65
  end
92
66
  end
67
+
68
+ def diff_message(left, right, diff)
69
+ methods = diff.map(&:first).uniq
70
+ left_params = params_for_comparison(left).to_h.slice(*methods)
71
+ right_params = params_for_comparison(right).to_h.slice(*methods)
72
+
73
+ methods.map do |method_name|
74
+ <<~DIFF
75
+ #{join_signature(left, method_name, left_params)}
76
+ #{join_signature(right, method_name, right_params)}
77
+ DIFF
78
+ end.join("\n")
79
+ end
80
+
81
+ def join_signature(object, method_name, params)
82
+ inspector = @inspectors[object]
83
+ display_name = inspector.display_name_for(method_name)
84
+
85
+ signature = if params[method_name]
86
+ "#{display_name}(#{params[method_name].join(", ")})"
87
+ else
88
+ "#{display_name} not defined"
89
+ end
90
+
91
+ "#{object}: #{signature}"
92
+ end
93
93
  end
94
94
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DuckTyper
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duck_typer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thiago A. Silva