ruact 0.0.5 → 0.0.7
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 +74 -18
- 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 +79 -425
- data/lib/ruact/doctor.rb +133 -4
- data/lib/ruact/erb_preprocessor.rb +71 -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/manifest_resolver.rb +149 -0
- data/lib/ruact/railtie.rb +70 -203
- data/lib/ruact/render_pipeline.rb +8 -1
- 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 +109 -0
- data/lib/ruact.rb +20 -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 +70 -328
- 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/manifest_resolver_spec.rb +159 -0
- 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 +15 -5
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +40 -45
- data/vendor/javascript/ruact-server-functions-runtime/index.js +42 -98
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +72 -75
- data/vendor/javascript/ruact-server-functions-runtime/package.json +1 -1
- data/vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs +96 -0
- data/vendor/javascript/vite-plugin-ruact/index.js +372 -6
- data/vendor/javascript/vite-plugin-ruact/manifest-contract.test.mjs +182 -0
- data/vendor/javascript/vite-plugin-ruact/manifest-http.test.mjs +125 -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 +58 -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
|
@@ -23,10 +23,25 @@ require "rack/test"
|
|
|
23
23
|
|
|
24
24
|
require "ruact/controller"
|
|
25
25
|
require "ruact/server"
|
|
26
|
+
require "active_model"
|
|
26
27
|
|
|
27
28
|
require_relative "controller_request_spec" unless defined?(ControllerRequestSpecSupport)
|
|
28
29
|
|
|
29
30
|
module ServerBucketSpecSupport
|
|
31
|
+
# Story 13.3 (FR98) — an ActiveModel record with a presence validation, so a
|
|
32
|
+
# host action can exercise the non-exception `if record.valid? … else …`
|
|
33
|
+
# happy-failure path that `ruact_errors` round-trips.
|
|
34
|
+
class ValidatedPost
|
|
35
|
+
include ActiveModel::Model
|
|
36
|
+
include ActiveModel::Attributes
|
|
37
|
+
|
|
38
|
+
attribute :title, :string
|
|
39
|
+
|
|
40
|
+
validates :title, presence: true
|
|
41
|
+
|
|
42
|
+
def self.name = "ValidatedPost"
|
|
43
|
+
end
|
|
44
|
+
|
|
30
45
|
# Serializable model — only :id/:title exposed; :secret must never leak.
|
|
31
46
|
class BucketPost
|
|
32
47
|
include Ruact::Serializable
|
|
@@ -96,6 +111,33 @@ module ServerBucketSpecSupport
|
|
|
96
111
|
def unserializable
|
|
97
112
|
@rec = UnserializableRecord.new
|
|
98
113
|
end
|
|
114
|
+
|
|
115
|
+
# Story 13.3 (FR98) — opted-in FAILED validation: `ruact_errors(post)` after
|
|
116
|
+
# an invalid save attempt registers the canonical errors; Bucket-2 injects
|
|
117
|
+
# them under the reserved `errors` key alongside the serialized ivars.
|
|
118
|
+
def validated_create_invalid
|
|
119
|
+
post = ServerBucketSpecSupport::ValidatedPost.new(title: nil)
|
|
120
|
+
@created = post.valid? # false → populates post.errors
|
|
121
|
+
ruact_errors(post)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Story 13.3 (FR98) — opted-in SUCCESS: the SAME call path yields
|
|
125
|
+
# `errors: {}` (success symmetry), so the client reads `result.errors`
|
|
126
|
+
# uniformly on both branches.
|
|
127
|
+
def validated_create_valid
|
|
128
|
+
post = ServerBucketSpecSupport::ValidatedPost.new(title: "Hi")
|
|
129
|
+
@created = post.valid? # true → post.errors empty
|
|
130
|
+
ruact_errors(post)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Story 13.3 (FR98) — a stray dev ivar literally named `@errors` must NOT
|
|
134
|
+
# clobber the reserved round-trip key: the collector is the source of truth.
|
|
135
|
+
def stray_errors_ivar
|
|
136
|
+
@errors = "a dev string that must not win"
|
|
137
|
+
post = ServerBucketSpecSupport::ValidatedPost.new(title: nil)
|
|
138
|
+
post.valid?
|
|
139
|
+
ruact_errors(post)
|
|
140
|
+
end
|
|
99
141
|
end
|
|
100
142
|
|
|
101
143
|
# Review round 2 — an auth-style before_action that redirects on a Bucket-2
|
|
@@ -144,8 +186,31 @@ module ServerBucketSpecSupport
|
|
|
144
186
|
render json: { token: form_authenticity_token }
|
|
145
187
|
end
|
|
146
188
|
end
|
|
189
|
+
|
|
190
|
+
# Story 10.0 (AC5) — the scaffold's resource-controller shape: includes BOTH
|
|
191
|
+
# Ruact::Controller and Ruact::Server (ApplicationController contributes
|
|
192
|
+
# Controller; the resource controller adds Server). An ivar-only GET page
|
|
193
|
+
# action (implicit `default_render`) backed by a conventional template must
|
|
194
|
+
# flow Server#default_render → super (GET is never a function call) → the
|
|
195
|
+
# now-fixed Controller#default_render → HTML shell, NOT a 500.
|
|
196
|
+
class ServerImplicitController < ActionController::Base
|
|
197
|
+
include Ruact::Controller
|
|
198
|
+
include Ruact::Server
|
|
199
|
+
|
|
200
|
+
def show; end
|
|
201
|
+
end
|
|
147
202
|
end
|
|
148
203
|
|
|
204
|
+
# Story 10.0 (AC5) — conventional template under the shared app root so the
|
|
205
|
+
# Server-including controller's implicit GET reaches Controller#default_render.
|
|
206
|
+
ControllerRequestSpecSupport.write_view(
|
|
207
|
+
"server_bucket_spec_support/server_implicit", "show", <<~ERB
|
|
208
|
+
<div>
|
|
209
|
+
<DemoButton label={"hi"} />
|
|
210
|
+
</div>
|
|
211
|
+
ERB
|
|
212
|
+
)
|
|
213
|
+
|
|
149
214
|
if defined?(ControllerRequestSpecSupport) &&
|
|
150
215
|
!ControllerRequestSpecSupport.instance_variable_get(:@__ruact_server_bucket_routes_appended)
|
|
151
216
|
ControllerRequestSpecSupport.instance_variable_set(:@__ruact_server_bucket_routes_appended, true)
|
|
@@ -159,9 +224,14 @@ if defined?(ControllerRequestSpecSupport) &&
|
|
|
159
224
|
post "/bucket/vary_wildcard", to: "server_bucket_spec_support/bucket_server#vary_wildcard"
|
|
160
225
|
post "/bucket/before_redirect", to: "server_bucket_spec_support/bucket_before_redirect#never_runs"
|
|
161
226
|
post "/bucket/unserializable", to: "server_bucket_spec_support/bucket_server#unserializable"
|
|
227
|
+
post "/bucket/validated_create_invalid", to: "server_bucket_spec_support/bucket_server#validated_create_invalid"
|
|
228
|
+
post "/bucket/validated_create_valid", to: "server_bucket_spec_support/bucket_server#validated_create_valid"
|
|
229
|
+
post "/bucket/stray_errors_ivar", to: "server_bucket_spec_support/bucket_server#stray_errors_ivar"
|
|
162
230
|
post "/bucket/dual_redirecting", to: "server_bucket_spec_support/bucket_dual#redirecting"
|
|
163
231
|
post "/bucket/protected", to: "server_bucket_spec_support/bucket_forgery#create_protected"
|
|
164
232
|
get "/bucket/csrf_token", to: "server_bucket_spec_support/bucket_forgery#csrf_token"
|
|
233
|
+
# Story 10.0 (AC5) — Server-including controller's implicit GET page action.
|
|
234
|
+
get "/server-implicit/show", to: "server_bucket_spec_support/server_implicit#show"
|
|
165
235
|
end
|
|
166
236
|
end
|
|
167
237
|
|
|
@@ -177,6 +247,10 @@ RSpec.describe "Story 9.2: Ruact::Server dual-bucket response negotiation", :sto
|
|
|
177
247
|
before do
|
|
178
248
|
Rails.logger = Logger.new(IO::NULL)
|
|
179
249
|
ControllerRequestSpecSupport.boot!
|
|
250
|
+
# Story 10.0 (AC5) — pin Rails.root so the Server-path implicit-render
|
|
251
|
+
# template (`Rails.root/app/views`) is found regardless of full-suite
|
|
252
|
+
# ordering leftovers (see controller_request_spec.rb for the rationale).
|
|
253
|
+
Rails.root = ControllerRequestSpecSupport.app_root
|
|
180
254
|
end
|
|
181
255
|
|
|
182
256
|
def reset_config
|
|
@@ -349,4 +423,72 @@ RSpec.describe "Story 9.2: Ruact::Server dual-bucket response negotiation", :sto
|
|
|
349
423
|
end
|
|
350
424
|
end
|
|
351
425
|
end
|
|
426
|
+
|
|
427
|
+
# Story 13.3 (FR98) — the Bucket-2 (imperative `await`) half of the
|
|
428
|
+
# Inertia-style validation `errors` round-trip (AC3).
|
|
429
|
+
describe "Story 13.3: Bucket-2 errors round-trip (FR98)", :story_13_3 do
|
|
430
|
+
it "FAILED validation injects the canonical errors under the reserved `errors` key" do
|
|
431
|
+
post "/bucket/validated_create_invalid", "{}", json_headers
|
|
432
|
+
expect(last_response.status).to eq(200)
|
|
433
|
+
body = JSON.parse(last_response.body)
|
|
434
|
+
expect(body).to eq("created" => false, "errors" => { "title" => ["Title can't be blank"] })
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
it "SUCCESS yields `errors: {}` through the same call path (success symmetry)" do
|
|
438
|
+
post "/bucket/validated_create_valid", "{}", json_headers
|
|
439
|
+
expect(last_response.status).to eq(200)
|
|
440
|
+
body = JSON.parse(last_response.body)
|
|
441
|
+
expect(body).to eq("created" => true, "errors" => {})
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
it "is set with Vary: Accept like every other non-GET shape" do
|
|
445
|
+
post "/bucket/validated_create_valid", "{}", json_headers
|
|
446
|
+
expect(last_response.headers["Vary"]).to match(/\bAccept\b/)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
it "the collector wins over a stray dev ivar literally named @errors" do
|
|
450
|
+
post "/bucket/stray_errors_ivar", "{}", json_headers
|
|
451
|
+
expect(last_response.status).to eq(200)
|
|
452
|
+
body = JSON.parse(last_response.body)
|
|
453
|
+
expect(body.fetch("errors")).to eq("title" => ["Title can't be blank"])
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
it "preserves the 9.2 untouched-collector contract: an empty opted-out action still 204s" do
|
|
457
|
+
# `empty_action` never calls `ruact_errors` → no `errors` key, 204 No Content.
|
|
458
|
+
post "/bucket/empty", "{}", json_headers
|
|
459
|
+
expect(last_response.status).to eq(204)
|
|
460
|
+
expect(last_response.body).to eq("")
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
it "preserves the 9.2 untouched-collector contract: a populated action carries NO errors key" do
|
|
464
|
+
# `with_ivars` sets ivars but never opts in → body has no `errors` key.
|
|
465
|
+
post "/bucket/with_ivars", "{}", json_headers
|
|
466
|
+
expect(JSON.parse(last_response.body)).not_to have_key("errors")
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# AC5(e) coexistence: the RAISED `ActiveRecord::RecordInvalid` path still
|
|
470
|
+
# renders the structured 422 error payload — proven (unchanged) by
|
|
471
|
+
# server_rescue_request_spec.rb's `record_invalid` example. 13.3 is the
|
|
472
|
+
# NON-raised 200 path above; the two contracts stay distinct.
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Story 10.0 (AC5) — the Server-path inherits the default_render graceful
|
|
476
|
+
# degradation fix. A GET is never a function call, so Server#default_render
|
|
477
|
+
# delegates to super → the fixed Controller#default_render.
|
|
478
|
+
describe "Story 10.0: Server path inherits non-HTML Accept graceful degradation (AC5)", :story_10_0 do
|
|
479
|
+
it "GET with Accept: */* returns the HTML shell, not a 500" do
|
|
480
|
+
get "/server-implicit/show", {}, { "HTTP_ACCEPT" => "*/*" }
|
|
481
|
+
expect(last_response.status).to(eq(200),
|
|
482
|
+
"expected 200, got #{last_response.status} body=#{last_response.body[0, 400]}")
|
|
483
|
+
expect(last_response.headers["Content-Type"]).to include("text/html")
|
|
484
|
+
expect(last_response.body).to include("DemoButton")
|
|
485
|
+
expect(last_response.body).not_to include("__ruact_component__ called outside a ruact_render flow")
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
it "GET with Accept: text/x-component returns a Flight payload (function-call path untouched)" do
|
|
489
|
+
get "/server-implicit/show", {}, { "HTTP_ACCEPT" => "text/x-component" }
|
|
490
|
+
expect(last_response.status).to eq(200)
|
|
491
|
+
expect(last_response.headers["Content-Type"]).to include("text/x-component")
|
|
492
|
+
end
|
|
493
|
+
end
|
|
352
494
|
end
|
|
@@ -39,7 +39,7 @@ RSpec.describe "Ruact::Server.ruact_function_name", :story_9_3 do
|
|
|
39
39
|
end.to raise_error(Ruact::ConfigurationError, /reserved/)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
it "rejects a runtime-bound name (revalidate
|
|
42
|
+
it "rejects a runtime-bound name (revalidate)" do
|
|
43
43
|
expect do
|
|
44
44
|
host.ruact_function_name(:create, as: "revalidate")
|
|
45
45
|
end.to raise_error(Ruact::ConfigurationError, /reserved|ruact runtime/)
|