duck_typer 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/README.md +29 -2
- data/Rakefile +10 -0
- 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 +4 -4
- data/sig/duck_typer.rbs +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb7a9d41a358d3238e054a878eca9968910ee1b7917a4379022cbcc7587d0bdd
|
|
4
|
+
data.tar.gz: 17724ad54c3caee0ad1ae7dd46a43f431cd064a00f4f1b53995875095b8d52a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef5827a7b0adb92cc6e79c8e152435df3acd66d2905fb075c8535a5af58450a1055121be58f246bcdd8d62fc79124a931bc2904d1c7061a1224b7f19b1c26676
|
|
7
|
+
data.tar.gz: bd6f3660f2db388d41de049e90ab0dc3d8715aa83878555dd6faec700079f6fe1acae98887d19800de505c8756962868ba5b431b8ded4e61c6533df8b5a203b5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.1] - 2026-03-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- CHANGELOG
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- Add `rake ci` task combining StandardRB, Minitest, and RSpec
|
|
10
|
+
- Add `check_lockfile` task to catch `Gemfile.lock` drift
|
|
11
|
+
- Simplify CI config to a single `rake ci` step
|
|
12
|
+
- Remove empty RBS signature file
|
|
13
|
+
- Add MIT license to gemspec
|
|
14
|
+
|
|
15
|
+
## [0.3.0] - 2026-03-07
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Keyword argument order is now ignored when comparing interfaces —
|
|
19
|
+
`m(a:, b:)` and `m(b:, a:)` are treated as equivalent
|
|
20
|
+
- Support for `:nokey` parameter type (`def foo(**nil)`)
|
|
21
|
+
- `failure_message` now returns `nil` when interfaces match
|
|
22
|
+
- README Limitations section
|
|
23
|
+
- Self-referential test verifying `ClassMethodInspector` and
|
|
24
|
+
`InstanceMethodInspector` share compatible interfaces
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Improved failure message: "implement compatible interfaces" /
|
|
28
|
+
"method signatures differ"
|
|
29
|
+
- Extract `calculate_diff` private method
|
|
30
|
+
|
|
31
|
+
## [0.2.1] - 2026-03-06
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- README improvements: Minitest section moved before RSpec,
|
|
35
|
+
partial interface wording clarified
|
|
36
|
+
|
|
37
|
+
## [0.2.0] - 2026-03-06
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- Renamed `assert_interface_matches` to `assert_interfaces_match`
|
|
41
|
+
(breaking change)
|
|
42
|
+
|
|
43
|
+
## [0.1.0] - 2026-03-06
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- Initial release
|
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
|
data/Rakefile
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "rake/testtask"
|
|
5
5
|
require "rspec/core/rake_task"
|
|
6
|
+
require "standard/rake"
|
|
6
7
|
|
|
7
8
|
Rake::TestTask.new(:minitest) do |t|
|
|
8
9
|
t.pattern = "test/**/*_test.rb"
|
|
@@ -10,6 +11,15 @@ end
|
|
|
10
11
|
|
|
11
12
|
RSpec::Core::RakeTask.new(:rspec)
|
|
12
13
|
|
|
14
|
+
task :check_lockfile do
|
|
15
|
+
sh "bundle install --quiet"
|
|
16
|
+
|
|
17
|
+
unless `git diff Gemfile.lock`.empty?
|
|
18
|
+
abort "Gemfile.lock is out of date. Commit the updated lockfile."
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
13
22
|
task test: %i[minitest rspec]
|
|
23
|
+
task ci: %i[check_lockfile standard test]
|
|
14
24
|
|
|
15
25
|
task default: %i[]
|
|
@@ -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
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.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thiago A. Silva
|
|
@@ -18,6 +18,7 @@ extensions: []
|
|
|
18
18
|
extra_rdoc_files: []
|
|
19
19
|
files:
|
|
20
20
|
- ".standard.yml"
|
|
21
|
+
- CHANGELOG.md
|
|
21
22
|
- CONTRIBUTING.md
|
|
22
23
|
- LICENSE
|
|
23
24
|
- README.md
|
|
@@ -31,12 +32,11 @@ files:
|
|
|
31
32
|
- lib/duck_typer/minitest.rb
|
|
32
33
|
- lib/duck_typer/rspec.rb
|
|
33
34
|
- lib/duck_typer/version.rb
|
|
34
|
-
- sig/duck_typer.rbs
|
|
35
35
|
homepage: https://github.com/thoughtbot/duck_typer
|
|
36
|
-
licenses:
|
|
36
|
+
licenses:
|
|
37
|
+
- MIT
|
|
37
38
|
metadata:
|
|
38
39
|
allowed_push_host: https://rubygems.org
|
|
39
|
-
homepage_uri: https://github.com/thoughtbot/duck_typer
|
|
40
40
|
source_code_uri: https://github.com/thoughtbot/duck_typer
|
|
41
41
|
changelog_uri: https://github.com/thoughtbot/duck_typer/blob/main/CHANGELOG.md
|
|
42
42
|
rubygems_mfa_required: 'true'
|
data/sig/duck_typer.rbs
DELETED