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 +4 -4
- data/README.md +29 -2
- data/lib/duck_typer/interface_checker/params_normalizer.rb +29 -11
- data/lib/duck_typer/interface_checker/result.rb +7 -5
- data/lib/duck_typer/interface_checker.rb +35 -35
- data/lib/duck_typer/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8effd9ab824cbdb302917cfce5b6e268d19a25b4320a9e55cfc26b20de81c4e9
|
|
4
|
+
data.tar.gz: 57f892ab39e37d9a791996814fd250ee1b5235d18158458ce8c3f784b2533e7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
58
|
-
|
|
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
|
-
|
|
13
|
-
sequential_name = ("a".."z").to_enum
|
|
12
|
+
KEYWORD_TYPES = %i[key keyreq].freeze
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
class << self
|
|
15
|
+
def call(params)
|
|
16
|
+
sequential_name = ("a".."z").to_enum
|
|
17
17
|
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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:,
|
|
8
|
+
def initialize(left:, right:, match:, diff_message:)
|
|
9
9
|
@left = left
|
|
10
10
|
@right = right
|
|
11
11
|
@match = match
|
|
12
|
-
@
|
|
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
|
|
22
|
-
|
|
23
|
+
Expected #{@left} and #{@right} to implement compatible \
|
|
24
|
+
interfaces, but the following method signatures differ:
|
|
23
25
|
|
|
24
|
-
#{@
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
diff = calculate_diff(left, right)
|
|
24
|
+
match = -> { diff.empty? }
|
|
25
|
+
diff_message = -> { diff_message(left, right, diff) }
|
|
26
26
|
|
|
27
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
data/lib/duck_typer/version.rb
CHANGED