view_component_storybook 0.8.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -128
  3. data/app/controllers/view_component/storybook/stories_controller.rb +10 -11
  4. data/app/views/view_component/storybook/stories/show.html.erb +8 -1
  5. data/config/locales/en.yml +32 -0
  6. data/lib/view_component/storybook/content_concern.rb +42 -0
  7. data/lib/view_component/storybook/controls/base_options_config.rb +41 -0
  8. data/lib/view_component/storybook/controls/boolean_config.rb +7 -6
  9. data/lib/view_component/storybook/controls/color_config.rb +4 -5
  10. data/lib/view_component/storybook/controls/control_config.rb +24 -36
  11. data/lib/view_component/storybook/controls/controls_helpers.rb +76 -0
  12. data/lib/view_component/storybook/controls/custom_config.rb +52 -0
  13. data/lib/view_component/storybook/controls/date_config.rb +14 -11
  14. data/lib/view_component/storybook/controls/multi_options_config.rb +46 -0
  15. data/lib/view_component/storybook/controls/number_config.rb +9 -9
  16. data/lib/view_component/storybook/controls/object_config.rb +13 -5
  17. data/lib/view_component/storybook/controls/options_config.rb +17 -30
  18. data/lib/view_component/storybook/controls/simple_control_config.rb +48 -0
  19. data/lib/view_component/storybook/controls/text_config.rb +1 -1
  20. data/lib/view_component/storybook/controls.rb +5 -1
  21. data/lib/view_component/storybook/dsl/{controls_dsl.rb → legacy_controls_dsl.rb} +18 -21
  22. data/lib/view_component/storybook/dsl.rb +1 -2
  23. data/lib/view_component/storybook/engine.rb +13 -2
  24. data/lib/view_component/storybook/method_args/component_constructor_args.rb +23 -0
  25. data/lib/view_component/storybook/method_args/control_method_args.rb +91 -0
  26. data/lib/view_component/storybook/method_args/dry_initializer_component_constructor_args.rb +45 -0
  27. data/lib/view_component/storybook/method_args/method_args.rb +52 -0
  28. data/lib/view_component/storybook/method_args/method_parameters_names.rb +97 -0
  29. data/lib/view_component/storybook/method_args.rb +17 -0
  30. data/lib/view_component/storybook/slots/slot.rb +24 -0
  31. data/lib/view_component/storybook/slots/slot_config.rb +79 -0
  32. data/lib/view_component/storybook/slots.rb +14 -0
  33. data/lib/view_component/storybook/stories.rb +60 -10
  34. data/lib/view_component/storybook/story.rb +18 -0
  35. data/lib/view_component/storybook/story_config.rb +143 -15
  36. data/lib/view_component/storybook/tasks/write_stories_json.rake +6 -0
  37. data/lib/view_component/storybook/version.rb +1 -1
  38. data/lib/view_component/storybook.rb +25 -0
  39. metadata +64 -20
  40. data/lib/view_component/storybook/controls/array_config.rb +0 -36
  41. data/lib/view_component/storybook/dsl/story_dsl.rb +0 -39
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Controls
6
+ class MultiOptionsConfig < BaseOptionsConfig
7
+ TYPES = %i[multi-select check inline-check].freeze
8
+
9
+ validates :type, inclusion: { in: TYPES }, unless: -> { type.nil? }
10
+ validate :validate_default_value, unless: -> { options.nil? || default_value.nil? }
11
+
12
+ def initialize(type, options, default_value, labels: nil, param: nil, name: nil)
13
+ super(type, options, Array.wrap(default_value), labels: labels, param: param, name: name)
14
+ end
15
+
16
+ def value_from_params(params)
17
+ params_value = super(params)
18
+
19
+ if params_value.is_a?(String)
20
+ params_value = params_value.split(',')
21
+ params_value = params_value.map(&:to_sym) if symbol_values
22
+ end
23
+ params_value
24
+ end
25
+
26
+ def to_csf_params
27
+ super.deep_merge(argTypes: { param => { options: options } })
28
+ end
29
+
30
+ private
31
+
32
+ def csf_control_params
33
+ labels.nil? ? super : super.merge(labels: labels)
34
+ end
35
+
36
+ def symbol_values
37
+ @symbol_values ||= default_value.first.is_a?(Symbol)
38
+ end
39
+
40
+ def validate_default_value
41
+ errors.add(:default_value, :inclusion) unless default_value.to_set <= options.to_set
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module Storybook
5
5
  module Controls
6
- class NumberConfig < ControlConfig
6
+ class NumberConfig < SimpleControlConfig
7
7
  TYPES = %i[number range].freeze
8
8
 
9
9
  attr_reader :type, :min, :max, :step
@@ -11,27 +11,27 @@ module ViewComponent
11
11
  validates :type, presence: true
12
12
  validates :type, inclusion: { in: TYPES }, unless: -> { type.nil? }
13
13
 
14
- def initialize(type, component, param, value, min: nil, max: nil, step: nil, name: nil)
15
- super(component, param, value, name: name)
14
+ def initialize(type, default_value, min: nil, max: nil, step: nil, param: nil, name: nil)
15
+ super(default_value, param: param, name: name)
16
16
  @type = type
17
17
  @min = min
18
18
  @max = max
19
19
  @step = step
20
20
  end
21
21
 
22
- def value_from_param(param)
23
- if param.is_a?(String) && param.present?
24
- (param.to_f % 1) > 0 ? param.to_f : param.to_i
22
+ def value_from_params(params)
23
+ params_value = super(params)
24
+ if params_value.is_a?(String) && params_value.present?
25
+ (params_value.to_f % 1) > 0 ? params_value.to_f : params_value.to_i
25
26
  else
26
- super(param)
27
+ params_value
27
28
  end
28
29
  end
29
30
 
30
31
  private
31
32
 
32
33
  def csf_control_params
33
- params = super
34
- params.merge(min: min, max: max, step: step).compact
34
+ super.merge(min: min, max: max, step: step).compact
35
35
  end
36
36
  end
37
37
  end
@@ -3,16 +3,24 @@
3
3
  module ViewComponent
4
4
  module Storybook
5
5
  module Controls
6
- class ObjectConfig < ControlConfig
6
+ class ObjectConfig < SimpleControlConfig
7
7
  def type
8
8
  :object
9
9
  end
10
10
 
11
- def value_from_param(param)
12
- if param.is_a?(String)
13
- JSON.parse(param).deep_symbolize_keys
11
+ def value_from_params(params)
12
+ params_value = super(params)
13
+ if params_value.is_a?(String)
14
+ parsed_json = JSON.parse(params_value)
15
+ if parsed_json.is_a?(Array)
16
+ parsed_json.map do |item|
17
+ item.is_a?(Hash) ? item.deep_symbolize_keys : item
18
+ end
19
+ else
20
+ parsed_json.deep_symbolize_keys
21
+ end
14
22
  else
15
- super(param)
23
+ params_value
16
24
  end
17
25
  end
18
26
  end
@@ -3,46 +3,33 @@
3
3
  module ViewComponent
4
4
  module Storybook
5
5
  module Controls
6
- class OptionsConfig < ControlConfig
7
- class << self
8
- # support the options being a Hash or an Array. Storybook supports either.
9
- def inclusion_in(config)
10
- case config.options
11
- when Hash
12
- config.options.values
13
- when Array
14
- config.options
15
- end
16
- end
17
- end
18
-
19
- TYPES = %i[select multi-select radio inline-radio check inline-check].freeze
6
+ class OptionsConfig < BaseOptionsConfig
7
+ TYPES = %i[select radio inline-radio].freeze
20
8
 
21
- attr_reader :type, :options, :symbol_value
22
-
23
- validates :type, :options, presence: true
24
9
  validates :type, inclusion: { in: TYPES }, unless: -> { type.nil? }
25
- validates :value, inclusion: { in: method(:inclusion_in) }, unless: -> { options.nil? || value.nil? }
26
-
27
- def initialize(type, component, param, options, default_value, name: nil)
28
- super(component, param, default_value, name: name)
29
- @type = type
30
- @options = options
31
- @symbol_value = default_value.is_a?(Symbol)
32
- end
10
+ validates :default_value, inclusion: { in: ->(config) { config.options } }, unless: -> { options.nil? || default_value.nil? }
33
11
 
34
- def value_from_param(param)
35
- if param.is_a?(String) && symbol_value
36
- param.to_sym
12
+ def value_from_params(params)
13
+ params_value = super(params)
14
+ if params_value.is_a?(String) && symbol_value
15
+ params_value.to_sym
37
16
  else
38
- super(param)
17
+ params_value
39
18
  end
40
19
  end
41
20
 
21
+ def to_csf_params
22
+ super.deep_merge(argTypes: { param => { options: options } })
23
+ end
24
+
42
25
  private
43
26
 
44
27
  def csf_control_params
45
- super.merge(options: options)
28
+ labels.nil? ? super : super.merge(labels: labels)
29
+ end
30
+
31
+ def symbol_value
32
+ @symbol_value ||= default_value.is_a?(Symbol)
46
33
  end
47
34
  end
48
35
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Controls
6
+ ##
7
+ # A simple Control Config maps to one Storybook Control
8
+ # It has a value and pulls its value from params by key
9
+ class SimpleControlConfig < ControlConfig
10
+ attr_reader :default_value
11
+
12
+ def initialize(default_value, param: nil, name: nil)
13
+ super(param: param, name: name)
14
+ @default_value = default_value
15
+ end
16
+
17
+ def to_csf_params
18
+ validate!
19
+ {
20
+ args: { param => csf_value },
21
+ argTypes: { param => { control: csf_control_params, name: name } }
22
+ }
23
+ end
24
+
25
+ def value_from_params(params)
26
+ params.key?(param) ? params[param] : default_value
27
+ end
28
+
29
+ private
30
+
31
+ # provide extension points for subclasses to vary the value
32
+ def type
33
+ # :nocov:
34
+ raise NotImplementedError
35
+ # :nocov:
36
+ end
37
+
38
+ def csf_value
39
+ default_value
40
+ end
41
+
42
+ def csf_control_params
43
+ { type: type }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module Storybook
5
5
  module Controls
6
- class TextConfig < ControlConfig
6
+ class TextConfig < SimpleControlConfig
7
7
  def type
8
8
  :text
9
9
  end
@@ -8,14 +8,18 @@ module ViewComponent
8
8
  extend ActiveSupport::Autoload
9
9
 
10
10
  autoload :ControlConfig
11
+ autoload :SimpleControlConfig
11
12
  autoload :TextConfig
12
13
  autoload :BooleanConfig
13
14
  autoload :ColorConfig
14
15
  autoload :NumberConfig
16
+ autoload :BaseOptionsConfig
15
17
  autoload :OptionsConfig
16
- autoload :ArrayConfig
18
+ autoload :MultiOptionsConfig
17
19
  autoload :DateConfig
18
20
  autoload :ObjectConfig
21
+ autoload :CustomConfig
22
+ autoload :ControlsHelpers
19
23
  end
20
24
  end
21
25
  end
@@ -3,68 +3,65 @@
3
3
  module ViewComponent
4
4
  module Storybook
5
5
  module Dsl
6
- class ControlsDsl
7
- attr_reader :component, :controls
8
-
9
- def initialize(component)
10
- @component = component
11
- @controls = []
6
+ class LegacyControlsDsl
7
+ def controls
8
+ @controls ||= []
12
9
  end
13
10
 
14
11
  def text(param, value, name: nil)
15
- controls << Controls::TextConfig.new(component, param, value, name: name)
12
+ controls << Controls::TextConfig.new(value, param: param, name: name)
16
13
  end
17
14
 
18
15
  def boolean(param, value, name: nil)
19
- controls << Controls::BooleanConfig.new(component, param, value, name: name)
16
+ controls << Controls::BooleanConfig.new(value, param: param, name: name)
20
17
  end
21
18
 
22
19
  def number(param, value, name: nil, min: nil, max: nil, step: nil)
23
- controls << Controls::NumberConfig.new(:number, component, param, value, name: name, min: min, max: max, step: step)
20
+ controls << Controls::NumberConfig.new(:number, value, param: param, name: name, min: min, max: max, step: step)
24
21
  end
25
22
 
26
23
  def range(param, value, name: nil, min: nil, max: nil, step: nil)
27
- controls << Controls::NumberConfig.new(:range, component, param, value, name: name, min: min, max: max, step: step)
24
+ controls << Controls::NumberConfig.new(:range, value, param: param, name: name, min: min, max: max, step: step)
28
25
  end
29
26
 
30
27
  def color(param, value, name: nil, preset_colors: nil)
31
- controls << Controls::ColorConfig.new(component, param, value, name: name, preset_colors: preset_colors)
28
+ controls << Controls::ColorConfig.new(value, param: param, name: name, preset_colors: preset_colors)
32
29
  end
33
30
 
34
31
  def object(param, value, name: nil)
35
- controls << Controls::ObjectConfig.new(component, param, value, name: name)
32
+ controls << Controls::ObjectConfig.new(value, param: param, name: name)
36
33
  end
37
34
 
38
35
  def select(param, options, value, name: nil)
39
- controls << Controls::OptionsConfig.new(:select, component, param, options, value, name: name)
36
+ controls << Controls::OptionsConfig.new(:select, options, value, param: param, name: name)
40
37
  end
41
38
 
42
39
  def multi_select(param, options, value, name: nil)
43
- controls << Controls::OptionsConfig.new(:'multi-select', component, param, options, value, name: name)
40
+ controls << Controls::OptionsConfig.new(:'multi-select', options, value, param: param, name: name)
44
41
  end
45
42
 
46
43
  def radio(param, options, value, name: nil)
47
- controls << Controls::OptionsConfig.new(:radio, component, param, options, value, name: name)
44
+ controls << Controls::OptionsConfig.new(:radio, options, value, param: param, name: name)
48
45
  end
49
46
 
50
47
  def inline_radio(param, options, value, name: nil)
51
- controls << Controls::OptionsConfig.new(:'inline-radio', component, param, options, value, name: name)
48
+ controls << Controls::OptionsConfig.new(:'inline-radio', options, value, param: param, name: name)
52
49
  end
53
50
 
54
51
  def check(param, options, value, name: nil)
55
- controls << Controls::OptionsConfig.new(:check, component, param, options, value, name: name)
52
+ controls << Controls::OptionsConfig.new(:check, options, value, param: param, name: name)
56
53
  end
57
54
 
58
55
  def inline_check(param, options, value, name: nil)
59
- controls << Controls::OptionsConfig.new(:'inline-check', component, param, options, value, name: name)
56
+ controls << Controls::OptionsConfig.new(:'inline-check', options, value, param: param, name: name)
60
57
  end
61
58
 
62
- def array(param, value, separator = ",", name: nil)
63
- controls << Controls::ArrayConfig.new(component, param, value, separator, name: name)
59
+ def array(param, value, _separator = nil, name: nil)
60
+ controls << Controls::ObjectConfig.new(value, param: param, name: name)
64
61
  end
65
62
 
66
63
  def date(param, value, name: nil)
67
- controls << Controls::DateConfig.new(component, param, value, name: name)
64
+ controls << Controls::DateConfig.new(value, param: param, name: name)
68
65
  end
69
66
 
70
67
  def respond_to_missing?(_method, *)
@@ -7,8 +7,7 @@ module ViewComponent
7
7
  module Dsl
8
8
  extend ActiveSupport::Autoload
9
9
 
10
- autoload :StoryDsl
11
- autoload :ControlsDsl
10
+ autoload :LegacyControlsDsl
12
11
  end
13
12
  end
14
13
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails"
4
- require "view_component/storybook"
5
4
 
6
5
  module ViewComponent
7
6
  module Storybook
@@ -12,6 +11,7 @@ module ViewComponent
12
11
  options = app.config.view_component_storybook
13
12
 
14
13
  options.show_stories = Rails.env.development? if options.show_stories.nil?
14
+ options.stories_route ||= ViewComponent::Storybook.stories_route
15
15
 
16
16
  if options.show_stories
17
17
  options.stories_path ||= defined?(Rails.root) ? Rails.root.join("test/components/stories") : nil
@@ -33,7 +33,7 @@ module ViewComponent
33
33
 
34
34
  if options.show_stories
35
35
  app.routes.prepend do
36
- get "/rails/stories/*stories/:story" => "view_component/storybook/stories#show", :internal => true
36
+ get "#{options.stories_route}/*stories/:story" => "view_component/storybook/stories#show", :internal => true
37
37
  end
38
38
  end
39
39
  end
@@ -44,3 +44,14 @@ module ViewComponent
44
44
  end
45
45
  end
46
46
  end
47
+
48
+ # :nocov:
49
+ unless defined?(ViewComponent::Storybook)
50
+ ActiveSupport::Deprecation.warn(
51
+ "This manually engine loading is deprecated and will be removed in v1.0.0. " \
52
+ "Remove `require \"view_component/storybook/engine\"`."
53
+ )
54
+
55
+ require "view_component/storybook"
56
+ end
57
+ # :nocov:
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module MethodArgs
6
+ ##
7
+ # Class representing the constructor args for a Component
8
+ class ComponentConstructorArgs < ControlMethodArgs
9
+ def self.from_component_class(component_class, *args, **kwargs)
10
+ if DryInitializerComponentConstructorArgs.dry_initialize?(component_class)
11
+ DryInitializerComponentConstructorArgs.new(component_class, *args, **kwargs)
12
+ else
13
+ new(component_class, *args, **kwargs)
14
+ end
15
+ end
16
+
17
+ def initialize(component_class, *args, **kwargs)
18
+ super(component_class.instance_method(:initialize), *args, **kwargs)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module MethodArgs
6
+ ##
7
+ # Class representing arguments passed to a method which can be validated
8
+ # against the args of the target method
9
+ # In addition the args and kwargs can contain Controls the values of which can
10
+ # be resolved from a params hash
11
+ class ControlMethodArgs < MethodArgs
12
+ include ActiveModel::Validations::Callbacks
13
+
14
+ attr_reader :param_prefix
15
+
16
+ validate :validate_controls
17
+ before_validation :assign_control_params
18
+
19
+ def with_param_prefix(prefix)
20
+ @param_prefix = prefix
21
+ self
22
+ end
23
+
24
+ ##
25
+ # resolve the controls values from the params
26
+ # call the target method or block with those values
27
+ def call(params, &target_block)
28
+ method_args = resolve_method_args(params)
29
+
30
+ (target_block || target_method).call(*method_args.args, **method_args.kwargs)
31
+ end
32
+
33
+ def resolve_method_args(params)
34
+ assign_control_params
35
+
36
+ args_from_params = args.map do |arg|
37
+ value_from_params(arg, params)
38
+ end
39
+ kwargs_from_params = kwargs.transform_values do |arg|
40
+ value_from_params(arg, params)
41
+ end
42
+
43
+ MethodArgs.new(target_method, *args_from_params, **kwargs_from_params)
44
+ end
45
+
46
+ def controls
47
+ @controls ||= (args + kwargs.values).select(&method(:control?))
48
+ end
49
+
50
+ private
51
+
52
+ def assign_control_params
53
+ return if @assigned_control_params
54
+
55
+ args.each_with_index do |arg, index|
56
+ add_param_if_control(arg, target_method_params_names.arg_name(index))
57
+ end
58
+
59
+ kwargs.each do |key, arg|
60
+ add_param_if_control(arg, key)
61
+ end
62
+
63
+ @assigned_control_params = true
64
+ end
65
+
66
+ def add_param_if_control(arg, param)
67
+ return unless control?(arg)
68
+
69
+ arg.param(param) if arg.param.nil? # don't overrite if set
70
+ # Always add prefix
71
+ arg.prefix_param(param_prefix) if param_prefix.present?
72
+ end
73
+
74
+ def value_from_params(arg, params)
75
+ control?(arg) ? arg.value_from_params(params) : arg
76
+ end
77
+
78
+ def control?(arg)
79
+ arg.is_a?(Controls::ControlConfig)
80
+ end
81
+
82
+ def validate_controls
83
+ controls.reject(&:valid?).each do |control|
84
+ control_errors = control.errors.full_messages.join(', ')
85
+ errors.add(:controls, :invalid_control, control_name: control.name, control_errors: control_errors)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module MethodArgs
6
+ ##
7
+ # Class representing the constructor args for a Component the extends dry-initializer
8
+ class DryInitializerComponentConstructorArgs < ControlMethodArgs
9
+ INITIALIZE_METHOD = :__dry_initializer_initialize__
10
+
11
+ ##
12
+ # Method Parameters names that are dry-initialize aware
13
+ # THe main difference is that Dry Initializer keyword args cannot be deduced from the constructor initialize params
14
+ # Instead we have to introspect the dry_initializer config for the option definitions
15
+ class DryConstructorParametersNames < MethodParametersNames
16
+ attr_reader :dry_initializer
17
+
18
+ def initialize(component_class)
19
+ super(component_class.instance_method(INITIALIZE_METHOD))
20
+ @dry_initializer = component_class.dry_initializer
21
+ end
22
+
23
+ # Required keywords are the only thing we need.
24
+ # We could define kwarg_names similarly but wihout the `optional` check. However, dry-initializer
25
+ # always ends has supports_keyrest? == true so kwarg_names isn't needed
26
+ def req_kwarg_names
27
+ @req_kwarg_names ||= dry_initializer.definitions.map do |name, definition|
28
+ name if definition.option && !definition.optional
29
+ end.compact
30
+ end
31
+ end
32
+
33
+ def self.dry_initialize?(component_class)
34
+ component_class.private_method_defined?(INITIALIZE_METHOD)
35
+ end
36
+
37
+ def initialize(component_class, *args, **kwargs)
38
+ super(component_class.instance_method(INITIALIZE_METHOD), *args, **kwargs)
39
+
40
+ @target_method_params_names = DryConstructorParametersNames.new(component_class)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module MethodArgs
6
+ ##
7
+ # Class representing arguments passed to a method which can be validated
8
+ # against the args of the target method
9
+ class MethodArgs
10
+ include ActiveModel::Validations
11
+
12
+ attr_reader :args, :kwargs, :target_method, :target_method_params_names
13
+
14
+ validate :validate_args, :validate_kwargs
15
+
16
+ def initialize(target_method, *args, **kwargs)
17
+ @target_method = target_method
18
+ @args = args
19
+ @kwargs = kwargs
20
+ @target_method_params_names = MethodParametersNames.new(target_method)
21
+ end
22
+
23
+ private
24
+
25
+ def validate_args
26
+ arg_count = args.count
27
+
28
+ if arg_count > target_method_params_names.max_arg_count
29
+ errors.add(:args, :too_many, max: target_method_params_names.max_arg_count, count: arg_count)
30
+ elsif arg_count < target_method_params_names.min_arg_count
31
+ errors.add(:args, :too_few, min: target_method_params_names.min_arg_count, count: arg_count)
32
+ end
33
+ end
34
+
35
+ def validate_kwargs
36
+ kwargs.each_key do |kwarg|
37
+ unless target_method_params_names.include_kwarg?(kwarg)
38
+ errors.add(:kwargs, :invalid_arg, kwarg: kwarg)
39
+ end
40
+ end
41
+
42
+ return if target_method_params_names.covers_required_kwargs?(kwargs.keys)
43
+
44
+ expected_keys = target_method_params_names.req_kwarg_names.join(', ')
45
+ actual_keys = kwargs.keys.join(', ')
46
+
47
+ errors.add(:kwargs, :invalid, expected_keys: expected_keys, actual_keys: actual_keys)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end