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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c357093ac549ba934b47e7923d236062457e52b494588adb8a2fdef41dc3b0a9
4
- data.tar.gz: f900f3ad9409f4a8f0707b30aa88fd34ad6f330437e84fd6b3e8560d36edaaef
3
+ metadata.gz: fb7a9d41a358d3238e054a878eca9968910ee1b7917a4379022cbcc7587d0bdd
4
+ data.tar.gz: 17724ad54c3caee0ad1ae7dd46a43f431cd064a00f4f1b53995875095b8d52a9
5
5
  SHA512:
6
- metadata.gz: 86a751c5a4a61ef768bee2aa753c6c16e6e3b9e58860c2ed3ed71c0bf1dd5cf3668c3b4258caa59dc588d5b15964481cf10bdc3913da6654327654a198321de4
7
- data.tar.gz: c8a249693663d8a70cfb7af3aa8fa58d422ed19bea2626586ad6c93a6dd8ec9c99c86823cc8084c923e2922c7cd51e73413333844469bfdbe57d19b879fedc10
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 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
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
- 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.1"
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.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
@@ -1,4 +0,0 @@
1
- module DuckTyper
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end