duck_typer 0.3.2 → 0.4.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/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +24 -0
- data/CLAUDE.md +23 -0
- data/CONTRIBUTING.md +3 -3
- data/README.md +66 -11
- data/lib/duck_typer/bulk_interface_checker.rb +4 -2
- data/lib/duck_typer/interface_checker.rb +9 -8
- data/lib/duck_typer/minitest.rb +4 -2
- data/lib/duck_typer/params_normalizer.rb +58 -27
- data/lib/duck_typer/rspec.rb +7 -4
- data/lib/duck_typer/version.rb +1 -1
- metadata +5 -4
- data/lib/duck_typer/null_params_normalizer.rb +0 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 881833f4db9f3c47049ad45e41c804a74a97db030438f43f933a388dc0f41cba
|
|
4
|
+
data.tar.gz: ad0b846adb0389c1db0cd49d9d245cb6c006c136e76ea0e7b3980a8d1fb4d3ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be49c0476b7afe2c3df16fcede0480f242cfab68c0d1d0a637bba8ac4aa6070bada1c9bbd0350c556193340d62cc2a5665b9949b1c838840127574c2761ff57a
|
|
7
|
+
data.tar.gz: e9f4543f7117f6e7e3709a153cb316ce90974a984fc302bec71dc84c78fff89a706585cffc9581a24b6f9fa679df3dd0f971a0a82b44c1b78644ff1a8948ba1e
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.1.0
|
data/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 3.1.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-03-09
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Strict mode (`strict: true`) for interface comparison: positional
|
|
7
|
+
argument names must match exactly. Available on
|
|
8
|
+
`assert_interfaces_match`, `have_matching_interfaces`, and the
|
|
9
|
+
RSpec shared example. Keyword argument names always matter
|
|
10
|
+
regardless of this setting.
|
|
11
|
+
- `assert_duck_types_match` as an alias for `assert_interfaces_match`
|
|
12
|
+
- `have_matching_duck_types` as an alias for `have_matching_interfaces`
|
|
13
|
+
- `BulkInterfaceChecker` now raises `ArgumentError` when fewer than
|
|
14
|
+
two classes are given
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- RSpec shared example was broken on Ruby 3.1 due to proc argument
|
|
18
|
+
destructuring differences between Ruby versions
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- `ParamsNormalizer` refactored into a factory (`ParamsNormalizer.for(strict:)`)
|
|
22
|
+
with extracted modules: `KeywordNormalizer`, `SequentialNormalizer`,
|
|
23
|
+
`DefaultParamsNormalizer`, `StrictParamsNormalizer`, and
|
|
24
|
+
`NullParamsNormalizer` — all consolidated in a single file
|
|
25
|
+
- CI now runs against Ruby 3.1 (minimum) and 3.4 (latest)
|
|
26
|
+
|
|
3
27
|
## [0.3.2] - 2026-03-07
|
|
4
28
|
|
|
5
29
|
### Changed
|
data/CLAUDE.md
CHANGED
|
@@ -15,6 +15,29 @@
|
|
|
15
15
|
- After releasing, delete the generated `.gem` files from the root
|
|
16
16
|
directory — they are not committed and should not linger.
|
|
17
17
|
|
|
18
|
+
## Ruby version
|
|
19
|
+
|
|
20
|
+
For now, use the minimum required Ruby version for development —
|
|
21
|
+
this ensures the gem works for everyone who meets that requirement.
|
|
22
|
+
|
|
23
|
+
When updating the minimum required Ruby version, update it in all
|
|
24
|
+
of these places:
|
|
25
|
+
|
|
26
|
+
- `.ruby-version`
|
|
27
|
+
- `.tool-versions`
|
|
28
|
+
- `duck_typer.gemspec` (`spec.required_ruby_version`)
|
|
29
|
+
- `.github/workflows/ci.yml` (`ruby-version`)
|
|
30
|
+
|
|
31
|
+
## Git
|
|
32
|
+
|
|
33
|
+
- Before staging specific files, always run `git status` to check
|
|
34
|
+
whether the staging area already has changes that would be
|
|
35
|
+
unintentionally included in the commit.
|
|
36
|
+
- After staging, double-check the staged changes match what was
|
|
37
|
+
asked before committing.
|
|
38
|
+
- Always run `bundle exec rake ci` before pushing.
|
|
39
|
+
- Always ask for confirmation before pushing.
|
|
40
|
+
|
|
18
41
|
## Commands
|
|
19
42
|
|
|
20
43
|
- `bundle exec rake test` — run Minitest and RSpec suites
|
data/CONTRIBUTING.md
CHANGED
|
@@ -34,11 +34,11 @@ Run the setup script.
|
|
|
34
34
|
Make sure everything passes:
|
|
35
35
|
|
|
36
36
|
```
|
|
37
|
-
bundle exec rake
|
|
38
|
-
bundle exec standardrb
|
|
37
|
+
bundle exec rake ci
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
Make your change, with new passing tests.
|
|
40
|
+
Make your change, with new passing tests. Before pushing, run
|
|
41
|
+
`bundle exec rake ci` again to make sure nothing is broken.
|
|
42
42
|
|
|
43
43
|
Push to your fork. Write a [good commit message][commit]. Submit a
|
|
44
44
|
pull request.
|
data/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/thoughtbot/duck_typer/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
+
<div align="center">
|
|
6
|
+
<img alt="DuckTyper mascot" src="assets/swan_mugshot.png" width="300">
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
> If it quacks like a duck, it's a duck... or is it?
|
|
10
|
+
|
|
5
11
|
DuckTyper enforces duck-typed interfaces in Ruby by comparing the
|
|
6
12
|
public method signatures of classes, surfacing mismatches through
|
|
7
13
|
your test suite.
|
|
@@ -39,7 +45,7 @@ aligned as the design changes.
|
|
|
39
45
|
Add to your Gemfile:
|
|
40
46
|
|
|
41
47
|
```ruby
|
|
42
|
-
gem "duck_typer"
|
|
48
|
+
gem "duck_typer", group: :test
|
|
43
49
|
```
|
|
44
50
|
|
|
45
51
|
Then run:
|
|
@@ -103,10 +109,17 @@ classes share compatible interfaces:
|
|
|
103
109
|
|
|
104
110
|
```ruby
|
|
105
111
|
def test_payment_processors_have_compatible_interfaces
|
|
106
|
-
assert_interfaces_match [
|
|
112
|
+
assert_interfaces_match [
|
|
113
|
+
StripeProcessor,
|
|
114
|
+
PaypalProcessor,
|
|
115
|
+
BraintreeProcessor
|
|
116
|
+
]
|
|
107
117
|
end
|
|
108
118
|
```
|
|
109
119
|
|
|
120
|
+
> If you prefer duck typing terminology, `assert_duck_types_match`
|
|
121
|
+
> is available as an alias.
|
|
122
|
+
|
|
110
123
|
By default, DuckTyper checks instance method interfaces. To check
|
|
111
124
|
class-level interfaces instead, pass `type: :class_methods`:
|
|
112
125
|
|
|
@@ -125,6 +138,19 @@ assert_interfaces_match [StripeProcessor, PaypalProcessor],
|
|
|
125
138
|
This is useful if your class implements multiple interfaces, in
|
|
126
139
|
which case you can write an assertion for each.
|
|
127
140
|
|
|
141
|
+
To enforce that positional argument names also match (strict
|
|
142
|
+
mode), pass `strict: true`:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
assert_interfaces_match [StripeProcessor, PaypalProcessor],
|
|
146
|
+
strict: true
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
By default, positional argument names are ignored — only their
|
|
150
|
+
count and kind (required, optional, rest) are compared. In strict
|
|
151
|
+
mode, names must match exactly. Keyword argument names always
|
|
152
|
+
matter regardless of this setting.
|
|
153
|
+
|
|
128
154
|
### RSpec
|
|
129
155
|
|
|
130
156
|
Require the RSpec integration in your `spec_helper.rb`:
|
|
@@ -141,21 +167,35 @@ share compatible interfaces:
|
|
|
141
167
|
```ruby
|
|
142
168
|
RSpec.describe "payment processors" do
|
|
143
169
|
it "have compatible interfaces" do
|
|
144
|
-
expect([StripeProcessor, PaypalProcessor, BraintreeProcessor])
|
|
170
|
+
expect([StripeProcessor, PaypalProcessor, BraintreeProcessor])
|
|
171
|
+
.to have_matching_interfaces
|
|
145
172
|
end
|
|
146
173
|
end
|
|
147
174
|
```
|
|
148
175
|
|
|
176
|
+
> If you prefer duck typing terminology, `have_matching_duck_types`
|
|
177
|
+
> is available as an alias.
|
|
178
|
+
|
|
149
179
|
For class-level interfaces, pass `type: :class_methods`:
|
|
150
180
|
|
|
151
181
|
```ruby
|
|
152
|
-
expect([StripeProcessor, PaypalProcessor])
|
|
182
|
+
expect([StripeProcessor, PaypalProcessor])
|
|
183
|
+
.to have_matching_interfaces(type: :class_methods)
|
|
153
184
|
```
|
|
154
185
|
|
|
155
186
|
To check only a subset of methods, use `methods:`:
|
|
156
187
|
|
|
157
188
|
```ruby
|
|
158
|
-
expect([StripeProcessor, PaypalProcessor])
|
|
189
|
+
expect([StripeProcessor, PaypalProcessor])
|
|
190
|
+
.to have_matching_interfaces(methods: %i[charge refund])
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
To enforce that positional argument names also match, pass
|
|
194
|
+
`strict: true`:
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
expect([StripeProcessor, PaypalProcessor])
|
|
198
|
+
.to have_matching_interfaces(strict: true)
|
|
159
199
|
```
|
|
160
200
|
|
|
161
201
|
#### Shared example
|
|
@@ -178,23 +218,30 @@ Then use it in your specs:
|
|
|
178
218
|
|
|
179
219
|
```ruby
|
|
180
220
|
RSpec.describe "payment processors" do
|
|
181
|
-
it_behaves_like "an interface", [
|
|
221
|
+
it_behaves_like "an interface", [
|
|
222
|
+
StripeProcessor,
|
|
223
|
+
PaypalProcessor,
|
|
224
|
+
BraintreeProcessor
|
|
225
|
+
]
|
|
182
226
|
end
|
|
183
227
|
```
|
|
184
228
|
|
|
185
|
-
The same `type
|
|
229
|
+
The same `type:`, `methods:`, and `strict:` options are supported:
|
|
186
230
|
|
|
187
231
|
```ruby
|
|
188
232
|
it_behaves_like "an interface", [StripeProcessor, PaypalProcessor],
|
|
189
233
|
type: :class_methods,
|
|
190
|
-
methods: %i[charge refund]
|
|
234
|
+
methods: %i[charge refund],
|
|
235
|
+
strict: true
|
|
191
236
|
```
|
|
192
237
|
|
|
193
238
|
## Limitations
|
|
194
239
|
|
|
195
|
-
DuckTyper checks the **structure** of public method
|
|
196
|
-
— the number of parameters, their kinds (required,
|
|
197
|
-
keyword, rest, block), and keyword argument names.
|
|
240
|
+
By default, DuckTyper checks the **structure** of public method
|
|
241
|
+
signatures — the number of parameters, their kinds (required,
|
|
242
|
+
optional, keyword, rest, block), and keyword argument names. In
|
|
243
|
+
strict mode, positional argument names are also compared. It does
|
|
244
|
+
not
|
|
198
245
|
verify the following, which should be covered by your regular
|
|
199
246
|
test suite:
|
|
200
247
|
|
|
@@ -217,6 +264,14 @@ Some things are intentionally out of scope:
|
|
|
217
264
|
`initialize`: how an object is constructed is not an interface
|
|
218
265
|
concern.
|
|
219
266
|
|
|
267
|
+
## Stability
|
|
268
|
+
|
|
269
|
+
DuckTyper is intentionally minimal. It reflects Ruby's own method
|
|
270
|
+
introspection API, which rarely changes — so the gem rarely needs
|
|
271
|
+
to either. When it does change, it will most likely be for additive reasons:
|
|
272
|
+
new API options, better error messages, or broader test framework
|
|
273
|
+
support. It is safe to depend on without worrying about churn.
|
|
274
|
+
|
|
220
275
|
## Development
|
|
221
276
|
|
|
222
277
|
After checking out the repo, run `bin/setup` to install
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
module DuckTyper
|
|
4
4
|
# Runs interface checks across all consecutive pairs of classes in a list.
|
|
5
5
|
class BulkInterfaceChecker
|
|
6
|
-
def initialize(objects, type: :instance_methods, partial_interface_methods: nil)
|
|
6
|
+
def initialize(objects, type: :instance_methods, partial_interface_methods: nil, strict: false)
|
|
7
|
+
raise ArgumentError, "more than one class is required" if objects.size < 2
|
|
8
|
+
|
|
7
9
|
@objects = objects
|
|
8
|
-
@checker = InterfaceChecker.new(type:, partial_interface_methods:)
|
|
10
|
+
@checker = InterfaceChecker.new(type:, partial_interface_methods:, strict:)
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
def call(&block)
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
require_relative "interface_checker/result"
|
|
4
4
|
require_relative "method_inspector"
|
|
5
5
|
require_relative "params_normalizer"
|
|
6
|
-
require_relative "null_params_normalizer"
|
|
7
6
|
|
|
8
7
|
module DuckTyper
|
|
9
8
|
# Compares the public method signatures of two classes and reports mismatches.
|
|
10
9
|
class InterfaceChecker
|
|
11
|
-
def initialize(type: :instance_methods, partial_interface_methods: nil)
|
|
10
|
+
def initialize(type: :instance_methods, partial_interface_methods: nil, strict: false)
|
|
12
11
|
@type = type
|
|
13
12
|
@partial_interface_methods = partial_interface_methods
|
|
13
|
+
@strict = strict
|
|
14
14
|
@inspectors = Hash.new { |h, k| h[k] = MethodInspector.for(k, @type) }
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -25,18 +25,19 @@ module DuckTyper
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
27
|
def calculate_diff(left, right)
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
normalizer = ParamsNormalizer.for(strict: @strict)
|
|
29
|
+
left_params = params_for_comparison(left, normalizer)
|
|
30
|
+
right_params = params_for_comparison(right, normalizer)
|
|
30
31
|
|
|
31
32
|
(left_params - right_params) + (right_params - left_params)
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
def params_for_comparison(object,
|
|
35
|
+
def params_for_comparison(object, params_normalizer)
|
|
35
36
|
methods = @partial_interface_methods || @inspectors[object].public_methods
|
|
36
37
|
|
|
37
38
|
methods.map do |method_name|
|
|
38
39
|
params = method_params(method_name, object)
|
|
39
|
-
args =
|
|
40
|
+
args = params_normalizer.call(params).map do |type, name|
|
|
40
41
|
case type
|
|
41
42
|
when :key then "#{name}: :opt"
|
|
42
43
|
when :keyreq then "#{name}:"
|
|
@@ -61,8 +62,8 @@ module DuckTyper
|
|
|
61
62
|
|
|
62
63
|
def diff_message(left, right, diff)
|
|
63
64
|
methods = diff.map(&:first).uniq
|
|
64
|
-
left_params = params_for_comparison(left, NullParamsNormalizer)
|
|
65
|
-
right_params = params_for_comparison(right, NullParamsNormalizer)
|
|
65
|
+
left_params = params_for_comparison(left, ParamsNormalizer::NullParamsNormalizer)
|
|
66
|
+
right_params = params_for_comparison(right, ParamsNormalizer::NullParamsNormalizer)
|
|
66
67
|
|
|
67
68
|
methods.map do |method_name|
|
|
68
69
|
<<~DIFF
|
data/lib/duck_typer/minitest.rb
CHANGED
|
@@ -4,13 +4,15 @@ require_relative "../duck_typer"
|
|
|
4
4
|
|
|
5
5
|
module DuckTyper
|
|
6
6
|
module Minitest
|
|
7
|
-
def assert_interfaces_match(objects, type: :instance_methods, methods: nil)
|
|
7
|
+
def assert_interfaces_match(objects, type: :instance_methods, methods: nil, strict: false)
|
|
8
8
|
checker = BulkInterfaceChecker
|
|
9
|
-
.new(objects, type:, partial_interface_methods: methods)
|
|
9
|
+
.new(objects, type:, partial_interface_methods: methods, strict:)
|
|
10
10
|
|
|
11
11
|
checker.call do |result|
|
|
12
12
|
assert result.match?, result.failure_message
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
alias_method :assert_duck_types_match, :assert_interfaces_match
|
|
15
17
|
end
|
|
16
18
|
end
|
|
@@ -1,46 +1,77 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module DuckTyper
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
# Factory for parameter normalization. Use ParamsNormalizer.for(strict:)
|
|
5
|
+
# to get the right normalizer for the given comparison mode.
|
|
6
|
+
module ParamsNormalizer # :nodoc:
|
|
7
|
+
def self.for(strict:)
|
|
8
|
+
strict ? StrictParamsNormalizer : DefaultParamsNormalizer
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Normalizes method parameters for default interface comparison.
|
|
12
|
+
# Sorts keywords alphabetically and replaces positional argument
|
|
13
|
+
# names with sequential placeholders.
|
|
14
|
+
module DefaultParamsNormalizer # :nodoc:
|
|
15
|
+
def self.call(params)
|
|
16
|
+
KeywordNormalizer.call(params).then { |p| SequentialNormalizer.call(p) }
|
|
16
17
|
end
|
|
18
|
+
end
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
# Normalizes method parameters for strict interface comparison,
|
|
21
|
+
# where positional argument names are significant. Sorts keywords
|
|
22
|
+
# alphabetically but preserves positional argument names.
|
|
23
|
+
module StrictParamsNormalizer # :nodoc:
|
|
24
|
+
def self.call(params)
|
|
25
|
+
KeywordNormalizer.call(params)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
# Sorts keyword argument parameters alphabetically, making keyword
|
|
30
|
+
# order irrelevant for interface comparison.
|
|
31
|
+
module KeywordNormalizer # :nodoc:
|
|
32
|
+
KEYWORD_TYPES = %i[key keyreq].freeze
|
|
33
|
+
|
|
34
|
+
def self.call(params)
|
|
35
|
+
keywords, sequentials = params.partition { |type, _| KEYWORD_TYPES.include?(type) }
|
|
24
36
|
|
|
25
37
|
sequentials + keywords.sort_by { |_, name| name }
|
|
26
38
|
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Replaces positional parameter names with sequential placeholders
|
|
42
|
+
# (a, b, c, ...), focusing comparison on structure rather than naming.
|
|
43
|
+
module SequentialNormalizer # :nodoc:
|
|
44
|
+
SEQUENTIAL_TYPES = %i[req opt rest keyrest block].freeze
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
def call(params)
|
|
48
|
+
sequential_name = ("a".."z").to_enum
|
|
27
49
|
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
params.map do |type, name|
|
|
51
|
+
if SEQUENTIAL_TYPES.include?(type)
|
|
52
|
+
name = next_sequential_param(sequential_name)
|
|
53
|
+
end
|
|
30
54
|
|
|
31
|
-
|
|
32
|
-
if SEQUENTIAL_TYPES.include?(type)
|
|
33
|
-
name = next_sequential_param(sequential_name)
|
|
55
|
+
[type, name]
|
|
34
56
|
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
35
60
|
|
|
36
|
-
|
|
61
|
+
def next_sequential_param(enumerator)
|
|
62
|
+
enumerator.next.to_sym
|
|
63
|
+
rescue StopIteration
|
|
64
|
+
raise TooManyParametersError, "too many positional parameters, maximum supported is 26"
|
|
37
65
|
end
|
|
38
66
|
end
|
|
67
|
+
end
|
|
39
68
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
69
|
+
# A no-op params processor that returns params unchanged. Used when
|
|
70
|
+
# interface comparison should preserve original parameter names rather
|
|
71
|
+
# than normalizing them.
|
|
72
|
+
module NullParamsNormalizer # :nodoc:
|
|
73
|
+
def self.call(params)
|
|
74
|
+
params
|
|
44
75
|
end
|
|
45
76
|
end
|
|
46
77
|
end
|
data/lib/duck_typer/rspec.rb
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../duck_typer"
|
|
4
4
|
|
|
5
|
-
RSpec::Matchers.define :have_matching_interfaces do |type: :instance_methods, methods: nil|
|
|
5
|
+
RSpec::Matchers.define :have_matching_interfaces do |type: :instance_methods, methods: nil, strict: false|
|
|
6
6
|
match do |objects|
|
|
7
7
|
checker = DuckTyper::BulkInterfaceChecker
|
|
8
|
-
.new(objects, type:, partial_interface_methods: methods)
|
|
8
|
+
.new(objects, type:, partial_interface_methods: methods, strict:)
|
|
9
9
|
|
|
10
10
|
@failures = checker.call.reject(&:match?)
|
|
11
11
|
@failures.empty?
|
|
@@ -16,17 +16,20 @@ RSpec::Matchers.define :have_matching_interfaces do |type: :instance_methods, me
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
RSpec::Matchers.alias_matcher :have_matching_duck_types, :have_matching_interfaces
|
|
20
|
+
|
|
19
21
|
module DuckTyper
|
|
20
22
|
module RSpec
|
|
21
23
|
def self.define_shared_example(name = "an interface")
|
|
22
|
-
::RSpec.shared_examples name do
|
|
24
|
+
::RSpec.shared_examples name do |*objects, type: :instance_methods, methods: nil, strict: false|
|
|
25
|
+
objects = objects.first
|
|
23
26
|
# We intentionally avoid reusing the have_matching_interfaces matcher
|
|
24
27
|
# here. Since this shared example is defined in gem code, RSpec filters
|
|
25
28
|
# it from its backtrace, causing the Failure/Error: line to show an
|
|
26
29
|
# internal RSpec constant instead of useful context.
|
|
27
30
|
it "has compatible interfaces" do
|
|
28
31
|
checker = DuckTyper::BulkInterfaceChecker
|
|
29
|
-
.new(objects, type:, partial_interface_methods: methods)
|
|
32
|
+
.new(objects, type:, partial_interface_methods: methods, strict:)
|
|
30
33
|
|
|
31
34
|
failures = checker.call.reject(&:match?)
|
|
32
35
|
|
data/lib/duck_typer/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: duck_typer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thiago A. Silva
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description:
|
|
14
14
|
email:
|
|
@@ -17,7 +17,9 @@ executables: []
|
|
|
17
17
|
extensions: []
|
|
18
18
|
extra_rdoc_files: []
|
|
19
19
|
files:
|
|
20
|
+
- ".ruby-version"
|
|
20
21
|
- ".standard.yml"
|
|
22
|
+
- ".tool-versions"
|
|
21
23
|
- CHANGELOG.md
|
|
22
24
|
- CLAUDE.md
|
|
23
25
|
- CONTRIBUTING.md
|
|
@@ -30,7 +32,6 @@ files:
|
|
|
30
32
|
- lib/duck_typer/interface_checker/result.rb
|
|
31
33
|
- lib/duck_typer/method_inspector.rb
|
|
32
34
|
- lib/duck_typer/minitest.rb
|
|
33
|
-
- lib/duck_typer/null_params_normalizer.rb
|
|
34
35
|
- lib/duck_typer/params_normalizer.rb
|
|
35
36
|
- lib/duck_typer/rspec.rb
|
|
36
37
|
- lib/duck_typer/version.rb
|
|
@@ -57,7 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
57
58
|
- !ruby/object:Gem::Version
|
|
58
59
|
version: '0'
|
|
59
60
|
requirements: []
|
|
60
|
-
rubygems_version: 3.
|
|
61
|
+
rubygems_version: 3.3.3
|
|
61
62
|
signing_key:
|
|
62
63
|
specification_version: 4
|
|
63
64
|
summary: Enforce duck-typed interfaces in Ruby through your test suite.
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module DuckTyper
|
|
4
|
-
# A no-op params processor that returns params unchanged. Used when
|
|
5
|
-
# interface comparison should preserve original parameter names rather
|
|
6
|
-
# than normalizing them.
|
|
7
|
-
class NullParamsNormalizer # :nodoc:
|
|
8
|
-
def self.call(params)
|
|
9
|
-
params
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|