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,199 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- module Ruact
6
- module ServerFunctions
7
- RSpec.describe Registry, :story_8_0a do
8
- subject(:registry) { described_class.new }
9
-
10
- let(:posts_controller) do
11
- Class.new { def self.name = "PostsController" }
12
- end
13
-
14
- let(:bar_controller) do
15
- Class.new { def self.name = "BarController" }
16
- end
17
-
18
- describe "#register (Story 8.0a)" do
19
- it "stores a single entry keyed by its Ruby symbol", :aggregate_failures do
20
- entry = registry.register(:create_post, kind: :action, controller: posts_controller)
21
-
22
- expect(entry).to be_a(RegistryEntry)
23
- expect(entry.ruby_symbol).to eq(:create_post)
24
- expect(entry.js_identifier).to eq("createPost")
25
- expect(entry.kind).to eq(:action)
26
- expect(entry.controller).to eq(posts_controller)
27
- expect(registry.entries.keys).to eq([:create_post])
28
- end
29
-
30
- it "captures the implementation block verbatim for downstream invocation" do
31
- block = -> { :pong }
32
- entry = registry.register(:demo_ping, kind: :action, controller: posts_controller, &block)
33
- expect(entry.block).to be(block)
34
- end
35
-
36
- it "allows registering an action and a query with the same symbol in separate registries" do
37
- actions = described_class.new
38
- queries = described_class.new
39
- actions.register(:create_post, kind: :action, controller: posts_controller)
40
- queries.register(:create_post, kind: :query, controller: posts_controller)
41
- expect(actions.entries.keys).to eq([:create_post])
42
- expect(queries.entries.keys).to eq([:create_post])
43
- end
44
-
45
- it "raises Ruact::ConfigurationError for SCREAMING_SNAKE symbols (Story 8.0a)" do
46
- expect { registry.register(:RECALCULATE, kind: :action, controller: posts_controller) }
47
- .to raise_error(Ruact::ConfigurationError) do |error|
48
- expect(error.message).to include(":RECALCULATE")
49
- end
50
- end
51
-
52
- it "raises Ruact::ConfigurationError on JS-identifier collision and names both " \
53
- "Ruby symbols and both controllers (Story 8.0a)", :aggregate_failures do
54
- registry.register(:foo_bar, kind: :action, controller: posts_controller)
55
- expect { registry.register(:foo__bar, kind: :action, controller: bar_controller) }
56
- .to raise_error(Ruact::ConfigurationError) do |error|
57
- expect(error.message).to include(":foo_bar")
58
- expect(error.message).to include(":foo__bar")
59
- expect(error.message).to include("PostsController")
60
- expect(error.message).to include("BarController")
61
- expect(error.message).to include('"fooBar"')
62
- end
63
- end
64
-
65
- it "allows re-registering the same Ruby symbol (replace semantics, dev reload)" do
66
- registry.register(:create_post, kind: :action, controller: posts_controller)
67
- expect do
68
- registry.register(:create_post, kind: :action, controller: posts_controller)
69
- end.not_to raise_error
70
- expect(registry.size).to eq(1)
71
- end
72
-
73
- it "tolerates a nil controller (Rails-console registration path)" do
74
- expect { registry.register(:create_post, kind: :action) }.not_to raise_error
75
- end
76
-
77
- it "rejects kinds other than :action / :query (Chunk1 Major 2026-05-13)" do
78
- expect { registry.register(:create_post, kind: :wat, controller: posts_controller) }
79
- .to raise_error(Ruact::ConfigurationError) do |error|
80
- expect(error.message).to include(":create_post")
81
- expect(error.message).to include("PostsController")
82
- expect(error.message).to include(":wat")
83
- expect(error.message).to include("[:action, :query]")
84
- end
85
- end
86
-
87
- it "wraps NameBridge symbol-shape failures with AC7 'invalid server-function " \
88
- "symbol :SYMBOL in CONTROLLER' framing (Re-run patch m5)" do
89
- expect { registry.register(:RECALCULATE, kind: :action, controller: posts_controller) }
90
- .to raise_error(Ruact::ConfigurationError) do |error|
91
- expect(error.message).to start_with("invalid server-function symbol :RECALCULATE in PostsController")
92
- end
93
- end
94
- end
95
-
96
- describe "#entries (Story 8.0a)" do
97
- it "returns a frozen snapshot independent of subsequent mutations" do
98
- registry.register(:create_post, kind: :action, controller: posts_controller)
99
- snapshot = registry.entries
100
- expect(snapshot).to be_frozen
101
- registry.register(:list_posts, kind: :query, controller: posts_controller)
102
- expect(snapshot.keys).to eq([:create_post])
103
- end
104
- end
105
-
106
- describe "#clear! (Story 8.0a)" do
107
- it "wipes all entries and returns self" do
108
- registry.register(:create_post, kind: :action, controller: posts_controller)
109
- expect(registry.clear!).to be(registry)
110
- expect(registry).to be_empty
111
- end
112
- end
113
-
114
- describe "Story 8.3 — mixed controller+standalone collision", :story_8_3 do
115
- let(:posts_controller_class) do
116
- Class.new { def self.name = "PostsController" }
117
- end
118
-
119
- let(:standalone_create_post_module) do
120
- Module.new do
121
- extend Ruact::ServerAction
122
-
123
- def self.name
124
- "CreatePost"
125
- end
126
- end
127
- end
128
-
129
- it "raises Ruact::ConfigurationError when the same Ruby symbol is declared in a controller " \
130
- "AND in a standalone module — message names BOTH hosts" do
131
- registry.register(:create_post, kind: :action, controller: posts_controller_class)
132
-
133
- expect do
134
- registry.register(:create_post, kind: :action, controller: standalone_create_post_module)
135
- end.to raise_error(Ruact::ConfigurationError) do |error|
136
- expect(error.message).to include(":create_post")
137
- expect(error.message).to include("PostsController")
138
- expect(error.message).to include("CreatePost")
139
- expect(error.message).to include("declared in BOTH")
140
- end
141
- end
142
-
143
- it "raises Ruact::ConfigurationError when the same symbol is declared in standalone " \
144
- "first, then in a controller (order-independent)" do
145
- registry.register(:create_post, kind: :action, controller: standalone_create_post_module)
146
-
147
- expect do
148
- registry.register(:create_post, kind: :action, controller: posts_controller_class)
149
- end.to raise_error(Ruact::ConfigurationError) do |error|
150
- expect(error.message).to include("PostsController")
151
- expect(error.message).to include("CreatePost")
152
- end
153
- end
154
-
155
- it "describe_controller names a Module host correctly (no inspection fallback) " \
156
- "when one side of the collision is a Module" do
157
- registry.register(:create_post, kind: :action, controller: standalone_create_post_module)
158
- another_module = Module.new do
159
- extend Ruact::ServerAction
160
-
161
- def self.name
162
- "AdminCreatePost"
163
- end
164
- end
165
-
166
- # Cross-bridge JS-identifier collision: two DIFFERENT Ruby symbols
167
- # producing the SAME JS identifier — bridges into `js_identifier ==`
168
- # branch of detect_collision!. The bridge collapses underscores,
169
- # so `:create_post` and `:create__post` both → "createPost".
170
- expect do
171
- registry.register(:create__post, kind: :action, controller: another_module)
172
- end.to raise_error(Ruact::ConfigurationError) do |error|
173
- expect(error.message).to include("CreatePost")
174
- expect(error.message).to include("AdminCreatePost")
175
- expect(error.message).to include('"createPost"')
176
- end
177
- end
178
- end
179
-
180
- describe "Ruact module-level accessors (Story 8.0a)" do
181
- it "returns two independent Registry singletons" do
182
- expect(Ruact.action_registry).to be_a(described_class)
183
- expect(Ruact.query_registry).to be_a(described_class)
184
- expect(Ruact.action_registry).not_to equal(Ruact.query_registry)
185
- end
186
-
187
- it "memoizes the same instance across calls" do
188
- expect(Ruact.action_registry).to equal(Ruact.action_registry)
189
- expect(Ruact.query_registry).to equal(Ruact.query_registry)
190
- end
191
-
192
- it "both registries are empty at boot (Story 8.1 / 9.1 populate them)" do
193
- expect(Ruact.action_registry).to be_empty
194
- expect(Ruact.query_registry).to be_empty
195
- end
196
- end
197
- end
198
- end
199
- end
@@ -1,224 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Story 8.3 — covers `Ruact::ServerAction` extended onto a Module: AC1
4
- # (registration shape + no method defined on host module) and AC6 (guard-
5
- # rail matrix mirroring the controller-DSL path adapted to module context).
6
-
7
- require "spec_helper"
8
-
9
- RSpec.describe Ruact::ServerAction, :story_8_3 do
10
- describe "AC1 — extend Ruact::ServerAction + ruact_action registers a standalone host" do
11
- it "registers a standalone host in Ruact.action_registry with controller: <the Module>" do
12
- mod = Module.new do
13
- extend Ruact::ServerAction
14
-
15
- def self.name
16
- "AC1RegistrationModule"
17
- end
18
-
19
- ruact_action(:standalone_create_post) { |_params| { ok: true } }
20
- end
21
-
22
- entries = Ruact.action_registry.entries
23
- expect(entries).to include(:standalone_create_post)
24
-
25
- entry = entries[:standalone_create_post]
26
- expect(entry).to be_a(Ruact::ServerFunctions::RegistryEntry)
27
- expect(entry.kind).to eq(:action)
28
- expect(entry.controller).to be(mod)
29
- expect(entry.controller).to be_a(Module)
30
- expect(entry.controller).not_to be_a(Class)
31
- expect(entry.js_identifier).to eq("standaloneCreatePost")
32
- expect(entry.block).to be_a(Proc)
33
- end
34
-
35
- it "does NOT define an instance method on the host module — the block is reachable " \
36
- "only through the gem endpoint, never as a Ruby method" do
37
- mod = Module.new do
38
- extend Ruact::ServerAction
39
-
40
- def self.name
41
- "AC1NoMethodModule"
42
- end
43
-
44
- ruact_action(:no_method_exposed) { |_params| nil }
45
- end
46
-
47
- expect(mod.respond_to?(:no_method_exposed)).to be(false)
48
- expect(mod.instance_methods).not_to include(:no_method_exposed)
49
- expect(mod.singleton_methods).not_to include(:no_method_exposed)
50
- # Direct dispatch attempts (someone trying to `Mod.send(:no_method_exposed)`
51
- # should fail with NoMethodError) — proves the surface is endpoint-only.
52
- expect { mod.send(:no_method_exposed, {}) }.to raise_error(NoMethodError)
53
- end
54
- end
55
-
56
- describe "AC6 — guard-rail matrix (mirror controller-DSL adapted to module context)" do
57
- let(:host) do
58
- Module.new do
59
- extend Ruact::ServerAction
60
-
61
- def self.name
62
- "GuardRailModule"
63
- end
64
- end
65
- end
66
-
67
- it "raises ArgumentError when given a String instead of a Symbol" do
68
- expect do
69
- host.module_eval { ruact_action("create_post") { |_p| nil } }
70
- end.to raise_error(ArgumentError, /ruact_action requires a Symbol/)
71
- end
72
-
73
- it "raises ArgumentError when the block is missing" do
74
- expect { host.module_eval { ruact_action(:create_post) } }
75
- .to raise_error(ArgumentError, /requires a block/)
76
- end
77
-
78
- it "raises ArgumentError when the block accepts no positional argument" do
79
- expect do
80
- host.module_eval { ruact_action(:create_post) {} } # no positional arg
81
- end.to raise_error(ArgumentError, /exactly one positional parameter/)
82
- end
83
-
84
- it "raises ArgumentError when the block accepts more than one positional argument" do
85
- expect do
86
- host.module_eval { ruact_action(:create_post) { |_a, _b| nil } }
87
- end.to raise_error(ArgumentError, /exactly one positional parameter/)
88
- end
89
-
90
- it "raises ArgumentError when the block has a required keyword argument" do
91
- block_with_kwarg = ->(_p, required:) { required }
92
- expect do
93
- host.module_eval { ruact_action(:create_post, &block_with_kwarg) }
94
- end.to raise_error(ArgumentError, /no required keyword arguments/)
95
- end
96
-
97
- it "ACCEPTS a block with a single positional arg" do
98
- expect do
99
- host.module_eval { ruact_action(:create_post) { |_p| nil } }
100
- end.not_to raise_error
101
- end
102
-
103
- it "ACCEPTS a block with a splat positional arg" do
104
- another_host = Module.new do
105
- extend Ruact::ServerAction
106
-
107
- def self.name
108
- "SplatHost"
109
- end
110
- end
111
- expect do
112
- another_host.module_eval { ruact_action(:create_post) { |*_args| nil } }
113
- end.not_to raise_error
114
- end
115
-
116
- it "ACCEPTS a block with optional keyword args" do
117
- another_host = Module.new do
118
- extend Ruact::ServerAction
119
-
120
- def self.name
121
- "OptKwHost"
122
- end
123
- end
124
- expect do
125
- another_host.module_eval { ruact_action(:create_post) { |_p, key: nil| key } }
126
- end.not_to raise_error
127
- end
128
-
129
- it "raises Ruact::ConfigurationError for a bad naming-bridge symbol (:Create_Post)" do
130
- expect do
131
- host.module_eval { ruact_action(:Create_Post) { |_p| nil } }
132
- end.to raise_error(Ruact::ConfigurationError) do |error|
133
- expect(error.message).to include(":Create_Post")
134
- expect(error.message).to include("GuardRailModule")
135
- end
136
- end
137
-
138
- it "raises Ruact::ConfigurationError for a JS-reserved-word target (:class → \"class\")" do
139
- expect do
140
- host.module_eval { ruact_action(:class) { |_p| nil } }
141
- end.to raise_error(Ruact::ConfigurationError, /JS reserved word/)
142
- end
143
-
144
- it "raises Ruact::ConfigurationError for a ruact-runtime-reserved target (:revalidate)" do
145
- expect do
146
- host.module_eval { ruact_action(:revalidate) { |_p| nil } }
147
- end.to raise_error(Ruact::ConfigurationError) do |error|
148
- expect(error.message).to include("revalidate")
149
- end
150
- end
151
-
152
- it "raises Ruact::ConfigurationError for the second standalone host declaring the same symbol" do
153
- host.module_eval { ruact_action(:dup_symbol) { |_p| nil } }
154
- second_host = Module.new do
155
- extend Ruact::ServerAction
156
-
157
- def self.name
158
- "SecondHostModule"
159
- end
160
- end
161
- expect do
162
- second_host.module_eval { ruact_action(:dup_symbol) { |_p| nil } }
163
- end.to raise_error(Ruact::ConfigurationError) do |error|
164
- expect(error.message).to include(":dup_symbol")
165
- expect(error.message).to include("GuardRailModule")
166
- expect(error.message).to include("SecondHostModule")
167
- end
168
- end
169
-
170
- it "does NOT have a FRAMEWORK_RESERVED_METHODS check — standalone modules can use " \
171
- "names that would clobber an ActionController method (e.g., :params), since the " \
172
- "module has no ActionController surface" do
173
- expect do
174
- Module.new do
175
- extend Ruact::ServerAction
176
-
177
- def self.name
178
- "FrameworkResvHost"
179
- end
180
-
181
- ruact_action(:params) { |_p| nil }
182
- end
183
- end.not_to raise_error
184
- end
185
-
186
- it "Story 8.3 review R4 — rejects Class hosts at first ruact_action call with a " \
187
- "documented Ruact::ConfigurationError pointing the dev to `include Ruact::Controller`" do
188
- klass = Class.new do
189
- extend Ruact::ServerAction
190
-
191
- def self.name
192
- "WronglyExtendedClass"
193
- end
194
- end
195
-
196
- expect do
197
- klass.class_eval { ruact_action(:bad_host) { |_p| nil } }
198
- end.to raise_error(Ruact::ConfigurationError) do |error|
199
- expect(error.message).to include("WronglyExtendedClass")
200
- expect(error.message).to include("standalone HOST MODULES")
201
- expect(error.message).to include("include Ruact::Controller")
202
- end
203
- end
204
-
205
- it "does NOT install a method_added hook — a later `def` on the host module does not " \
206
- "raise (standalone modules don't define action methods at all)" do
207
- expect do
208
- Module.new do
209
- extend Ruact::ServerAction
210
-
211
- def self.name
212
- "MethodAddedHost"
213
- end
214
-
215
- ruact_action(:registered_action) { |_p| nil }
216
-
217
- # A later method definition on the module would not trigger anything
218
- # — the registry holds the block; the module surface is irrelevant.
219
- define_method(:registered_action) { :unrelated_def }
220
- end
221
- end.not_to raise_error
222
- end
223
- end
224
- end
@@ -1,142 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Story 8.3 — `Ruact::ServerFunctions::StandaloneContext`: AC3 surface
4
- # (exposes params/request/session/cookies/headers; render/redirect_to/head
5
- # raise NoMethodError; current_user memoizes + falls back to env key +
6
- # raises CurrentUserNotConfiguredError when neither path produces a value).
7
-
8
- require "spec_helper"
9
- require "action_controller"
10
- require "action_dispatch"
11
-
12
- module Ruact
13
- module ServerFunctions
14
- RSpec.describe StandaloneContext, :story_8_3 do
15
- let(:env) { {} }
16
- let(:request) do
17
- ActionDispatch::Request.new(env.merge(
18
- "REQUEST_METHOD" => "POST",
19
- "rack.input" => StringIO.new("")
20
- ))
21
- end
22
- let(:params) { ActionController::Parameters.new("title" => "Hello") }
23
- let(:context) { described_class.new(params: params, request: request) }
24
-
25
- describe "exposed surface (AC3)" do
26
- it "exposes params (the action-call args, as ActionController::Parameters)" do
27
- expect(context.params).to be_a(ActionController::Parameters)
28
- expect(context.params[:title]).to eq("Hello")
29
- end
30
-
31
- it "exposes the live request" do
32
- expect(context.request).to be(request)
33
- end
34
-
35
- it "exposes headers via request.headers" do
36
- expect(context.headers).to be(request.headers)
37
- end
38
- end
39
-
40
- describe "blocked surface — render / redirect_to / head (AC3)" do
41
- it "render raises NoMethodError with the documented hint" do
42
- expect { context.render(json: { ok: true }) }
43
- .to raise_error(NoMethodError, /does not expose `render`/)
44
- end
45
-
46
- it "redirect_to raises NoMethodError with the documented hint" do
47
- expect { context.redirect_to("/login") }
48
- .to raise_error(NoMethodError, /does not expose `redirect_to`/)
49
- end
50
-
51
- it "head raises NoMethodError with the documented hint" do
52
- expect { context.head(:no_content) }
53
- .to raise_error(NoMethodError, /does not expose `head`/)
54
- end
55
- end
56
-
57
- describe "current_user resolver path (AC3)" do
58
- around do |example|
59
- Ruact.instance_variable_set(:@config, nil)
60
- Ruact.instance_variable_set(:@configured_at_least_once, false)
61
- example.run
62
- ensure
63
- Ruact.instance_variable_set(:@config, nil)
64
- Ruact.instance_variable_set(:@configured_at_least_once, false)
65
- end
66
-
67
- before do
68
- # Silence the warn-on-reconfigure noise.
69
- allow(Rails).to receive(:logger).and_return(Logger.new(IO::NULL))
70
- end
71
-
72
- it "raises Ruact::CurrentUserNotConfiguredError when no resolver is configured AND no env key is set" do
73
- expect { context.current_user }.to raise_error(Ruact::CurrentUserNotConfiguredError) do |err|
74
- expect(err.message).to include("Ruact.current_user requires Ruact.config.current_user_resolver")
75
- expect(err.message).to include("Devise")
76
- expect(err.message).to include("hand-rolled session")
77
- end
78
- end
79
-
80
- it "returns the configured resolver's value, passing request.env to the lambda" do
81
- user = Struct.new(:id).new(42)
82
- Ruact.configure do |c|
83
- c.current_user_resolver = lambda { |env_arg|
84
- expect(env_arg).to be(request.env)
85
- user
86
- }
87
- end
88
- expect(context.current_user).to be(user)
89
- end
90
-
91
- it "memoizes current_user across multiple calls (resolver invoked once)" do
92
- call_count = 0
93
- Ruact.configure do |c|
94
- c.current_user_resolver = lambda { |_env|
95
- call_count += 1
96
- :user
97
- }
98
- end
99
- context.current_user
100
- context.current_user
101
- context.current_user
102
- expect(call_count).to eq(1)
103
- end
104
-
105
- it "prefers an upstream-set request.env['ruact.current_user'] over the resolver" do
106
- env["ruact.current_user"] = :upstream_user
107
- resolver_called = false
108
- Ruact.configure do |c|
109
- c.current_user_resolver = lambda { |_env|
110
- resolver_called = true
111
- :resolver_user
112
- }
113
- end
114
- expect(context.current_user).to eq(:upstream_user)
115
- expect(resolver_called).to be(false)
116
- end
117
-
118
- it "falls back to the resolver when the env key is absent (not just nil)" do
119
- # Pitfall: env.key? differs from env.fetch — a `nil` value should still
120
- # prefer the env path. Verify by setting nil explicitly.
121
- env["ruact.current_user"] = nil
122
- Ruact.configure do |c|
123
- c.current_user_resolver = ->(_env) { :resolver_user }
124
- end
125
- expect(context.current_user).to be_nil
126
- end
127
- end
128
-
129
- describe "__ruact_current_user_read? (Pitfall #4 dev warning flag)" do
130
- it "is false when the block never reads current_user" do
131
- expect(context.__ruact_current_user_read?).to be(false)
132
- end
133
-
134
- it "flips to true when the block reads current_user" do
135
- Ruact.configure { |c| c.current_user_resolver = ->(_env) { :u } }
136
- context.current_user
137
- expect(context.__ruact_current_user_read?).to be(true)
138
- end
139
- end
140
- end
141
- end
142
- end