bulma_x 0.2.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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +22 -0
  4. data/CHANGELOG.md +30 -0
  5. data/CONTRIBUTING.md +50 -0
  6. data/LICENSE +661 -0
  7. data/README.md +315 -0
  8. data/Rakefile +12 -0
  9. data/lib/bulma_x/base_component.rb +125 -0
  10. data/lib/bulma_x/base_input.rb +37 -0
  11. data/lib/bulma_x/block.rb +7 -0
  12. data/lib/bulma_x/box.rb +7 -0
  13. data/lib/bulma_x/breadcrumbs.rb +35 -0
  14. data/lib/bulma_x/button.rb +42 -0
  15. data/lib/bulma_x/card.rb +55 -0
  16. data/lib/bulma_x/checkbox.rb +23 -0
  17. data/lib/bulma_x/columns.rb +81 -0
  18. data/lib/bulma_x/component_dsl.rb +19 -0
  19. data/lib/bulma_x/dropdown.rb +65 -0
  20. data/lib/bulma_x/dsl/options.rb +129 -0
  21. data/lib/bulma_x/dsl/slots.rb +234 -0
  22. data/lib/bulma_x/dsl/validations.rb +74 -0
  23. data/lib/bulma_x/field.rb +150 -0
  24. data/lib/bulma_x/figure.rb +27 -0
  25. data/lib/bulma_x/file.rb +54 -0
  26. data/lib/bulma_x/footer.rb +7 -0
  27. data/lib/bulma_x/form.rb +27 -0
  28. data/lib/bulma_x/grid.rb +90 -0
  29. data/lib/bulma_x/help.rb +7 -0
  30. data/lib/bulma_x/hero.rb +36 -0
  31. data/lib/bulma_x/icon.rb +66 -0
  32. data/lib/bulma_x/image.rb +42 -0
  33. data/lib/bulma_x/input.rb +53 -0
  34. data/lib/bulma_x/level.rb +43 -0
  35. data/lib/bulma_x/link.rb +44 -0
  36. data/lib/bulma_x/media.rb +19 -0
  37. data/lib/bulma_x/message.rb +27 -0
  38. data/lib/bulma_x/modal.rb +26 -0
  39. data/lib/bulma_x/navbar.rb +162 -0
  40. data/lib/bulma_x/notification.rb +15 -0
  41. data/lib/bulma_x/pagination.rb +86 -0
  42. data/lib/bulma_x/panel.rb +29 -0
  43. data/lib/bulma_x/paragraph.rb +7 -0
  44. data/lib/bulma_x/progress.rb +36 -0
  45. data/lib/bulma_x/radio.rb +33 -0
  46. data/lib/bulma_x/section.rb +35 -0
  47. data/lib/bulma_x/select.rb +57 -0
  48. data/lib/bulma_x/shared/aria_options.rb +19 -0
  49. data/lib/bulma_x/shared/data_options.rb +19 -0
  50. data/lib/bulma_x/shared/flex_options.rb +57 -0
  51. data/lib/bulma_x/shared/global_options.rb +49 -0
  52. data/lib/bulma_x/shared/spacing_options.rb +80 -0
  53. data/lib/bulma_x/shared/text_options.rb +31 -0
  54. data/lib/bulma_x/slot.rb +13 -0
  55. data/lib/bulma_x/subtitle.rb +9 -0
  56. data/lib/bulma_x/table.rb +78 -0
  57. data/lib/bulma_x/tabs.rb +43 -0
  58. data/lib/bulma_x/tag.rb +25 -0
  59. data/lib/bulma_x/textarea.rb +29 -0
  60. data/lib/bulma_x/title.rb +21 -0
  61. data/lib/bulma_x/version.rb +5 -0
  62. data/lib/bulma_x/vertical_menu.rb +71 -0
  63. data/lib/bulma_x.rb +9 -0
  64. metadata +123 -0
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaX
4
+ class Columns < BaseComponent
5
+ option :mobile, values: BOOLEAN, default: false
6
+ option :desktop, values: BOOLEAN, default: false
7
+ option :gapless, values: BOOLEAN, default: false
8
+ option :multiline, values: BOOLEAN, default: false
9
+ option :vcentered, values: BOOLEAN, default: false
10
+ option :centered, values: BOOLEAN, default: false
11
+
12
+ option :gap, values: (0..8).to_a.push(nil), default: nil
13
+ option :gap_mobile, values: (0..8).to_a.push(nil), default: nil
14
+ option :gap_tablet, values: (0..8).to_a.push(nil), default: nil
15
+ option :gap_desktop, values: (0..8).to_a.push(nil), default: nil
16
+ option :gap_widescreen, values: (0..8).to_a.push(nil), default: nil
17
+ option :gap_fullhd, values: (0..8).to_a.push(nil), default: nil
18
+
19
+ slots :column, component: 'Column'
20
+
21
+ def view_template
22
+ super do
23
+ slots(:column).each { render it }
24
+ end
25
+ end
26
+
27
+ class Column < BaseComponent
28
+ PROPORTIONS = %w[three-quarters two-thirds half one-third one-quarter full four-fifths three-fifths two-fifths
29
+ one-fifth].freeze
30
+ SPANS = (1..12).to_a.freeze
31
+
32
+ option :proportion, values: PROPORTIONS, default: nil
33
+ option :proportion_mobile, values: PROPORTIONS, default: nil
34
+ option :proportion_tablet, values: PROPORTIONS, default: nil
35
+ option :proportion_desktop, values: PROPORTIONS, default: nil
36
+ option :proportion_widescreen, values: PROPORTIONS, default: nil
37
+ option :proportion_fullhd, values: PROPORTIONS, default: nil
38
+
39
+ option :span, values: SPANS, default: nil
40
+ option :offset, values: PROPORTIONS + SPANS, default: nil
41
+ option :narrow, values: [nil, :always, :mobile, :table, :touch, :desktop, :widescreen, :fullhd], default: nil
42
+
43
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
44
+ def root_classes
45
+ super +
46
+ [
47
+ 'column',
48
+ @proportion && "is-#{@proportion}",
49
+ @proportion_mobile && "is-#{@proportion_mobile}-mobile",
50
+ @proportion_tablet && "is-#{@proportion_tablet}-tablet",
51
+ @proportion_desktop && "is-#{@proportion_desktop}-desktop",
52
+ @proportion_widescreen && "is-#{@proportion_widescreen}-widescreen",
53
+ @proportion_fullhd && "is-#{@proportion_fullhd}-fullhd",
54
+ @span && "is-#{@span}",
55
+ @offset && "is-offset-#{@offset}",
56
+ @narrow && (@narrow == :always ? 'is-narrow' : "is-narrow-#{@narrow}")
57
+ ]
58
+ end
59
+ end
60
+
61
+ def root_classes
62
+ super +
63
+ [
64
+ 'columns',
65
+ @mobile && 'is-mobile',
66
+ @desktop && 'is-desktop',
67
+ @gapless && 'is-gapless',
68
+ @multiline && 'is-multiline',
69
+ @centered && 'is-centered',
70
+ @vcentered && 'is-vcentered',
71
+ @gap && "is-column-gap-#{@gap}",
72
+ @gap_mobile && "is-column-gap-#{@gap_mobile}-mobile",
73
+ @gap_tablet && "is-column-gap-#{@gap_tablet}-tablet",
74
+ @gap_desktop && "is-column-gap-#{@gap_desktop}-desktop",
75
+ @gap_widescreen && "is-column-gap-#{@gap_widescreen}-widescreen",
76
+ @gap_fullhd && "is-column-gap-#{@gap_fullhd}-fullhd"
77
+ ]
78
+ end
79
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
80
+ end
81
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaX
4
+ module ComponentDsl
5
+ def self.included(base)
6
+ base.include(Dsl::Options)
7
+ base.include(Dsl::Slots)
8
+ base.include(Dsl::Validations)
9
+
10
+ base.instance_eval do
11
+ def self.inherited(subclass)
12
+ super
13
+ inherit_options(subclass)
14
+ inherit_validations(subclass)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaX
4
+ class Dropdown < BaseComponent
5
+ option :direction, values: %i[down up], default: 'down'
6
+ option :opened, values: BOOLEAN, default: false
7
+
8
+ slot :trigger, classes: ['dropdown-trigger']
9
+ slot :menu, classes: ['dropdown-menu'], attributes: { role: 'menu' }
10
+ slot :content, component: 'Content'
11
+ slots :item, component: 'Item'
12
+
13
+ def view_template
14
+ super do
15
+ slot(:trigger)
16
+ slot(:menu)
17
+
18
+ slot(:content)
19
+ end
20
+ end
21
+
22
+ def root_classes
23
+ super +
24
+ [
25
+ 'dropdown is-hoverable',
26
+ @direction == :up && 'is-up',
27
+ @opened && 'is-active'
28
+ ]
29
+ end
30
+
31
+ class Item < BaseComponent
32
+ option :active, values: BOOLEAN, default: false
33
+ option :disabled, values: BOOLEAN, default: false
34
+ option :divider, values: BOOLEAN, default: false
35
+
36
+ def root_classes
37
+ if @divider
38
+ super + [
39
+ 'dropdown-divider'
40
+ ]
41
+ else
42
+ super + [
43
+ 'dropdown-item',
44
+ @active && 'is-active',
45
+ @disabled && 'is-disabled'
46
+ ]
47
+ end
48
+ end
49
+
50
+ def root_tag = @divider ? :hr : :a
51
+ end
52
+
53
+ class Content < BaseComponent
54
+ root_slot classes: ['dropdown-content']
55
+
56
+ slots :item, component: Item # TODO: fix reference to component
57
+
58
+ def view_template
59
+ super do
60
+ slots(:item).each { render it }
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaX
4
+ module Dsl
5
+ module Options
6
+ module ClassMethods
7
+ attr_reader :options, :default_options
8
+
9
+ def option(attribute_name, values: nil, default: nil, override: false)
10
+ @options ||= Hash.new { |h, k| h[k] = [] }
11
+ @default_options ||= {}
12
+
13
+ if @options.key?(attribute_name) && !override
14
+ raise ArgumentError,
15
+ "Option :#{attribute_name} already defined on #{self}, use override: true to redefine it"
16
+ end
17
+
18
+ @options[attribute_name] = values
19
+ @default_options[attribute_name] = default.dup.freeze
20
+
21
+ define_prefixed_helpers(attribute_name, values)
22
+ define_values_helper(attribute_name, values)
23
+ end
24
+
25
+ def default_option(attribute_name, value)
26
+ @default_options ||= {}
27
+
28
+ raise ArgumentError, "Option :#{attribute_name} does not exist" unless @options.key?(attribute_name)
29
+
30
+ @default_options[attribute_name] = value.dup.freeze
31
+ end
32
+
33
+ def remove_option(attribute_name)
34
+ @options.delete(attribute_name)
35
+ @default_options.delete(attribute_name)
36
+ end
37
+
38
+ protected
39
+
40
+ def inherit_options(subclass)
41
+ subclass.instance_variable_set(:@options, @options.dup)
42
+ subclass.instance_variable_set(:@default_options, @default_options.dup)
43
+ end
44
+
45
+ private
46
+
47
+ def define_prefixed_helpers(attribute_name, values)
48
+ case values
49
+ in [true, false] | [false, true]
50
+ define_boolean_helper(attribute_name)
51
+ in Array
52
+ define_helpers(attribute_name, values)
53
+ in Range | nil
54
+ # Do nothing
55
+ else
56
+ raise ArgumentError, '#options expect an array of values'
57
+ end
58
+ end
59
+
60
+ def define_boolean_helper(attribute_name)
61
+ define_method(attribute_name) do
62
+ tap { instance_variable_set(:"@#{attribute_name}", true) }
63
+ end
64
+ define_method(:"#{attribute_name}?") do
65
+ instance_variable_get(:"@#{attribute_name}")
66
+ end
67
+ define_method(:"not_#{attribute_name}") do
68
+ tap { instance_variable_set(:"@#{attribute_name}", false) }
69
+ end
70
+ end
71
+
72
+ def define_helpers(attribute_name, values)
73
+ values.each do |value|
74
+ define_method(:"#{attribute_name}_#{value}") do
75
+ tap { instance_variable_set(:"@#{attribute_name}", value) }
76
+ end
77
+ end
78
+ end
79
+
80
+ def define_values_helper(attribute_name, values)
81
+ class_eval do
82
+ define_singleton_method(:"#{attribute_name}_values") do
83
+ values
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def self.included(base)
90
+ base.extend(ClassMethods)
91
+ end
92
+
93
+ def build_from_options(**build_options)
94
+ @build_options = build_options
95
+ return if class_options.nil?
96
+ return if build_options.nil?
97
+
98
+ assert_options_validity!(build_options)
99
+
100
+ class_options.each_key do |attribute|
101
+ value = if build_options.key?(attribute)
102
+ build_options[attribute]
103
+ else
104
+ class_default_options[attribute].dup
105
+ end
106
+
107
+ instance_variable_set(:"@#{attribute}", value)
108
+ end
109
+ end
110
+
111
+ def class_options = self.class.options
112
+ def class_default_options = self.class.default_options
113
+ def option_get(name) = instance_variable_get(:"@#{name}")
114
+ def option_set(name, value) = instance_variable_set(:"@#{name}", value)
115
+
116
+ def assert_options_validity!(build_options)
117
+ build_options.each do |key, value|
118
+ raise ArgumentError, "Unknown option :#{key} on #{self.class}" unless class_options.key?(key)
119
+
120
+ class_option = class_options[key]
121
+ next if class_option.nil? || class_option.include?(value)
122
+
123
+ raise ArgumentError,
124
+ "Option #{key.inspect} has invalid value #{value.inspect}, expected one of #{class_option.inspect}"
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaX
4
+ module Dsl
5
+ module Slots
6
+ module DeferredRender
7
+ def before_template(&)
8
+ @_vanishing = true
9
+ vanish do
10
+ prepare(&)
11
+ yield self if block_given?
12
+ end
13
+ @_vanishing = false
14
+ super
15
+ end
16
+
17
+ def prepare; end
18
+
19
+ def render(...)
20
+ return if @_vanishing
21
+
22
+ super
23
+ end
24
+ end
25
+
26
+ module ClassMethods
27
+ # slot defines a slot that will be accessible on the instance via slot
28
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
29
+ def slot(slot_name, tag: :div, classes: [], attributes: {}, component: Slot)
30
+ include DeferredRender
31
+
32
+ if component != Slot && (tag != :div || classes.any? || attributes.any?)
33
+ raise ArgumentError, 'When using a custom component, tag, classes and attributes are ignored'
34
+ end
35
+
36
+ define_tag_helper(slot_name, tag)
37
+ define_classes_helper(slot_name, classes)
38
+ define_attributes_helper(slot_name, attributes)
39
+
40
+ # TODO: Replace method usage by class variable for validation
41
+ # @enabled_slots ||= {}
42
+ # @enabled_slots[slot_name] = { collection: false, ...}
43
+
44
+ define_method(:"with_#{slot_name}") do |**options, &block|
45
+ @slots[slot_name] = { component:, options:, block: }
46
+
47
+ nil
48
+ end
49
+ private :"with_#{slot_name}"
50
+
51
+ define_method(:"#{slot_name}_slot_component") do
52
+ if component.is_a?(Class)
53
+ component
54
+ elsif component.is_a?(String)
55
+ if component == 'self'
56
+ self.class
57
+ else
58
+ self.class.const_get(component)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
64
+
65
+ # slots defines a collection of slot that will be accessible on the instance via slots
66
+
67
+ # rubocop:disable Metrics
68
+ def slots(slot_name, tag: :div, classes: [], attributes: {}, component: Slot)
69
+ include DeferredRender
70
+
71
+ if component != Slot && (tag != :div || classes.any? || attributes.any?)
72
+ raise ArgumentError, 'When using a custom component, tag, classes and attributes are ignored'
73
+ end
74
+
75
+ define_tag_helper(slot_name, tag)
76
+ define_classes_helper(slot_name, classes)
77
+ define_attributes_helper(slot_name, attributes)
78
+
79
+ define_method(:"with_#{slot_name}") do |**options, &block|
80
+ @slots[slot_name] ||= []
81
+
82
+ @slots[slot_name] << { component:, options:, block: }
83
+
84
+ nil
85
+ end
86
+ private :"with_#{slot_name}"
87
+
88
+ define_method(:"#{slot_name}_slot_component") do
89
+ if component.is_a?(Class)
90
+ component
91
+ elsif component.is_a?(String)
92
+ if component == 'self'
93
+ self.class
94
+ else
95
+ self.class.const_get(component)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ # rubocop:enable Metrics
101
+
102
+ # root_slot allows to redefine the root tag, classes and attributes in one go
103
+ def root_slot(**kwargs)
104
+ tag = kwargs[:tag] || :div
105
+ define_method(:root_tag) { tag }
106
+
107
+ classes = kwargs[:classes] || []
108
+ define_method(:root_classes) { base_classes + classes }
109
+
110
+ attributes = kwargs[:attributes] || {}
111
+ define_method(:root_attributes) { base_attributes.merge(attributes) }
112
+ end
113
+
114
+ def define_tag_helper(slot_name, tag) = define_method(:"#{slot_name}_tag") { tag }
115
+ def define_classes_helper(slot_name, classes) = define_method(:"#{slot_name}_classes") { classes }
116
+ def define_attributes_helper(slot_name, attributes) = define_method(:"#{slot_name}_attributes") { attributes }
117
+ end
118
+
119
+ def self.included(base)
120
+ base.extend(ClassMethods)
121
+ end
122
+
123
+ def initialize(...)
124
+ super
125
+ @slots = {}
126
+ end
127
+
128
+ # content is used to store a slot content for later use (via slot or slots)
129
+ # It is meant to be used during the render context :
130
+ # render Component.new { it.content(:slot_name) { 'My slot content' } }
131
+ def content(slot_name, **, &)
132
+ if respond_to?(:"with_#{slot_name}", true)
133
+ send(:"with_#{slot_name}", **, &)
134
+ else
135
+ raise ArgumentError,
136
+ "Content was called for slot `#{slot_name}`, but this slot does not exist on #{self.class}"
137
+ end
138
+ end
139
+
140
+ def slot?(slot_name, idx = 0)
141
+ if @slots[slot_name].is_a?(Enumerable)
142
+ @slots[slot_name].size > idx
143
+ else
144
+ @slots.key?(slot_name)
145
+ end
146
+ end
147
+ alias content? slot?
148
+
149
+ private
150
+
151
+ # slot as an instance methods displays a slot content. It has three usages:
152
+ # 1. In a deferred render context, it will fetch the stored slot info to render it. Note: content params have priority over view_template params
153
+ # 2. In a deferred render context without stored content, it uses the default rendering of the given component IF it differs from the default Slot
154
+ # 3. In a direct rendering context, we want to display the block content
155
+ # 4. In a legacy component that used to override slot method, we still support it for now
156
+ def slot(slot_name, **, &)
157
+ unless respond_to?(:"#{slot_name}_slot_component", true)
158
+ raise ArgumentError,
159
+ "Slot #{slot_name} is not defined on #{self.class}"
160
+ end
161
+
162
+ component = slot_name == :root ? Slot : public_send(:"#{slot_name}_slot_component")
163
+
164
+ if @slots.key?(slot_name)
165
+ render_content(slot_name, **, component:)
166
+ elsif component != Slot
167
+ render_custom_component(component, **, &)
168
+ elsif respond_to?(:"component_slot_#{slot_name}", true)
169
+ render_legacy_slot(slot_name, **, &)
170
+ else
171
+ render generic_slot(slot_name, **, &)
172
+ end
173
+ end
174
+
175
+ # slots as an instance method returns the list of the contents stored
176
+ # If no content is stored, it returns an empty array
177
+ # rubocop:disable Metrics/AbcSize
178
+ def slots(slot_name, **template_args)
179
+ if !@slots.key?(slot_name)
180
+ [] # no item given through with_<slot_name>
181
+ elsif !@slots[slot_name].is_a?(Enumerable)
182
+ raise ArgumentError, "Slot #{slot_name} is not a collection"
183
+ else
184
+ @slots[slot_name].map do |stored_slot|
185
+ stored_component_method_name = :"#{slot_name}_slot_component"
186
+
187
+ unless respond_to?(stored_component_method_name, true)
188
+ raise ArgumentError, "Slot #{slot_name} is not defined on #{self.class}"
189
+ end
190
+
191
+ stored_component = public_send(stored_component_method_name)
192
+
193
+ if stored_component == Slot
194
+ generic_slot(slot_name, **template_args, &stored_slot[:block])
195
+ else
196
+ stored_component.new(**template_args.merge(stored_slot[:options]), &stored_slot[:block])
197
+ end
198
+ end
199
+ end
200
+ end
201
+ # rubocop:enable Metrics/AbcSize
202
+
203
+ def render_content(slot_name, component:, **template_options)
204
+ slot_info = @slots[slot_name]
205
+ slot_options = slot_info[:options].merge(**template_options)
206
+
207
+ if component == Slot
208
+ render generic_slot(slot_name, **slot_options, &slot_info[:block])
209
+ else
210
+ render component.new(**slot_options, &slot_info[:block])
211
+ end
212
+ end
213
+
214
+ def render_custom_component(component, **, &)
215
+ render component.new(**, &)
216
+ end
217
+
218
+ def render_legacy_slot(slot_name, **, &)
219
+ # Warning.warn('using component_slot_ is deprecated and will be removed soon') # Deprecation silenced for now
220
+ send(:"component_slot_#{slot_name}", **, &)
221
+ end
222
+
223
+ def generic_slot(slot_name, **, &)
224
+ Slot.new(
225
+ tag: send(:"#{slot_name}_tag"),
226
+ classes: send(:"#{slot_name}_classes"),
227
+ attributes: send(:"#{slot_name}_attributes"),
228
+ **,
229
+ &
230
+ )
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaX
4
+ module Dsl
5
+ module Validations
6
+ class Error < StandardError; end
7
+ class InvalidValidator < StandardError; end
8
+
9
+ module ClassMethods
10
+ ValidatorResult = Data.define(:result, :source_location, :details, :validator)
11
+ Validator = Data.define(:option, :validator, :message) do
12
+ def valid?(component, value)
13
+ source_location = nil
14
+ result = case validator
15
+ in Symbol
16
+ source_location = component.method(validator).source_location
17
+ component.send(validator, value)
18
+ in Proc
19
+ source_location = validator.source_location
20
+ component.instance_exec(value, &validator)
21
+ end
22
+
23
+ ValidatorResult.new(result, source_location.join(':'), message, self)
24
+ end
25
+ end
26
+
27
+ def validate(*options, validator, message: nil)
28
+ options.each do |option|
29
+ if validator.is_a?(Proc) && validator.arity != 1
30
+ raise InvalidValidator,
31
+ 'Your validator must accept exactly on argument (the option value received during initialization)'
32
+ end
33
+
34
+ validators << Validator.new(option, validator, message)
35
+ end
36
+ end
37
+
38
+ def remove_validations(option_name)
39
+ removed, @validators = validators.partition { _1.option == option_name }
40
+
41
+ removed
42
+ end
43
+
44
+ def validators = @validators ||= []
45
+
46
+ protected
47
+
48
+ def inherit_validations(subclass)
49
+ deep_dup_validators = validators.dup
50
+ subclass.instance_variable_set(:@validators, deep_dup_validators)
51
+ end
52
+ end
53
+
54
+ def self.included(base)
55
+ base.extend(ClassMethods)
56
+ end
57
+
58
+ def validate!(**options)
59
+ self.class.validators.none? do |validator|
60
+ value = options[validator.option]
61
+ validation = validator.valid?(self, value)
62
+
63
+ next if validation.result
64
+
65
+ raise Error, <<~ERROR
66
+ Option :#{validator.option} failed validation for value #{value.inspect}.
67
+ #{"Details: #{validation.details}" if validation.details}
68
+ Validator was defined at #{validation.source_location}
69
+ ERROR
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end