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.
- checksums.yaml +4 -4
- data/README.md +20 -9
- data/app/controllers/view_component/storybook/stories_controller.rb +3 -2
- data/app/views/view_component/storybook/stories/show.html.erb +3 -1
- data/config/locales/en.yml +32 -0
- data/lib/view_component/storybook.rb +2 -0
- data/lib/view_component/storybook/controls.rb +2 -0
- data/lib/view_component/storybook/controls/array_config.rb +8 -7
- data/lib/view_component/storybook/controls/boolean_config.rb +7 -6
- data/lib/view_component/storybook/controls/color_config.rb +4 -5
- data/lib/view_component/storybook/controls/control_config.rb +22 -38
- data/lib/view_component/storybook/controls/custom_config.rb +54 -0
- data/lib/view_component/storybook/controls/date_config.rb +14 -11
- data/lib/view_component/storybook/controls/number_config.rb +9 -9
- data/lib/view_component/storybook/controls/object_config.rb +11 -5
- data/lib/view_component/storybook/controls/options_config.rb +9 -8
- data/lib/view_component/storybook/controls/simple_control_config.rb +48 -0
- data/lib/view_component/storybook/controls/text_config.rb +1 -1
- data/lib/view_component/storybook/dsl.rb +1 -0
- data/lib/view_component/storybook/dsl/controls_dsl.rb +31 -61
- data/lib/view_component/storybook/dsl/legacy_controls_dsl.rb +98 -0
- data/lib/view_component/storybook/dsl/story_dsl.rb +17 -5
- data/lib/view_component/storybook/method_args.rb +16 -0
- data/lib/view_component/storybook/method_args/control_method_args.rb +88 -0
- data/lib/view_component/storybook/method_args/method_args.rb +19 -0
- data/lib/view_component/storybook/method_args/method_parameters_names.rb +97 -0
- data/lib/view_component/storybook/method_args/validatable_method_args.rb +50 -0
- data/lib/view_component/storybook/stories.rb +43 -0
- data/lib/view_component/storybook/story_config.rb +43 -9
- data/lib/view_component/storybook/tasks/write_stories_json.rake +6 -0
- data/lib/view_component/storybook/version.rb +1 -1
- 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 :
|
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
|
-
|
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
|
29
|
-
|
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
|