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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +54 -2
  3. data/.rubocop_todo.yml +3 -115
  4. data/CHANGELOG.md +68 -17
  5. data/bench/server_functions_dispatch_bench.rb +109 -142
  6. data/bench/server_functions_dispatch_bench.results.md +29 -0
  7. data/docs/internal/decisions/server-functions-api.md +402 -0
  8. data/lib/generators/ruact/install/install_generator.rb +310 -25
  9. data/lib/generators/ruact/install/templates/Procfile.dev.tt +2 -0
  10. data/lib/generators/ruact/install/templates/dev.tt +16 -0
  11. data/lib/generators/ruact/install/templates/package.json.tt +17 -0
  12. data/lib/generators/ruact/install/templates/vite.config.js.tt +3 -1
  13. data/lib/generators/ruact/scaffold/scaffold_attribute.rb +114 -0
  14. data/lib/generators/ruact/scaffold/scaffold_form_helpers.rb +114 -0
  15. data/lib/generators/ruact/scaffold/scaffold_generator.rb +491 -0
  16. data/lib/generators/ruact/scaffold/scaffold_shadcn_preflight.rb +229 -0
  17. data/lib/generators/ruact/scaffold/templates/components/DeleteDialog.tsx.tt +104 -0
  18. data/lib/generators/ruact/scaffold/templates/components/Form.tsx.tt +212 -0
  19. data/lib/generators/ruact/scaffold/templates/components/List.tsx.tt +338 -0
  20. data/lib/generators/ruact/scaffold/templates/components/agnostic/DeleteDialog.tsx.tt +92 -0
  21. data/lib/generators/ruact/scaffold/templates/components/agnostic/Form.tsx.tt +174 -0
  22. data/lib/generators/ruact/scaffold/templates/components/agnostic/List.tsx.tt +253 -0
  23. data/lib/generators/ruact/scaffold/templates/controller.rb.tt +130 -0
  24. data/lib/generators/ruact/scaffold/templates/queries/application_query.rb.tt +13 -0
  25. data/lib/generators/ruact/scaffold/templates/queries/query.rb.tt +59 -0
  26. data/lib/generators/ruact/scaffold/templates/request_spec.rb.tt +77 -0
  27. data/lib/generators/ruact/scaffold/templates/views/edit.html.erb.tt +7 -0
  28. data/lib/generators/ruact/scaffold/templates/views/index.html.erb.tt +6 -0
  29. data/lib/generators/ruact/scaffold/templates/views/new.html.erb.tt +5 -0
  30. data/lib/generators/ruact/scaffold/templates/views/show.html.erb.tt +16 -0
  31. data/lib/ruact/client_manifest.rb +37 -36
  32. data/lib/ruact/component_contract.rb +115 -0
  33. data/lib/ruact/configuration.rb +80 -28
  34. data/lib/ruact/controller.rb +69 -421
  35. data/lib/ruact/doctor.rb +133 -4
  36. data/lib/ruact/erb_preprocessor.rb +65 -12
  37. data/lib/ruact/erb_preprocessor_hook.rb +4 -1
  38. data/lib/ruact/errors.rb +30 -44
  39. data/lib/ruact/html_converter.rb +22 -1
  40. data/lib/ruact/railtie.rb +56 -200
  41. data/lib/ruact/server.rb +28 -6
  42. data/lib/ruact/server_functions/codegen.rb +49 -188
  43. data/lib/ruact/server_functions/codegen_v2.rb +23 -6
  44. data/lib/ruact/server_functions/codegen_v2_query_params.rb +140 -0
  45. data/lib/ruact/server_functions/error_payload.rb +1 -1
  46. data/lib/ruact/server_functions/error_rendering.rb +22 -29
  47. data/lib/ruact/server_functions/name_bridge.rb +14 -16
  48. data/lib/ruact/server_functions/query_source.rb +35 -8
  49. data/lib/ruact/server_functions/route_source.rb +3 -4
  50. data/lib/ruact/server_functions/snapshot.rb +12 -139
  51. data/lib/ruact/server_functions/validation_errors.rb +70 -0
  52. data/lib/ruact/server_functions.rb +21 -25
  53. data/lib/ruact/signed_references.rb +162 -0
  54. data/lib/ruact/string_distance.rb +72 -0
  55. data/lib/ruact/validation_errors_collector.rb +139 -0
  56. data/lib/ruact/version.rb +1 -1
  57. data/lib/ruact/view_helper.rb +102 -0
  58. data/lib/ruact.rb +19 -19
  59. data/lib/tasks/ruact.rake +10 -53
  60. data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +3 -0
  61. data/spec/ruact/client_manifest_spec.rb +36 -0
  62. data/spec/ruact/component_contract_spec.rb +119 -0
  63. data/spec/ruact/configuration_spec.rb +51 -34
  64. data/spec/ruact/controller_request_spec.rb +264 -0
  65. data/spec/ruact/controller_spec.rb +63 -326
  66. data/spec/ruact/doctor_spec.rb +201 -0
  67. data/spec/ruact/erb_preprocessor_hook_spec.rb +4 -1
  68. data/spec/ruact/erb_preprocessor_spec.rb +127 -0
  69. data/spec/ruact/errors_spec.rb +0 -45
  70. data/spec/ruact/html_converter_spec.rb +50 -0
  71. data/spec/ruact/install_generator_spec.rb +591 -4
  72. data/spec/ruact/query_request_spec.rb +109 -1
  73. data/spec/ruact/scaffold_generator_spec.rb +1835 -0
  74. data/spec/ruact/server_bucket_request_spec.rb +142 -0
  75. data/spec/ruact/server_function_name_spec.rb +1 -1
  76. data/spec/ruact/server_functions/codegen_spec.rb +158 -269
  77. data/spec/ruact/server_functions/name_bridge_spec.rb +9 -9
  78. data/spec/ruact/server_functions/query_source_spec.rb +51 -0
  79. data/spec/ruact/server_functions/railtie_integration_spec.rb +71 -268
  80. data/spec/ruact/server_functions/rake_spec.rb +29 -29
  81. data/spec/ruact/server_functions/snapshot_spec.rb +53 -213
  82. data/spec/ruact/server_rescue_request_spec.rb +6 -6
  83. data/spec/ruact/server_spec.rb +8 -9
  84. data/spec/ruact/signed_references_spec.rb +164 -0
  85. data/spec/ruact/string_distance_spec.rb +38 -0
  86. data/spec/ruact/validation_errors_spec.rb +116 -0
  87. data/spec/ruact/view_helper_spec.rb +79 -0
  88. data/spec/spec_helper.rb +0 -5
  89. data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +44 -47
  90. data/vendor/javascript/ruact-server-functions-runtime/index.js +151 -107
  91. data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +72 -75
  92. data/vendor/javascript/ruact-server-functions-runtime/package.json +2 -2
  93. data/vendor/javascript/ruact-server-functions-runtime/usequery.test.mjs +187 -0
  94. data/vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs +96 -0
  95. data/vendor/javascript/vite-plugin-ruact/index.js +353 -7
  96. data/vendor/javascript/vite-plugin-ruact/manifest-contract.test.mjs +182 -0
  97. data/vendor/javascript/vite-plugin-ruact/package-lock.json +16 -0
  98. data/vendor/javascript/vite-plugin-ruact/package.json +3 -1
  99. data/vendor/javascript/vite-plugin-ruact/registry.test.mjs +199 -0
  100. data/vendor/javascript/vite-plugin-ruact/runtime/bootstrap.jsx +68 -0
  101. data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +235 -0
  102. data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +473 -0
  103. data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +114 -139
  104. data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +199 -262
  105. data/vendor/javascript/vite-plugin-ruact/tsconfig.json +18 -0
  106. data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold-agnostic.json +18 -0
  107. data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold.json +20 -0
  108. data/vendor/javascript/vite-plugin-ruact/type-tests/emitted-module.test-d.ts +23 -0
  109. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostDeleteDialog.tsx +90 -0
  110. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostForm.tsx +238 -0
  111. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostList.tsx +339 -0
  112. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostDeleteDialog.tsx +85 -0
  113. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostForm.tsx +216 -0
  114. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostList.tsx +269 -0
  115. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/ambient.d.ts +78 -0
  116. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/ambient.d.ts +166 -0
  117. data/vendor/javascript/vite-plugin-ruact/type-tests/typed-query.test-d.ts +48 -0
  118. data/vendor/javascript/vite-plugin-ruact/type-tests/usequery.test-d.ts +26 -0
  119. metadata +55 -15
  120. data/lib/generators/ruact/install/templates/application.jsx.tt +0 -51
  121. data/lib/ruact/server_action.rb +0 -131
  122. data/lib/ruact/server_functions/endpoint_controller.rb +0 -237
  123. data/lib/ruact/server_functions/registry.rb +0 -148
  124. data/lib/ruact/server_functions/registry_entry.rb +0 -26
  125. data/lib/ruact/server_functions/standalone_context.rb +0 -103
  126. data/lib/ruact/server_functions/standalone_dispatcher.rb +0 -178
  127. data/spec/ruact/server_functions/csrf_request_spec.rb +0 -380
  128. data/spec/ruact/server_functions/dispatch_request_spec.rb +0 -819
  129. data/spec/ruact/server_functions/registry_spec.rb +0 -199
  130. data/spec/ruact/server_functions/standalone_action_spec.rb +0 -224
  131. data/spec/ruact/server_functions/standalone_context_spec.rb +0 -142
  132. data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +0 -273
@@ -1,273 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Story 8.3 — `Ruact::ServerFunctions::StandaloneDispatcher`: AC2 + AC3
4
- # dispatch shape: JSON / multipart / URL-encoded bodies; nil → 204; Hash →
5
- # 200 JSON; Array → 200 JSON; `Ruact::ActionError` → status + body; unknown
6
- # content-type → empty params; params shadow is `ActionController::Parameters`.
7
-
8
- require "spec_helper"
9
- require "action_controller"
10
- require "action_dispatch"
11
- require "rack"
12
- require "securerandom"
13
-
14
- module Ruact
15
- module ServerFunctions
16
- RSpec.describe StandaloneDispatcher, :story_8_3 do
17
- let(:posts_module) do
18
- Module.new do
19
- extend Ruact::ServerAction
20
-
21
- def self.name
22
- "StandaloneDispatcherHost"
23
- end
24
- end
25
- end
26
-
27
- def make_request(body:, content_type:)
28
- env = Rack::MockRequest.env_for(
29
- "/__ruact/fn/whatever",
30
- method: "POST",
31
- input: body,
32
- "CONTENT_TYPE" => content_type
33
- )
34
- ActionDispatch::Request.new(env)
35
- end
36
-
37
- def make_response
38
- ActionDispatch::Response.new.tap do |resp|
39
- # Touch the response so its internal state is initialized.
40
- resp.request = nil
41
- end
42
- end
43
-
44
- def register_and_entry(symbol, &block)
45
- posts_module.module_eval { ruact_action(symbol, &block) }
46
- Ruact.action_registry.entries[symbol]
47
- end
48
-
49
- describe "AC2 — content-type routing into params shadow" do
50
- it "parses application/json bodies" do
51
- entry = register_and_entry(:json_echo, &:to_unsafe_h)
52
- request = make_request(body: '{"title":"Hi"}', content_type: "application/json")
53
- response = make_response
54
-
55
- described_class.dispatch(entry, request, response)
56
-
57
- expect(response.status).to eq(200)
58
- expect(response.headers["Content-Type"]).to include("application/json")
59
- expect(JSON.parse(response.body)).to eq("title" => "Hi")
60
- end
61
-
62
- it "parses multipart/form-data bodies (Story 8.3 review R5 — AC9 multipart coverage)" do
63
- # Hand-build a multipart body so the dispatcher exercises the same
64
- # `request.request_parameters` path the runtime's `<form action>`
65
- # wire shape produces. Mirrors the `multipart_post` helper in
66
- # dispatch_request_spec.rb.
67
- boundary = "----RuactDispatcherSpec#{SecureRandom.hex(8)}"
68
- body = +""
69
- body << "--#{boundary}\r\n"
70
- body << "Content-Disposition: form-data; name=\"title\"\r\n\r\n"
71
- body << "From multipart\r\n"
72
- body << "--#{boundary}\r\n"
73
- body << "Content-Disposition: form-data; name=\"body\"\r\n\r\n"
74
- body << "Multipart body\r\n"
75
- body << "--#{boundary}--\r\n"
76
-
77
- entry = register_and_entry(:multipart_echo, &:to_unsafe_h)
78
- request = make_request(body: body, content_type: "multipart/form-data; boundary=#{boundary}")
79
- response = make_response
80
-
81
- described_class.dispatch(entry, request, response)
82
-
83
- expect(response.status).to eq(200)
84
- expect(JSON.parse(response.body)).to eq(
85
- "title" => "From multipart",
86
- "body" => "Multipart body"
87
- )
88
- end
89
-
90
- it "parses application/x-www-form-urlencoded bodies" do
91
- entry = register_and_entry(:form_echo, &:to_unsafe_h)
92
- request = make_request(
93
- body: "title=Hello&body=World",
94
- content_type: "application/x-www-form-urlencoded"
95
- )
96
- response = make_response
97
-
98
- described_class.dispatch(entry, request, response)
99
-
100
- expect(response.status).to eq(200)
101
- expect(JSON.parse(response.body)).to eq("title" => "Hello", "body" => "World")
102
- end
103
-
104
- it "returns an empty params hash for unknown content types" do
105
- entry = register_and_entry(:unknown_ct) { |params| { "keys" => params.to_unsafe_h.keys } }
106
- request = make_request(body: "ignored", content_type: "application/xml")
107
- response = make_response
108
-
109
- described_class.dispatch(entry, request, response)
110
-
111
- expect(response.status).to eq(200)
112
- expect(JSON.parse(response.body)).to eq("keys" => [])
113
- end
114
-
115
- it "treats an empty JSON body as an empty hash" do
116
- entry = register_and_entry(:empty_json, &:to_unsafe_h)
117
- request = make_request(body: "", content_type: "application/json")
118
- response = make_response
119
-
120
- described_class.dispatch(entry, request, response)
121
-
122
- expect(response.status).to eq(200)
123
- expect(JSON.parse(response.body)).to eq({})
124
- end
125
-
126
- it "wraps a scalar JSON top-level value under the `_value` key (mirrors controller path)" do
127
- entry = register_and_entry(:scalar_json) { |params| { value: params[:_value] } }
128
- request = make_request(body: "42", content_type: "application/json")
129
- response = make_response
130
-
131
- described_class.dispatch(entry, request, response)
132
-
133
- expect(response.status).to eq(200)
134
- expect(JSON.parse(response.body)).to eq("value" => 42)
135
- end
136
-
137
- it "exposes the params shadow as ActionController::Parameters (strong params API)" do
138
- entry = register_and_entry(:strong) do |params|
139
- permitted = params.require(:post).permit(:title)
140
- { "permitted" => permitted.to_h }
141
- end
142
- request = make_request(
143
- body: '{"post":{"title":"Hi","evil":"ignored"}}',
144
- content_type: "application/json"
145
- )
146
- response = make_response
147
-
148
- described_class.dispatch(entry, request, response)
149
-
150
- expect(response.status).to eq(200)
151
- expect(JSON.parse(response.body)).to eq("permitted" => { "title" => "Hi" })
152
- end
153
- end
154
-
155
- describe "AC2 — response shape" do
156
- it "renders 204 No Content when the block returns nil" do
157
- entry = register_and_entry(:nil_return) { |_params| nil }
158
- request = make_request(body: "{}", content_type: "application/json")
159
- response = make_response
160
-
161
- described_class.dispatch(entry, request, response)
162
-
163
- expect(response.status).to eq(204)
164
- expect(response.body.to_s).to eq("")
165
- end
166
-
167
- it "renders 200 + JSON when the block returns a Hash" do
168
- entry = register_and_entry(:hash_return) { |_params| { ok: true } }
169
- request = make_request(body: "{}", content_type: "application/json")
170
- response = make_response
171
-
172
- described_class.dispatch(entry, request, response)
173
-
174
- expect(response.status).to eq(200)
175
- expect(JSON.parse(response.body)).to eq("ok" => true)
176
- end
177
-
178
- it "renders 200 + JSON when the block returns an Array" do
179
- entry = register_and_entry(:array_return) { |_params| [1, 2, 3] }
180
- request = make_request(body: "{}", content_type: "application/json")
181
- response = make_response
182
-
183
- described_class.dispatch(entry, request, response)
184
-
185
- expect(response.status).to eq(200)
186
- expect(JSON.parse(response.body)).to eq([1, 2, 3])
187
- end
188
-
189
- it "renders 200 + JSON when the block returns a scalar" do
190
- entry = register_and_entry(:string_return) { |_params| "pong" }
191
- request = make_request(body: "{}", content_type: "application/json")
192
- response = make_response
193
-
194
- described_class.dispatch(entry, request, response)
195
-
196
- expect(response.status).to eq(200)
197
- expect(JSON.parse(response.body)).to eq("pong")
198
- end
199
- end
200
-
201
- describe "Story 8.3 review R3 — malformed JSON → structured 400" do
202
- it "renders 400 + JSON {error} when the JSON body is malformed " \
203
- "(parity with the controller-DSL path's malformed-JSON handler)" do
204
- entry = register_and_entry(:malformed_demo) { |_p| { ok: true } }
205
- request = make_request(body: "{ not json", content_type: "application/json")
206
- response = make_response
207
-
208
- described_class.dispatch(entry, request, response)
209
-
210
- expect(response.status).to eq(400)
211
- body = JSON.parse(response.body)
212
- expect(body.fetch("error")).to match(/ruact action :malformed_demo received malformed JSON body/)
213
- end
214
-
215
- it "does NOT invoke the block when the body cannot be parsed" do
216
- block_called = false
217
- entry = register_and_entry(:never_runs) do |_p|
218
- block_called = true
219
- { ok: true }
220
- end
221
- request = make_request(body: "{ broken", content_type: "application/json")
222
- response = make_response
223
-
224
- described_class.dispatch(entry, request, response)
225
-
226
- expect(block_called).to be(false)
227
- expect(response.status).to eq(400)
228
- end
229
- end
230
-
231
- describe "AC2 — Ruact::ActionError → status + body" do
232
- it "renders the error's integer status + JSON body verbatim" do
233
- entry = register_and_entry(:raise_action_error) do |_params|
234
- raise Ruact::ActionError.new(status: 422, body: { error: "invalid" })
235
- end
236
- request = make_request(body: "{}", content_type: "application/json")
237
- response = make_response
238
-
239
- described_class.dispatch(entry, request, response)
240
-
241
- expect(response.status).to eq(422)
242
- expect(JSON.parse(response.body)).to eq("error" => "invalid")
243
- end
244
-
245
- it "translates a Symbol status to the matching HTTP code" do
246
- entry = register_and_entry(:raise_symbol_status) do |_params|
247
- raise Ruact::ActionError.new(status: :unauthorized, body: { error: "no" })
248
- end
249
- request = make_request(body: "{}", content_type: "application/json")
250
- response = make_response
251
-
252
- described_class.dispatch(entry, request, response)
253
-
254
- expect(response.status).to eq(401)
255
- expect(JSON.parse(response.body)).to eq("error" => "no")
256
- end
257
-
258
- it "renders nil body as empty when ActionError.body is nil" do
259
- entry = register_and_entry(:raise_no_body) do |_params|
260
- raise Ruact::ActionError.new(status: 418, body: nil)
261
- end
262
- request = make_request(body: "{}", content_type: "application/json")
263
- response = make_response
264
-
265
- described_class.dispatch(entry, request, response)
266
-
267
- expect(response.status).to eq(418)
268
- expect(response.body.to_s).to eq("")
269
- end
270
- end
271
- end
272
- end
273
- end