view_component_storybook 0.8.0 → 0.9.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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -9
  3. data/app/controllers/view_component/storybook/stories_controller.rb +3 -2
  4. data/app/views/view_component/storybook/stories/show.html.erb +3 -1
  5. data/config/locales/en.yml +32 -0
  6. data/lib/view_component/storybook.rb +2 -0
  7. data/lib/view_component/storybook/controls.rb +2 -0
  8. data/lib/view_component/storybook/controls/array_config.rb +8 -7
  9. data/lib/view_component/storybook/controls/boolean_config.rb +7 -6
  10. data/lib/view_component/storybook/controls/color_config.rb +4 -5
  11. data/lib/view_component/storybook/controls/control_config.rb +22 -38
  12. data/lib/view_component/storybook/controls/custom_config.rb +54 -0
  13. data/lib/view_component/storybook/controls/date_config.rb +14 -11
  14. data/lib/view_component/storybook/controls/number_config.rb +9 -9
  15. data/lib/view_component/storybook/controls/object_config.rb +11 -5
  16. data/lib/view_component/storybook/controls/options_config.rb +9 -8
  17. data/lib/view_component/storybook/controls/simple_control_config.rb +48 -0
  18. data/lib/view_component/storybook/controls/text_config.rb +1 -1
  19. data/lib/view_component/storybook/dsl.rb +1 -0
  20. data/lib/view_component/storybook/dsl/controls_dsl.rb +31 -61
  21. data/lib/view_component/storybook/dsl/legacy_controls_dsl.rb +98 -0
  22. data/lib/view_component/storybook/dsl/story_dsl.rb +17 -5
  23. data/lib/view_component/storybook/method_args.rb +16 -0
  24. data/lib/view_component/storybook/method_args/control_method_args.rb +88 -0
  25. data/lib/view_component/storybook/method_args/method_args.rb +19 -0
  26. data/lib/view_component/storybook/method_args/method_parameters_names.rb +97 -0
  27. data/lib/view_component/storybook/method_args/validatable_method_args.rb +50 -0
  28. data/lib/view_component/storybook/stories.rb +43 -0
  29. data/lib/view_component/storybook/story_config.rb +43 -9
  30. data/lib/view_component/storybook/tasks/write_stories_json.rake +6 -0
  31. data/lib/view_component/storybook/version.rb +1 -1
  32. metadata +24 -15
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/dependencies/autoload"
4
+
5
+ module ViewComponent
6
+ module Storybook
7
+ module MethodArgs
8
+ extend ActiveSupport::Autoload
9
+
10
+ autoload :MethodArgs
11
+ autoload :MethodParametersNames
12
+ autoload :ValidatableMethodArgs
13
+ autoload :ControlMethodArgs
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,88 @@
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
+ # I addition the args and kwargs can contain Controls the values of which can
10
+ # be resolved from a params hash
11
+ class ControlMethodArgs < ValidatableMethodArgs
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
+ def resolve_method_args(params)
25
+ assign_control_params
26
+
27
+ args_from_params = args.map do |arg|
28
+ value_from_params(arg, params)
29
+ end
30
+ kwargs_from_params = kwargs.transform_values do |arg|
31
+ value_from_params(arg, params)
32
+ end
33
+
34
+ MethodArgs.new(args_from_params, kwargs_from_params, block)
35
+ end
36
+
37
+ def controls
38
+ @controls ||= (args + kwargs.values).select(&method(:control?))
39
+ end
40
+
41
+ private
42
+
43
+ def assign_control_params
44
+ return if @assigned_control_params
45
+
46
+ args.each_with_index do |arg, index|
47
+ add_param_if_control(arg, target_method_params_names.arg_name(index))
48
+ end
49
+
50
+ kwargs.each do |key, arg|
51
+ add_param_if_control(arg, key)
52
+ end
53
+
54
+ @assigned_control_params = true
55
+ end
56
+
57
+ def add_param_if_control(arg, param)
58
+ return unless control?(arg)
59
+
60
+ arg.param(param) if arg.param.nil? # don't overrite if set
61
+ # Always add prefix
62
+ arg.param("#{param_prefix}__#{arg.param}".to_sym) if param_prefix.present?
63
+ end
64
+
65
+ def value_from_params(arg, params)
66
+ if control?(arg)
67
+ value = arg.value_from_params(params)
68
+ value = arg.default_value if value.nil? # nil only not falsey
69
+ value
70
+ else
71
+ arg
72
+ end
73
+ end
74
+
75
+ def control?(arg)
76
+ arg.is_a?(ViewComponent::Storybook::Controls::ControlConfig)
77
+ end
78
+
79
+ def validate_controls
80
+ controls.reject(&:valid?).each do |control|
81
+ control_errors = control.errors.full_messages.join(', ')
82
+ errors.add(:controls, :invalid_control, control_name: control.name, control_errors: control_errors)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module MethodArgs
6
+ ##
7
+ # Simple class representing arguments passed to a method
8
+ class MethodArgs
9
+ attr_reader :args, :kwargs, :block
10
+
11
+ def initialize(args, kwargs, block)
12
+ @args = args
13
+ @kwargs = kwargs
14
+ @block = block
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module MethodArgs
6
+ class MethodParametersNames
7
+ REQ_KWARG_TYPE = :keyreq
8
+ KWARG_TYPES = [REQ_KWARG_TYPE, :key].freeze
9
+ REQ_ARG_TYPE = :req
10
+ ARG_TYPES = [REQ_ARG_TYPE, :opt].freeze
11
+ KWARG_REST = :keyrest
12
+ REST = :rest
13
+
14
+ attr_reader :target_method
15
+
16
+ def initialize(target_method)
17
+ @target_method = target_method
18
+ end
19
+
20
+ def arg_name(pos)
21
+ if pos < named_arg_count
22
+ arg_names[pos]
23
+ else
24
+ offset_pos = pos - named_arg_count
25
+ "#{rest_arg_name}#{offset_pos}".to_sym
26
+ end
27
+ end
28
+
29
+ def include_kwarg?(kwarg_name)
30
+ supports_keyrest? || kwarg_names.include?(kwarg_name)
31
+ end
32
+
33
+ def covers_required_kwargs?(names)
34
+ names.to_set >= req_kwarg_names.to_set
35
+ end
36
+
37
+ def max_arg_count
38
+ supports_rest? ? Float::INFINITY : named_arg_count
39
+ end
40
+
41
+ def min_arg_count
42
+ req_arg_count
43
+ end
44
+
45
+ def req_kwarg_names
46
+ @req_kwarg_names ||= parameters.map do |type, name|
47
+ name if type == REQ_KWARG_TYPE
48
+ end.compact
49
+ end
50
+
51
+ private
52
+
53
+ def parameters
54
+ @parameters ||= target_method.parameters
55
+ end
56
+
57
+ def kwarg_names
58
+ @kwarg_names ||= parameters.map do |type, name|
59
+ name if KWARG_TYPES.include?(type)
60
+ end.compact.to_set
61
+ end
62
+
63
+ def arg_names
64
+ @arg_names ||= parameters.map do |type, name|
65
+ name if ARG_TYPES.include?(type)
66
+ end.compact
67
+ end
68
+
69
+ def req_arg_names
70
+ @req_arg_names ||= parameters.map do |type, name|
71
+ name if type == REQ_ARG_TYPE
72
+ end.compact
73
+ end
74
+
75
+ def named_arg_count
76
+ @named_arg_count ||= arg_names.count
77
+ end
78
+
79
+ def req_arg_count
80
+ @req_arg_count ||= req_arg_names.count
81
+ end
82
+
83
+ def rest_arg_name
84
+ @rest_arg_name ||= parameters.map { |type, name| name if type == REST }.first
85
+ end
86
+
87
+ def supports_keyrest?
88
+ @supports_keyrest ||= parameters.map(&:first).include?(KWARG_REST)
89
+ end
90
+
91
+ def supports_rest?
92
+ @supports_rest ||= parameters.map(&:first).include?(REST)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,50 @@
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 ValidatableMethodArgs < MethodArgs
10
+ include ActiveModel::Validations
11
+
12
+ attr_reader :target_method_params_names
13
+
14
+ validate :validate_args, :validate_kwargs
15
+
16
+ def initialize(target_method, *args, **kwargs, &block)
17
+ @target_method_params_names = MethodParametersNames.new(target_method)
18
+ super(args, kwargs, block)
19
+ end
20
+
21
+ private
22
+
23
+ def validate_args
24
+ arg_count = args.count
25
+
26
+ if arg_count > target_method_params_names.max_arg_count
27
+ errors.add(:args, :too_many, max: target_method_params_names.max_arg_count, count: arg_count)
28
+ elsif arg_count < target_method_params_names.min_arg_count
29
+ errors.add(:args, :too_few, min: target_method_params_names.min_arg_count, count: arg_count)
30
+ end
31
+ end
32
+
33
+ def validate_kwargs
34
+ kwargs.each_key do |kwarg|
35
+ unless target_method_params_names.include_kwarg?(kwarg)
36
+ errors.add(:kwargs, :invalid_arg, kwarg: kwarg)
37
+ end
38
+ end
39
+
40
+ return if target_method_params_names.covers_required_kwargs?(kwargs.keys)
41
+
42
+ expected_keys = target_method_params_names.req_kwarg_names.join(', ')
43
+ actual_keys = kwargs.keys.join(', ')
44
+
45
+ errors.add(:kwargs, :invalid, expected_keys: expected_keys, actual_keys: actual_keys)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -4,10 +4,13 @@ module ViewComponent
4
4
  module Storybook
5
5
  class Stories
6
6
  extend ActiveSupport::DescendantsTracker
7
+ include ActiveModel::Validations
7
8
 
8
9
  class_attribute :story_configs, default: []
9
10
  class_attribute :parameters, :title, :stories_layout
10
11
 
12
+ validate :validate_story_configs
13
+
11
14
  class << self
12
15
  def story(name, component = default_component, &block)
13
16
  story_config = StoryConfig.configure(story_id(name), name, component, layout, &block)
@@ -26,6 +29,7 @@ module ViewComponent
26
29
  end
27
30
 
28
31
  def to_csf_params
32
+ validate!
29
33
  csf_params = { title: title }
30
34
  csf_params[:parameters] = parameters if parameters.present?
31
35
  csf_params[:stories] = story_configs.map(&:to_csf_params)
@@ -70,6 +74,19 @@ module ViewComponent
70
74
  story_configs.find { |config| config.name == name.to_sym }
71
75
  end
72
76
 
77
+ # validation - ActiveModel::Validations like but on the class vs the instance
78
+ def valid?
79
+ # use an instance so we can enjoy the benefits of ActiveModel::Validations
80
+ @validation_instance = new
81
+ @validation_instance.valid?
82
+ end
83
+
84
+ delegate :errors, to: :@validation_instance
85
+
86
+ def validate!
87
+ valid? || raise(ValidationError, @validation_instance)
88
+ end
89
+
73
90
  private
74
91
 
75
92
  def inherited(other)
@@ -95,6 +112,32 @@ module ViewComponent
95
112
  "#{stories_name}/#{name.to_s.parameterize}".underscore
96
113
  end
97
114
  end
115
+
116
+ protected
117
+
118
+ def validate_story_configs
119
+ story_configs.reject(&:valid?).each do |story_config|
120
+ story_errors = story_config.errors.full_messages.join(', ')
121
+ errors.add(:story_configs, :invalid_story, story_name: story_config.name, story_errors: story_errors)
122
+ end
123
+
124
+ story_names = story_configs.map(&:name)
125
+ duplicate_names = story_names.group_by(&:itself).map { |k, v| k if v.length > 1 }.compact
126
+ return if duplicate_names.empty?
127
+
128
+ duplicate_name_sentence = duplicate_names.map { |name| "'#{name}'" }.to_sentence
129
+ errors.add(:story_configs, :duplicate_stories, count: duplicate_names.count, duplicate_names: duplicate_name_sentence)
130
+ end
131
+
132
+ class ValidationError < StandardError
133
+ attr_reader :stories
134
+
135
+ def initialize(stories)
136
+ @stories = stories
137
+
138
+ super("#{@stories.class.name} invalid: (#{@stories.errors.full_messages.join(', ')})")
139
+ end
140
+ end
98
141
  end
99
142
  end
100
143
  end
@@ -6,31 +6,42 @@ module ViewComponent
6
6
  include ActiveModel::Validations
7
7
 
8
8
  attr_reader :id, :name, :component
9
- attr_accessor :controls, :parameters, :layout, :content_block
9
+ attr_accessor :parameters, :layout, :content_block
10
+
11
+ validate :validate_constructor_args
10
12
 
11
13
  def initialize(id, name, component, layout)
12
14
  @id = id
13
15
  @name = name
14
16
  @component = component
15
17
  @layout = layout
16
- @controls = []
18
+ end
19
+
20
+ def constructor_args(*args, **kwargs, &block)
21
+ if args.empty? && kwargs.empty? && block.nil?
22
+ @constructor_args ||= ViewComponent::Storybook::MethodArgs::ControlMethodArgs.new(component_constructor)
23
+ else
24
+ @constructor_args = ViewComponent::Storybook::MethodArgs::ControlMethodArgs.new(
25
+ component_constructor,
26
+ *args,
27
+ **kwargs,
28
+ &block
29
+ )
30
+ end
17
31
  end
18
32
 
19
33
  def to_csf_params
34
+ validate!
20
35
  csf_params = { name: name, parameters: { server: { id: id } } }
21
36
  csf_params.deep_merge!(parameters: parameters) if parameters.present?
22
- controls.each do |control|
37
+ constructor_args.controls.each do |control|
23
38
  csf_params.deep_merge!(control.to_csf_params)
24
39
  end
25
40
  csf_params
26
41
  end
27
42
 
28
- def values_from_params(params)
29
- controls.map do |control|
30
- value = control.value_from_param(params[control.param])
31
- value = control.value if value.nil? # nil only not falsey
32
- [control.param, value]
33
- end.to_h
43
+ def validate!
44
+ valid? || raise(ValidationError, self)
34
45
  end
35
46
 
36
47
  def self.configure(id, name, component, layout, &configuration)
@@ -38,6 +49,29 @@ module ViewComponent
38
49
  ViewComponent::Storybook::Dsl::StoryDsl.evaluate!(config, &configuration)
39
50
  config
40
51
  end
52
+
53
+ def validate_constructor_args
54
+ return if constructor_args.valid?
55
+
56
+ constructor_args_errors = constructor_args.errors.full_messages.join(', ')
57
+ errors.add(:constructor_args, :invalid, errors: constructor_args_errors)
58
+ end
59
+
60
+ class ValidationError < StandardError
61
+ attr_reader :story_config
62
+
63
+ def initialize(story_config)
64
+ @story_config = story_config
65
+
66
+ super("'#{@story_config.name}' invalid: (#{@story_config.errors.full_messages.join(', ')})")
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def component_constructor
73
+ component.instance_method(:initialize)
74
+ end
41
75
  end
42
76
  end
43
77
  end
@@ -4,10 +4,16 @@ namespace :view_component_storybook do
4
4
  desc "Write CSF JSON stories for all Stories"
5
5
  task write_stories_json: :environment do
6
6
  puts "Writing Stories JSON"
7
+ exceptions = []
7
8
  ViewComponent::Storybook::Stories.all.each do |stories|
8
9
  json_path = stories.write_csf_json
9
10
  puts "#{stories.name} => #{json_path}"
11
+ rescue StandardError => e
12
+ exceptions << e
10
13
  end
14
+
15
+ raise StandardError, exceptions.map(:message).join(", ") if exceptions.present?
16
+
11
17
  puts "Done"
12
18
  end
13
19
  end