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 +4 -4
- data/CHANGELOG.md +22 -0
- data/CLAUDE.md +56 -0
- data/lib/duck_typer/interface_checker.rb +19 -25
- data/lib/duck_typer/{interface_checker/method_inspector.rb → method_inspector.rb} +12 -8
- data/lib/duck_typer/null_params_normalizer.rb +12 -0
- data/lib/duck_typer/params_normalizer.rb +47 -0
- data/lib/duck_typer/version.rb +1 -1
- metadata +5 -3
- data/lib/duck_typer/interface_checker/params_normalizer.rb +0 -48
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 24207ce1f63f8b0871e06127e2644497f4c60314bbb5fab55f0e8a9f2b30349b
|
|
4
|
+
data.tar.gz: 8dbebe87508eb66d745af1ce8a0fd6d30865b5929b2de47e092cba9122bcab29
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 "
|
|
5
|
-
require_relative "
|
|
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
|
|
40
|
-
|
|
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(
|
|
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)
|
|
71
|
-
right_params = params_for_comparison(right)
|
|
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,
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
86
|
-
"#{display_name}(#{
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
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.3.
|
|
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
|