weft 0.1.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +134 -28
- data/README.md +46 -23
- data/docs/app-patterns.md +8 -7
- data/docs/arbre.md +49 -18
- data/docs/configuration.md +42 -35
- data/docs/dsl.md +356 -105
- data/docs/error-handling.md +64 -24
- data/docs/examples/active-search.md +10 -10
- data/docs/examples/browser-dialogs.md +10 -10
- data/docs/examples/bulk-update.md +11 -11
- data/docs/examples/click-to-edit.md +14 -14
- data/docs/examples/click-to-load.md +8 -8
- data/docs/examples/delete-row.md +17 -19
- data/docs/examples/edit-row.md +17 -14
- data/docs/examples/file-upload.md +5 -5
- data/docs/examples/infinite-scroll.md +8 -8
- data/docs/examples/inline-expansion.md +8 -8
- data/docs/examples/inline-validation.md +17 -17
- data/docs/examples/lazy-loading.md +8 -8
- data/docs/examples/live-ticker.md +1 -1
- data/docs/examples/modal-dialog.md +3 -3
- data/docs/examples/progress-bar.md +1 -1
- data/docs/examples/reset-user-input.md +7 -7
- data/docs/examples/tabs.md +4 -4
- data/docs/examples/tooltip.md +8 -8
- data/docs/examples/updating-other-content.md +9 -9
- data/docs/examples/value-select.md +11 -11
- data/docs/params.md +112 -0
- data/docs/routing.md +13 -13
- data/docs/tutorial.md +46 -48
- data/lib/weft/action.rb +4 -2
- data/lib/weft/autoloading.rb +69 -0
- data/lib/weft/component.rb +97 -31
- data/lib/weft/configuration.rb +37 -5
- data/lib/weft/context/expansion.rb +184 -0
- data/lib/weft/context/interception.rb +22 -2
- data/lib/weft/context/modifiers.rb +78 -0
- data/lib/weft/context/traversal.rb +80 -0
- data/lib/weft/context/wiring.rb +85 -0
- data/lib/weft/context.rb +70 -164
- data/lib/weft/defaults/error_component.rb +57 -21
- data/lib/weft/defaults/error_page.rb +12 -10
- data/lib/weft/defaults/not_found_component.rb +14 -12
- data/lib/weft/defaults/not_found_page.rb +9 -8
- data/lib/weft/dsl/actions.rb +9 -9
- data/lib/weft/dsl/inclusions.rb +48 -11
- data/lib/weft/dsl/params.rb +265 -0
- data/lib/weft/dsl/recoveries.rb +36 -6
- data/lib/weft/dsl/sandbox.rb +26 -0
- data/lib/weft/dsl/triggers.rb +28 -8
- data/lib/weft/dsl/updates.rb +31 -8
- data/lib/weft/error.rb +12 -1
- data/lib/weft/page/assets.rb +222 -0
- data/lib/weft/page/head.rb +87 -0
- data/lib/weft/page.rb +55 -239
- data/lib/weft/params/assembly.rb +170 -0
- data/lib/weft/params.rb +138 -0
- data/lib/weft/presets.rb +96 -0
- data/lib/weft/redirect.rb +7 -7
- data/lib/weft/registry/eligibility.rb +5 -19
- data/lib/weft/registry.rb +58 -18
- data/lib/weft/resolver.rb +48 -20
- data/lib/weft/router/actions.rb +106 -22
- data/lib/weft/router/errors.rb +223 -83
- data/lib/weft/router/oob_includes.rb +202 -17
- data/lib/weft/router/streaming.rb +86 -20
- data/lib/weft/router.rb +33 -25
- data/lib/weft/version.rb +1 -1
- data/lib/weft.rb +37 -24
- metadata +32 -8
- data/lib/weft/attributes.rb +0 -65
- data/lib/weft/dsl/attributes.rb +0 -43
- data/lib/weft/shorthands.rb +0 -57
data/lib/weft/router/errors.rb
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
require "weft/context"
|
|
7
|
+
require "weft/dsl/sandbox"
|
|
8
|
+
require "weft/error"
|
|
9
|
+
require "weft/page"
|
|
10
|
+
require "weft/params"
|
|
11
|
+
require "weft/resolver"
|
|
12
|
+
|
|
3
13
|
module Weft
|
|
4
14
|
class Router
|
|
5
15
|
# Error-and-recovery slice of the Router. Walks recovers chains for
|
|
@@ -8,32 +18,39 @@ module Weft
|
|
|
8
18
|
# knob (D1, D2 no-op, D3 no-op), and houses the safety-net fallbacks
|
|
9
19
|
# for unmatched errors and recovery handlers that themselves raise.
|
|
10
20
|
#
|
|
11
|
-
# Also owns the schema-gated auto-injection of recovery
|
|
12
|
-
# (:exception, :request_path, :status_code, :
|
|
21
|
+
# Also owns the schema-gated auto-injection of recovery params
|
|
22
|
+
# (:exception, :request_path, :status_code, :component_tag, :retry_url,
|
|
23
|
+
# :attempts_remaining) and the identity a recovery fragment inherits from
|
|
24
|
+
# the component it stands in for.
|
|
13
25
|
#
|
|
14
|
-
# Depends on Router internals: `
|
|
15
|
-
# `
|
|
26
|
+
# Depends on Router internals: `headers`, `status`, `request`,
|
|
27
|
+
# `redirect`, `htmx_request?`.
|
|
16
28
|
module Errors # rubocop:disable Metrics/ModuleLength
|
|
17
|
-
# Each entry: { redirect_safe: true } means the auto-injected
|
|
29
|
+
# Each entry: { redirect_safe: true } means the auto-injected param is
|
|
18
30
|
# included even when building a redirect URL. The component-context
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{ key: :
|
|
31
|
+
# params (:exception, :retry_url) are not redirect-safe — Exception
|
|
32
|
+
# objects aren't URL-encodable, and the destination Page rebuilds its
|
|
33
|
+
# own URL.
|
|
34
|
+
# :attempts_remaining is populated only on the SSE push path (nil, and
|
|
35
|
+
# therefore skipped, on HTTP recoveries) — its presence tells a recovery
|
|
36
|
+
# component it is rendering inside a live stream, counting down to the
|
|
37
|
+
# close; 0 marks the final frame.
|
|
38
|
+
AUTO_INJECTED_PARAMS = [
|
|
39
|
+
{ key: :exception, redirect_safe: false },
|
|
40
|
+
{ key: :request_path, redirect_safe: true },
|
|
41
|
+
{ key: :status_code, redirect_safe: true },
|
|
42
|
+
{ key: :component_tag, redirect_safe: false },
|
|
43
|
+
{ key: :retry_url, redirect_safe: false },
|
|
44
|
+
{ key: :attempts_remaining, redirect_safe: false }
|
|
28
45
|
].freeze
|
|
29
|
-
private_constant :
|
|
46
|
+
private_constant :AUTO_INJECTED_PARAMS
|
|
30
47
|
|
|
31
48
|
private
|
|
32
49
|
|
|
33
50
|
# Walk a Page-context recovers chain (B1, B2, C1 page-context, C4).
|
|
34
51
|
# `originating_page_class` is nil for routing misses (no specific Page);
|
|
35
52
|
# the gem-default chain on Weft::Page handles those.
|
|
36
|
-
def handle_page_chain_failure(error, originating_page_class:,
|
|
53
|
+
def handle_page_chain_failure(error, originating_page_class:, originating_params: nil, originating_wire: nil)
|
|
37
54
|
root = originating_page_class || Weft::Page
|
|
38
55
|
entry = root.recovery_for(error)
|
|
39
56
|
return page_safety_net(error) unless entry
|
|
@@ -43,36 +60,41 @@ module Weft
|
|
|
43
60
|
return htmx_redirect_to_error_page(error) if d1_applies?(entry, error)
|
|
44
61
|
|
|
45
62
|
target = root.resolve_recovery_target(entry)
|
|
46
|
-
|
|
63
|
+
block_delta = invoke_recovery_block(entry, originating_params || Weft::Params.new({}), error)
|
|
47
64
|
|
|
48
65
|
if page_target?(target)
|
|
49
|
-
dispatch_page_recovery(target,
|
|
66
|
+
dispatch_page_recovery(target, block_delta, error, entry, universe: originating_wire)
|
|
50
67
|
else
|
|
51
|
-
render_recovery_component(target,
|
|
68
|
+
render_recovery_component(target, block_delta, error,
|
|
69
|
+
component_ctx: { status: recovery_status(error, entry) },
|
|
70
|
+
universe: originating_wire)
|
|
52
71
|
end
|
|
53
72
|
end
|
|
54
73
|
|
|
55
|
-
def recovery_merged_attrs(entry, originating_attrs, error)
|
|
56
|
-
block_result = invoke_recovery_block(entry, originating_attrs, error)
|
|
57
|
-
originating_attrs.merge(block_result)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
74
|
# Render or redirect for a Page recovery target. htmx requests get the
|
|
61
75
|
# Page's body content as a fragment; traditional requests get the full
|
|
62
|
-
# document. Status comes from the exception.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
# document. Status comes from the exception, or the entry's override.
|
|
77
|
+
# The page renders against the request's universe; the recovery values
|
|
78
|
+
# ride as overlays (one universe per request).
|
|
79
|
+
def dispatch_page_recovery(page_class, block_delta, error, entry = nil, universe: nil)
|
|
80
|
+
wire_status = recovery_status(error, entry)
|
|
81
|
+
overlays = block_delta.merge(auto_param_overlay(error, { status: wire_status }))
|
|
82
|
+
status wire_status
|
|
83
|
+
wire = universe || filtered_params
|
|
84
|
+
htmx_request? ? page_body_html(page_class, wire, overlays) : render_full_page(page_class, wire, overlays)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def render_full_page(page_class, wire_params, overlays)
|
|
88
|
+
klass = page_class
|
|
89
|
+
Weft::Context.new({}, nil, wire_params: wire_params, overlays: overlays) { insert_tag(klass) }.to_s
|
|
68
90
|
end
|
|
69
91
|
|
|
70
92
|
# Extract the rendered HTML inside a Page's <body>. For htmx fragment
|
|
71
93
|
# responses to full-document failures — the surrounding doc shell is
|
|
72
94
|
# already on the client; only the body content should swap.
|
|
73
|
-
def page_body_html(page_class,
|
|
95
|
+
def page_body_html(page_class, wire_params, overlays)
|
|
74
96
|
klass = page_class
|
|
75
|
-
ctx = Weft::Context.new({}, nil) { insert_tag(klass
|
|
97
|
+
ctx = Weft::Context.new({}, nil, wire_params: wire_params, overlays: overlays) { insert_tag(klass) }
|
|
76
98
|
page_instance = ctx.children.first
|
|
77
99
|
body_el = page_instance.children.find { |c| c.respond_to?(:tag_name) && c.tag_name == "body" }
|
|
78
100
|
body_el ? body_el.children.join : page_instance.to_s
|
|
@@ -92,14 +114,17 @@ module Weft
|
|
|
92
114
|
end
|
|
93
115
|
end
|
|
94
116
|
|
|
95
|
-
|
|
117
|
+
# +state+ is the bag the request had composed when it broke — what the
|
|
118
|
+
# recovery block reads. Bags are for blocks; the wire hash below is for
|
|
119
|
+
# URLs, and stays a plain hash so building one can't force a derivation.
|
|
120
|
+
def render_error(component_class, state, error)
|
|
96
121
|
entry = component_class.recovery_for(error)
|
|
97
122
|
if entry
|
|
98
123
|
# D1: htmx + :redirect knob + gem-default fallthrough → HX-Redirect.
|
|
99
124
|
return htmx_redirect_to_error_page(error) if d1_applies?(entry, error)
|
|
100
125
|
|
|
101
126
|
begin
|
|
102
|
-
return render_recovery(component_class, entry,
|
|
127
|
+
return render_recovery(component_class, entry, state, error)
|
|
103
128
|
rescue StandardError => e
|
|
104
129
|
# The recovery handler itself raised — fall through to the hardcoded
|
|
105
130
|
# safety net rather than recursing. Surface the original error;
|
|
@@ -109,7 +134,14 @@ module Weft
|
|
|
109
134
|
end
|
|
110
135
|
|
|
111
136
|
status recovery_status(error)
|
|
112
|
-
render_generic_error(component_class,
|
|
137
|
+
render_generic_error(component_class, error)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The failing component's own declared wire params. Never the bag: this
|
|
141
|
+
# feeds retry URLs and redirect query strings, where materializing a
|
|
142
|
+
# derivation would run user code in the middle of error handling.
|
|
143
|
+
def error_wire_params(component_class)
|
|
144
|
+
Weft::Resolver.resolve(component_class, filtered_params)
|
|
113
145
|
end
|
|
114
146
|
|
|
115
147
|
# D1 applies when: the htmx_errors knob is :redirect, the request is htmx,
|
|
@@ -124,44 +156,114 @@ module Weft
|
|
|
124
156
|
|
|
125
157
|
def htmx_redirect_to_error_page(error)
|
|
126
158
|
target = Weft.configuration.error_page
|
|
127
|
-
|
|
128
|
-
headers["HX-Redirect"] = target.redirect_url(
|
|
159
|
+
params_for_url = inject_auto_params(target, {}, error, on_redirect: true)
|
|
160
|
+
headers["HX-Redirect"] = target.redirect_url(params_for_url)
|
|
129
161
|
status 204
|
|
130
162
|
""
|
|
131
163
|
end
|
|
132
164
|
|
|
133
|
-
# Execute a matched recovery entry: invoke block,
|
|
134
|
-
#
|
|
165
|
+
# Execute a matched recovery entry: invoke the block, then dispatch to
|
|
166
|
+
# either a Page-redirect (HX-Redirect / 302) or a fragment render
|
|
135
167
|
# depending on the target's type.
|
|
136
|
-
def render_recovery(component_class, entry,
|
|
137
|
-
block_result = invoke_recovery_block(entry,
|
|
138
|
-
merged_attrs = resolved_attrs.merge(block_result)
|
|
168
|
+
def render_recovery(component_class, entry, state, error)
|
|
169
|
+
block_result = invoke_recovery_block(entry, state, error)
|
|
139
170
|
target = component_class.resolve_recovery_target(entry)
|
|
171
|
+
wire = error_wire_params(component_class)
|
|
140
172
|
component_ctx = {
|
|
141
|
-
originating_id: component_class
|
|
142
|
-
|
|
173
|
+
originating_id: resolved_dom_id(component_class,
|
|
174
|
+
unbuilt_instance(component_class, filtered_params), wire),
|
|
175
|
+
originating_tag: component_tag_for(component_class),
|
|
176
|
+
retry_url: compute_retry_url(component_class, wire),
|
|
177
|
+
status: recovery_status(error, entry)
|
|
143
178
|
}
|
|
144
179
|
|
|
145
180
|
if page_target?(target)
|
|
146
|
-
redirect_to_recovery_page(target,
|
|
181
|
+
redirect_to_recovery_page(target, wire.merge(block_result), error, component_ctx)
|
|
147
182
|
else
|
|
148
|
-
render_recovery_component(target,
|
|
183
|
+
render_recovery_component(target, block_result, error, component_ctx: component_ctx)
|
|
149
184
|
end
|
|
150
185
|
end
|
|
151
186
|
|
|
187
|
+
# The failing component's wrapper tag, so a recovery fragment can render
|
|
188
|
+
# as a like-for-like element (a div swapped into <tr> position is invalid
|
|
189
|
+
# table content). `allocate` skips initialize — no param resolution, so
|
|
190
|
+
# reading the tag can't re-raise the failure; a tag_name that depends on
|
|
191
|
+
# instance state falls back to nil (target renders its own default).
|
|
192
|
+
def component_tag_for(component_class)
|
|
193
|
+
component_class.allocate.tag_name
|
|
194
|
+
rescue StandardError
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# An instance constructed only to be asked about itself. Params resolve
|
|
199
|
+
# at construction, so it answers for its own identity and URL before
|
|
200
|
+
# `build` runs, and unlike the class-level derivation it honors a
|
|
201
|
+
# `weft_dom_id` override. nil when the construction itself is what
|
|
202
|
+
# raises. Where the same component goes on to render, pass that instance
|
|
203
|
+
# rather than making a second one — the two would carry separate bags,
|
|
204
|
+
# and a derivation behind a declared param would run in each.
|
|
205
|
+
def unbuilt_instance(component_class, wire_params, overlays: {}, branch_bag: nil)
|
|
206
|
+
component_class.new(Weft::Context.new({}, nil, wire_params: wire_params,
|
|
207
|
+
overlays: overlays, branch_bag: branch_bag))
|
|
208
|
+
rescue StandardError
|
|
209
|
+
nil
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# The DOM id a component wears, asked of an unbuilt instance first and
|
|
213
|
+
# falling back to the class-level derivation from an already-resolved
|
|
214
|
+
# params hash — that covers a construction that raises before the
|
|
215
|
+
# instance exists.
|
|
216
|
+
#
|
|
217
|
+
# Identity that raises when *asked* does not fall through to the
|
|
218
|
+
# class-level derivation, on purpose: a raising `weft_dom_id` is an
|
|
219
|
+
# override, and the convention it overrode would answer with a
|
|
220
|
+
# different id — landing the fragment on some other component's
|
|
221
|
+
# element. Landing nowhere beats landing somewhere wrong.
|
|
222
|
+
def resolved_dom_id(component_class, instance, fallback_params = nil)
|
|
223
|
+
return instance.weft_dom_id if instance
|
|
224
|
+
return component_class.weft_dom_id_for(fallback_params) if fallback_params
|
|
225
|
+
|
|
226
|
+
unresolved_dom_id(component_class)
|
|
227
|
+
rescue StandardError
|
|
228
|
+
unresolved_dom_id(component_class)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Identity can itself raise — a `weft_dom_id` reading a record deleted
|
|
232
|
+
# between renders — and the id it would have had is unrecoverable.
|
|
233
|
+
#
|
|
234
|
+
# TEMPORARY: an unresolvable identity gets a unique throwaway, so it can
|
|
235
|
+
# never collide with or displace another fragment. Such a fragment simply
|
|
236
|
+
# lands nowhere, which is the honest outcome when we cannot say where it
|
|
237
|
+
# belongs. Component identity is due a design pass of its own; this is a
|
|
238
|
+
# placeholder until it gets one.
|
|
239
|
+
def unresolved_dom_id(component_class)
|
|
240
|
+
"#{component_class.weft_dom_id_for}-unresolved-#{SecureRandom.hex(4)}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# A recovery fragment stands in the failing component's place, so it
|
|
244
|
+
# wears the failing component's id — a swap addressed anywhere else
|
|
245
|
+
# lands somewhere else, or nowhere at all. A page-context failure has no
|
|
246
|
+
# originating component, and its target keeps its own id.
|
|
247
|
+
def claim_dom_id(component, dom_id)
|
|
248
|
+
component.set_attribute("id", dom_id) if dom_id
|
|
249
|
+
component
|
|
250
|
+
end
|
|
251
|
+
|
|
152
252
|
# GET URL to render the failing component fresh: its resolved_component_path
|
|
153
|
-
# plus the resolved
|
|
253
|
+
# plus the resolved params as query string. For action endpoints this gives
|
|
154
254
|
# the underlying component's view (not the action URL).
|
|
155
|
-
def compute_retry_url(component_class,
|
|
255
|
+
def compute_retry_url(component_class, resolved_params)
|
|
156
256
|
url = component_class.resolved_component_path
|
|
157
|
-
|
|
158
|
-
|
|
257
|
+
query = resolved_params.compact
|
|
258
|
+
query.empty? ? url : "#{url}?#{URI.encode_www_form(query)}"
|
|
159
259
|
end
|
|
160
260
|
|
|
161
|
-
|
|
261
|
+
# The block reads the state the request had composed, exactly as the
|
|
262
|
+
# build that failed would have read it.
|
|
263
|
+
def invoke_recovery_block(entry, state, error)
|
|
162
264
|
return {} unless entry[:block]
|
|
163
265
|
|
|
164
|
-
result =
|
|
266
|
+
result = Weft::DSL::Sandbox.run(state, error, &entry[:block])
|
|
165
267
|
result.is_a?(Hash) ? result : {}
|
|
166
268
|
end
|
|
167
269
|
|
|
@@ -169,9 +271,10 @@ module Weft
|
|
|
169
271
|
target.is_a?(Class) && defined?(Weft::Page) && target <= Weft::Page
|
|
170
272
|
end
|
|
171
273
|
|
|
172
|
-
def redirect_to_recovery_page(target,
|
|
173
|
-
|
|
174
|
-
|
|
274
|
+
def redirect_to_recovery_page(target, merged_params, error, component_ctx = {})
|
|
275
|
+
params_for_url = inject_auto_params(target, merged_params, error,
|
|
276
|
+
on_redirect: true, component_ctx: component_ctx)
|
|
277
|
+
url = target.redirect_url(params_for_url)
|
|
175
278
|
|
|
176
279
|
if request.env["HTTP_HX_REQUEST"]
|
|
177
280
|
headers["HX-Redirect"] = url
|
|
@@ -182,26 +285,60 @@ module Weft
|
|
|
182
285
|
end
|
|
183
286
|
end
|
|
184
287
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
288
|
+
# Fragment-only sibling of render_recovery for SSE push failures. Walks
|
|
289
|
+
# the chain via component_recovery_for (a stream swap can't take a Page),
|
|
290
|
+
# and returns the recovery target's children-only HTML — the client swap
|
|
291
|
+
# is innerHTML into the original component's persistent wrapper, so the
|
|
292
|
+
# frame ships content, not wrapper (like-for-like with a normal push).
|
|
293
|
+
# No status (headers are long flushed on a live stream), no oob includes
|
|
294
|
+
# (their params presume the successful build that didn't happen), and no
|
|
295
|
+
# htmx_errors redirect knob (an HTTP-path concern). Returns nil when the
|
|
296
|
+
# chain yields nothing; the caller owns rescue and logging.
|
|
297
|
+
def render_push_recovery(component_class, state, error, attempts_remaining:)
|
|
298
|
+
entry = component_class.component_recovery_for(error)
|
|
299
|
+
return nil unless entry
|
|
300
|
+
|
|
301
|
+
block_delta = invoke_recovery_block(entry, state, error)
|
|
302
|
+
target = component_class.resolve_recovery_target(entry)
|
|
303
|
+
component_ctx = {
|
|
304
|
+
retry_url: compute_retry_url(component_class, error_wire_params(component_class)),
|
|
305
|
+
attempts_remaining: attempts_remaining,
|
|
306
|
+
status: recovery_status(error, entry)
|
|
307
|
+
}
|
|
308
|
+
overlays = block_delta.merge(auto_param_overlay(error, component_ctx))
|
|
309
|
+
build_component_with_wire(target, filtered_params, overlays: overlays).content
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# The target resolves its own schema from the request's universe; the
|
|
313
|
+
# recovery values — block delta plus the auto-injected params — ride
|
|
314
|
+
# as overlays, reaching any depth of the recovery render. The failing
|
|
315
|
+
# component's resolution never crowns a new universe, so its defaults
|
|
316
|
+
# and derivations can't leak into the target's.
|
|
317
|
+
def render_recovery_component(target, block_delta, error, component_ctx:, universe: nil)
|
|
318
|
+
overlays = block_delta.merge(auto_param_overlay(error, component_ctx))
|
|
319
|
+
status component_ctx.fetch(:status) { recovery_status(error) }
|
|
320
|
+
component = build_component_with_wire(target, universe || filtered_params, overlays: overlays)
|
|
321
|
+
claim_dom_id(component, component_ctx[:originating_id]).to_s
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# The auto-injected values as an overlay: non-nil values only — a nil
|
|
325
|
+
# here would read as "clear the wire's value," which absence must not do.
|
|
326
|
+
# Declaration-driven reads make schema-gating unnecessary on renders
|
|
327
|
+
# (an unread overlay key is inert); redirects still gate via
|
|
328
|
+
# inject_auto_params below.
|
|
329
|
+
def auto_param_overlay(error, component_ctx)
|
|
330
|
+
auto_param_values(error, component_ctx).compact
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Schema-gated auto-injection of recovery params for REDIRECT URLs. A
|
|
334
|
+
# recovers target opts in to receiving each value by declaring
|
|
335
|
+
# `param :<key>` — only keys the target's schema knows about (and only
|
|
336
|
+
# redirect-safe, non-nil values) ride the URL.
|
|
337
|
+
def inject_auto_params(target, base_params, error, on_redirect:, component_ctx: {})
|
|
338
|
+
schema = target.respond_to?(:params) ? target.params : {}
|
|
339
|
+
values = auto_param_values(error, component_ctx)
|
|
340
|
+
injected = base_params.dup
|
|
341
|
+
AUTO_INJECTED_PARAMS.each do |meta|
|
|
205
342
|
next if on_redirect && !meta[:redirect_safe]
|
|
206
343
|
next unless schema.key?(meta[:key])
|
|
207
344
|
|
|
@@ -211,23 +348,26 @@ module Weft
|
|
|
211
348
|
injected
|
|
212
349
|
end
|
|
213
350
|
|
|
214
|
-
def
|
|
351
|
+
def auto_param_values(error, component_ctx)
|
|
215
352
|
{
|
|
216
353
|
exception: error,
|
|
217
354
|
request_path: request.path,
|
|
218
|
-
status_code: recovery_status(error),
|
|
219
|
-
|
|
220
|
-
retry_url: component_ctx[:retry_url]
|
|
355
|
+
status_code: component_ctx[:status] || recovery_status(error),
|
|
356
|
+
component_tag: component_ctx[:originating_tag],
|
|
357
|
+
retry_url: component_ctx[:retry_url],
|
|
358
|
+
attempts_remaining: component_ctx[:attempts_remaining]
|
|
221
359
|
}
|
|
222
360
|
end
|
|
223
361
|
|
|
224
|
-
|
|
225
|
-
|
|
362
|
+
# The matched entry's status: override wins; otherwise the error's own
|
|
363
|
+
# semantics (HTTPError carries a status, anything else reports 500).
|
|
364
|
+
def recovery_status(error, entry = nil)
|
|
365
|
+
entry&.[](:status) || (error.is_a?(Weft::HTTPError) ? error.status : 500)
|
|
226
366
|
end
|
|
227
367
|
|
|
228
|
-
def render_generic_error(component_class,
|
|
368
|
+
def render_generic_error(component_class, error)
|
|
229
369
|
component_name = component_class.name || "Component"
|
|
230
|
-
retry_url = compute_retry_url(component_class,
|
|
370
|
+
retry_url = compute_retry_url(component_class, error_wire_params(component_class))
|
|
231
371
|
|
|
232
372
|
Weft::Context.new({}, nil) do
|
|
233
373
|
error_style = "padding:1rem; border:1px solid #fca5a5; border-radius:6px; " \
|