duck_typer 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 06033bd68c3648b66718874887865c212abf0da7597d8904d511551fc2e651da
4
+ data.tar.gz: 18a800f2632fe5233946b4220df58a0728a5443d63881fa08d8f1296c4455e48
5
+ SHA512:
6
+ metadata.gz: 99787de2400a1d21667f64c981e6ad1047c866a00645ec1f2b8534c3d1a692a1204a8e6f6d422c45caca61ef17123aa814a82f046ea5848c62a6b926cf35027c
7
+ data.tar.gz: 9034b7b8e3df7342e51ec9bbf1cb8cfc58d0f1f0d9d64839c53545585a2d5dfa5af0ee7c6d6cfc6fac613015ce8dfc9dbb9e5579d68ff1b607414fd608a46dbc
data/.standard.yml ADDED
@@ -0,0 +1,5 @@
1
+ ignore:
2
+ - "test/**/*":
3
+ - Style/Semicolon
4
+ - "spec/**/*":
5
+ - Style/Semicolon
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,63 @@
1
+ # Contributing
2
+
3
+ [Bug reports][bugs] and [pull requests][prs] are welcome on GitHub
4
+ at https://github.com/thoughtbot/duck_typer.
5
+
6
+ [bugs]: https://github.com/thoughtbot/duck_typer/issues/new
7
+ [prs]: https://github.com/thoughtbot/duck_typer/pulls
8
+
9
+ Please create a [new discussion][discussion] if you want to share
10
+ ideas for new features.
11
+
12
+ [discussion]: https://github.com/thoughtbot/duck_typer/discussions/new?category=ideas
13
+
14
+ We love contributions from everyone.
15
+ By participating in this project,
16
+ you agree to abide by the thoughtbot [code of conduct].
17
+
18
+ [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct
19
+
20
+ We expect everyone to follow the code of conduct
21
+ anywhere in thoughtbot's project codebases,
22
+ issue trackers, chatrooms, and mailing lists.
23
+
24
+ ## Contributing Code
25
+
26
+ Fork the repo.
27
+
28
+ Run the setup script.
29
+
30
+ ```
31
+ ./bin/setup
32
+ ```
33
+
34
+ Make sure everything passes:
35
+
36
+ ```
37
+ bundle exec rake test
38
+ bundle exec standardrb
39
+ ```
40
+
41
+ Make your change, with new passing tests.
42
+
43
+ Push to your fork. Write a [good commit message][commit]. Submit a
44
+ pull request.
45
+
46
+ [commit]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
47
+
48
+ Others will give constructive feedback.
49
+ This is a time for discussion and improvements,
50
+ and making the necessary changes will be required before we can
51
+ merge the contribution.
52
+
53
+ ## Publishing to RubyGems
54
+
55
+ When the gem is ready to be shared as a formal release, it can be
56
+ [published][published] to RubyGems.
57
+
58
+ [published]: https://guides.rubyonrails.org/plugins.html#publishing-your-gem
59
+
60
+ 1. Bump the version number in `DuckTyper::VERSION`
61
+ 2. Run `bundle exec rake build`
62
+ 3. Run `bundle exec rake install`
63
+ 4. Run `bundle exec rake release`
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) thoughtbot, inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # DuckTyper
2
+
3
+ [![CI](https://github.com/thoughtbot/duck_typer/actions/workflows/ci.yml/badge.svg)](https://github.com/thoughtbot/duck_typer/actions/workflows/ci.yml)
4
+
5
+ DuckTyper enforces duck-typed interfaces in Ruby by comparing the
6
+ public method signatures of classes, surfacing mismatches through
7
+ your test suite.
8
+
9
+ ## Why DuckTyper?
10
+
11
+ Ruby is a duck-typed language. When multiple classes play the same
12
+ role, what matters is not what they _are_, but what they _do_ — the
13
+ methods they respond to and the signatures they expose. No base
14
+ class required. No type annotations. No interface declarations.
15
+
16
+ Most approaches to enforcing this kind of contract pull Ruby away
17
+ from its dynamic nature: abstract base classes that raise
18
+ `NotImplementedError`, type-checking libraries that annotate method
19
+ signatures, or inheritance hierarchies that couple unrelated
20
+ classes. These work, but they're not very Ruby.
21
+
22
+ DuckTyper takes a different approach. It compares public method
23
+ signatures directly and reports mismatches through your test suite —
24
+ the natural place to enforce design constraints in Ruby. There's
25
+ nothing to annotate and nothing to inherit from. The classes remain
26
+ independent; DuckTyper simply verifies that they're speaking the
27
+ same language. The interface itself needs no declaration — it is
28
+ the intersection of methods your classes define in common, a living
29
+ document that evolves naturally.
30
+
31
+ It's also useful during active development. When an interface
32
+ evolves, implementations can easily fall out of sync. DuckTyper
33
+ catches that immediately and reports clear, precise error messages
34
+ showing exactly which signatures diverged — keeping your classes
35
+ aligned as the design changes.
36
+
37
+ ## Installation
38
+
39
+ Add to your Gemfile:
40
+
41
+ ```ruby
42
+ gem "duck_typer"
43
+ ```
44
+
45
+ Then run:
46
+
47
+ ```bash
48
+ bundle install
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ When interfaces don't match, DuckTyper reports the differing
54
+ signatures:
55
+
56
+ ```
57
+ Expected StripeProcessor and BraintreeProcessor to have compatible
58
+ method signatures, but the following signatures do not match:
59
+
60
+ StripeProcessor: charge(amount, currency:)
61
+ BraintreeProcessor: charge(amount, currency:, description:)
62
+
63
+ StripeProcessor: refund(transaction_id)
64
+ BraintreeProcessor: refund(transaction_id, amount)
65
+ ```
66
+
67
+ ### Minitest
68
+
69
+ Require the Minitest integration and include the module in your
70
+ test class:
71
+
72
+ ```ruby
73
+ require "duck_typer/minitest"
74
+
75
+ class PaymentProcessorTest < Minitest::Test
76
+ include DuckTyper::Minitest
77
+ end
78
+ ```
79
+
80
+ To make `assert_interface_matches` available across all tests,
81
+ require the integration in `test_helper.rb` and include the module
82
+ in your base test class:
83
+
84
+ ```ruby
85
+ # In test_helper.rb
86
+ require "duck_typer/minitest"
87
+
88
+ class ActiveSupport::TestCase
89
+ include DuckTyper::Minitest
90
+ end
91
+ ```
92
+
93
+ If you're not using Rails, include it in `Minitest::Test` directly:
94
+
95
+ ```ruby
96
+ class Minitest::Test
97
+ include DuckTyper::Minitest
98
+ end
99
+ ```
100
+
101
+ Then use `assert_interface_matches` to assert that a list of
102
+ classes share compatible interfaces:
103
+
104
+ ```ruby
105
+ def test_payment_processors_have_compatible_interfaces
106
+ assert_interface_matches [StripeProcessor, PaypalProcessor, BraintreeProcessor]
107
+ end
108
+ ```
109
+
110
+ The same `type:` and `methods:` options are supported:
111
+
112
+ ```ruby
113
+ assert_interface_matches [StripeProcessor, PaypalProcessor],
114
+ type: :class_methods,
115
+ methods: %i[charge refund]
116
+ ```
117
+
118
+ ### RSpec
119
+
120
+ Require the RSpec integration in your `spec_helper.rb`:
121
+
122
+ ```ruby
123
+ require "duck_typer/rspec"
124
+ ```
125
+
126
+ #### Matcher
127
+
128
+ Use `have_matching_interfaces` to assert that a list of classes
129
+ share compatible interfaces:
130
+
131
+ ```ruby
132
+ RSpec.describe "payment processors" do
133
+ it "have compatible interfaces" do
134
+ expect([StripeProcessor, PaypalProcessor, BraintreeProcessor]).to have_matching_interfaces
135
+ end
136
+ end
137
+ ```
138
+
139
+ For class-level interfaces, pass `type: :class_methods`:
140
+
141
+ ```ruby
142
+ expect([StripeProcessor, PaypalProcessor]).to have_matching_interfaces(type: :class_methods)
143
+ ```
144
+
145
+ To check only a subset of methods, use `methods:`:
146
+
147
+ ```ruby
148
+ expect([StripeProcessor, PaypalProcessor]).to have_matching_interfaces(methods: %i[charge refund])
149
+ ```
150
+
151
+ #### Shared example
152
+
153
+ If you prefer shared examples, register one in `spec_helper.rb`
154
+ by calling:
155
+
156
+ ```ruby
157
+ DuckTyper::RSpec.define_shared_example
158
+ ```
159
+
160
+ This registers a shared example named `"an interface"`. To avoid
161
+ conflicts with an existing shared example of the same name, pass
162
+ a custom name:
163
+
164
+ ```ruby
165
+ DuckTyper::RSpec.define_shared_example("a compatible interface")
166
+ ```
167
+
168
+ Then use it in your specs:
169
+
170
+ ```ruby
171
+ RSpec.describe "payment processors" do
172
+ it_behaves_like "an interface", [StripeProcessor, PaypalProcessor, BraintreeProcessor]
173
+ end
174
+ ```
175
+
176
+ The same `type:` and `methods:` options are supported:
177
+
178
+ ```ruby
179
+ it_behaves_like "an interface", [StripeProcessor, PaypalProcessor],
180
+ type: :class_methods,
181
+ methods: %i[charge refund]
182
+ ```
183
+
184
+ ## Development
185
+
186
+ After checking out the repo, run `bin/setup` to install
187
+ dependencies. You can also run `bin/console` for an interactive
188
+ prompt that will allow you to experiment.
189
+
190
+ To install this gem onto your local machine, run
191
+ `bundle exec rake install`. To release a new version, update the
192
+ version number in `version.rb`, and then run
193
+ `bundle exec rake release`, which will create a git tag for the
194
+ version, push git commits and the created tag, and push the `.gem`
195
+ file to [rubygems.org](https://rubygems.org).
196
+
197
+ ## Contributing
198
+
199
+ See the [CONTRIBUTING] document.
200
+ Thank you, [contributors]!
201
+
202
+ [CONTRIBUTING]: CONTRIBUTING.md
203
+ [contributors]: https://github.com/thoughtbot/duck_typer/graphs/contributors
204
+
205
+ ## License
206
+
207
+ DuckTyper is Copyright (c) thoughtbot, inc.
208
+ It is free software, and may be redistributed
209
+ under the terms specified in the [LICENSE] file.
210
+
211
+ [LICENSE]: /LICENSE
212
+
213
+ ## About thoughtbot
214
+
215
+ ![thoughtbot](https://thoughtbot.com/thoughtbot-logo-for-readmes.svg)
216
+
217
+ This repo is maintained and funded by thoughtbot, inc.
218
+ The names and logos for thoughtbot are trademarks of thoughtbot, inc.
219
+
220
+ We love open source software!
221
+ See [our other projects][community].
222
+ We are [available for hire][hire].
223
+
224
+ [community]: https://thoughtbot.com/community?utm_source=github
225
+ [hire]: https://thoughtbot.com/hire-us?utm_source=github
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+ require "rspec/core/rake_task"
6
+
7
+ Rake::TestTask.new(:minitest) do |t|
8
+ t.pattern = "test/**/*_test.rb"
9
+ end
10
+
11
+ RSpec::Core::RakeTask.new(:rspec)
12
+
13
+ task test: %i[minitest rspec]
14
+
15
+ task default: %i[]
@@ -0,0 +1,17 @@
1
+ module DuckTyper
2
+ # Runs interface checks across all consecutive pairs of classes in a list.
3
+ class BulkInterfaceChecker
4
+ def initialize(objects, type: :instance_methods, partial_interface_methods: nil)
5
+ @objects = objects
6
+ @checker = InterfaceChecker.new(type:, partial_interface_methods:)
7
+ end
8
+
9
+ def call(&block)
10
+ @objects.each_cons(2).map do |left, right|
11
+ result = @checker.call(left, right)
12
+ block&.call(left, right, result)
13
+ result
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ module DuckTyper
2
+ class InterfaceChecker
3
+ class MethodInspector
4
+ def self.for(object, type)
5
+ if type == :class_methods
6
+ ClassMethodInspector
7
+ else
8
+ InstanceMethodInspector
9
+ end.new(object)
10
+ end
11
+ end
12
+
13
+ class ClassMethodInspector
14
+ def initialize(object)
15
+ @object = object
16
+ end
17
+
18
+ def public_methods
19
+ @object.public_methods - Object.methods
20
+ end
21
+
22
+ def parameters_for(method_name)
23
+ @object.method(method_name).parameters
24
+ end
25
+
26
+ def display_name_for(method_name)
27
+ "self.#{method_name}"
28
+ end
29
+ end
30
+
31
+ class InstanceMethodInspector
32
+ def initialize(object)
33
+ @object = object
34
+ end
35
+
36
+ def public_methods
37
+ @object.public_instance_methods - Object.methods
38
+ end
39
+
40
+ def parameters_for(method_name)
41
+ @object.instance_method(method_name).parameters
42
+ end
43
+
44
+ def display_name_for(method_name)
45
+ method_name
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,28 @@
1
+ module DuckTyper
2
+ class InterfaceChecker
3
+ # Normalizes method parameters to enable interface comparison. For
4
+ # example, two methods may use different names for positional
5
+ # arguments, but if the parameter types and order match, they should
6
+ # be considered equivalent. This class replaces argument names with
7
+ # sequential placeholders when appropriate, focusing the comparison on
8
+ # parameter structure rather than naming.
9
+ class ParamsNormalizer
10
+ def self.call(params)
11
+ sequential_name = ("a".."z").to_enum
12
+
13
+ params.map do |type, name|
14
+ name = next_sequential_param(sequential_name) if %i[req opt rest keyrest block].include?(type)
15
+
16
+ [type, name]
17
+ end
18
+ end
19
+
20
+ def self.next_sequential_param(enumerator)
21
+ enumerator.next
22
+ rescue StopIteration
23
+ raise TooManyParametersError, "too many positional parameters, maximum supported is 26"
24
+ end
25
+ private_class_method :next_sequential_param
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ module DuckTyper
2
+ class InterfaceChecker
3
+ class Result
4
+ attr_reader :left, :right
5
+
6
+ def initialize(left:, right:, match:, method_signatures:)
7
+ @left = left
8
+ @right = right
9
+ @match = match
10
+ @method_signatures = method_signatures
11
+ end
12
+
13
+ def match?
14
+ @match.call
15
+ end
16
+
17
+ def failure_message
18
+ <<~MSG
19
+ Expected #{@left} and #{@right} to have compatible method \
20
+ signatures, but the following signatures do not match:
21
+
22
+ #{method_signatures}
23
+ MSG
24
+ end
25
+
26
+ private
27
+
28
+ def method_signatures
29
+ @method_signatures.call
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,92 @@
1
+ require_relative "interface_checker/result"
2
+ require_relative "interface_checker/method_inspector"
3
+ require_relative "interface_checker/params_normalizer"
4
+
5
+ module DuckTyper
6
+ # Compares the public method signatures of two classes and reports mismatches.
7
+ class InterfaceChecker
8
+ TYPES = %i[instance_methods class_methods].freeze
9
+
10
+ def initialize(type: :instance_methods, partial_interface_methods: nil)
11
+ unless TYPES.include?(type)
12
+ raise ArgumentError, "Invalid type #{type.inspect}, must be one of #{TYPES}"
13
+ end
14
+
15
+ @type = type
16
+ @partial_interface_methods = partial_interface_methods
17
+ @inspectors = Hash.new { |h, k| h[k] = MethodInspector.for(k, @type) }
18
+ end
19
+
20
+ def call(left, right)
21
+ left_params = params_for_comparison(left, ParamsNormalizer)
22
+ right_params = params_for_comparison(right, ParamsNormalizer)
23
+ diff = (left_params - right_params) + (right_params - left_params)
24
+
25
+ match = -> { match?(left_params, right_params) }
26
+ method_signatures = -> { build_method_signatures(left, right, diff) }
27
+
28
+ Result.new(left:, right:, match:, method_signatures:)
29
+ end
30
+
31
+ private
32
+
33
+ def match?(left_params, right_params)
34
+ diff = (left_params - right_params) + (right_params - left_params)
35
+ diff.empty?
36
+ end
37
+
38
+ def build_method_signatures(left, right, diff)
39
+ methods = diff.map(&:first).uniq
40
+ left_params = params_for_comparison(left).to_h.slice(*methods)
41
+ right_params = params_for_comparison(right).to_h.slice(*methods)
42
+
43
+ methods.map do |method_name|
44
+ <<~DIFF
45
+ #{join_signature(left, method_name, left_params)}
46
+ #{join_signature(right, method_name, right_params)}
47
+ DIFF
48
+ end.join("\n")
49
+ end
50
+
51
+ def join_signature(object, method_name, params)
52
+ inspector = @inspectors[object]
53
+ display_name = inspector.display_name_for(method_name)
54
+
55
+ signature = if params[method_name]
56
+ "#{display_name}(#{params[method_name].join(", ")})"
57
+ else
58
+ "#{display_name} not defined"
59
+ end
60
+
61
+ "#{object}: #{signature}"
62
+ end
63
+
64
+ def method_params(inspector, method_name, object)
65
+ inspector.parameters_for(method_name)
66
+ rescue NameError
67
+ raise MethodNotFoundError, "undefined method `#{method_name}' for #{object}"
68
+ end
69
+
70
+ def params_for_comparison(object, params_processor = -> { _1 })
71
+ inspector = @inspectors[object]
72
+ methods = @partial_interface_methods || inspector.public_methods
73
+
74
+ methods.map do |method_name|
75
+ params = method_params(inspector, method_name, object)
76
+ args = params_processor.call(params).map do |type, name|
77
+ case type
78
+ when :key then "#{name}: :opt"
79
+ when :keyreq then "#{name}:"
80
+ when :keyrest then "**#{name}"
81
+ when :block then "&#{name}"
82
+ when :req then name.to_s
83
+ when :opt then "#{name} = :opt"
84
+ when :rest then "*#{name}"
85
+ end
86
+ end
87
+
88
+ [method_name, args]
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "../duck_typer"
2
+
3
+ module DuckTyper
4
+ module Minitest
5
+ def assert_interface_matches(objects, type: :instance_methods, methods: nil)
6
+ checker = BulkInterfaceChecker
7
+ .new(objects, type:, partial_interface_methods: methods)
8
+
9
+ checker.call do |_, _, result|
10
+ assert result.match?, result.failure_message
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ require_relative "../duck_typer"
2
+
3
+ RSpec::Matchers.define :have_matching_interfaces do |type: :instance_methods, methods: nil|
4
+ match do |objects|
5
+ checker = DuckTyper::BulkInterfaceChecker
6
+ .new(objects, type:, partial_interface_methods: methods)
7
+
8
+ @failures = checker.call.reject(&:match?)
9
+ @failures.empty?
10
+ end
11
+
12
+ failure_message do
13
+ @failures.map(&:failure_message).join("\n")
14
+ end
15
+ end
16
+
17
+ module DuckTyper
18
+ module RSpec
19
+ def self.define_shared_example(name = "an interface")
20
+ ::RSpec.shared_examples name do |objects, type: :instance_methods, methods: nil|
21
+ it "has compatible interfaces" do
22
+ failures = DuckTyper::BulkInterfaceChecker.new(objects, type:, partial_interface_methods: methods).call.reject(&:match?)
23
+ raise ::RSpec::Expectations::ExpectationNotMetError, failures.map(&:failure_message).join("\n") if failures.any?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DuckTyper
4
+ VERSION = "0.1.0"
5
+ end
data/lib/duck_typer.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "duck_typer/version"
4
+ require_relative "duck_typer/interface_checker"
5
+ require_relative "duck_typer/bulk_interface_checker"
6
+
7
+ # DuckTyper enforces duck-typed interfaces in Ruby by comparing the public
8
+ # method signatures of classes, surfacing mismatches through your test suite.
9
+ module DuckTyper
10
+ class MethodNotFoundError < StandardError; end
11
+ class TooManyParametersError < StandardError; end
12
+ end
@@ -0,0 +1,4 @@
1
+ module DuckTyper
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: duck_typer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Thiago A. Silva
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-03-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - thiagoaraujos@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".standard.yml"
21
+ - CONTRIBUTING.md
22
+ - LICENSE
23
+ - README.md
24
+ - Rakefile
25
+ - lib/duck_typer.rb
26
+ - lib/duck_typer/bulk_interface_checker.rb
27
+ - lib/duck_typer/interface_checker.rb
28
+ - lib/duck_typer/interface_checker/method_inspector.rb
29
+ - lib/duck_typer/interface_checker/params_normalizer.rb
30
+ - lib/duck_typer/interface_checker/result.rb
31
+ - lib/duck_typer/minitest.rb
32
+ - lib/duck_typer/rspec.rb
33
+ - lib/duck_typer/version.rb
34
+ - sig/duck_typer.rbs
35
+ homepage: https://github.com/thoughtbot/duck_typer
36
+ licenses: []
37
+ metadata:
38
+ allowed_push_host: https://rubygems.org
39
+ homepage_uri: https://github.com/thoughtbot/duck_typer
40
+ source_code_uri: https://github.com/thoughtbot/duck_typer
41
+ changelog_uri: https://github.com/thoughtbot/duck_typer/blob/main/CHANGELOG.md
42
+ rubygems_mfa_required: 'true'
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 3.1.0
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.5.22
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Enforce duck-typed interfaces in Ruby through your test suite.
62
+ test_files: []