ruact 0.0.4 → 0.0.6
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/.github/workflows/ci.yml +54 -2
- data/.rubocop_todo.yml +3 -115
- data/CHANGELOG.md +68 -17
- data/bench/server_functions_dispatch_bench.rb +109 -142
- data/bench/server_functions_dispatch_bench.results.md +29 -0
- data/docs/internal/decisions/server-functions-api.md +402 -0
- data/lib/generators/ruact/install/install_generator.rb +310 -25
- data/lib/generators/ruact/install/templates/Procfile.dev.tt +2 -0
- data/lib/generators/ruact/install/templates/dev.tt +16 -0
- data/lib/generators/ruact/install/templates/package.json.tt +17 -0
- data/lib/generators/ruact/install/templates/vite.config.js.tt +3 -1
- data/lib/generators/ruact/scaffold/scaffold_attribute.rb +114 -0
- data/lib/generators/ruact/scaffold/scaffold_form_helpers.rb +114 -0
- data/lib/generators/ruact/scaffold/scaffold_generator.rb +491 -0
- data/lib/generators/ruact/scaffold/scaffold_shadcn_preflight.rb +229 -0
- data/lib/generators/ruact/scaffold/templates/components/DeleteDialog.tsx.tt +104 -0
- data/lib/generators/ruact/scaffold/templates/components/Form.tsx.tt +212 -0
- data/lib/generators/ruact/scaffold/templates/components/List.tsx.tt +338 -0
- data/lib/generators/ruact/scaffold/templates/components/agnostic/DeleteDialog.tsx.tt +92 -0
- data/lib/generators/ruact/scaffold/templates/components/agnostic/Form.tsx.tt +174 -0
- data/lib/generators/ruact/scaffold/templates/components/agnostic/List.tsx.tt +253 -0
- data/lib/generators/ruact/scaffold/templates/controller.rb.tt +130 -0
- data/lib/generators/ruact/scaffold/templates/queries/application_query.rb.tt +13 -0
- data/lib/generators/ruact/scaffold/templates/queries/query.rb.tt +59 -0
- data/lib/generators/ruact/scaffold/templates/request_spec.rb.tt +77 -0
- data/lib/generators/ruact/scaffold/templates/views/edit.html.erb.tt +7 -0
- data/lib/generators/ruact/scaffold/templates/views/index.html.erb.tt +6 -0
- data/lib/generators/ruact/scaffold/templates/views/new.html.erb.tt +5 -0
- data/lib/generators/ruact/scaffold/templates/views/show.html.erb.tt +16 -0
- data/lib/ruact/client_manifest.rb +37 -36
- data/lib/ruact/component_contract.rb +115 -0
- data/lib/ruact/configuration.rb +80 -28
- data/lib/ruact/controller.rb +69 -421
- data/lib/ruact/doctor.rb +133 -4
- data/lib/ruact/erb_preprocessor.rb +65 -12
- data/lib/ruact/erb_preprocessor_hook.rb +4 -1
- data/lib/ruact/errors.rb +30 -44
- data/lib/ruact/html_converter.rb +22 -1
- data/lib/ruact/railtie.rb +56 -200
- data/lib/ruact/server.rb +28 -6
- data/lib/ruact/server_functions/codegen.rb +49 -188
- data/lib/ruact/server_functions/codegen_v2.rb +23 -6
- data/lib/ruact/server_functions/codegen_v2_query_params.rb +140 -0
- data/lib/ruact/server_functions/error_payload.rb +1 -1
- data/lib/ruact/server_functions/error_rendering.rb +22 -29
- data/lib/ruact/server_functions/name_bridge.rb +14 -16
- data/lib/ruact/server_functions/query_source.rb +35 -8
- data/lib/ruact/server_functions/route_source.rb +3 -4
- data/lib/ruact/server_functions/snapshot.rb +12 -139
- data/lib/ruact/server_functions/validation_errors.rb +70 -0
- data/lib/ruact/server_functions.rb +21 -25
- data/lib/ruact/signed_references.rb +162 -0
- data/lib/ruact/string_distance.rb +72 -0
- data/lib/ruact/validation_errors_collector.rb +139 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +102 -0
- data/lib/ruact.rb +19 -19
- data/lib/tasks/ruact.rake +10 -53
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +3 -0
- data/spec/ruact/client_manifest_spec.rb +36 -0
- data/spec/ruact/component_contract_spec.rb +119 -0
- data/spec/ruact/configuration_spec.rb +51 -34
- data/spec/ruact/controller_request_spec.rb +264 -0
- data/spec/ruact/controller_spec.rb +63 -326
- data/spec/ruact/doctor_spec.rb +201 -0
- data/spec/ruact/erb_preprocessor_hook_spec.rb +4 -1
- data/spec/ruact/erb_preprocessor_spec.rb +127 -0
- data/spec/ruact/errors_spec.rb +0 -45
- data/spec/ruact/html_converter_spec.rb +50 -0
- data/spec/ruact/install_generator_spec.rb +591 -4
- data/spec/ruact/query_request_spec.rb +109 -1
- data/spec/ruact/scaffold_generator_spec.rb +1835 -0
- data/spec/ruact/server_bucket_request_spec.rb +142 -0
- data/spec/ruact/server_function_name_spec.rb +1 -1
- data/spec/ruact/server_functions/codegen_spec.rb +158 -269
- data/spec/ruact/server_functions/name_bridge_spec.rb +9 -9
- data/spec/ruact/server_functions/query_source_spec.rb +51 -0
- data/spec/ruact/server_functions/railtie_integration_spec.rb +71 -268
- data/spec/ruact/server_functions/rake_spec.rb +29 -29
- data/spec/ruact/server_functions/snapshot_spec.rb +53 -213
- data/spec/ruact/server_rescue_request_spec.rb +6 -6
- data/spec/ruact/server_spec.rb +8 -9
- data/spec/ruact/signed_references_spec.rb +164 -0
- data/spec/ruact/string_distance_spec.rb +38 -0
- data/spec/ruact/validation_errors_spec.rb +116 -0
- data/spec/ruact/view_helper_spec.rb +79 -0
- data/spec/spec_helper.rb +0 -5
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +44 -47
- data/vendor/javascript/ruact-server-functions-runtime/index.js +151 -107
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +72 -75
- data/vendor/javascript/ruact-server-functions-runtime/package.json +2 -2
- data/vendor/javascript/ruact-server-functions-runtime/usequery.test.mjs +187 -0
- data/vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs +96 -0
- data/vendor/javascript/vite-plugin-ruact/index.js +353 -7
- data/vendor/javascript/vite-plugin-ruact/manifest-contract.test.mjs +182 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +16 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +3 -1
- data/vendor/javascript/vite-plugin-ruact/registry.test.mjs +199 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/bootstrap.jsx +68 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +235 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +473 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +114 -139
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +199 -262
- data/vendor/javascript/vite-plugin-ruact/tsconfig.json +18 -0
- data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold-agnostic.json +18 -0
- data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold.json +20 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/emitted-module.test-d.ts +23 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostDeleteDialog.tsx +90 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostForm.tsx +238 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostList.tsx +339 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostDeleteDialog.tsx +85 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostForm.tsx +216 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostList.tsx +269 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/ambient.d.ts +78 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/ambient.d.ts +166 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/typed-query.test-d.ts +48 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/usequery.test-d.ts +26 -0
- metadata +55 -15
- data/lib/generators/ruact/install/templates/application.jsx.tt +0 -51
- data/lib/ruact/server_action.rb +0 -131
- data/lib/ruact/server_functions/endpoint_controller.rb +0 -237
- data/lib/ruact/server_functions/registry.rb +0 -148
- data/lib/ruact/server_functions/registry_entry.rb +0 -26
- data/lib/ruact/server_functions/standalone_context.rb +0 -103
- data/lib/ruact/server_functions/standalone_dispatcher.rb +0 -178
- data/spec/ruact/server_functions/csrf_request_spec.rb +0 -380
- data/spec/ruact/server_functions/dispatch_request_spec.rb +0 -819
- data/spec/ruact/server_functions/registry_spec.rb +0 -199
- data/spec/ruact/server_functions/standalone_action_spec.rb +0 -224
- data/spec/ruact/server_functions/standalone_context_spec.rb +0 -142
- data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +0 -273
|
@@ -5,11 +5,16 @@ require "ruact/server_functions/name_bridge"
|
|
|
5
5
|
|
|
6
6
|
module Ruact
|
|
7
7
|
module ServerFunctions
|
|
8
|
-
# Renders the snapshot Hash into the TypeScript
|
|
9
|
-
# `app/javascript/.ruact/server-functions.ts`. Pure
|
|
10
|
-
# single write-if-changed call.
|
|
8
|
+
# Renders the route-driven (version-2) snapshot Hash into the TypeScript
|
|
9
|
+
# module emitted to `app/javascript/.ruact/server-functions.ts`. Pure
|
|
10
|
+
# string-building plus a single write-if-changed call.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
12
|
+
# Story 9.9 — the v1 (registry / `_makeRef`) render path was demolished;
|
|
13
|
+
# {.render} now dispatches only the version-2 (route-driven) shape. The
|
|
14
|
+
# actual rendering lives in the nested {V2} module (kept separate so the
|
|
15
|
+
# singleton class stays within its size budget).
|
|
16
|
+
#
|
|
17
|
+
# The output of {V2.render} MUST be byte-identical to the JS-side codegen in
|
|
13
18
|
# `gem/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs`.
|
|
14
19
|
# The cross-implementation parity test under
|
|
15
20
|
# `gem/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs`
|
|
@@ -22,9 +27,7 @@ module Ruact
|
|
|
22
27
|
|
|
23
28
|
# Story 9.3 — the route-driven snapshot schema. A version-2 snapshot
|
|
24
29
|
# carries route-derived entries (`http_method` + `path` + `segments`,
|
|
25
|
-
# no `ruby_symbol`) and renders `_makeServerFunction(descriptor)` calls
|
|
26
|
-
# instead of `_makeRef("<sym>")`. {.render} dispatches on `version` so
|
|
27
|
-
# the v1 (registry-driven) path stays byte-for-byte untouched.
|
|
30
|
+
# no `ruby_symbol`) and renders `_makeServerFunction(descriptor)` calls.
|
|
28
31
|
VERSION_V2 = 2
|
|
29
32
|
|
|
30
33
|
RUNTIME_IMPORT = '"ruact/server-functions-runtime"'
|
|
@@ -45,13 +48,6 @@ module Ruact
|
|
|
45
48
|
# against the codegen-emitted module — no call-site cast, no
|
|
46
49
|
# wrapper closure.
|
|
47
50
|
#
|
|
48
|
-
# Runtime behavior is unchanged — `_makeRef` always resolves with the
|
|
49
|
-
# JSON-decoded value (or `null` for empty bodies). The intersection is
|
|
50
|
-
# a TYPE-ONLY surface: when callers `await` the result, they see
|
|
51
|
-
# `Promise<unknown>`; when React invokes the function from a
|
|
52
|
-
# `<form action>` prop, the `Promise<void>` overload is selected and
|
|
53
|
-
# the return value is discarded by React.
|
|
54
|
-
#
|
|
55
51
|
# See the 2026-05-17 entry in `gem/docs/internal/decisions/server-functions-api.md`
|
|
56
52
|
# ("R1 — intersection-type refinement") for the option (a)→(a′)
|
|
57
53
|
# evolution and the empirical typecheck-probe that motivated it.
|
|
@@ -66,10 +62,27 @@ module Ruact
|
|
|
66
62
|
# params) gets the param-accepting signature; one with no kwargs keeps
|
|
67
63
|
# the bare {QUERY_SIGNATURE}. Queries are read-only (never reachable via
|
|
68
64
|
# `<form action>`), so neither widens to the action intersection.
|
|
65
|
+
#
|
|
66
|
+
# Story 13.4 — {QUERY_PARAMS_SIGNATURE}'s open `Record<string, unknown>`
|
|
67
|
+
# is now the FALLBACK only: emitted for a pre-13.4 snapshot entry that
|
|
68
|
+
# carries the `accepts_params` boolean but no structured `params`, and for
|
|
69
|
+
# a `**keyrest`-only query (open by design). A query whose entry carries
|
|
70
|
+
# the structured `params` metadata gets a typed object literal built from
|
|
71
|
+
# the declared keys instead (see {V2.render_query_export}).
|
|
69
72
|
QUERY_PARAMS_SIGNATURE = "(params: Record<string, unknown>) => Promise<unknown>"
|
|
70
73
|
|
|
74
|
+
# Story 13.4 — the VALUE type of every typed query param. `Method#parameters`
|
|
75
|
+
# exposes only names + required/optional (never types or default values),
|
|
76
|
+
# so per-param scalar precision is not reflection-honest; this is the exact
|
|
77
|
+
# FR88 query-string wire contract instead (the kwargs sanitizer in
|
|
78
|
+
# `query_dispatch.rb` accepts only these). Keys and optionality ARE exact,
|
|
79
|
+
# which is what kills the `Record<string, unknown>`/`any` gap (named keys,
|
|
80
|
+
# missing-required and unknown-key become compile errors). Per-param scalar
|
|
81
|
+
# narrowing is deferred to a future explicit param-type DSL.
|
|
82
|
+
QUERY_PARAM_VALUE_TYPE = "string | number | boolean | null"
|
|
83
|
+
|
|
71
84
|
# Story 8.2 — fixed re-export appended AFTER the per-function block.
|
|
72
|
-
# Emitted in BOTH branches (empty + populated
|
|
85
|
+
# Emitted in BOTH branches (empty + populated) so
|
|
73
86
|
# `import { revalidate } from "@/.ruact/server-functions"` works on
|
|
74
87
|
# day one of any host app. Ruby + JS codegens emit byte-identically.
|
|
75
88
|
REVALIDATE_REEXPORT = "export { revalidate } from #{RUNTIME_IMPORT};\n".freeze
|
|
@@ -86,8 +99,7 @@ module Ruact
|
|
|
86
99
|
# in JS-identifier terms (leading letter / underscore / `$`, then alnum
|
|
87
100
|
# / underscore / `$`). The codegen validates every entry it consumes
|
|
88
101
|
# because the JSON bridge is a trust boundary — a malformed snapshot
|
|
89
|
-
#
|
|
90
|
-
# otherwise inject TS at module top level.
|
|
102
|
+
# would otherwise inject TS at module top level.
|
|
91
103
|
VALID_JS_IDENTIFIER = /\A[A-Za-z_$][A-Za-z0-9_$]*\z/
|
|
92
104
|
|
|
93
105
|
ALLOWED_KINDS = %w[action query].freeze
|
|
@@ -95,24 +107,21 @@ module Ruact
|
|
|
95
107
|
# JS comments (both `//` line comments and `/* … */` block comments via
|
|
96
108
|
# the spec's LineTerminator production) end on LF, CR, U+2028, and U+2029.
|
|
97
109
|
# A snapshot value that smuggles any of these would break out of the
|
|
98
|
-
# leading comment header in the emitted module. The
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
LINE_TERMINATORS = /[\r\n
]/
|
|
110
|
+
# leading comment header in the emitted module. The regex covers both
|
|
111
|
+
# Unicode line separators (written as explicit escapes); a parity test
|
|
112
|
+
# keeps both renderers in sync.
|
|
113
|
+
LINE_TERMINATORS = /[\r\n\u2028\u2029]/
|
|
103
114
|
|
|
104
115
|
class << self
|
|
105
|
-
# Renders +snapshot+ into the TS module text. Pure; no I/O.
|
|
116
|
+
# Renders +snapshot+ into the TS module text. Pure; no I/O. Story 9.9 —
|
|
117
|
+
# only the route-driven (version-2) shape is supported.
|
|
106
118
|
#
|
|
107
|
-
# @param snapshot [Hash] result of {
|
|
108
|
-
#
|
|
109
|
-
# entries).
|
|
119
|
+
# @param snapshot [Hash] result of {Snapshot.dump_v2}; must contain
|
|
120
|
+
# `:version`, `:generated_at`, `:functions` (string-keyed entries).
|
|
110
121
|
# @return [String] TS module bytes, terminated by a single trailing newline.
|
|
111
|
-
# @raise [Ruact::ConfigurationError]
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
# outside {ALLOWED_KINDS}, or duplicate `js_identifier` — mirror of
|
|
115
|
-
# the JS-side `validateSnapshot` per the 2026-05-14 Re-run patch).
|
|
122
|
+
# @raise [Ruact::ConfigurationError] on a non-Hash snapshot, a missing
|
|
123
|
+
# required key, a non-v2 `version`, or any trust-boundary violation in
|
|
124
|
+
# {V2.render}.
|
|
116
125
|
def render(snapshot)
|
|
117
126
|
unless snapshot.is_a?(Hash)
|
|
118
127
|
raise Ruact::ConfigurationError,
|
|
@@ -125,41 +134,15 @@ module Ruact
|
|
|
125
134
|
|
|
126
135
|
validate_metadata!(version, generated_at)
|
|
127
136
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
io << "// Source: tmp/cache/ruact/server-functions.json (version #{version})\n"
|
|
135
|
-
io << "// Generated at: #{generated_at}\n"
|
|
136
|
-
io << "import { _makeRef } from #{RUNTIME_IMPORT};\n"
|
|
137
|
-
|
|
138
|
-
if functions.empty?
|
|
139
|
-
io << "\n// (no server functions registered yet — Stories 8.1 / 9.1 populate)\n"
|
|
140
|
-
# `noUnusedLocals` would otherwise flag the `_makeRef` import. The
|
|
141
|
-
# `void` discard pattern keeps the import alive at zero runtime
|
|
142
|
-
# cost; once an action/query is registered the export below
|
|
143
|
-
# references `_makeRef` directly and this line is omitted.
|
|
144
|
-
io << "void _makeRef;\n"
|
|
145
|
-
else
|
|
146
|
-
io << "\n"
|
|
147
|
-
functions.each do |entry|
|
|
148
|
-
io << render_export(entry)
|
|
149
|
-
end
|
|
137
|
+
unless version.to_s == VERSION_V2.to_s
|
|
138
|
+
raise Ruact::ConfigurationError,
|
|
139
|
+
"ruact server-function codegen: unsupported snapshot version " \
|
|
140
|
+
"#{version.inspect} (only the route-driven version #{VERSION_V2} is " \
|
|
141
|
+
"supported as of Story 9.9); the bridge JSON is corrupted — " \
|
|
142
|
+
"regenerate via `bin/rails ruact:server_functions:generate`."
|
|
150
143
|
end
|
|
151
144
|
|
|
152
|
-
|
|
153
|
-
# re-export lands in both branches (empty registry + populated).
|
|
154
|
-
# The codegen owns the canonical import path
|
|
155
|
-
# `@/.ruact/server-functions` and is the only stable surface devs
|
|
156
|
-
# are told to import from in the docs (per the Story 8.0 ADR);
|
|
157
|
-
# without this line, devs would need a second import statement
|
|
158
|
-
# from a less-stable runtime-package path.
|
|
159
|
-
io << "\n"
|
|
160
|
-
io << REVALIDATE_REEXPORT
|
|
161
|
-
|
|
162
|
-
io
|
|
145
|
+
V2.render(version, generated_at, functions)
|
|
163
146
|
end
|
|
164
147
|
|
|
165
148
|
# Writes the rendered TS module to +output_path+, only if it changed.
|
|
@@ -174,19 +157,8 @@ module Ruact
|
|
|
174
157
|
|
|
175
158
|
private
|
|
176
159
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
kind = (entry["kind"] || entry[:kind]).to_s
|
|
180
|
-
ruby_sym = entry["ruby_symbol"] || entry[:ruby_symbol]
|
|
181
|
-
|
|
182
|
-
signature = kind == "query" ? QUERY_SIGNATURE : ACTION_SIGNATURE
|
|
183
|
-
|
|
184
|
-
"export const #{js_id}: #{signature} =\n _makeRef(#{json_escape(ruby_sym.to_s)});\n"
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
# Pass-2 patch 2026-05-14 — wrap raw `KeyError` from `Hash#fetch` so
|
|
188
|
-
# the rake / Railtie call sites get a consistent `Ruact::ConfigurationError`
|
|
189
|
-
# for every snapshot-shape failure, not a mixture of error classes.
|
|
160
|
+
# Wraps raw `KeyError` from `Hash#fetch` so call sites get a consistent
|
|
161
|
+
# `Ruact::ConfigurationError` for every snapshot-shape failure.
|
|
190
162
|
def fetch_snapshot_key!(snapshot, sym_key, str_key)
|
|
191
163
|
return snapshot[sym_key] if snapshot.key?(sym_key)
|
|
192
164
|
return snapshot[str_key] if snapshot.key?(str_key)
|
|
@@ -197,10 +169,6 @@ module Ruact
|
|
|
197
169
|
"corrupted — regenerate via `bin/rails ruact:server_functions:generate`."
|
|
198
170
|
end
|
|
199
171
|
|
|
200
|
-
# Mirror of the JS-side `validateSnapshot` (2026-05-14 Re-run parity
|
|
201
|
-
# fix). The Ruby renderer also reads from the on-disk JSON bridge in
|
|
202
|
-
# the rake-task and Railtie paths, so the same trust-boundary guards
|
|
203
|
-
# belong here.
|
|
204
172
|
def validate_metadata!(version, generated_at)
|
|
205
173
|
unless version.is_a?(Integer) || version.is_a?(String)
|
|
206
174
|
raise Ruact::ConfigurationError,
|
|
@@ -226,113 +194,6 @@ module Ruact
|
|
|
226
194
|
"a line break (LF, CR, U+2028, or U+2029) — would break out of " \
|
|
227
195
|
"the header comment; snapshot JSON is corrupted."
|
|
228
196
|
end
|
|
229
|
-
|
|
230
|
-
def validate_functions!(functions)
|
|
231
|
-
unless functions.is_a?(Array)
|
|
232
|
-
raise Ruact::ConfigurationError,
|
|
233
|
-
"ruact server-function codegen: snapshot.functions must be an " \
|
|
234
|
-
"Array, got #{functions.class}"
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
seen = {}
|
|
238
|
-
functions.each do |entry|
|
|
239
|
-
unless entry.is_a?(Hash)
|
|
240
|
-
raise Ruact::ConfigurationError,
|
|
241
|
-
"ruact server-function codegen: snapshot.functions entry is " \
|
|
242
|
-
"not a Hash: #{entry.inspect}"
|
|
243
|
-
end
|
|
244
|
-
js_id = entry["js_identifier"] || entry[:js_identifier]
|
|
245
|
-
kind = (entry["kind"] || entry[:kind]).to_s
|
|
246
|
-
ruby_sym = entry["ruby_symbol"] || entry[:ruby_symbol]
|
|
247
|
-
|
|
248
|
-
validate_ruby_symbol!(ruby_sym)
|
|
249
|
-
validate_js_identifier!(js_id, ruby_sym)
|
|
250
|
-
validate_kind!(kind, ruby_sym)
|
|
251
|
-
validate_not_reserved!(js_id, ruby_sym)
|
|
252
|
-
validate_no_duplicate!(seen, js_id)
|
|
253
|
-
seen[js_id] = true
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
# Pass-2 patch 2026-05-14 — without this guard, a missing or empty
|
|
258
|
-
# `ruby_symbol` on a snapshot entry would render `_makeRef("")` and
|
|
259
|
-
# silently emit an export that can never resolve at runtime (the
|
|
260
|
-
# placeholder rejects on call but the empty string is a meaningless
|
|
261
|
-
# registration name). Treat as a corrupt-snapshot signal.
|
|
262
|
-
def validate_ruby_symbol!(ruby_sym)
|
|
263
|
-
return if ruby_sym.is_a?(String) && !ruby_sym.empty?
|
|
264
|
-
return if ruby_sym.is_a?(Symbol) && !ruby_sym.empty?
|
|
265
|
-
|
|
266
|
-
raise Ruact::ConfigurationError,
|
|
267
|
-
"ruact server-function codegen: snapshot.functions entry has " \
|
|
268
|
-
"missing or empty ruby_symbol (got #{ruby_sym.inspect}); the " \
|
|
269
|
-
"bridge JSON is corrupted — regenerate via " \
|
|
270
|
-
"`bin/rails ruact:server_functions:generate`."
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def validate_js_identifier!(js_id, ruby_sym)
|
|
274
|
-
return if js_id.is_a?(String) && js_id.match?(VALID_JS_IDENTIFIER)
|
|
275
|
-
|
|
276
|
-
raise Ruact::ConfigurationError,
|
|
277
|
-
"ruact server-function codegen rejected a snapshot entry: " \
|
|
278
|
-
"ruby_symbol=#{ruby_sym.inspect} js_identifier=#{js_id.inspect} " \
|
|
279
|
-
"is not a valid JS identifier (must match #{VALID_JS_IDENTIFIER.inspect}). " \
|
|
280
|
-
"The snapshot JSON is corrupted or was hand-edited — regenerate via " \
|
|
281
|
-
"`bin/rails ruact:server_functions:generate`."
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def validate_kind!(kind, ruby_sym)
|
|
285
|
-
return if ALLOWED_KINDS.include?(kind)
|
|
286
|
-
|
|
287
|
-
raise Ruact::ConfigurationError,
|
|
288
|
-
"ruact server-function codegen: snapshot.functions entry has " \
|
|
289
|
-
"invalid kind #{kind.inspect} (must be \"action\" or \"query\") " \
|
|
290
|
-
"for ruby_symbol=#{ruby_sym.inspect}"
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def validate_not_reserved!(js_id, ruby_sym)
|
|
294
|
-
if NameBridge::RESERVED_JS_IDENTIFIERS.include?(js_id)
|
|
295
|
-
raise Ruact::ConfigurationError,
|
|
296
|
-
"ruact server-function codegen: js_identifier #{js_id.inspect} " \
|
|
297
|
-
"is a reserved JS word — ruby_symbol=#{ruby_sym.inspect} would " \
|
|
298
|
-
"emit an invalid TS module. NameBridge should have rejected this; " \
|
|
299
|
-
"regenerate via `bin/rails ruact:server_functions:generate`."
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
# Story 8.2 R12 — even if NameBridge somehow lets a reserved
|
|
303
|
-
# ruact name through (e.g. a hand-edited bridge JSON), the
|
|
304
|
-
# codegen MUST refuse — otherwise the rendered module would
|
|
305
|
-
# bind `revalidate` / `_makeRef` twice (once via the
|
|
306
|
-
# re-export / import, once via the action `export const`)
|
|
307
|
-
# and crash at module load.
|
|
308
|
-
return unless NameBridge::RESERVED_BY_RUACT.include?(js_id)
|
|
309
|
-
|
|
310
|
-
raise Ruact::ConfigurationError,
|
|
311
|
-
"ruact server-function codegen: js_identifier #{js_id.inspect} " \
|
|
312
|
-
"is reserved by the ruact runtime/codegen surface (would clash " \
|
|
313
|
-
"with the module's `revalidate` re-export or `_makeRef` import) — " \
|
|
314
|
-
"ruby_symbol=#{ruby_sym.inspect} cannot be exported. NameBridge " \
|
|
315
|
-
"should have rejected this; regenerate via " \
|
|
316
|
-
"`bin/rails ruact:server_functions:generate`."
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
def validate_no_duplicate!(seen, js_id)
|
|
320
|
-
return unless seen.key?(js_id)
|
|
321
|
-
|
|
322
|
-
raise Ruact::ConfigurationError,
|
|
323
|
-
"ruact server-function codegen: duplicate js_identifier " \
|
|
324
|
-
"#{js_id.inspect} in snapshot — two entries would emit " \
|
|
325
|
-
"conflicting `export const` declarations. The snapshot JSON is " \
|
|
326
|
-
"corrupted or was hand-edited — regenerate via " \
|
|
327
|
-
"`bin/rails ruact:server_functions:generate`."
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
# Wraps `ruby_symbol` in a JSON-escaped string literal so backslashes,
|
|
331
|
-
# double quotes, and control characters cannot break out of the
|
|
332
|
-
# `_makeRef("<here>")` argument.
|
|
333
|
-
def json_escape(str)
|
|
334
|
-
JSON.dump(str)
|
|
335
|
-
end
|
|
336
197
|
end
|
|
337
198
|
end
|
|
338
199
|
end
|
|
@@ -25,8 +25,8 @@ module Ruact
|
|
|
25
25
|
|
|
26
26
|
# Story 9.5 — the verb a v2 QUERY entry may carry. Queries are reads,
|
|
27
27
|
# mounted by {Ruact::Routing#ruact_queries} as named GET routes; the
|
|
28
|
-
# 2026-06-02 ADR addendum
|
|
29
|
-
#
|
|
28
|
+
# 2026-06-02 ADR addendum restored HTTP GET semantics for queries, so a
|
|
29
|
+
# query entry is GET-only.
|
|
30
30
|
QUERY_HTTP_METHODS = %w[GET].to_set.freeze
|
|
31
31
|
|
|
32
32
|
class << self
|
|
@@ -87,13 +87,19 @@ module Ruact
|
|
|
87
87
|
|
|
88
88
|
# Story 9.5 — a query export binds a `_makeQuery` accessor carrying
|
|
89
89
|
# its GET descriptor `{ path, kind: "query" }`. `useQuery(<id>, …)`
|
|
90
|
-
# consumes it
|
|
91
|
-
#
|
|
92
|
-
# only when the query method declares kwargs (FR88).
|
|
90
|
+
# consumes it — reads go to `GET /q/<jsId>`. The signature accepts
|
|
91
|
+
# params only when the query method declares kwargs (FR88).
|
|
93
92
|
def render_query_export(entry)
|
|
94
93
|
js_id = fetch(entry, "js_identifier")
|
|
95
94
|
path = fetch(entry, "path")
|
|
96
|
-
|
|
95
|
+
# Story 13.4 — the typed `params` signature (or the back-compat
|
|
96
|
+
# fallback) is built by {QueryParams}, kept separate so this
|
|
97
|
+
# singleton class stays within its size budget.
|
|
98
|
+
signature = QueryParams.signature(
|
|
99
|
+
params: fetch(entry, "params"),
|
|
100
|
+
params_rest: fetch(entry, "params_rest"),
|
|
101
|
+
accepts_params: fetch(entry, "accepts_params")
|
|
102
|
+
)
|
|
97
103
|
|
|
98
104
|
descriptor = "{ path: #{JSON.dump(path)}, kind: \"query\" }"
|
|
99
105
|
"export const #{js_id}: #{signature} =\n _makeQuery(#{descriptor});\n"
|
|
@@ -123,6 +129,11 @@ module Ruact
|
|
|
123
129
|
path = fetch(entry, "path")
|
|
124
130
|
validate_path!(js_id, path)
|
|
125
131
|
validate_segments!(js_id, path, fetch(entry, "segments"))
|
|
132
|
+
# Story 13.4 — the structured query `params` metadata is a trust
|
|
133
|
+
# boundary (it becomes TS object keys); {QueryParams} rejects a
|
|
134
|
+
# malformed/corrupted snapshot rather than emit injected — or
|
|
135
|
+
# silently over-widened — TS.
|
|
136
|
+
QueryParams.validate!(js_id, fetch(entry, "params"), fetch(entry, "params_rest")) if kind == "query"
|
|
126
137
|
seen[js_id] = true
|
|
127
138
|
end
|
|
128
139
|
|
|
@@ -210,3 +221,9 @@ module Ruact
|
|
|
210
221
|
end
|
|
211
222
|
end
|
|
212
223
|
end
|
|
224
|
+
|
|
225
|
+
# Story 13.4 — the typed query `params` signature builder + its trust-boundary
|
|
226
|
+
# validation, kept in their own module so {V2}'s singleton class stays within
|
|
227
|
+
# its size budget (same split rationale as V2 itself vs {Codegen}). Required
|
|
228
|
+
# after V2 + the signature constants are defined; consumed by V2 above.
|
|
229
|
+
require_relative "codegen_v2_query_params"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ruact
|
|
6
|
+
module ServerFunctions
|
|
7
|
+
module Codegen
|
|
8
|
+
module V2
|
|
9
|
+
# Story 13.4 (FR99) — builds the TYPED query accessor `params` signature
|
|
10
|
+
# from the per-kwarg metadata {QuerySource} derives, and validates that
|
|
11
|
+
# metadata as a trust boundary. Extracted from {V2} for the same reason
|
|
12
|
+
# {V2} was extracted from {Codegen}: keep each singleton class within its
|
|
13
|
+
# size budget. Constants ({QUERY_SIGNATURE}, {QUERY_PARAMS_SIGNATURE},
|
|
14
|
+
# {QUERY_PARAM_VALUE_TYPE}, {VALID_JS_IDENTIFIER}, {LINE_TERMINATORS}) are
|
|
15
|
+
# reached from {Codegen} via lexical scope.
|
|
16
|
+
#
|
|
17
|
+
# MUST stay byte-identical to the JS-side `querySignatureV2` /
|
|
18
|
+
# `validateQueryParams` in
|
|
19
|
+
# `gem/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs`.
|
|
20
|
+
module QueryParams
|
|
21
|
+
class << self
|
|
22
|
+
# Picks the query accessor's `params` signature.
|
|
23
|
+
#
|
|
24
|
+
# When +params+ is an Array (the Story 13.4 structured metadata),
|
|
25
|
+
# build a typed object literal — one property per declared keyword,
|
|
26
|
+
# required/optional exact, value type the FR88 wire union (AC1).
|
|
27
|
+
# Empty params + no `**keyrest` → the bare {QUERY_SIGNATURE}
|
|
28
|
+
# (byte-identical to a no-kwargs query). A `**keyrest` keeps the open
|
|
29
|
+
# `Record<string, unknown>` (intersected with any named keys) so a
|
|
30
|
+
# legitimate dynamic key is never narrowed away (fail open).
|
|
31
|
+
#
|
|
32
|
+
# Falls back to the pre-13.4 +accepts_params+ boolean when no
|
|
33
|
+
# +params+ metadata is present (an older or hand-written snapshot)
|
|
34
|
+
# so no consumer breaks.
|
|
35
|
+
#
|
|
36
|
+
# @param params [Array<Hash>, nil] per-kwarg `{ "name", "required" }`.
|
|
37
|
+
# @param params_rest [Boolean, nil] whether a `**keyrest` is declared.
|
|
38
|
+
# @param accepts_params [Boolean, nil] the back-compat fallback flag.
|
|
39
|
+
# @return [String] the TS signature.
|
|
40
|
+
def signature(params:, params_rest:, accepts_params:)
|
|
41
|
+
return fallback(accepts_params) unless params.is_a?(Array)
|
|
42
|
+
|
|
43
|
+
rest = params_rest ? true : false
|
|
44
|
+
return QUERY_SIGNATURE if params.empty? && !rest
|
|
45
|
+
|
|
46
|
+
build(params, rest)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Validates the structured `params` metadata. Reject a malformed or
|
|
50
|
+
# corrupted snapshot loudly rather than emit broken — or silently
|
|
51
|
+
# over-widened — TS. Absent `params`/`params_rest` is valid (the
|
|
52
|
+
# {signature} fallback handles it).
|
|
53
|
+
#
|
|
54
|
+
# @param js_id [String]
|
|
55
|
+
# @param params [Array<Hash>, nil]
|
|
56
|
+
# @param params_rest [Object, nil] expected Boolean or nil; a
|
|
57
|
+
# non-Boolean truthy value would otherwise silently flip a query to
|
|
58
|
+
# the open `& Record<string, unknown>` shape (Story 13.4 review R1).
|
|
59
|
+
# @raise [Ruact::ConfigurationError]
|
|
60
|
+
def validate!(js_id, params, params_rest = nil)
|
|
61
|
+
validate_rest!(js_id, params_rest)
|
|
62
|
+
return if params.nil?
|
|
63
|
+
|
|
64
|
+
unless params.is_a?(Array)
|
|
65
|
+
raise Ruact::ConfigurationError,
|
|
66
|
+
"ruact server-function codegen: v2 query entry #{js_id.inspect} has invalid " \
|
|
67
|
+
"params #{params.inspect} (must be an Array of { name, required } objects)."
|
|
68
|
+
end
|
|
69
|
+
params.each { |param| validate_entry!(js_id, param) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def validate_rest!(js_id, params_rest)
|
|
75
|
+
return if params_rest.nil? || params_rest == true || params_rest == false
|
|
76
|
+
|
|
77
|
+
raise Ruact::ConfigurationError,
|
|
78
|
+
"ruact server-function codegen: v2 query entry #{js_id.inspect} has a non-Boolean " \
|
|
79
|
+
"params_rest #{params_rest.inspect}; snapshot JSON is corrupted."
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def fallback(accepts_params)
|
|
83
|
+
accepts_params ? QUERY_PARAMS_SIGNATURE : QUERY_SIGNATURE
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Builds `(params: { q: <UNION>; limit?: <UNION> }) => Promise<unknown>`
|
|
87
|
+
# in declaration order. A `**keyrest`-only query (no named keys)
|
|
88
|
+
# reuses the open {QUERY_PARAMS_SIGNATURE}; with named keys present
|
|
89
|
+
# the rest is an intersection so the named keys still autocomplete.
|
|
90
|
+
def build(params, rest)
|
|
91
|
+
props = params.map do |param|
|
|
92
|
+
name = param["name"] || param[:name]
|
|
93
|
+
required = param.key?("required") ? param["required"] : param[:required]
|
|
94
|
+
optional = required ? "" : "?"
|
|
95
|
+
"#{format_key(name)}#{optional}: #{QUERY_PARAM_VALUE_TYPE}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
return QUERY_PARAMS_SIGNATURE if props.empty? # keyrest-only
|
|
99
|
+
|
|
100
|
+
object = "{ #{props.join('; ')} }"
|
|
101
|
+
object += " & Record<string, unknown>" if rest
|
|
102
|
+
"(params: #{object}) => Promise<unknown>"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Kwarg names are Ruby symbols → almost always valid bare TS keys;
|
|
106
|
+
# quote (via JSON) any that are not so a corrupted snapshot cannot
|
|
107
|
+
# break out of the object literal. Mirrors the JS `formatParamKey`.
|
|
108
|
+
def format_key(name)
|
|
109
|
+
name.to_s.match?(VALID_JS_IDENTIFIER) ? name.to_s : JSON.dump(name.to_s)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validate_entry!(js_id, param)
|
|
113
|
+
unless param.is_a?(Hash)
|
|
114
|
+
raise Ruact::ConfigurationError,
|
|
115
|
+
"ruact server-function codegen: v2 query entry #{js_id.inspect} has a params " \
|
|
116
|
+
"element that is not an object: #{param.inspect}; snapshot JSON is corrupted."
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
name = param["name"] || param[:name]
|
|
120
|
+
unless name.is_a?(String) && !name.empty? && !name.match?(LINE_TERMINATORS)
|
|
121
|
+
raise Ruact::ConfigurationError,
|
|
122
|
+
"ruact server-function codegen: v2 query entry #{js_id.inspect} has a params " \
|
|
123
|
+
"name #{name.inspect} that is not a non-empty single-line String; snapshot JSON is corrupted."
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Presence-aware (a plain `||` would mis-read a legitimate
|
|
127
|
+
# `required: false` as the absent key).
|
|
128
|
+
required = param.key?("required") ? param["required"] : param[:required]
|
|
129
|
+
return if [true, false].include?(required)
|
|
130
|
+
|
|
131
|
+
raise Ruact::ConfigurationError,
|
|
132
|
+
"ruact server-function codegen: v2 query entry #{js_id.inspect} params name " \
|
|
133
|
+
"#{name.inspect} has a non-Boolean `required` #{required.inspect}; snapshot JSON is corrupted."
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -6,7 +6,7 @@ require_relative "error_suggestion"
|
|
|
6
6
|
module Ruact
|
|
7
7
|
module ServerFunctions
|
|
8
8
|
# Story 8.4 — Builds the structured JSON body returned by
|
|
9
|
-
# {
|
|
9
|
+
# {ErrorRendering#__ruact_render_action_error} for any server-action
|
|
10
10
|
# exception that bubbles past a host's `rescue_from` chain.
|
|
11
11
|
#
|
|
12
12
|
# The function is pure (no `Rails.env`, no `Ruact.config` reads) — the
|
|
@@ -4,33 +4,23 @@ require_relative "error_payload"
|
|
|
4
4
|
|
|
5
5
|
module Ruact
|
|
6
6
|
module ServerFunctions
|
|
7
|
-
# Story 9.1 — the shared
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# the
|
|
7
|
+
# Story 9.1 — the shared core for the Story 8.4 structured-error rendering
|
|
8
|
+
# and the Story 8.5 upload guard, included by {Ruact::Server} (the
|
|
9
|
+
# route-driven concern hosts include). Story 9.9 demolished the v1 endpoint
|
|
10
|
+
# that previously shared this code, so {Ruact::Server} is now the sole home.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# - {Ruact::Server} (v2 — the route-driven concern hosts include)
|
|
15
|
-
#
|
|
16
|
-
# Keeping one source guarantees the 8.4/8.5 wire contract is byte-for-byte
|
|
17
|
-
# identical across both homes by construction; behavioral differences are
|
|
18
|
-
# expressed exclusively through the three private hooks below, which each
|
|
19
|
-
# home may override:
|
|
12
|
+
# Behavioral specialization is expressed through three private hooks, which
|
|
13
|
+
# the including controller may override:
|
|
20
14
|
#
|
|
21
15
|
# - {#__ruact_error_action_name} — where the payload's `action_name`
|
|
22
|
-
# comes from. Default: the controller's own `action_name
|
|
23
|
-
# v2 host controllers). The v1 endpoint overrides it with its
|
|
24
|
-
# registry-symbol / `path_parameters[:name]` fallback chain.
|
|
16
|
+
# comes from. Default: the controller's own `action_name`.
|
|
25
17
|
# - {#__ruact_render_structured_error?} — whether the rescue handler
|
|
26
18
|
# renders the structured JSON payload for this request, or re-raises so
|
|
27
|
-
# Rails' default error handling proceeds.
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# POST-only). {Ruact::Server} skips GET/HEAD so page actions stay
|
|
33
|
-
# byte-for-byte untouched.
|
|
19
|
+
# Rails' default error handling proceeds. {Ruact::Server} gates this on
|
|
20
|
+
# its function-call predicate.
|
|
21
|
+
# - {#__ruact_upload_guard_applicable?} — whether the upload guard applies
|
|
22
|
+
# to this request at all. {Ruact::Server} skips GET/HEAD so page actions
|
|
23
|
+
# stay byte-for-byte untouched.
|
|
34
24
|
#
|
|
35
25
|
# All methods are private on the including controller; nothing here is
|
|
36
26
|
# public API surface.
|
|
@@ -98,28 +88,31 @@ module Ruact
|
|
|
98
88
|
end
|
|
99
89
|
|
|
100
90
|
# Hook — where the structured payload's `action_name` field comes from.
|
|
101
|
-
# The controller's own `action_name` is correct for
|
|
91
|
+
# The controller's own `action_name` is correct for host controllers
|
|
102
92
|
# (it is populated by routing before any callback runs, including the
|
|
103
93
|
# prepended upload guard — no early-rejection fallback dance needed).
|
|
104
94
|
def __ruact_error_action_name
|
|
105
95
|
action_name
|
|
106
96
|
end
|
|
107
97
|
|
|
108
|
-
# Hook — render the structured payload for this request? The
|
|
109
|
-
#
|
|
110
|
-
#
|
|
98
|
+
# Hook — render the structured payload for this request? The default is
|
|
99
|
+
# "always"; {Ruact::Server} overrides it to gate on its function-call
|
|
100
|
+
# predicate (page/Flight requests re-raise so Rails' default error
|
|
101
|
+
# handling proceeds untouched).
|
|
111
102
|
def __ruact_render_structured_error?(_error)
|
|
112
103
|
true
|
|
113
104
|
end
|
|
114
105
|
|
|
115
|
-
# Hook — does the upload guard apply to this request? The
|
|
116
|
-
#
|
|
106
|
+
# Hook — does the upload guard apply to this request? The default is
|
|
107
|
+
# "always"; {Ruact::Server} overrides it to skip GET/HEAD.
|
|
117
108
|
def __ruact_upload_guard_applicable?
|
|
118
109
|
true
|
|
119
110
|
end
|
|
120
111
|
|
|
121
112
|
# Story 8.4 — Status mapping per AC1:
|
|
122
113
|
# - `Ruact::BadRequestError` → 400 (Story 9.5 — FR88 kwargs rejection)
|
|
114
|
+
# - `Ruact::InvalidSignedGlobalIDError` → 400 (Story 13.2 — FR96 forged/
|
|
115
|
+
# expired/wrong-purpose signed reference; an invalid client credential)
|
|
123
116
|
# - `ActiveRecord::RecordInvalid` → 422
|
|
124
117
|
# - `ActionController::InvalidAuthenticityToken` → 403
|
|
125
118
|
# - `Ruact::UploadTooLargeError` → 413
|
|
@@ -128,7 +121,7 @@ module Ruact
|
|
|
128
121
|
# at load time (parity with {ErrorSuggestion}).
|
|
129
122
|
def __ruact_status_for(error)
|
|
130
123
|
case error.class.name
|
|
131
|
-
when "Ruact::BadRequestError" then 400
|
|
124
|
+
when "Ruact::BadRequestError", "Ruact::InvalidSignedGlobalIDError" then 400
|
|
132
125
|
when "ActiveRecord::RecordInvalid" then 422
|
|
133
126
|
when "ActionController::InvalidAuthenticityToken" then 403
|
|
134
127
|
when "Ruact::UploadTooLargeError" then 413
|