vident 1.0.2 → 2.0.1

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +45 -17
  4. data/lib/vident/caching.rb +4 -110
  5. data/lib/vident/capabilities/caching.rb +98 -0
  6. data/lib/vident/capabilities/child_element_rendering.rb +92 -0
  7. data/lib/vident/capabilities/class_list_building.rb +23 -0
  8. data/lib/vident/capabilities/declarable.rb +39 -0
  9. data/lib/vident/capabilities/identifiable.rb +54 -0
  10. data/lib/vident/capabilities/inspectable.rb +17 -0
  11. data/lib/vident/capabilities/root_element_rendering.rb +31 -0
  12. data/lib/vident/capabilities/stimulus_data_emitting.rb +98 -0
  13. data/lib/vident/capabilities/stimulus_declaring.rb +79 -0
  14. data/lib/vident/capabilities/stimulus_draft.rb +51 -0
  15. data/lib/vident/capabilities/stimulus_mutation.rb +60 -0
  16. data/lib/vident/capabilities/stimulus_parsing.rb +144 -0
  17. data/lib/vident/capabilities/tailwind.rb +18 -0
  18. data/lib/vident/component.rb +14 -76
  19. data/lib/vident/engine.rb +6 -5
  20. data/lib/vident/error.rb +16 -0
  21. data/lib/{vident2 → vident}/internals/action_builder.rb +18 -22
  22. data/lib/vident/internals/attribute_writer.rb +17 -0
  23. data/lib/{vident2 → vident}/internals/class_list_builder.rb +5 -22
  24. data/lib/vident/internals/declaration.rb +13 -0
  25. data/lib/{vident2 → vident}/internals/declarations.rb +6 -18
  26. data/lib/{vident2 → vident}/internals/draft.rb +3 -16
  27. data/lib/{vident2 → vident}/internals/dsl.rb +6 -32
  28. data/lib/vident/internals/plan.rb +9 -0
  29. data/lib/vident/internals/registry.rb +37 -0
  30. data/lib/{vident2 → vident}/internals/resolver.rb +101 -91
  31. data/lib/{vident2 → vident}/internals/target_builder.rb +1 -7
  32. data/lib/vident/stable_id.rb +3 -3
  33. data/lib/{vident2 → vident}/stimulus/action.rb +11 -24
  34. data/lib/vident/stimulus/base.rb +26 -0
  35. data/lib/{vident2 → vident}/stimulus/class_map.rb +6 -18
  36. data/lib/{vident2 → vident}/stimulus/collection.rb +6 -8
  37. data/lib/vident/stimulus/combinable.rb +30 -0
  38. data/lib/vident/stimulus/controller.rb +45 -0
  39. data/lib/vident/stimulus/naming.rb +9 -9
  40. data/lib/vident/stimulus/null.rb +7 -0
  41. data/lib/{vident2 → vident}/stimulus/outlet.rb +12 -32
  42. data/lib/{vident2 → vident}/stimulus/param.rb +5 -11
  43. data/lib/{vident2 → vident}/stimulus/target.rb +5 -14
  44. data/lib/vident/stimulus/value.rb +57 -0
  45. data/lib/vident/stimulus_null.rb +4 -8
  46. data/lib/vident/tailwind.rb +4 -17
  47. data/lib/vident/types.rb +28 -0
  48. data/lib/vident/version.rb +1 -6
  49. data/lib/vident.rb +46 -36
  50. data/skills/vident/SKILL.md +122 -19
  51. data/skills/vident/api-reference.md +259 -115
  52. data/skills/vident/examples.md +23 -10
  53. metadata +38 -60
  54. data/lib/vident/child_element_helper.rb +0 -64
  55. data/lib/vident/class_list_builder.rb +0 -112
  56. data/lib/vident/component_attribute_resolver.rb +0 -106
  57. data/lib/vident/component_class_lists.rb +0 -37
  58. data/lib/vident/stimulus/primitive.rb +0 -38
  59. data/lib/vident/stimulus.rb +0 -31
  60. data/lib/vident/stimulus_action.rb +0 -133
  61. data/lib/vident/stimulus_action_collection.rb +0 -11
  62. data/lib/vident/stimulus_attribute_base.rb +0 -67
  63. data/lib/vident/stimulus_attributes.rb +0 -129
  64. data/lib/vident/stimulus_builder.rb +0 -136
  65. data/lib/vident/stimulus_class.rb +0 -59
  66. data/lib/vident/stimulus_class_collection.rb +0 -11
  67. data/lib/vident/stimulus_collection_base.rb +0 -51
  68. data/lib/vident/stimulus_component.rb +0 -75
  69. data/lib/vident/stimulus_controller.rb +0 -41
  70. data/lib/vident/stimulus_controller_collection.rb +0 -14
  71. data/lib/vident/stimulus_data_attribute_builder.rb +0 -32
  72. data/lib/vident/stimulus_helper.rb +0 -66
  73. data/lib/vident/stimulus_outlet.rb +0 -90
  74. data/lib/vident/stimulus_outlet_collection.rb +0 -11
  75. data/lib/vident/stimulus_param.rb +0 -42
  76. data/lib/vident/stimulus_param_collection.rb +0 -11
  77. data/lib/vident/stimulus_target.rb +0 -47
  78. data/lib/vident/stimulus_target_collection.rb +0 -18
  79. data/lib/vident/stimulus_value.rb +0 -39
  80. data/lib/vident/stimulus_value_collection.rb +0 -11
  81. data/lib/vident2/caching.rb +0 -93
  82. data/lib/vident2/component.rb +0 -538
  83. data/lib/vident2/engine.rb +0 -18
  84. data/lib/vident2/error.rb +0 -30
  85. data/lib/vident2/internals/attribute_writer.rb +0 -22
  86. data/lib/vident2/internals/declaration.rb +0 -17
  87. data/lib/vident2/internals/plan.rb +0 -12
  88. data/lib/vident2/internals/registry.rb +0 -41
  89. data/lib/vident2/phlex/html.rb +0 -84
  90. data/lib/vident2/phlex.rb +0 -9
  91. data/lib/vident2/stimulus/controller.rb +0 -59
  92. data/lib/vident2/stimulus/naming.rb +0 -26
  93. data/lib/vident2/stimulus/null.rb +0 -16
  94. data/lib/vident2/stimulus/value.rb +0 -77
  95. data/lib/vident2/tailwind.rb +0 -19
  96. data/lib/vident2/version.rb +0 -5
  97. data/lib/vident2/view_component/base.rb +0 -124
  98. data/lib/vident2/view_component.rb +0 -9
  99. data/lib/vident2.rb +0 -50
@@ -3,26 +3,11 @@
3
3
  require_relative "registry"
4
4
  require_relative "draft"
5
5
 
6
- module Vident2
6
+ module Vident
7
7
  module Internals
8
- # @api private
9
- # Resolves Declarations + instance state into a Draft of typed
10
- # Stimulus::* values. The implied controller seeds first (unless
11
- # `no_stimulus_controller`); prop and root_element_attributes paths
12
- # both APPEND.
13
- #
14
- # Two entry points:
15
- # `call` — pure; returns a new Draft. Use `:static` or `:all`.
16
- # `resolve_procs_into` — mutates an existing Draft. Called at render
17
- # time so DSL procs can reach `helpers` /
18
- # `view_context` (not wired at `after_initialize`).
19
- #
20
- # Phases: `:static` skips anything with a `when_proc` or top-level Proc
21
- # in args; `:procs` processes only those; `:all` does everything.
22
- #
23
- # Procs nested inside a Hash descriptor (`action(method: -> { ... })`)
24
- # escape the gate — unsupported shape; use the fluent builder or a
25
- # top-level Proc.
8
+ # Resolves raw Declarations and instance state into a Draft of typed Stimulus values.
9
+ # Phase `:static` skips proc-bearing entries; `:procs` processes only those; `:all` does both.
10
+ # Procs nested inside a Hash descriptor escape the phase gate — use the fluent builder instead.
26
11
  module Resolver
27
12
  module_function
28
13
 
@@ -31,28 +16,41 @@ module Vident2
31
16
 
32
17
  draft = Draft.new
33
18
  implied = build_implied_controller(instance)
19
+ alias_map = build_alias_map(declarations)
34
20
 
35
21
  seed_implied_controller(draft, instance)
36
- resolve_declarations(draft, declarations, instance, implied, phase: phase)
37
- absorb_stimulus_props(draft, instance, implied, phase: phase)
38
- absorb_root_element_attributes(draft, instance, implied, phase: phase)
22
+ resolve_declarations(draft, declarations, instance, implied, phase:, alias_map:)
23
+ absorb_stimulus_props(draft, instance, implied, phase:, alias_map:)
24
+ absorb_root_element_attributes(draft, instance, implied, phase:, alias_map:)
39
25
 
40
26
  draft
41
27
  end
42
28
 
43
- # Caller owns idempotence (Component uses `@__vident2_procs_resolved`).
44
29
  def resolve_procs_into(draft, declarations, instance)
45
30
  implied = build_implied_controller(instance)
46
- resolve_declarations(draft, declarations, instance, implied, phase: :procs)
47
- absorb_stimulus_props(draft, instance, implied, phase: :procs)
48
- absorb_root_element_attributes(draft, instance, implied, phase: :procs)
31
+ alias_map = build_alias_map(declarations)
32
+ resolve_declarations(draft, declarations, instance, implied, phase: :procs, alias_map:)
33
+ absorb_stimulus_props(draft, instance, implied, phase: :procs, alias_map:)
34
+ absorb_root_element_attributes(draft, instance, implied, phase: :procs, alias_map:)
49
35
  draft
50
36
  end
51
37
 
38
+ def build_alias_map(declarations)
39
+ map = {}
40
+ declarations.controllers.each do |decl|
41
+ alias_name = decl.meta[:as]
42
+ next unless alias_name
43
+ raw_path = decl.args.first
44
+ next if raw_path.nil?
45
+ map[alias_name] = raw_path.to_s
46
+ end
47
+ map
48
+ end
49
+
52
50
  def build_implied_controller(instance)
53
51
  path = instance.class.stimulus_identifier_path
54
52
  name = instance.class.stimulus_identifier
55
- ::Vident2::Stimulus::Controller.new(path: path, name: name)
53
+ ::Vident::Stimulus::Controller.new(path: path, name: name)
56
54
  end
57
55
 
58
56
  def seed_implied_controller(draft, instance)
@@ -60,27 +58,40 @@ module Vident2
60
58
  draft.add_controllers(build_implied_controller(instance))
61
59
  end
62
60
 
63
- def resolve_declarations(draft, declarations, instance, implied, phase:)
64
- resolve_positional(draft, :controllers, declarations.controllers, instance, phase: phase) do |args, meta, _inst|
65
- ::Vident2::Stimulus::Controller.parse(*args, implied: implied, **meta_for_controller(meta))
61
+ def resolve_declarations(draft, declarations, instance, implied, phase:, alias_map: {})
62
+ resolve_positional(draft, :controllers, declarations.controllers, instance, phase:) do |args, meta, _inst|
63
+ ::Vident::Stimulus::Controller.parse(*args, implied: implied, **meta_for_controller(meta))
66
64
  end
67
65
 
68
- resolve_positional(draft, :actions, declarations.actions, instance, phase: phase) do |args, _meta, _inst|
69
- parse_single(::Vident2::Stimulus::Action, args, implied: implied, component_id: instance_id(instance))
66
+ resolve_positional(draft, :actions, declarations.actions, instance, phase:) do |args, _meta, _inst|
67
+ parse_single(::Vident::Stimulus::Action, resolve_action_aliases(args, alias_map), implied: implied, component_id: instance_id(instance))
70
68
  end
71
69
 
72
- resolve_positional(draft, :targets, declarations.targets, instance, phase: phase) do |args, _meta, _inst|
73
- parse_single(::Vident2::Stimulus::Target, args, implied: implied, component_id: instance_id(instance))
70
+ resolve_positional(draft, :targets, declarations.targets, instance, phase:) do |args, _meta, _inst|
71
+ parse_single(::Vident::Stimulus::Target, args, implied: implied, component_id: instance_id(instance))
74
72
  end
75
73
 
76
- resolve_keyed(draft, :outlets, declarations.outlets, instance, phase: phase) do |key, args, _meta|
74
+ resolve_keyed(draft, :outlets, declarations.outlets, instance, phase:) do |key, args, _meta|
77
75
  parsed_args = [key_for_parse(key), *args]
78
- parse_single(::Vident2::Stimulus::Outlet, parsed_args, implied: implied, component_id: instance_id(instance))
76
+ parse_single(::Vident::Stimulus::Outlet, parsed_args, implied: implied, component_id: instance_id(instance))
79
77
  end
80
78
 
81
- resolve_keyed_values(draft, declarations, instance, implied, phase: phase)
82
- resolve_keyed_scalars(draft, :params, declarations.params, instance, implied, ::Vident2::Stimulus::Param, phase: phase)
83
- resolve_keyed_scalars(draft, :class_maps, declarations.class_maps, instance, implied, ::Vident2::Stimulus::ClassMap, phase: phase)
79
+ resolve_keyed_values(draft, declarations, instance, implied, phase:)
80
+ resolve_keyed_scalars(draft, :params, declarations.params, instance, implied, ::Vident::Stimulus::Param, phase:)
81
+ resolve_keyed_scalars(draft, :class_maps, declarations.class_maps, instance, implied, ::Vident::Stimulus::ClassMap, phase:)
82
+ end
83
+
84
+ # Unknown aliases raise — a symbolic controller ref is declared intent, not a guess.
85
+ def resolve_action_aliases(args, alias_map)
86
+ return args if alias_map.empty?
87
+ args.map do |arg|
88
+ next arg unless arg.is_a?(Hash) && arg[:controller].is_a?(Symbol)
89
+ sym = arg[:controller]
90
+ unless alias_map.key?(sym)
91
+ raise ::Vident::DeclarationError, "Unknown controller alias :#{sym} in action. Declared aliases: #{alias_map.keys.inspect}"
92
+ end
93
+ arg.merge(controller: alias_map[sym])
94
+ end
84
95
  end
85
96
 
86
97
  def resolve_positional(draft, kind, entries, instance, phase:)
@@ -95,8 +106,6 @@ module Vident2
95
106
  end
96
107
  end
97
108
 
98
- # If `args` is a single Array element, unwrap it — positional kinds
99
- # treat Array values as the singular parser's arg tuple.
100
109
  def splat_single_array(args)
101
110
  (args.size == 1 && args[0].is_a?(Array)) ? args[0] : args
102
111
  end
@@ -120,22 +129,21 @@ module Vident2
120
129
  if decl.meta[:from_prop]
121
130
  raw = read_prop(instance, key)
122
131
  next if raw.nil?
123
- draft.add_values(::Vident2::Stimulus::Value.parse(key, raw, implied: implied))
132
+ draft.add_values(::Vident::Stimulus::Value.parse(key, raw, implied: implied))
124
133
  next
125
134
  end
126
135
 
127
136
  raw = resolve_value_meta(decl, instance)
128
137
  next if raw.nil?
129
- draft.add_values(::Vident2::Stimulus::Value.parse(key, raw, implied: implied))
138
+ draft.add_values(::Vident::Stimulus::Value.parse(key, raw, implied: implied))
130
139
  end
131
140
 
132
- # values_from_props is a plain Symbol list (no Declarations, so
133
- # phase_matches? doesn't apply). Ivar reads only; run once.
141
+ # values_from_props has no when_proc, so phase_matches? doesn't apply; skip on :procs pass.
134
142
  return if phase == :procs
135
143
  declarations.values_from_props.each do |name|
136
144
  raw = read_prop(instance, name)
137
145
  next if raw.nil?
138
- draft.add_values(::Vident2::Stimulus::Value.parse(name, raw, implied: implied))
146
+ draft.add_values(::Vident::Stimulus::Value.parse(name, raw, implied: implied))
139
147
  end
140
148
  end
141
149
 
@@ -149,95 +157,103 @@ module Vident2
149
157
  end
150
158
  end
151
159
 
152
- # Declaration-level phase gate. Nested Procs inside Hash args
153
- # aren't inspected — see module docstring.
154
160
  def phase_matches?(decl, phase)
155
161
  return true if phase == :all
156
162
  has_proc = decl.when_proc || decl.args.any? { |a| a.is_a?(Proc) }
157
163
  (phase == :procs) ? has_proc : !has_proc
158
164
  end
159
165
 
160
- def absorb_stimulus_props(draft, instance, implied, phase:)
161
- absorb_input(draft, :controllers, instance_ivar(instance, :@stimulus_controllers), instance, implied, phase: phase)
162
- absorb_input(draft, :actions, instance_ivar(instance, :@stimulus_actions), instance, implied, phase: phase)
163
- absorb_input(draft, :targets, instance_ivar(instance, :@stimulus_targets), instance, implied, phase: phase)
164
- absorb_input(draft, :outlets, instance_ivar(instance, :@stimulus_outlets), instance, implied, phase: phase)
165
- absorb_input(draft, :values, instance_ivar(instance, :@stimulus_values), instance, implied, phase: phase)
166
- absorb_input(draft, :params, instance_ivar(instance, :@stimulus_params), instance, implied, phase: phase)
167
- absorb_input(draft, :class_maps, instance_ivar(instance, :@stimulus_classes), instance, implied, phase: phase)
166
+ def absorb_stimulus_props(draft, instance, implied, phase:, alias_map: {})
167
+ absorb_input(draft, :controllers, instance_ivar(instance, :@stimulus_controllers), instance, implied, phase:, alias_map:)
168
+ absorb_input(draft, :actions, instance_ivar(instance, :@stimulus_actions), instance, implied, phase:, alias_map:)
169
+ absorb_input(draft, :targets, instance_ivar(instance, :@stimulus_targets), instance, implied, phase:, alias_map:)
170
+ absorb_input(draft, :outlets, instance_ivar(instance, :@stimulus_outlets), instance, implied, phase:, alias_map:)
171
+ absorb_input(draft, :values, instance_ivar(instance, :@stimulus_values), instance, implied, phase:, alias_map:)
172
+ absorb_input(draft, :params, instance_ivar(instance, :@stimulus_params), instance, implied, phase:, alias_map:)
173
+ absorb_input(draft, :class_maps, instance_ivar(instance, :@stimulus_classes), instance, implied, phase:, alias_map:)
168
174
  end
169
175
 
170
- def absorb_root_element_attributes(draft, instance, implied, phase:)
176
+ def absorb_root_element_attributes(draft, instance, implied, phase:, alias_map: {})
171
177
  return unless instance.respond_to?(:resolved_root_element_attributes, true)
172
178
  attrs = instance.send(:resolved_root_element_attributes)
173
179
  return unless attrs.is_a?(Hash) && !attrs.empty?
174
180
 
175
- absorb_input(draft, :controllers, attrs[:stimulus_controllers], instance, implied, phase: phase)
176
- absorb_input(draft, :actions, attrs[:stimulus_actions], instance, implied, phase: phase)
177
- absorb_input(draft, :targets, attrs[:stimulus_targets], instance, implied, phase: phase)
178
- absorb_input(draft, :outlets, attrs[:stimulus_outlets], instance, implied, phase: phase)
179
- absorb_input(draft, :values, attrs[:stimulus_values], instance, implied, phase: phase)
180
- absorb_input(draft, :params, attrs[:stimulus_params], instance, implied, phase: phase)
181
- absorb_input(draft, :class_maps, attrs[:stimulus_classes], instance, implied, phase: phase)
181
+ absorb_input(draft, :controllers, attrs[:stimulus_controllers], instance, implied, phase:, alias_map:)
182
+ absorb_input(draft, :actions, attrs[:stimulus_actions], instance, implied, phase:, alias_map:)
183
+ absorb_input(draft, :targets, attrs[:stimulus_targets], instance, implied, phase:, alias_map:)
184
+ absorb_input(draft, :outlets, attrs[:stimulus_outlets], instance, implied, phase:, alias_map:)
185
+ absorb_input(draft, :values, attrs[:stimulus_values], instance, implied, phase:, alias_map:)
186
+ absorb_input(draft, :params, attrs[:stimulus_params], instance, implied, phase:, alias_map:)
187
+ absorb_input(draft, :class_maps, attrs[:stimulus_classes], instance, implied, phase:, alias_map:)
182
188
  end
183
189
 
184
- # Fold a prop / root_element_attributes value into the Draft.
185
- # Each Hash value / Array element may be a Proc; phase-gated.
186
- def absorb_input(draft, kind, input, instance, implied, phase:)
190
+ def absorb_input(draft, kind, input, instance, implied, phase:, alias_map: {})
187
191
  return if input.nil?
188
192
 
189
193
  kind_meta = Registry.fetch(kind)
190
194
  case input
191
- when Hash
192
- input.each do |key, raw|
195
+ in Hash => h
196
+ h.each do |key, raw|
193
197
  is_proc = raw.is_a?(Proc)
194
198
  next unless phase_allows?(is_proc, phase)
195
199
  absorbed = is_proc ? instance.instance_exec(&raw) : raw
196
200
  next if absorbed.nil?
197
- if kind_meta.keyed
201
+ if kind_meta.keyed?
198
202
  parsed = kind_meta.value_class.parse(key, absorbed, implied: implied, component_id: instance_id(instance))
199
203
  draft.public_send(:"add_#{kind}", parsed)
200
204
  else
201
- parsed = parse_entry(kind_meta, [key, absorbed], implied: implied, component_id: instance_id(instance))
205
+ entry = resolve_absorb_alias(kind, [key, absorbed], alias_map)
206
+ parsed = parse_entry(kind_meta, entry, implied: implied, component_id: instance_id(instance))
202
207
  draft.public_send(:"add_#{kind}", parsed) if parsed
203
208
  end
204
209
  end
205
- when Array
206
- input.each do |entry|
210
+ in Array => a
211
+ a.each do |entry|
207
212
  is_proc = entry.is_a?(Proc)
208
213
  next unless phase_allows?(is_proc, phase)
209
- parsed = absorb_one(kind_meta, entry, instance, implied)
214
+ parsed = absorb_one(kind_meta, entry, instance, implied, kind:, alias_map:)
210
215
  draft.public_send(:"add_#{kind}", parsed) if parsed
211
216
  end
212
217
  else
213
218
  is_proc = input.is_a?(Proc)
214
219
  return unless phase_allows?(is_proc, phase)
215
- parsed = absorb_one(kind_meta, input, instance, implied)
220
+ parsed = absorb_one(kind_meta, input, instance, implied, kind:, alias_map:)
216
221
  draft.public_send(:"add_#{kind}", parsed) if parsed
217
222
  end
218
223
  end
219
224
 
220
- # Element-level gate (raw boolean; parallels Declaration-level `phase_matches?`).
225
+ def resolve_absorb_alias(kind, entry, alias_map)
226
+ return entry unless kind == :actions && alias_map.any?
227
+ return entry unless entry.is_a?(Hash) && entry[:controller].is_a?(Symbol)
228
+ sym = entry[:controller]
229
+ unless alias_map.key?(sym)
230
+ raise ::Vident::DeclarationError, "Unknown controller alias :#{sym} in stimulus_actions input. Declared aliases: #{alias_map.keys.inspect}"
231
+ end
232
+ entry.merge(controller: alias_map[sym])
233
+ end
234
+
235
+ # Uses pattern matching so an unknown phase raises NoMatchingPatternError early.
221
236
  def phase_allows?(is_proc, phase)
222
237
  case phase
223
- when :all then true
224
- when :static then !is_proc
225
- when :procs then is_proc
238
+ in :all then true
239
+ in :static then !is_proc
240
+ in :procs then is_proc
226
241
  end
227
242
  end
228
243
 
229
- def absorb_one(kind_meta, entry, instance, implied)
244
+ def absorb_one(kind_meta, entry, instance, implied, kind: nil, alias_map: {})
230
245
  entry = instance.instance_exec(&entry) if entry.is_a?(Proc)
231
246
  return nil if entry.nil?
232
247
  return entry if entry.is_a?(kind_meta.value_class)
233
248
 
249
+ entry = resolve_absorb_alias(kind, entry, alias_map) if kind
234
250
  parse_entry(kind_meta, entry, implied: implied, component_id: instance_id(instance))
235
251
  end
236
252
 
237
253
  def parse_entry(kind_meta, entry, implied:, component_id:)
238
254
  case entry
239
255
  when Hash
240
- if kind_meta.keyed
256
+ if kind_meta.keyed?
241
257
  first_key, first_val = entry.first
242
258
  kind_meta.value_class.parse(first_key, first_val, implied: implied, component_id: component_id)
243
259
  else
@@ -250,15 +266,13 @@ module Vident2
250
266
  end
251
267
  end
252
268
 
253
- # Evaluate proc args in the instance binding. Only nil drops
254
- # false / blank strings / empty collections reach the parser.
269
+ # Only nil drops false, blank strings, and empty collections reach the parser.
255
270
  def resolve_args(args, instance)
256
271
  resolved = args.map { |arg| arg.is_a?(Proc) ? instance.instance_exec(&arg) : arg }
257
272
  return nil if resolved.any?(&:nil?)
258
273
  resolved
259
274
  end
260
275
 
261
- # Values accept the raw in args (proc or literal) or meta (`static:`).
262
276
  def resolve_value_meta(decl, instance)
263
277
  return decl.meta[:static] if decl.meta.key?(:static)
264
278
  return nil if decl.args.empty?
@@ -279,7 +293,6 @@ module Vident2
279
293
 
280
294
  def key_for_parse(key) = key
281
295
 
282
- # Controller parse takes `as:` as a kwarg, not a positional.
283
296
  def meta_for_controller(meta) = meta.slice(:as)
284
297
 
285
298
  def instance_ivar(instance, name)
@@ -287,13 +300,10 @@ module Vident2
287
300
  instance.instance_variable_get(name)
288
301
  end
289
302
 
290
- # Raw ivar calling `#id` would trigger auto-generation, and
291
- # outlet auto-selectors include the `#<id>` prefix only when the
292
- # user explicitly set one.
303
+ # Must match the mutation-API path (#id, memoised) so DSL outlet
304
+ # auto-selectors and runtime-added outlets scope identically.
293
305
  def instance_id(instance)
294
- return nil unless instance.instance_variable_defined?(:@id)
295
- raw = instance.instance_variable_get(:@id)
296
- raw.presence
306
+ instance.id
297
307
  end
298
308
 
299
309
  def read_prop(instance, name)
@@ -2,14 +2,8 @@
2
2
 
3
3
  require_relative "declaration"
4
4
 
5
- module Vident2
5
+ module Vident
6
6
  module Internals
7
- # @api private
8
- # Fluent chain returned by `target(...)` inside a `stimulus do` block.
9
- # The only current chain method is `.when` (conditional inclusion);
10
- # the target itself has no other DSL-facing knobs.
11
- #
12
- # target(:row).when { @rows.any? }
13
7
  class TargetBuilder
14
8
  def initialize(*args)
15
9
  @args = args
@@ -5,9 +5,9 @@ require "digest/md5"
5
5
 
6
6
  module Vident
7
7
  class StableId
8
- class GeneratorNotSetError < StandardError; end
8
+ class GeneratorNotSetError < ::Vident::ConfigurationError; end
9
9
 
10
- class StrategyNotConfiguredError < StandardError; end
10
+ class StrategyNotConfiguredError < ::Vident::ConfigurationError; end
11
11
 
12
12
  RANDOM_FALLBACK = ->(generator) do
13
13
  return Random.hex(16) unless generator
@@ -25,7 +25,7 @@ module Vident
25
25
  end
26
26
 
27
27
  class << self
28
- # Callable(generator_or_nil) -> String. Starts nil; host app must configure it.
28
+ # Callable(generator_or_nil) -> String. Host app must configure before first render.
29
29
  attr_accessor :strategy
30
30
 
31
31
  def set_current_sequence_generator(seed:)
@@ -3,18 +3,14 @@
3
3
  require "literal"
4
4
  require_relative "naming"
5
5
  require_relative "controller"
6
+ require_relative "base"
6
7
 
7
- module Vident2
8
+ module Vident
8
9
  module Stimulus
9
10
  # `data-action` fragment: single action descriptor like
10
11
  # `"click->admin--users#handleClick"`.
11
- #
12
- # Fields folded in from v1's `StimulusAction::Descriptor` — there's no
13
- # separate Descriptor class in V2; Hash DSL input parses directly into
14
- # an `Action`.
15
- class Action < ::Literal::Data
16
- # Stimulus action options (`:once`, `:prevent`, etc.). Keep in sync
17
- # with https://stimulus.hotwired.dev/reference/actions#options.
12
+ class Action < Base
13
+ # Keep in sync with https://stimulus.hotwired.dev/reference/actions#options.
18
14
  VALID_OPTIONS = %i[once prevent stop passive !passive capture self].freeze
19
15
 
20
16
  prop :controller, Controller
@@ -24,15 +20,12 @@ module Vident2
24
20
  prop :keyboard, _Nilable(String), default: nil
25
21
  prop :window, _Boolean, default: false
26
22
 
27
- # `.parse(*args, implied:)` grammar mirrors v1 `StimulusAction#parse_arguments`:
28
- # (Symbol) -> :method on implied controller, no event
29
- # (String) -> pre-qualified "event->ctrl#method" / "ctrl#method"
30
- # (Hash) -> keyword descriptor (method:/event:/...)
31
- # (Symbol, Symbol) -> (event, method) on implied
32
- # (String, Symbol) -> (controller_path, method) — no event
33
- # (Symbol, String, Symbol) -> (event, controller_path, method)
34
23
  def self.parse(*args, implied:, component_id: nil)
35
24
  case args
25
+ in [Action => a]
26
+ a
27
+ in [Symbol => event, Action => a]
28
+ a.with(event: event.to_s)
36
29
  in [Hash => h]
37
30
  from_descriptor(h, implied: implied)
38
31
  in [Symbol => method_sym]
@@ -62,11 +55,10 @@ module Vident2
62
55
  event: event.to_s
63
56
  )
64
57
  else
65
- raise ::Vident2::ParseError, "Action.parse: invalid arguments #{args.inspect}"
58
+ raise ::Vident::ParseError, "Action.parse: invalid arguments #{args.inspect}"
66
59
  end
67
60
  end
68
61
 
69
- # Serialised descriptor, e.g. `"click.esc:prevent@window->foo--bar#handle"`.
70
62
  def to_s
71
63
  head =
72
64
  if event
@@ -86,18 +78,15 @@ module Vident2
86
78
  def to_h = {action: to_s}
87
79
  alias_method :to_hash, :to_h
88
80
 
89
- # Actions space-join under a single `:action` key, preserving order.
90
81
  def self.to_data_hash(actions)
91
82
  return {} if actions.empty?
92
83
  {action: actions.map(&:to_s).join(" ")}
93
84
  end
94
85
 
95
- # `.parse({event:, method:, controller:, options:, keyboard:, window:})`
96
- # Keyword-descriptor entry point, used by the DSL Hash form.
97
86
  def self.from_descriptor(h, implied:)
98
87
  invalid_options = Array(h[:options]) - VALID_OPTIONS
99
88
  unless invalid_options.empty?
100
- raise ::Vident2::ParseError,
89
+ raise ::Vident::ParseError,
101
90
  "Action.parse: invalid option(s) #{invalid_options.inspect}. Valid: #{VALID_OPTIONS.inspect}"
102
91
  end
103
92
 
@@ -114,9 +103,7 @@ module Vident2
114
103
  )
115
104
  end
116
105
 
117
- # Pre-qualified string form, e.g. `"click->admin/users#show"` or
118
- # `"admin--users#show"`. Pass-through: the controller segment is NOT
119
- # re-stimulized. Flagged for deprecation.
106
+ # Pass-through: the controller segment is NOT re-stimulized.
120
107
  def self.parse_qualified_string(s)
121
108
  if s.include?("->")
122
109
  event_part, ctrl_method = s.split("->", 2)
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "literal"
4
+ require_relative "combinable"
5
+
6
+ module Vident
7
+ module Stimulus
8
+ # Shared frozen-value base for the Stimulus primitive value classes
9
+ # (Action, Target, Controller, Outlet, Value, Param, ClassMap).
10
+ # Provides `Combinable` (`with`, pattern-matching `deconstruct_keys`)
11
+ # and a default `to_data_hash(items)` that subclasses override when
12
+ # they need non-trivial collection semantics (space-join etc.).
13
+ #
14
+ # Subclasses still override `to_h` / `to_hash` per class — Literal
15
+ # auto-generates a prop-hash `to_h` from the prop DSL that would
16
+ # shadow any default here, so the data-attribute-shape override
17
+ # must live on each concrete class.
18
+ class Base < ::Literal::Data
19
+ include Combinable
20
+
21
+ def self.to_data_hash(items)
22
+ items.to_h(&:to_data_pair)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -2,19 +2,14 @@
2
2
 
3
3
  require "literal"
4
4
  require_relative "naming"
5
+ require_relative "base"
5
6
  require_relative "controller"
6
7
 
7
- module Vident2
8
+ module Vident
8
9
  module Stimulus
9
10
  # `data-<ctrl>-<name>-class` fragment — a named CSS class set readable
10
- # on the JS side as `this.<name>Class`. Renamed from v1's
11
- # `StimulusClass` (which collided with Ruby's `Class` — uncomfortable
12
- # to type in user code).
13
- #
14
- # `css` holds the final serialised string form (space-joined); the
15
- # parser normalises String / Array-of-String / Array-of-anything inputs
16
- # down to one shape so the Draft/Plan doesn't have to care.
17
- class ClassMap < ::Literal::Data
11
+ # on the JS side as `this.<name>Class`.
12
+ class ClassMap < Base
18
13
  prop :controller, Controller
19
14
  prop :name, String
20
15
  prop :css, String
@@ -34,7 +29,7 @@ module Vident2
34
29
  css: normalize_css(css_input)
35
30
  )
36
31
  else
37
- raise ::Vident2::ParseError, "ClassMap.parse: invalid arguments #{args.inspect}"
32
+ raise ::Vident::ParseError, "ClassMap.parse: invalid arguments #{args.inspect}"
38
33
  end
39
34
  end
40
35
 
@@ -45,7 +40,7 @@ module Vident2
45
40
  when Array
46
41
  input.map(&:to_s).reject(&:empty?).join(" ")
47
42
  else
48
- raise ::Vident2::ParseError, "ClassMap.parse: css must be a String or Array, got #{input.class}"
43
+ raise ::Vident::ParseError, "ClassMap.parse: css must be a String or Array, got #{input.class}"
49
44
  end
50
45
  end
51
46
 
@@ -57,13 +52,6 @@ module Vident2
57
52
 
58
53
  def to_h = {data_attribute_key => css}
59
54
  alias_method :to_hash, :to_h
60
-
61
- def self.to_data_hash(maps)
62
- maps.each_with_object({}) do |m, acc|
63
- key, str = m.to_data_pair
64
- acc[key] = str
65
- end
66
- end
67
55
  end
68
56
  end
69
57
  end
@@ -1,12 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Vident2
3
+ module Vident
4
4
  module Stimulus
5
- # Tiny aggregation wrapper for the plural `stimulus_<kind>s` parsers.
6
- # Parametric over `kind`: the kind decides the per-element combining
7
- # rule via its value class's `.to_data_hash`. Users interact with
8
- # this object by splatting `{**component.stimulus_targets(...)}` into
9
- # a `data:` option on a tag, so `#to_h` is the single required shape.
10
5
  class Collection
11
6
  include Enumerable
12
7
 
@@ -18,14 +13,17 @@ module Vident2
18
13
  end
19
14
 
20
15
  def each(&block) = @items.each(&block)
16
+
21
17
  def to_a = @items.dup
18
+
22
19
  def size = @items.size
20
+
23
21
  def length = @items.size
22
+
24
23
  def empty? = @items.empty?
24
+
25
25
  def any? = @items.any?
26
26
 
27
- # Delegates to the kind's `.to_data_hash` — same path AttributeWriter
28
- # uses at render time.
29
27
  def to_h
30
28
  @kind.value_class.to_data_hash(@items)
31
29
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ module Stimulus
5
+ # Shared `with(**overrides)` combinator for the frozen value classes.
6
+ # Mirrors Ruby's `Data.define#with` convention (which Literal::Data
7
+ # doesn't ship) so callers can decorate a value object without
8
+ # mutating it.
9
+ module Combinable
10
+ # Canonical Ruby Data-object hooks. The value classes override `to_h`
11
+ # to serialise to their data-attribute shape; without this module,
12
+ # `deconstruct_keys` would inherit that override and pattern-matching
13
+ # (`case a; in {event:}`) would silently fail.
14
+ def deconstruct_keys(keys)
15
+ h = self.class.literal_properties.properties_index.each_with_object({}) do |(name, _), acc|
16
+ acc[name.to_sym] = public_send(name)
17
+ end
18
+ keys ? h.slice(*keys) : h
19
+ end
20
+
21
+ def deconstruct
22
+ deconstruct_keys(nil).values
23
+ end
24
+
25
+ def with(**overrides)
26
+ self.class.new(**deconstruct_keys(nil).merge(overrides))
27
+ end
28
+ end
29
+ end
30
+ end