mocktail 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5a4729eb601d9f3ed3ece213a460dd0881105a505a812c4d035c14cec252223
4
- data.tar.gz: f4b3a3352f63de7de812ce8db616b570de740f1ce7550f8677eb47ecb99305fb
3
+ metadata.gz: e719e2144c3da8a2569e2ef5e7cf2c78d054625011fcf0bdf8f98f300bdcd805
4
+ data.tar.gz: 3635b7210ac6d5a7c05c4000f100ac1e07f4c8d5de7de81586e41f3b81e11ce5
5
5
  SHA512:
6
- metadata.gz: 469b39914d0d887b7cd8d25006f5dc6f3ca8a1a3b5594f39bfae5e3c21f36f2983a30ec2849c916129e216dd9fb731f476abbdc5a41ad0f80c740d7106793a7d
7
- data.tar.gz: d7c24487a15c7ddc9b540d6c9d5c2b3f07da3599eba61eee128a79f5faa3fd5f6c4e54b7ea4318119a86737ead855f30d814a7f39e30c1e20a6f3880f0c21ce2
6
+ metadata.gz: d84b7964c5fbe14e3ef2a8a881c1c328f0f4c37cbde2d1423965016fe26f153c6801032c5aa4a7018db6d98924aecbb031e3e473992178bb9d5fe46e53ac2ea5
7
+ data.tar.gz: 384e42b818b92ace165e975dafd1482e00a46fc3be4c4abf0bb54e2ff648f29da1b338a8845be5c64e1878abf738e4061345083c0e952206380119ecb82729cf
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 0.0.3
2
+
3
+ * Implement method_missing on all mocked instance methods to print out useful
4
+ information, like the target type, the attempted call, an example method
5
+ definition that would match the call (for paint-by-numbers-like TDD), and
6
+ did_you_mean gem integration of similar method names in case it was just a
7
+ miss
8
+ * Cleans artificially-generated argument errors of gem-internal backtraces
9
+
1
10
  # 0.0.2
2
11
 
3
12
  * Drop Ruby 2.7 support. Unbeknownst to me (since I developed mocktail using
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mocktail (0.0.2)
4
+ mocktail (0.0.3)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -1,5 +1,3 @@
1
- require_relative "../share/simulates_argument_error"
2
-
3
1
  module Mocktail
4
2
  class ValidatesArguments
5
3
  def self.disable!
@@ -30,28 +28,9 @@ module Mocktail
30
28
  def validate(dry_call)
31
29
  return if self.class.disabled?
32
30
 
33
- arg_params, kwarg_params = dry_call.original_method.parameters.reject { |type, _|
34
- type == :block
35
- }.partition { |type, _|
36
- [:req, :opt, :rest].include?(type)
37
- }
38
-
39
- unless args_match?(arg_params, dry_call.args) &&
40
- kwargs_match?(kwarg_params, dry_call.kwargs)
41
- raise @simulates_argument_error.simulate(arg_params, dry_call.args, kwarg_params, dry_call.kwargs)
31
+ if (error = @simulates_argument_error.simulate(dry_call))
32
+ raise error
42
33
  end
43
34
  end
44
-
45
- private
46
-
47
- def args_match?(arg_params, args)
48
- args.size >= arg_params.count { |type, _| type == :req } &&
49
- (arg_params.any? { |type, _| type == :rest } || args.size <= arg_params.size)
50
- end
51
-
52
- def kwargs_match?(kwarg_params, kwargs)
53
- kwarg_params.select { |type, _| type == :keyreq }.all? { |_, name| kwargs.key?(name) } &&
54
- (kwarg_params.any? { |type, _| type == :keyrest } || kwargs.keys.all? { |name| kwarg_params.any? { |_, key| name == key } })
55
- end
56
35
  end
57
36
  end
@@ -2,13 +2,13 @@ module Mocktail
2
2
  class DeclaresDryClass
3
3
  def initialize
4
4
  @handles_dry_call = HandlesDryCall.new
5
+ @raises_neato_no_method_error = RaisesNeatoNoMethodError.new
5
6
  end
6
7
 
7
8
  def declare(type)
8
- type_type = type_of(type)
9
9
  instance_methods = instance_methods_on(type)
10
10
  dry_class = Class.new(Object) {
11
- include type if type_type == :module
11
+ include type if type.instance_of?(Module)
12
12
 
13
13
  def initialize(*args, **kwargs, &blk)
14
14
  end
@@ -18,15 +18,19 @@ module Mocktail
18
18
  }
19
19
  alias_method :kind_of?, :is_a?
20
20
 
21
- if type_type == :class
21
+ if type.instance_of?(Class)
22
22
  define_method :instance_of?, ->(thing) {
23
23
  type == thing
24
24
  }
25
25
  end
26
26
  }
27
27
 
28
- add_stringify_methods!(dry_class, :to_s, type, type_type, instance_methods)
29
- add_stringify_methods!(dry_class, :inspect, type, type_type, instance_methods)
28
+ # These have special implementations, but if the user defines
29
+ # any of them on the object itself, then they'll be replaced with normal
30
+ # mocked methods. YMMV
31
+ add_stringify_methods!(dry_class, :to_s, type, instance_methods)
32
+ add_stringify_methods!(dry_class, :inspect, type, instance_methods)
33
+ define_method_missing_errors!(dry_class, type, instance_methods)
30
34
 
31
35
  define_double_methods!(dry_class, type, instance_methods)
32
36
 
@@ -43,7 +47,7 @@ module Mocktail
43
47
  singleton: false,
44
48
  double: self,
45
49
  original_type: type,
46
- dry_type: self.class,
50
+ dry_type: dry_class,
47
51
  method: method,
48
52
  original_method: type.instance_method(method),
49
53
  args: args,
@@ -54,7 +58,7 @@ module Mocktail
54
58
  end
55
59
  end
56
60
 
57
- def add_stringify_methods!(dry_class, method_name, type, type_type, instance_methods)
61
+ def add_stringify_methods!(dry_class, method_name, type, instance_methods)
58
62
  dry_class.define_singleton_method method_name, -> {
59
63
  if (id_matches = super().match(/:([0-9a-fx]+)>$/))
60
64
  "#<Class #{"including module " if type.instance_of?(Module)}for mocktail of #{type.name}:#{id_matches[1]}>"
@@ -74,20 +78,41 @@ module Mocktail
74
78
  end
75
79
  end
76
80
 
77
- def type_of(type)
78
- if type.is_a?(Class)
79
- :class
80
- elsif type.is_a?(Module)
81
- :module
82
- end
81
+ def define_method_missing_errors!(dry_class, type, instance_methods)
82
+ return if instance_methods.include?(:method_missing)
83
+
84
+ raises_neato_no_method_error = @raises_neato_no_method_error
85
+ dry_class.define_method :method_missing, ->(name, *args, **kwargs, &block) {
86
+ raises_neato_no_method_error.call(
87
+ Call.new(
88
+ singleton: false,
89
+ double: self,
90
+ original_type: type,
91
+ dry_type: self.class,
92
+ method: name,
93
+ original_method: nil,
94
+ args: args,
95
+ kwargs: kwargs,
96
+ block: block
97
+ )
98
+ )
99
+ }
83
100
  end
84
101
 
85
102
  def instance_methods_on(type)
86
- type.instance_methods.reject { |m|
87
- ignored_ancestors.include?(type.instance_method(m).owner)
103
+ methods = type.instance_methods + [
104
+ (:respond_to_missing? if type.private_method_defined?(:respond_to_missing?))
105
+ ].compact
106
+
107
+ methods.reject { |m|
108
+ ignore?(type, m)
88
109
  }
89
110
  end
90
111
 
112
+ def ignore?(type, method_name)
113
+ ignored_ancestors.include?(type.instance_method(method_name).owner)
114
+ end
115
+
91
116
  def ignored_ancestors
92
117
  Object.ancestors
93
118
  end
@@ -0,0 +1,79 @@
1
+ require_relative "share/stringifies_call"
2
+ require_relative "share/creates_identifier"
3
+
4
+ module Mocktail
5
+ class RaisesNeatoNoMethodError
6
+ def initialize
7
+ @stringifies_call = StringifiesCall.new
8
+ @creates_identifier = CreatesIdentifier.new
9
+ end
10
+
11
+ def call(call)
12
+ raise NoMethodError.new <<~MSG
13
+ No method `#{call.original_type.name}##{call.method}' exists for call:
14
+
15
+ #{@stringifies_call.stringify(call, anonymous_blocks: true, always_parens: true)}
16
+
17
+ Need to define the method? Here's a sample definition:
18
+
19
+ def #{call.method}#{params(call)}
20
+ end
21
+ #{corrections(call)}
22
+ MSG
23
+ end
24
+
25
+ private
26
+
27
+ def params(call)
28
+ return if (params_lists = [
29
+ params_list(call.args),
30
+ kwparams_list(call.kwargs),
31
+ block_param(call.block)
32
+ ].compact).empty?
33
+
34
+ "(#{params_lists.join(", ")})"
35
+ end
36
+
37
+ def params_list(args)
38
+ return if args.empty?
39
+
40
+ count_repeats(args.map { |arg|
41
+ @creates_identifier.create(arg, default: "arg")
42
+ }).join(", ")
43
+ end
44
+
45
+ def kwparams_list(kwargs)
46
+ return if kwargs.empty?
47
+
48
+ kwargs.keys.map { |key| "#{key}:" }.join(", ")
49
+ end
50
+
51
+ def block_param(block)
52
+ return if block.nil?
53
+
54
+ "&blk"
55
+ end
56
+
57
+ def count_repeats(identifiers)
58
+ identifiers.map.with_index { |id, i|
59
+ if (preceding_matches = identifiers[0...i].count { |other_id| id == other_id }) > 0
60
+ "#{id}#{preceding_matches + 1}"
61
+ else
62
+ id
63
+ end
64
+ }
65
+ end
66
+
67
+ def corrections(call)
68
+ return if (corrections = DidYouMean::SpellChecker.new(dictionary: call.original_type.instance_methods).correct(call.method)).empty?
69
+
70
+ <<~MSG
71
+
72
+ There #{corrections.size == 1 ? "is" : "are"} also #{corrections.size} similar method#{"s" if corrections.size != 1} on #{call.original_type.name}.
73
+
74
+ Did you mean?
75
+ #{corrections.map { |c| " #{c}" }.join("\n")}
76
+ MSG
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,13 @@
1
+ module Mocktail
2
+ class CreatesIdentifier
3
+ def create(s, default: "identifier", max_length: 24)
4
+ id = s.to_s.downcase.gsub(/[^\w\s]/, "").gsub(/^\d+/, "")[0...max_length].strip.gsub(/\s+/, "_")
5
+
6
+ if id.empty?
7
+ default
8
+ else
9
+ id
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,18 +1,22 @@
1
1
  module Mocktail
2
2
  class StringifiesCall
3
- def stringify(call)
4
- "#{call.method}#{args_to_s(call)}#{blockify(call.block)}"
3
+ def stringify(call, anonymous_blocks: false, always_parens: false)
4
+ "#{call.method}#{args_to_s(call, parens: always_parens)}#{blockify(call.block, anonymous: anonymous_blocks)}"
5
5
  end
6
6
 
7
7
  private
8
8
 
9
- def args_to_s(call)
10
- unless (args_lists = [
9
+ def args_to_s(call, parens: true)
10
+ args_lists = [
11
11
  argify(call.args),
12
12
  kwargify(call.kwargs),
13
13
  lambdafy(call.block)
14
- ].compact).empty?
14
+ ].compact
15
+
16
+ if !args_lists.empty?
15
17
  "(#{args_lists.join(", ")})"
18
+ elsif parens
19
+ "()"
16
20
  end
17
21
  end
18
22
 
@@ -31,9 +35,14 @@ module Mocktail
31
35
  "&lambda[#{source_locationify(block)}]"
32
36
  end
33
37
 
34
- def blockify(block)
38
+ def blockify(block, anonymous:)
35
39
  return unless block && !block.lambda?
36
- " { Proc at #{source_locationify(block)} }"
40
+
41
+ if anonymous
42
+ " {…}"
43
+ else
44
+ " { Proc at #{source_locationify(block)} }"
45
+ end
37
46
  end
38
47
 
39
48
  def source_locationify(block)
@@ -0,0 +1,15 @@
1
+ module Mocktail
2
+ class CleansBacktrace
3
+ BASE_PATH = (Pathname.new(__FILE__) + "../../..").to_s
4
+
5
+ def clean(error)
6
+ raise error
7
+ rescue => e
8
+ e.tap do |e|
9
+ e.set_backtrace(e.backtrace.drop_while { |frame|
10
+ frame.start_with?(BASE_PATH)
11
+ })
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module Mocktail
2
+ class ReconcilesArgsWithParams
3
+ def reconcile(signature)
4
+ args_match?(signature.positional_params, signature.positional_args) &&
5
+ kwargs_match?(signature.keyword_params, signature.keyword_args)
6
+ end
7
+
8
+ private
9
+
10
+ def args_match?(arg_params, args)
11
+ args.size >= arg_params.required.size &&
12
+ (arg_params.rest? || args.size <= arg_params.allowed.size)
13
+ end
14
+
15
+ def kwargs_match?(kwarg_params, kwargs)
16
+ kwarg_params.required.all? { |name| kwargs.key?(name) } &&
17
+ (kwarg_params.rest? || kwargs.keys.all? { |name| kwarg_params.allowed.include?(name) })
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ module Mocktail
2
+ class RecreatesMessage
3
+ def recreate(signature)
4
+ req_args = signature.positional_params.required.size
5
+ allowed_args = signature.positional_params.allowed.size
6
+ rest_args = signature.positional_params.rest?
7
+ req_kwargs = signature.keyword_params.required
8
+
9
+ if signature.positional_args.size < req_args || (!rest_args && signature.positional_args.size > allowed_args)
10
+ expected_desc = if rest_args
11
+ "#{req_args}+"
12
+ elsif allowed_args != req_args
13
+ "#{req_args}..#{allowed_args}"
14
+ else
15
+ req_args.to_s
16
+ end
17
+
18
+ "wrong number of arguments (given #{signature.positional_args.size}, expected #{expected_desc}#{"; required keyword#{"s" if req_kwargs.size > 1}: #{req_kwargs.join(", ")}" unless req_kwargs.empty?})"
19
+
20
+ elsif !(missing_kwargs = req_kwargs.reject { |name| signature.keyword_args.key?(name) }).empty?
21
+ "missing keyword#{"s" if missing_kwargs.size > 1}: #{missing_kwargs.map { |name| name.inspect }.join(", ")}"
22
+ elsif !(unknown_kwargs = signature.keyword_args.keys.reject { |name| signature.keyword_params.all.include?(name) }).empty?
23
+ "unknown keyword#{"s" if unknown_kwargs.size > 1}: #{unknown_kwargs.map { |name| name.inspect }.join(", ")}"
24
+ else
25
+ "unknown cause (this is probably a bug in Mocktail)"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ module Mocktail
2
+ class TransformsParams
3
+ def transform(dry_call)
4
+ params = dry_call.original_method.parameters
5
+
6
+ Signature.new(
7
+ positional_params: Params.new(
8
+ all: params.select { |type, _|
9
+ [:req, :opt, :rest].include?(type)
10
+ }.map { |_, name| name },
11
+ required: params.select { |t, _| t == :req }.map { |_, n| n },
12
+ optional: params.select { |t, _| t == :opt }.map { |_, n| n },
13
+ rest: params.find { |type, _| type == :rest } & [1]
14
+ ),
15
+ positional_args: dry_call.args,
16
+
17
+ keyword_params: Params.new(
18
+ all: params.select { |type, _|
19
+ [:keyreq, :key, :keyrest].include?(type)
20
+ }.map { |_, name| name },
21
+ required: params.select { |t, _| t == :keyreq }.map { |_, n| n },
22
+ optional: params.select { |t, _| t == :key }.map { |_, n| n },
23
+ rest: params.find { |type, _| type == :keyrest } & [1]
24
+ ),
25
+ keyword_args: dry_call.kwargs,
26
+
27
+ block_param: params.find { |type, _| type == :block } & [1],
28
+ block_arg: dry_call.block
29
+ )
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ require_relative "simulates_argument_error/transforms_params"
2
+ require_relative "simulates_argument_error/reconciles_args_with_params"
3
+ require_relative "simulates_argument_error/recreates_message"
4
+ require_relative "simulates_argument_error/cleans_backtrace"
5
+ require_relative "share/stringifies_call"
6
+
7
+ module Mocktail
8
+ class SimulatesArgumentError
9
+ def initialize
10
+ @transforms_params = TransformsParams.new
11
+ @reconciles_args_with_params = ReconcilesArgsWithParams.new
12
+ @recreates_message = RecreatesMessage.new
13
+ @cleans_backtrace = CleansBacktrace.new
14
+ @stringifies_call = StringifiesCall.new
15
+ end
16
+
17
+ def simulate(dry_call)
18
+ signature = @transforms_params.transform(dry_call)
19
+
20
+ unless @reconciles_args_with_params.reconcile(signature)
21
+ @cleans_backtrace.clean(
22
+ ArgumentError.new([
23
+ @recreates_message.recreate(signature),
24
+ "[Mocktail call: `#{@stringifies_call.stringify(dry_call)}']"
25
+ ].join(" "))
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ module Mocktail
2
+ class Signature < Struct.new(
3
+ :positional_params,
4
+ :positional_args,
5
+ :keyword_params,
6
+ :keyword_args,
7
+ :block_param,
8
+ :block_arg,
9
+ keyword_init: true
10
+ )
11
+ end
12
+
13
+ class Params < Struct.new(
14
+ :all,
15
+ :required,
16
+ :optional,
17
+ :rest,
18
+ keyword_init: true
19
+ )
20
+
21
+ def initialize(**params)
22
+ super
23
+ self.all ||= []
24
+ self.required ||= []
25
+ self.optional ||= []
26
+ end
27
+
28
+ def allowed
29
+ required + optional
30
+ end
31
+
32
+ def rest?
33
+ !!rest
34
+ end
35
+ end
36
+ end
@@ -3,6 +3,7 @@ require_relative "value/call"
3
3
  require_relative "value/demo_config"
4
4
  require_relative "value/double"
5
5
  require_relative "value/matcher_registry"
6
+ require_relative "value/signature"
6
7
  require_relative "value/stubbing"
7
8
  require_relative "value/top_shelf"
8
9
  require_relative "value/type_replacement"
@@ -1,5 +1,5 @@
1
1
  require_relative "raises_verification_error/gathers_calls_of_method"
2
- require_relative "raises_verification_error/stringifies_call"
2
+ require_relative "../share/stringifies_call"
3
3
 
4
4
  module Mocktail
5
5
  class RaisesVerificationError
@@ -1,3 +1,3 @@
1
1
  module Mocktail
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
data/lib/mocktail.rb CHANGED
@@ -6,11 +6,13 @@ require_relative "mocktail/imitates_type"
6
6
  require_relative "mocktail/initializes_mocktail"
7
7
  require_relative "mocktail/matcher_presentation"
8
8
  require_relative "mocktail/matchers"
9
+ require_relative "mocktail/raises_neato_no_method_error"
9
10
  require_relative "mocktail/registers_matcher"
10
11
  require_relative "mocktail/registers_stubbing"
11
12
  require_relative "mocktail/replaces_next"
12
13
  require_relative "mocktail/replaces_type"
13
14
  require_relative "mocktail/resets_state"
15
+ require_relative "mocktail/simulates_argument_error"
14
16
  require_relative "mocktail/value"
15
17
  require_relative "mocktail/verifies_call"
16
18
  require_relative "mocktail/version"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mocktail
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-02 00:00:00.000000000 Z
11
+ date: 2021-10-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -53,6 +53,7 @@ files:
53
53
  - lib/mocktail/matchers/not.rb
54
54
  - lib/mocktail/matchers/numeric.rb
55
55
  - lib/mocktail/matchers/that.rb
56
+ - lib/mocktail/raises_neato_no_method_error.rb
56
57
  - lib/mocktail/records_demonstration.rb
57
58
  - lib/mocktail/registers_matcher.rb
58
59
  - lib/mocktail/registers_stubbing.rb
@@ -61,14 +62,21 @@ files:
61
62
  - lib/mocktail/replaces_type/redefines_new.rb
62
63
  - lib/mocktail/replaces_type/redefines_singleton_methods.rb
63
64
  - lib/mocktail/resets_state.rb
65
+ - lib/mocktail/share/creates_identifier.rb
64
66
  - lib/mocktail/share/determines_matching_calls.rb
65
- - lib/mocktail/share/simulates_argument_error.rb
67
+ - lib/mocktail/share/stringifies_call.rb
68
+ - lib/mocktail/simulates_argument_error.rb
69
+ - lib/mocktail/simulates_argument_error/cleans_backtrace.rb
70
+ - lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb
71
+ - lib/mocktail/simulates_argument_error/recreates_message.rb
72
+ - lib/mocktail/simulates_argument_error/transforms_params.rb
66
73
  - lib/mocktail/value.rb
67
74
  - lib/mocktail/value/cabinet.rb
68
75
  - lib/mocktail/value/call.rb
69
76
  - lib/mocktail/value/demo_config.rb
70
77
  - lib/mocktail/value/double.rb
71
78
  - lib/mocktail/value/matcher_registry.rb
79
+ - lib/mocktail/value/signature.rb
72
80
  - lib/mocktail/value/stubbing.rb
73
81
  - lib/mocktail/value/top_shelf.rb
74
82
  - lib/mocktail/value/type_replacement.rb
@@ -76,7 +84,6 @@ files:
76
84
  - lib/mocktail/verifies_call/finds_verifiable_calls.rb
77
85
  - lib/mocktail/verifies_call/raises_verification_error.rb
78
86
  - lib/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb
79
- - lib/mocktail/verifies_call/raises_verification_error/stringifies_call.rb
80
87
  - lib/mocktail/version.rb
81
88
  - mocktail.gemspec
82
89
  homepage: https://github.com/testdouble/mocktail
@@ -1,28 +0,0 @@
1
- module Mocktail
2
- class SimulatesArgumentError
3
- def simulate(arg_params, args, kwarg_params, kwargs)
4
- req_args = arg_params.count { |type, _| type == :req }
5
- opt_args = arg_params.count { |type, _| type == :opt }
6
- rest_args = arg_params.any? { |type, _| type == :rest }
7
- req_kwargs = kwarg_params.select { |type, _| type == :keyreq }
8
-
9
- allowed_args = req_args + opt_args
10
- msg = if args.size < req_args || (!rest_args && args.size > allowed_args)
11
- expected_desc = if rest_args
12
- "#{req_args}+"
13
- elsif allowed_args != req_args
14
- "#{req_args}..#{allowed_args}"
15
- else
16
- req_args.to_s
17
- end
18
-
19
- "wrong number of arguments (given #{args.size}, expected #{expected_desc}#{"; required keyword#{"s" if req_kwargs.size > 1}: #{req_kwargs.map { |_, name| name }.join(", ")}" unless req_kwargs.empty?})"
20
-
21
- elsif !(missing_kwargs = req_kwargs.reject { |_, name| kwargs.key?(name) }).empty?
22
- "missing keyword#{"s" if missing_kwargs.size > 1}: #{missing_kwargs.map { |_, name| name.inspect }.join(", ")}"
23
- end
24
-
25
- ArgumentError.new(msg)
26
- end
27
- end
28
- end