ruact 0.0.5 → 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 (131) 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 +67 -18
  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 +40 -45
  90. data/vendor/javascript/ruact-server-functions-runtime/index.js +42 -98
  91. data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +72 -75
  92. data/vendor/javascript/ruact-server-functions-runtime/package.json +1 -1
  93. data/vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs +96 -0
  94. data/vendor/javascript/vite-plugin-ruact/index.js +353 -7
  95. data/vendor/javascript/vite-plugin-ruact/manifest-contract.test.mjs +182 -0
  96. data/vendor/javascript/vite-plugin-ruact/package-lock.json +16 -0
  97. data/vendor/javascript/vite-plugin-ruact/package.json +3 -1
  98. data/vendor/javascript/vite-plugin-ruact/registry.test.mjs +199 -0
  99. data/vendor/javascript/vite-plugin-ruact/runtime/bootstrap.jsx +68 -0
  100. data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +235 -0
  101. data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +473 -0
  102. data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +114 -139
  103. data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +199 -262
  104. data/vendor/javascript/vite-plugin-ruact/tsconfig.json +18 -0
  105. data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold-agnostic.json +18 -0
  106. data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold.json +20 -0
  107. data/vendor/javascript/vite-plugin-ruact/type-tests/emitted-module.test-d.ts +23 -0
  108. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostDeleteDialog.tsx +90 -0
  109. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostForm.tsx +238 -0
  110. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostList.tsx +339 -0
  111. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostDeleteDialog.tsx +85 -0
  112. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostForm.tsx +216 -0
  113. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostList.tsx +269 -0
  114. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/ambient.d.ts +78 -0
  115. data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/ambient.d.ts +166 -0
  116. data/vendor/javascript/vite-plugin-ruact/type-tests/typed-query.test-d.ts +48 -0
  117. data/vendor/javascript/vite-plugin-ruact/type-tests/usequery.test-d.ts +26 -0
  118. metadata +55 -15
  119. data/lib/generators/ruact/install/templates/application.jsx.tt +0 -51
  120. data/lib/ruact/server_action.rb +0 -131
  121. data/lib/ruact/server_functions/endpoint_controller.rb +0 -237
  122. data/lib/ruact/server_functions/registry.rb +0 -148
  123. data/lib/ruact/server_functions/registry_entry.rb +0 -26
  124. data/lib/ruact/server_functions/standalone_context.rb +0 -103
  125. data/lib/ruact/server_functions/standalone_dispatcher.rb +0 -178
  126. data/spec/ruact/server_functions/csrf_request_spec.rb +0 -380
  127. data/spec/ruact/server_functions/dispatch_request_spec.rb +0 -819
  128. data/spec/ruact/server_functions/registry_spec.rb +0 -199
  129. data/spec/ruact/server_functions/standalone_action_spec.rb +0 -224
  130. data/spec/ruact/server_functions/standalone_context_spec.rb +0 -142
  131. data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +0 -273
@@ -22,6 +22,10 @@ require "action_view/railtie"
22
22
 
23
23
  require "spec_helper"
24
24
  require "rack/test"
25
+ require "global_id"
26
+ require "active_support/core_ext/integer/time"
27
+ require "active_support/core_ext/numeric/time"
28
+ require "active_support/message_verifier"
25
29
 
26
30
  require "ruact/controller"
27
31
  require "ruact/server"
@@ -183,6 +187,41 @@ module QueryRequestSpecSupport # rubocop:disable Style/OneClassPerFile
183
187
  { "opt" => opt }
184
188
  end
185
189
  end
190
+
191
+ # Story 13.2 (FR96) — a GlobalID-locatable record used to exercise the signed
192
+ # reference round-trip through the real host request cycle.
193
+ class RefPost
194
+ include GlobalID::Identification
195
+
196
+ attr_reader :id
197
+
198
+ def initialize(id)
199
+ @id = id.to_s
200
+ self.class.store[@id] = self
201
+ end
202
+
203
+ def self.store
204
+ @store ||= {}
205
+ end
206
+
207
+ def self.find(id)
208
+ store.fetch(id.to_s)
209
+ end
210
+ end
211
+
212
+ # Story 13.2 (FR96) — a public query (skips auth) that resolves a signed
213
+ # reference handed back from the client. A tampered/expired/wrong-purpose
214
+ # token raises Ruact::InvalidSignedGlobalIDError, which the Story 8.4
215
+ # structured-error chain renders as a clean 400 (no ActiveRecord::RecordNotFound
216
+ # leak, no raw-id trust).
217
+ class SignedRefQuery < ApplicationQuery
218
+ ruact_skip_before_action :require_login
219
+
220
+ def resolve_ref(token:)
221
+ record = Ruact.locate_signed(token, for: :spec_ref)
222
+ { "id" => record.id }
223
+ end
224
+ end
186
225
  end
187
226
 
188
227
  if defined?(ControllerRequestSpecSupport) &&
@@ -191,7 +230,8 @@ if defined?(ControllerRequestSpecSupport) &&
191
230
  ControllerRequestSpecSupport.app_class.routes.append do
192
231
  ruact_queries QueryRequestSpecSupport::CatalogQuery,
193
232
  QueryRequestSpecSupport::PublicCatalogQuery,
194
- QueryRequestSpecSupport::SearchQuery
233
+ QueryRequestSpecSupport::SearchQuery,
234
+ QueryRequestSpecSupport::SignedRefQuery
195
235
  end
196
236
  end
197
237
 
@@ -596,3 +636,71 @@ RSpec.describe "Story 9.5: FR88 query kwargs sanitization", :story_9_5 do
596
636
  end
597
637
  end
598
638
  end
639
+
640
+ # Story 13.2 (FR96) — SignedGlobalID references resolved through the REAL host
641
+ # request cycle: a valid token round-trips; a tampered token surfaces as a clean
642
+ # 400 via the Story 8.4 structured-error chain (no ActiveRecord::RecordNotFound
643
+ # leak, no raw-id trust).
644
+ RSpec.describe "Story 13.2: SignedGlobalID record references (FR96)", :story_13_2 do
645
+ include Rack::Test::Methods
646
+
647
+ let(:app_class) { ControllerRequestSpecSupport.app_class }
648
+ let(:app) { app_class.instance }
649
+
650
+ before do
651
+ Rails.logger = Logger.new(IO::NULL)
652
+ ControllerRequestSpecSupport.boot!
653
+ # globalid signing context for the booted app (the 7.9 app sets
654
+ # secret_key_base but does not load the globalid railtie).
655
+ GlobalID.app = "ruact-test"
656
+ SignedGlobalID.verifier = ActiveSupport::MessageVerifier.new("a" * 64)
657
+ end
658
+
659
+ def sign(record, purpose: :spec_ref, expires_in: 1.hour)
660
+ Ruact.signed_global_id(record, for: purpose, expires_in: expires_in)
661
+ end
662
+
663
+ it "round-trips a valid token through the host chain (no auth needed — public query)" do
664
+ record = QueryRequestSpecSupport::RefPost.new("42")
665
+ get "/q/resolveRef", { "token" => sign(record) }
666
+ expect(last_response.status).to(eq(200), "body=#{last_response.body[0, 300]}")
667
+ expect(JSON.parse(last_response.body)).to eq("id" => "42")
668
+ end
669
+
670
+ it "rejects a tampered token as a clean 400 structured error (no RecordNotFound leak)" do
671
+ record = QueryRequestSpecSupport::RefPost.new("99")
672
+ get "/q/resolveRef", { "token" => "#{sign(record)}tamper" }
673
+
674
+ expect(last_response.status).to eq(400)
675
+ body = JSON.parse(last_response.body)
676
+ expect(body.fetch("_ruact_server_action_error")).to be(true)
677
+ expect(body.fetch("action_name")).to eq("resolve_ref")
678
+ expect(body.fetch("error_class")).to eq("Ruact::InvalidSignedGlobalIDError")
679
+ expect(last_response.body).not_to include("RecordNotFound")
680
+ end
681
+
682
+ it "rejects a wrong-purpose token as a clean 400" do
683
+ record = QueryRequestSpecSupport::RefPost.new("7")
684
+ get "/q/resolveRef", { "token" => sign(record, purpose: :other_purpose) }
685
+ expect(last_response.status).to eq(400)
686
+ expect(JSON.parse(last_response.body).fetch("error_class"))
687
+ .to eq("Ruact::InvalidSignedGlobalIDError")
688
+ end
689
+
690
+ it "does not leak dev-only error fields in production payload mode" do
691
+ record = QueryRequestSpecSupport::RefPost.new("5")
692
+ Ruact.configure { |c| c.dev_error_payload_enabled = false }
693
+ get "/q/resolveRef", { "token" => "#{sign(record)}tamper" }
694
+ expect(last_response.status).to eq(400)
695
+ body = JSON.parse(last_response.body)
696
+ # production payload is exactly the four baseline keys — no split backtrace,
697
+ # suggestion, or validation/upload blocks (the real dev-only field names).
698
+ expect(body.keys).to contain_exactly(
699
+ "_ruact_server_action_error", "action_name", "error_class", "message"
700
+ )
701
+ expect(body.keys).not_to include("app_frames", "gem_frames", "suggestion")
702
+ ensure
703
+ Ruact.instance_variable_set(:@config, nil)
704
+ Ruact.instance_variable_set(:@configured_at_least_once, false)
705
+ end
706
+ end