duck_typer 0.3.1 → 0.3.2

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: fb7a9d41a358d3238e054a878eca9968910ee1b7917a4379022cbcc7587d0bdd
4
- data.tar.gz: 17724ad54c3caee0ad1ae7dd46a43f431cd064a00f4f1b53995875095b8d52a9
3
+ metadata.gz: 24207ce1f63f8b0871e06127e2644497f4c60314bbb5fab55f0e8a9f2b30349b
4
+ data.tar.gz: 8dbebe87508eb66d745af1ce8a0fd6d30865b5929b2de47e092cba9122bcab29
5
5
  SHA512:
6
- metadata.gz: ef5827a7b0adb92cc6e79c8e152435df3acd66d2905fb075c8535a5af58450a1055121be58f246bcdd8d62fc79124a931bc2904d1c7061a1224b7f19b1c26676
7
- data.tar.gz: bd6f3660f2db388d41de049e90ab0dc3d8715aa83878555dd6faec700079f6fe1acae98887d19800de505c8756962868ba5b431b8ded4e61c6533df8b5a203b5
6
+ metadata.gz: d97a298dd8aaa903f2a9e2ae451f530a79aa433d9734ae220f89573f88d3ea9288dd1c9feb61c981eda95b272f2fea4b0763f67374bba972cb678ab52692a03b
7
+ data.tar.gz: 75b66d83757541fdd0cc1ead317aa3e15a7fe9878e1fb57a9b348b3a6c63bce5da2e5fe8091296914ea3bf2e637446af0031806baaed208f5ba01497a479c41c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2] - 2026-03-07
4
+
5
+ ### Changed
6
+ - Move type validation and `TYPES` constant to `MethodInspector`
7
+ - Refactor `ParamsNormalizer`: extract `SEQUENTIAL_TYPES` constant
8
+ and `sequentialize_params`, reorder methods for natural reading order
9
+ - Introduce `NullParamsNormalizer` as an explicit null object,
10
+ replacing an implicit identity lambda
11
+ - Move `MethodInspector`, `ParamsNormalizer`, and `NullParamsNormalizer`
12
+ to the `DuckTyper` namespace; nest `ClassMethodInspector` and
13
+ `InstanceMethodInspector` inside `MethodInspector`
14
+ - Use `assoc` to look up method params in `join_signature`
15
+ - Inline `inspector` locals throughout `InterfaceChecker`
16
+ - Mark `ParamsNormalizer`, `NullParamsNormalizer`, and `MethodInspector`
17
+ as internal (`:nodoc:`)
18
+
19
+ ### Added
20
+ - `ParamsNormalizer` test suite
21
+ - Test that each differing method appears only once in `failure_message`
22
+ - Test that `assert_interfaces_match` passes for matching pairs even
23
+ when another class in the list mismatches
24
+
3
25
  ## [0.3.1] - 2026-03-07
4
26
 
5
27
  ### Added
data/CLAUDE.md ADDED
@@ -0,0 +1,56 @@
1
+ # CLAUDE.md
2
+
3
+ ## Release
4
+
5
+ - Before writing to `CHANGELOG.md`, pull from `main` and do a
6
+ detailed analysis of all commits since the last tag (e.g.
7
+ `git log <last-tag>..HEAD`).
8
+ - Based on the analysis, determine the next version number: bump
9
+ the patch version for internal changes or bug fixes, the minor
10
+ version for new features, and the major version for breaking
11
+ changes. Confirm the version number with the programmer before
12
+ proceeding.
13
+ - Update `CHANGELOG.md` with the new version and a summary of
14
+ changes.
15
+ - After releasing, delete the generated `.gem` files from the root
16
+ directory — they are not committed and should not linger.
17
+
18
+ ## Commands
19
+
20
+ - `bundle exec rake test` — run Minitest and RSpec suites
21
+ - `bundle exec rake minitest` — run Minitest suite only
22
+ - `bundle exec rake ci` — run tests, linting, and lockfile drift check
23
+ (always run before pushing)
24
+ - `bundle exec standardrb` — lint
25
+
26
+ ## Tests
27
+
28
+ - **Minitest is the default test suite.** Test files live in `test/`.
29
+ - **RSpec is only for testing RSpec-specific functionality.**
30
+ Spec files live in `spec/`.
31
+ - New tests belong in `test/` unless they exercise the RSpec
32
+ integration.
33
+ - When two or more classes are interchangeably used (e.g. a class
34
+ and its null object), write an interface test using the gem's own
35
+ `assert_interfaces_match` (include `DuckTyper::Minitest`).
36
+ - When testing a transformation (e.g. `ParamsNormalizer`), always
37
+ include a mixed test that exercises all cases together, in addition
38
+ to focused tests for each case.
39
+
40
+ ## Code style
41
+
42
+ - Markdown files should be kept at 70 columns.
43
+ - File paths must match the Ruby namespace: `DuckTyper::Foo` lives
44
+ in `lib/duck_typer/foo.rb`, not under a subdirectory of another
45
+ class.
46
+ - Classes are only nested inside another class when they are
47
+ intrinsically part of it (e.g. `InterfaceChecker::Result`).
48
+ Otherwise they belong at the `DuckTyper` root level.
49
+ - Methods are defined in natural reading order: callers before
50
+ callees.
51
+ - Leave a blank line before the return value in multi-line methods.
52
+ - Extract constants for inline literals used in conditionals.
53
+ - Private methods mirror each other when they share a common role
54
+ (e.g. `sort_keyword_params` / `sequentialize_params`).
55
+ - Use `params` instead of `parameters` in names (e.g. method names,
56
+ variable names).
@@ -1,19 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "interface_checker/result"
4
- require_relative "interface_checker/method_inspector"
5
- require_relative "interface_checker/params_normalizer"
4
+ require_relative "method_inspector"
5
+ require_relative "params_normalizer"
6
+ require_relative "null_params_normalizer"
6
7
 
7
8
  module DuckTyper
8
9
  # Compares the public method signatures of two classes and reports mismatches.
9
10
  class InterfaceChecker
10
- TYPES = %i[instance_methods class_methods].freeze
11
-
12
11
  def initialize(type: :instance_methods, partial_interface_methods: nil)
13
- unless TYPES.include?(type)
14
- raise ArgumentError, "Invalid type #{type.inspect}, must be one of #{TYPES}"
15
- end
16
-
17
12
  @type = type
18
13
  @partial_interface_methods = partial_interface_methods
19
14
  @inspectors = Hash.new { |h, k| h[k] = MethodInspector.for(k, @type) }
@@ -36,18 +31,11 @@ module DuckTyper
36
31
  (left_params - right_params) + (right_params - left_params)
37
32
  end
38
33
 
39
- def method_params(inspector, method_name, object)
40
- inspector.parameters_for(method_name)
41
- rescue NameError
42
- raise MethodNotFoundError, "undefined method `#{method_name}' for #{object}"
43
- end
44
-
45
- def params_for_comparison(object, params_processor = -> { _1 })
46
- inspector = @inspectors[object]
47
- methods = @partial_interface_methods || inspector.public_methods
34
+ def params_for_comparison(object, params_processor)
35
+ methods = @partial_interface_methods || @inspectors[object].public_methods
48
36
 
49
37
  methods.map do |method_name|
50
- params = method_params(inspector, method_name, object)
38
+ params = method_params(method_name, object)
51
39
  args = params_processor.call(params).map do |type, name|
52
40
  case type
53
41
  when :key then "#{name}: :opt"
@@ -65,10 +53,16 @@ module DuckTyper
65
53
  end
66
54
  end
67
55
 
56
+ def method_params(method_name, object)
57
+ @inspectors[object].parameters_for(method_name)
58
+ rescue NameError
59
+ raise MethodNotFoundError, "undefined method `#{method_name}' for #{object}"
60
+ end
61
+
68
62
  def diff_message(left, right, diff)
69
63
  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)
64
+ left_params = params_for_comparison(left, NullParamsNormalizer)
65
+ right_params = params_for_comparison(right, NullParamsNormalizer)
72
66
 
73
67
  methods.map do |method_name|
74
68
  <<~DIFF
@@ -78,12 +72,12 @@ module DuckTyper
78
72
  end.join("\n")
79
73
  end
80
74
 
81
- def join_signature(object, method_name, params)
82
- inspector = @inspectors[object]
83
- display_name = inspector.display_name_for(method_name)
75
+ def join_signature(object, method_name, all_params)
76
+ display_name = @inspectors[object].display_name_for(method_name)
77
+ method_params = all_params.assoc(method_name)&.last
84
78
 
85
- signature = if params[method_name]
86
- "#{display_name}(#{params[method_name].join(", ")})"
79
+ signature = if method_params
80
+ "#{display_name}(#{method_params.join(", ")})"
87
81
  else
88
82
  "#{display_name} not defined"
89
83
  end
@@ -1,15 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DuckTyper
4
- class InterfaceChecker
5
- class MethodInspector
6
- def self.for(object, type)
7
- if type == :class_methods
8
- ClassMethodInspector
9
- else
10
- InstanceMethodInspector
11
- end.new(object)
4
+ module MethodInspector # :nodoc:
5
+ TYPES = %i[instance_methods class_methods].freeze
6
+
7
+ def self.for(object, type)
8
+ unless TYPES.include?(type)
9
+ raise ArgumentError, "Invalid type #{type.inspect}, must be one of #{TYPES}"
12
10
  end
11
+
12
+ if type == :class_methods
13
+ ClassMethodInspector
14
+ else
15
+ InstanceMethodInspector
16
+ end.new(object)
13
17
  end
14
18
 
15
19
  class ClassMethodInspector
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuckTyper
4
+ # Normalizes method parameters to enable interface comparison.
5
+ # Keyword argument order is irrelevant — m(a:, b:) and m(b:, a:)
6
+ # are equivalent — so keywords are sorted alphabetically. Positional
7
+ # argument names are also replaced with sequential placeholders,
8
+ # focusing the comparison on parameter structure rather than naming.
9
+ class ParamsNormalizer # :nodoc:
10
+ KEYWORD_TYPES = %i[key keyreq].freeze
11
+ SEQUENTIAL_TYPES = %i[req opt rest keyrest block].freeze
12
+
13
+ class << self
14
+ def call(params)
15
+ sort_keyword_params(params).then { sequentialize_params(_1) }
16
+ end
17
+
18
+ private
19
+
20
+ def sort_keyword_params(params)
21
+ keywords, sequentials = params.partition do |type, _|
22
+ KEYWORD_TYPES.include?(type)
23
+ end
24
+
25
+ sequentials + keywords.sort_by { |_, name| name }
26
+ end
27
+
28
+ def sequentialize_params(params)
29
+ sequential_name = ("a".."z").to_enum
30
+
31
+ params.map do |type, name|
32
+ if SEQUENTIAL_TYPES.include?(type)
33
+ name = next_sequential_param(sequential_name)
34
+ end
35
+
36
+ [type, name]
37
+ end
38
+ end
39
+
40
+ def next_sequential_param(enumerator)
41
+ enumerator.next.to_sym
42
+ rescue StopIteration
43
+ raise TooManyParametersError, "too many positional parameters, maximum supported is 26"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DuckTyper
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.2"
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.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thiago A. Silva
@@ -19,6 +19,7 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - ".standard.yml"
21
21
  - CHANGELOG.md
22
+ - CLAUDE.md
22
23
  - CONTRIBUTING.md
23
24
  - LICENSE
24
25
  - README.md
@@ -26,10 +27,11 @@ files:
26
27
  - lib/duck_typer.rb
27
28
  - lib/duck_typer/bulk_interface_checker.rb
28
29
  - lib/duck_typer/interface_checker.rb
29
- - lib/duck_typer/interface_checker/method_inspector.rb
30
- - lib/duck_typer/interface_checker/params_normalizer.rb
31
30
  - lib/duck_typer/interface_checker/result.rb
31
+ - lib/duck_typer/method_inspector.rb
32
32
  - lib/duck_typer/minitest.rb
33
+ - lib/duck_typer/null_params_normalizer.rb
34
+ - lib/duck_typer/params_normalizer.rb
33
35
  - lib/duck_typer/rspec.rb
34
36
  - lib/duck_typer/version.rb
35
37
  homepage: https://github.com/thoughtbot/duck_typer
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DuckTyper
4
- class InterfaceChecker
5
- # Normalizes method parameters to enable interface comparison. For
6
- # example, two methods may use different names for positional
7
- # arguments, but if the parameter types and order match, they should
8
- # be considered equivalent. This class replaces argument names with
9
- # sequential placeholders when appropriate, focusing the comparison on
10
- # parameter structure rather than naming.
11
- class ParamsNormalizer
12
- KEYWORD_TYPES = %i[key keyreq].freeze
13
-
14
- class << self
15
- def call(params)
16
- sequential_name = ("a".."z").to_enum
17
-
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
25
- end
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
45
- end
46
- end
47
- end
48
- end