vident 1.0.0.beta2 → 1.0.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/CHANGELOG.md +32 -1
- data/README.md +171 -17
- data/lib/generators/vident/install/install_generator.rb +53 -0
- data/lib/generators/vident/install/templates/vident.rb +20 -0
- data/lib/vident/caching.rb +3 -9
- data/lib/vident/child_element_helper.rb +22 -21
- data/lib/vident/component.rb +3 -10
- data/lib/vident/component_attribute_resolver.rb +21 -36
- data/lib/vident/component_class_lists.rb +4 -3
- data/lib/vident/stable_id.rb +48 -17
- data/lib/vident/stimulus/naming.rb +19 -0
- data/lib/vident/stimulus/primitive.rb +38 -0
- data/lib/vident/stimulus.rb +31 -0
- data/lib/vident/stimulus_action.rb +58 -23
- data/lib/vident/stimulus_attribute_base.rb +27 -23
- data/lib/vident/stimulus_attributes.rb +56 -185
- data/lib/vident/stimulus_builder.rb +66 -87
- data/lib/vident/stimulus_class.rb +3 -9
- data/lib/vident/stimulus_class_collection.rb +1 -5
- data/lib/vident/stimulus_collection_base.rb +4 -12
- data/lib/vident/stimulus_component.rb +8 -7
- data/lib/vident/stimulus_controller.rb +10 -13
- data/lib/vident/stimulus_data_attribute_builder.rb +15 -74
- data/lib/vident/stimulus_helper.rb +4 -12
- data/lib/vident/stimulus_null.rb +21 -0
- data/lib/vident/stimulus_outlet.rb +3 -9
- data/lib/vident/stimulus_outlet_collection.rb +1 -5
- data/lib/vident/stimulus_param.rb +42 -0
- data/lib/vident/stimulus_param_collection.rb +11 -0
- data/lib/vident/stimulus_target.rb +7 -17
- data/lib/vident/stimulus_target_collection.rb +2 -6
- data/lib/vident/stimulus_value.rb +14 -44
- data/lib/vident/stimulus_value_collection.rb +1 -5
- data/lib/vident/tailwind.rb +0 -2
- data/lib/vident/version.rb +1 -1
- data/lib/vident.rb +7 -12
- data/skills/vident/SKILL.md +628 -0
- metadata +10 -1
data/lib/vident/stable_id.rb
CHANGED
|
@@ -1,39 +1,70 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require "random/formatter"
|
|
6
|
-
rescue LoadError
|
|
7
|
-
# to support Ruby 3.0
|
|
8
|
-
require "securerandom"
|
|
9
|
-
end
|
|
3
|
+
require "random/formatter"
|
|
4
|
+
require "digest/md5"
|
|
10
5
|
|
|
11
6
|
module Vident
|
|
12
7
|
class StableId
|
|
8
|
+
class GeneratorNotSetError < StandardError; end
|
|
9
|
+
|
|
10
|
+
class StrategyNotConfiguredError < StandardError; end
|
|
11
|
+
|
|
12
|
+
RANDOM_FALLBACK = ->(generator) do
|
|
13
|
+
return Random.hex(16) unless generator
|
|
14
|
+
generator.next.join("-")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
STRICT = ->(generator) do
|
|
18
|
+
unless generator
|
|
19
|
+
raise GeneratorNotSetError,
|
|
20
|
+
"No Vident::StableId sequence generator is set on the current thread. " \
|
|
21
|
+
"Call Vident::StableId.set_current_sequence_generator(seed: ...) in a " \
|
|
22
|
+
"before_action (or wrap the render in StableId.with_sequence_generator)."
|
|
23
|
+
end
|
|
24
|
+
generator.next.join("-")
|
|
25
|
+
end
|
|
26
|
+
|
|
13
27
|
class << self
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
# Callable(generator_or_nil) -> String. Starts nil; host app must configure it.
|
|
29
|
+
attr_accessor :strategy
|
|
30
|
+
|
|
31
|
+
def set_current_sequence_generator(seed:)
|
|
32
|
+
::Thread.current[:vident_number_sequence_generator] = id_sequence_generator(seed)
|
|
16
33
|
end
|
|
17
|
-
alias_method :new_current_sequence_generator, :set_current_sequence_generator
|
|
18
34
|
|
|
19
35
|
def clear_current_sequence_generator
|
|
20
36
|
::Thread.current[:vident_number_sequence_generator] = nil
|
|
21
37
|
end
|
|
22
38
|
|
|
39
|
+
def with_sequence_generator(seed:)
|
|
40
|
+
previous = ::Thread.current[:vident_number_sequence_generator]
|
|
41
|
+
set_current_sequence_generator(seed: seed)
|
|
42
|
+
yield
|
|
43
|
+
ensure
|
|
44
|
+
::Thread.current[:vident_number_sequence_generator] = previous
|
|
45
|
+
end
|
|
46
|
+
|
|
23
47
|
def next_id_in_sequence
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
unless @strategy
|
|
49
|
+
raise StrategyNotConfiguredError,
|
|
50
|
+
"Vident::StableId.strategy is not configured. Run " \
|
|
51
|
+
"`bin/rails generate vident:install`, or set it manually in an " \
|
|
52
|
+
"initializer (e.g. `Vident::StableId.strategy = Vident::StableId::STRICT`)."
|
|
53
|
+
end
|
|
54
|
+
@strategy.call(::Thread.current[:vident_number_sequence_generator])
|
|
29
55
|
end
|
|
30
56
|
|
|
31
57
|
private
|
|
32
58
|
|
|
33
|
-
def id_sequence_generator
|
|
34
|
-
|
|
59
|
+
def id_sequence_generator(seed)
|
|
60
|
+
raise ArgumentError, "seed: cannot be nil" if seed.nil?
|
|
61
|
+
number_generator = Random.new(coerce_seed(seed))
|
|
35
62
|
Enumerator.produce { number_generator.hex(16) }.with_index
|
|
36
63
|
end
|
|
64
|
+
|
|
65
|
+
def coerce_seed(seed)
|
|
66
|
+
Digest::MD5.hexdigest(seed.to_s).to_i(16)
|
|
67
|
+
end
|
|
37
68
|
end
|
|
38
69
|
end
|
|
39
70
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vident
|
|
4
|
+
module Stimulus
|
|
5
|
+
# Vident's internal naming conventions for per-primitive wiring — the
|
|
6
|
+
# `add_stimulus_<plural>` mutator method, the `@stimulus_<plural>` prop
|
|
7
|
+
# ivar, and the `@stimulus_<plural>_collection` parsed-collection ivar.
|
|
8
|
+
# Mixed in by the consumers that need these helpers. Kept off `Primitive`
|
|
9
|
+
# so the primitive stays a clean domain value object and doesn't carry
|
|
10
|
+
# the implementation details of its consumers.
|
|
11
|
+
module Naming
|
|
12
|
+
def mutator_method(primitive) = :"add_stimulus_#{primitive.name}"
|
|
13
|
+
|
|
14
|
+
def prop_ivar(primitive) = :"@stimulus_#{primitive.name}"
|
|
15
|
+
|
|
16
|
+
def collection_ivar(primitive) = :"@stimulus_#{primitive.name}_collection"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vident
|
|
4
|
+
module Stimulus
|
|
5
|
+
# A Stimulus primitive kind: name, plus the Value/Collection classes that
|
|
6
|
+
# back it. Two concrete subclasses distinguish how the primitive behaves
|
|
7
|
+
# when a Hash is passed to the plural parser:
|
|
8
|
+
#
|
|
9
|
+
# - `Keyed` — `{a: 1, b: 2}` expands to one value object per pair.
|
|
10
|
+
# Used for values / params / classes / outlets.
|
|
11
|
+
# - `Positional` — `{...}` is a single-arg descriptor (e.g. Action's
|
|
12
|
+
# `{event:, method:, ...}` short form).
|
|
13
|
+
# Used for controllers / actions / targets.
|
|
14
|
+
class Primitive < ::Data.define(:name, :value_class, :collection_class)
|
|
15
|
+
# Short forms. `name` (Data field) is the plural — `:values`; `plural`
|
|
16
|
+
# is an alias for symmetry with `singular`.
|
|
17
|
+
alias_method :plural, :name
|
|
18
|
+
def singular = name.to_s.singularize.to_sym
|
|
19
|
+
|
|
20
|
+
# The primitive's key in Vident's attribute namespace. Used both as the
|
|
21
|
+
# parser method name (`def stimulus_values(...)`) and as the hash key
|
|
22
|
+
# in DSL attrs / component props / `root_element_attributes` — the same
|
|
23
|
+
# Symbol serves all three roles.
|
|
24
|
+
def key = :"stimulus_#{name}"
|
|
25
|
+
def singular_key = :"stimulus_#{singular}"
|
|
26
|
+
|
|
27
|
+
def keyed? = raise NotImplementedError
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class KeyedPrimitive < Primitive
|
|
31
|
+
def keyed? = true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class PositionalPrimitive < Primitive
|
|
35
|
+
def keyed? = false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vident
|
|
4
|
+
module Stimulus
|
|
5
|
+
# Registry of primitive kinds. Add an entry (paired with a Value/Collection
|
|
6
|
+
# class pair) to extend; plural parsers, mutators, and the data-attribute
|
|
7
|
+
# builder pick it up. Array order = data attribute emission order.
|
|
8
|
+
PRIMITIVES = [
|
|
9
|
+
PositionalPrimitive.new(:controllers, StimulusController, StimulusControllerCollection),
|
|
10
|
+
PositionalPrimitive.new(:actions, StimulusAction, StimulusActionCollection),
|
|
11
|
+
PositionalPrimitive.new(:targets, StimulusTarget, StimulusTargetCollection),
|
|
12
|
+
KeyedPrimitive.new(:outlets, StimulusOutlet, StimulusOutletCollection),
|
|
13
|
+
KeyedPrimitive.new(:values, StimulusValue, StimulusValueCollection),
|
|
14
|
+
KeyedPrimitive.new(:params, StimulusParam, StimulusParamCollection),
|
|
15
|
+
KeyedPrimitive.new(:classes, StimulusClass, StimulusClassCollection)
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
PRIMITIVES_BY_NAME = PRIMITIVES.to_h { |primitive| [primitive.name, primitive] }.freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def primitive(name)
|
|
22
|
+
PRIMITIVES_BY_NAME[name] or
|
|
23
|
+
raise ArgumentError, "Unknown stimulus primitive #{name.inspect}; valid: #{PRIMITIVES_BY_NAME.keys.inspect}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def each(&block) = PRIMITIVES.each(&block)
|
|
27
|
+
|
|
28
|
+
def names = PRIMITIVES.map(&:name)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -2,24 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
module Vident
|
|
4
4
|
class StimulusAction < StimulusAttributeBase
|
|
5
|
-
|
|
5
|
+
# https://stimulus.hotwired.dev/reference/actions#options
|
|
6
|
+
VALID_OPTIONS = [:once, :prevent, :stop, :passive, :"!passive", :capture, :self].freeze
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
# Typed descriptor for modifiers (`:once`/`:prevent`/etc., keyboard filter,
|
|
9
|
+
# `@window`) that the plain Array form can't express. Hash input to the
|
|
10
|
+
# parsers (`{event:, method:, ...}`) is desugared into one of these.
|
|
11
|
+
class Descriptor < ::Literal::Data
|
|
12
|
+
prop :method, _Union(Symbol, String)
|
|
13
|
+
prop :event, _Nilable(_Union(Symbol, String)), default: nil
|
|
14
|
+
prop :controller, _Nilable(String), default: nil
|
|
15
|
+
prop :options, _Array(Symbol), default: -> { [] }
|
|
16
|
+
prop :keyboard, _Nilable(String), default: nil
|
|
17
|
+
prop :window, _Boolean, default: false
|
|
13
18
|
end
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
attr_reader :event, :controller, :action, :options, :keyboard, :window
|
|
21
|
+
|
|
22
|
+
def initialize(*args, implied_controller: nil)
|
|
23
|
+
@options = []
|
|
24
|
+
@keyboard = nil
|
|
25
|
+
@window = false
|
|
26
|
+
super
|
|
17
27
|
end
|
|
18
28
|
|
|
19
|
-
def
|
|
20
|
-
|
|
29
|
+
def to_s
|
|
30
|
+
head =
|
|
31
|
+
if @event
|
|
32
|
+
ev = @event.to_s
|
|
33
|
+
ev = "#{ev}.#{@keyboard}" if @keyboard
|
|
34
|
+
ev = "#{ev}#{@options.map { |o| ":#{o}" }.join}" if @options.any?
|
|
35
|
+
ev = "#{ev}@window" if @window
|
|
36
|
+
"#{ev}->"
|
|
37
|
+
else
|
|
38
|
+
""
|
|
39
|
+
end
|
|
40
|
+
"#{head}#{@controller}##{@action}"
|
|
21
41
|
end
|
|
22
42
|
|
|
43
|
+
def data_attribute_name = "action"
|
|
44
|
+
|
|
45
|
+
def data_attribute_value = to_s
|
|
46
|
+
|
|
23
47
|
private
|
|
24
48
|
|
|
25
49
|
def parse_arguments(*args)
|
|
@@ -38,27 +62,25 @@ module Vident
|
|
|
38
62
|
end
|
|
39
63
|
|
|
40
64
|
def parse_single_argument(arg)
|
|
41
|
-
|
|
42
|
-
|
|
65
|
+
case arg
|
|
66
|
+
when Descriptor then apply_descriptor(arg)
|
|
67
|
+
when Hash then apply_descriptor(Descriptor.new(**arg))
|
|
68
|
+
when Symbol
|
|
43
69
|
@event = nil
|
|
44
70
|
@controller = implied_controller_name
|
|
45
71
|
@action = js_name(arg)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
parse_qualified_action_string(arg)
|
|
49
|
-
else
|
|
50
|
-
raise ArgumentError, "Invalid 'action' argument types (1): #{arg.class}"
|
|
72
|
+
when String then parse_qualified_action_string(arg)
|
|
73
|
+
else raise ArgumentError, "Invalid 'action' argument type (1): #{arg.class}"
|
|
51
74
|
end
|
|
52
75
|
end
|
|
53
76
|
|
|
77
|
+
# (:event, :method) or ("controller/path", :method)
|
|
54
78
|
def parse_two_arguments(part1, part2)
|
|
55
79
|
if part1.is_a?(Symbol) && part2.is_a?(Symbol)
|
|
56
|
-
# 2 symbol args = event + action
|
|
57
80
|
@event = part1.to_s
|
|
58
81
|
@controller = implied_controller_name
|
|
59
82
|
@action = js_name(part2)
|
|
60
83
|
elsif part1.is_a?(String) && part2.is_a?(Symbol)
|
|
61
|
-
# 1 string arg, 1 symbol = controller + action
|
|
62
84
|
@event = nil
|
|
63
85
|
@controller = stimulize_path(part1)
|
|
64
86
|
@action = js_name(part2)
|
|
@@ -67,9 +89,9 @@ module Vident
|
|
|
67
89
|
end
|
|
68
90
|
end
|
|
69
91
|
|
|
92
|
+
# (:event, "controller/path", :method)
|
|
70
93
|
def parse_three_arguments(part1, part2, part3)
|
|
71
94
|
if part1.is_a?(Symbol) && part2.is_a?(String) && part3.is_a?(Symbol)
|
|
72
|
-
# 1 symbol, 1 string, 1 symbol = event + controller + action
|
|
73
95
|
@event = part1.to_s
|
|
74
96
|
@controller = stimulize_path(part2)
|
|
75
97
|
@action = js_name(part3)
|
|
@@ -78,16 +100,29 @@ module Vident
|
|
|
78
100
|
end
|
|
79
101
|
end
|
|
80
102
|
|
|
103
|
+
def apply_descriptor(d)
|
|
104
|
+
invalid = d.options - VALID_OPTIONS
|
|
105
|
+
unless invalid.empty?
|
|
106
|
+
raise ArgumentError,
|
|
107
|
+
"Invalid action option(s) #{invalid.inspect}. Valid: #{VALID_OPTIONS.inspect}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@event = d.event&.to_s
|
|
111
|
+
@controller = d.controller ? stimulize_path(d.controller) : implied_controller_name
|
|
112
|
+
@action = d.method.is_a?(Symbol) ? js_name(d.method) : d.method.to_s
|
|
113
|
+
@options = d.options
|
|
114
|
+
@keyboard = d.keyboard
|
|
115
|
+
@window = d.window
|
|
116
|
+
end
|
|
117
|
+
|
|
81
118
|
def parse_qualified_action_string(action_string)
|
|
82
119
|
if action_string.include?("->")
|
|
83
|
-
# Has event: "click->controller#action"
|
|
84
120
|
event_part, controller_action = action_string.split("->", 2)
|
|
85
121
|
@event = event_part
|
|
86
122
|
controller_part, action_part = controller_action.split("#", 2)
|
|
87
123
|
@controller = controller_part
|
|
88
124
|
@action = action_part
|
|
89
125
|
else
|
|
90
|
-
# No event: "controller#action"
|
|
91
126
|
@event = nil
|
|
92
127
|
controller_part, action_part = action_string.split("#", 2)
|
|
93
128
|
@controller = controller_part
|
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
require "active_support/core_ext/string/inflections"
|
|
4
4
|
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
5
7
|
module Vident
|
|
6
8
|
class StimulusAttributeBase
|
|
9
|
+
# `"admin/users"` → `"admin--users"`; accepts Symbol or String.
|
|
10
|
+
def self.stimulize_path(path)
|
|
11
|
+
path.to_s.split("/").map(&:dasherize).join("--")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# `:my_thing` → `"myThing"`
|
|
15
|
+
def self.js_name(name)
|
|
16
|
+
name.to_s.camelize(:lower)
|
|
17
|
+
end
|
|
18
|
+
|
|
7
19
|
attr_reader :implied_controller
|
|
8
20
|
|
|
9
21
|
def initialize(*args, implied_controller: nil)
|
|
@@ -11,27 +23,17 @@ module Vident
|
|
|
11
23
|
parse_arguments(*args)
|
|
12
24
|
end
|
|
13
25
|
|
|
14
|
-
def inspect
|
|
15
|
-
"#<#{self.class.name} #{to_h}>"
|
|
16
|
-
end
|
|
26
|
+
def inspect = "#<#{self.class.name} #{to_h}>"
|
|
17
27
|
|
|
18
|
-
def to_s
|
|
19
|
-
raise NoMethodError, "Subclasses must implement to_s"
|
|
20
|
-
end
|
|
28
|
+
def to_s = raise(NoMethodError, "Subclasses must implement to_s")
|
|
21
29
|
|
|
22
|
-
def to_h
|
|
23
|
-
{data_attribute_name => data_attribute_value}
|
|
24
|
-
end
|
|
30
|
+
def to_h = {data_attribute_name => data_attribute_value}
|
|
25
31
|
|
|
26
32
|
alias_method :to_hash, :to_h
|
|
27
33
|
|
|
28
|
-
def data_attribute_name
|
|
29
|
-
raise NoMethodError, "Subclasses must implement data_attribute_name"
|
|
30
|
-
end
|
|
34
|
+
def data_attribute_name = raise(NoMethodError, "Subclasses must implement data_attribute_name")
|
|
31
35
|
|
|
32
|
-
def data_attribute_value
|
|
33
|
-
raise NoMethodError, "Subclasses must implement data_attribute_value"
|
|
34
|
-
end
|
|
36
|
+
def data_attribute_value = raise(NoMethodError, "Subclasses must implement data_attribute_value")
|
|
35
37
|
|
|
36
38
|
def implied_controller_path
|
|
37
39
|
raise ArgumentError, "implied_controller is required to get implied controller path" unless implied_controller
|
|
@@ -45,17 +47,19 @@ module Vident
|
|
|
45
47
|
|
|
46
48
|
private
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
def stimulize_path(path)
|
|
50
|
-
path.split("/").map { |p| p.to_s.dasherize }.join("--")
|
|
51
|
-
end
|
|
50
|
+
def stimulize_path(path) = self.class.stimulize_path(path)
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
def js_name(name) = self.class.js_name(name)
|
|
53
|
+
|
|
54
|
+
# Arrays/Hashes serialise as JSON; everything else via `to_s` (which is how
|
|
55
|
+
# `Vident::StimulusNull` emits the literal `"null"`).
|
|
56
|
+
def serialize_value(value)
|
|
57
|
+
case value
|
|
58
|
+
when Array, Hash then value.to_json
|
|
59
|
+
else value.to_s
|
|
60
|
+
end
|
|
56
61
|
end
|
|
57
62
|
|
|
58
|
-
# Subclasses must implement this method
|
|
59
63
|
def parse_arguments(*args)
|
|
60
64
|
raise NotImplementedError, "Subclasses must implement parse_arguments"
|
|
61
65
|
end
|