ruact 0.0.2 → 0.0.4
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/.codecov.yml +31 -0
- data/.github/workflows/ci.yml +160 -94
- data/.github/workflows/server-functions-bench.yml +54 -0
- data/.rubocop.yml +19 -1
- data/.rubocop_todo.yml +175 -0
- data/CHANGELOG.md +88 -5
- data/README.md +2 -0
- data/RELEASING.md +9 -3
- data/bench/server_functions_dispatch_bench.rb +309 -0
- data/bench/server_functions_dispatch_bench.results.md +121 -0
- data/docs/internal/README.md +9 -0
- data/docs/internal/decisions/server-functions-api.md +1779 -0
- data/lib/generators/ruact/install/install_generator.rb +43 -0
- data/lib/generators/ruact/install/templates/application.jsx.tt +1 -1
- data/lib/generators/ruact/install/templates/initializer.rb.tt +1 -1
- data/lib/ruact/client_manifest.rb +125 -12
- data/lib/ruact/configuration.rb +264 -23
- data/lib/ruact/controller.rb +459 -32
- data/lib/ruact/doctor.rb +34 -2
- data/lib/ruact/erb_preprocessor.rb +6 -6
- data/lib/ruact/errors.rb +100 -0
- data/lib/ruact/flight/serializer.rb +2 -2
- data/lib/ruact/html_converter.rb +131 -31
- data/lib/ruact/query.rb +107 -0
- data/lib/ruact/railtie.rb +220 -3
- data/lib/ruact/render_context.rb +30 -0
- data/lib/ruact/render_pipeline.rb +201 -59
- data/lib/ruact/routing.rb +81 -0
- data/lib/ruact/serializable.rb +11 -11
- data/lib/ruact/server.rb +341 -0
- data/lib/ruact/server_action.rb +131 -0
- data/lib/ruact/server_functions/backtrace_cleaner.rb +32 -0
- data/lib/ruact/server_functions/bucket_two_payload.rb +109 -0
- data/lib/ruact/server_functions/codegen.rb +344 -0
- data/lib/ruact/server_functions/codegen_v2.rb +212 -0
- data/lib/ruact/server_functions/endpoint_controller.rb +237 -0
- data/lib/ruact/server_functions/error_payload.rb +93 -0
- data/lib/ruact/server_functions/error_rendering.rb +190 -0
- data/lib/ruact/server_functions/error_suggestion.rb +38 -0
- data/lib/ruact/server_functions/name_bridge.rb +118 -0
- data/lib/ruact/server_functions/query_context.rb +62 -0
- data/lib/ruact/server_functions/query_dispatch.rb +313 -0
- data/lib/ruact/server_functions/query_source.rb +150 -0
- data/lib/ruact/server_functions/registry.rb +148 -0
- data/lib/ruact/server_functions/registry_entry.rb +26 -0
- data/lib/ruact/server_functions/route_source.rb +201 -0
- data/lib/ruact/server_functions/snapshot.rb +195 -0
- data/lib/ruact/server_functions/snapshot_writer.rb +65 -0
- data/lib/ruact/server_functions/standalone_context.rb +103 -0
- data/lib/ruact/server_functions/standalone_dispatcher.rb +178 -0
- data/lib/ruact/server_functions.rb +111 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +17 -9
- data/lib/ruact.rb +85 -6
- data/lib/rubocop/cop/ruact/no_shared_state.rb +1 -1
- data/lib/tasks/benchmark.rake +15 -11
- data/lib/tasks/ruact.rake +81 -0
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +1 -1
- data/spec/fixtures/flight/README.md +55 -7
- data/spec/fixtures/flight/bigint.txt +1 -0
- data/spec/fixtures/flight/infinity.txt +1 -0
- data/spec/fixtures/flight/nan.txt +1 -0
- data/spec/fixtures/flight/negative_infinity.txt +1 -0
- data/spec/fixtures/flight/undefined.txt +1 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +3 -0
- data/spec/ruact/client_manifest_spec.rb +108 -0
- data/spec/ruact/configuration_spec.rb +501 -0
- data/spec/ruact/controller_request_spec.rb +204 -0
- data/spec/ruact/controller_spec.rb +427 -39
- data/spec/ruact/doctor_spec.rb +118 -0
- data/spec/ruact/erb_preprocessor_hook_spec.rb +3 -3
- data/spec/ruact/erb_preprocessor_spec.rb +7 -7
- data/spec/ruact/errors_spec.rb +95 -0
- data/spec/ruact/flight/renderer_spec.rb +14 -3
- data/spec/ruact/flight/serializer_spec.rb +129 -88
- data/spec/ruact/html_converter_spec.rb +183 -5
- data/spec/ruact/install_generator_spec.rb +93 -0
- data/spec/ruact/query_request_spec.rb +598 -0
- data/spec/ruact/query_spec.rb +105 -0
- data/spec/ruact/railtie_spec.rb +2 -3
- data/spec/ruact/render_context_spec.rb +58 -0
- data/spec/ruact/render_pipeline_concurrency_spec.rb +78 -0
- data/spec/ruact/render_pipeline_spec.rb +784 -330
- data/spec/ruact/serializable_spec.rb +8 -8
- data/spec/ruact/server_bucket_request_spec.rb +352 -0
- data/spec/ruact/server_function_name_spec.rb +53 -0
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +63 -0
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +200 -0
- data/spec/ruact/server_functions/codegen_spec.rb +508 -0
- data/spec/ruact/server_functions/csrf_request_spec.rb +380 -0
- data/spec/ruact/server_functions/dispatch_request_spec.rb +819 -0
- data/spec/ruact/server_functions/error_payload_spec.rb +222 -0
- data/spec/ruact/server_functions/error_suggestion_spec.rb +79 -0
- data/spec/ruact/server_functions/name_bridge_spec.rb +212 -0
- data/spec/ruact/server_functions/query_context_spec.rb +72 -0
- data/spec/ruact/server_functions/query_source_spec.rb +142 -0
- data/spec/ruact/server_functions/railtie_integration_spec.rb +412 -0
- data/spec/ruact/server_functions/rake_spec.rb +86 -0
- data/spec/ruact/server_functions/registry_spec.rb +199 -0
- data/spec/ruact/server_functions/route_source_spec.rb +202 -0
- data/spec/ruact/server_functions/snapshot_spec.rb +256 -0
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +71 -0
- data/spec/ruact/server_functions/standalone_action_spec.rb +224 -0
- data/spec/ruact/server_functions/standalone_context_spec.rb +142 -0
- data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +273 -0
- data/spec/ruact/server_rescue_request_spec.rb +416 -0
- data/spec/ruact/server_spec.rb +180 -0
- data/spec/ruact/server_upload_request_spec.rb +311 -0
- data/spec/ruact/view_helper_spec.rb +23 -17
- data/spec/spec_helper.rb +52 -1
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +135 -0
- data/spec/support/flight_wire_parser_spec.rb +93 -0
- data/spec/support/matchers/flight_fixture_matcher.rb +356 -0
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +250 -0
- data/spec/support/rails_stub.rb +75 -5
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +173 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.js +614 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +827 -0
- data/vendor/javascript/ruact-server-functions-runtime/package.json +29 -0
- data/vendor/javascript/ruact-server-functions-runtime/usequery.test.mjs +181 -0
- data/vendor/javascript/vite-plugin-ruact/index.js +164 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +1429 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +15 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +804 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +961 -0
- data/vendor/javascript/vite-plugin-ruact/vitest.config.mjs +21 -0
- metadata +91 -5
- data/lib/ruact/component_registry.rb +0 -31
- data/lib/tasks/rsc.rake +0 -9
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Story 9.2 — unit spec for the pure Bucket-2 response serializer. Pins the
|
|
4
|
+
# prop-exposure policy mirrored from Ruact::Flight::Serializer#serialize_unknown
|
|
5
|
+
# (Serializable → ruact_props only; strict → raise; vetted as_json fallback),
|
|
6
|
+
# producing a plain JSON-ready Hash (no Flight wire encoding). Pure function —
|
|
7
|
+
# no Rails / request / Ruact.config reads (NFR26 / AC8).
|
|
8
|
+
|
|
9
|
+
require "spec_helper"
|
|
10
|
+
require "ruact/server_functions/bucket_two_payload"
|
|
11
|
+
|
|
12
|
+
# Fixtures live OUTSIDE the example group (no leaky constants in the block).
|
|
13
|
+
module B2Fixtures
|
|
14
|
+
# A Serializable model exposing only some attributes.
|
|
15
|
+
class Post
|
|
16
|
+
include Ruact::Serializable
|
|
17
|
+
|
|
18
|
+
attr_reader :id, :title, :secret
|
|
19
|
+
|
|
20
|
+
def initialize(id:, title:, secret:)
|
|
21
|
+
@id = id
|
|
22
|
+
@title = title
|
|
23
|
+
@secret = secret
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
ruact_props :id, :title
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Serializable whose prop is itself a Serializable (nested).
|
|
30
|
+
class AuthoredPost
|
|
31
|
+
include Ruact::Serializable
|
|
32
|
+
|
|
33
|
+
attr_reader :title, :author
|
|
34
|
+
|
|
35
|
+
def initialize(title:, author:)
|
|
36
|
+
@title = title
|
|
37
|
+
@author = author
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
ruact_props :title, :author
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class Author
|
|
44
|
+
include Ruact::Serializable
|
|
45
|
+
|
|
46
|
+
attr_reader :name, :password_digest
|
|
47
|
+
|
|
48
|
+
def initialize(name:, password_digest:)
|
|
49
|
+
@name = name
|
|
50
|
+
@password_digest = password_digest
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
ruact_props :name
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A plain object with as_json (AR-like).
|
|
57
|
+
class PlainRecord
|
|
58
|
+
def as_json(_opts = nil)
|
|
59
|
+
{ "id" => 7, "leaked" => "everything" }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class SelfReturningAsJson
|
|
64
|
+
def as_json(_opts = nil)
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class RaisingAsJson
|
|
70
|
+
def as_json(_opts = nil)
|
|
71
|
+
raise "boom in as_json"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
RSpec.describe Ruact::ServerFunctions::BucketTwoPayload, :story_9_2 do
|
|
77
|
+
describe ".build (AC2 — keyed by ivar name, all exposed ivars)" do
|
|
78
|
+
it "keys the result by the assigns names and serializes each value" do
|
|
79
|
+
result = described_class.build(
|
|
80
|
+
{ "post" => B2Fixtures::Post.new(id: 1, title: "Hi", secret: "x"), "count" => 3 },
|
|
81
|
+
strict: true
|
|
82
|
+
)
|
|
83
|
+
expect(result).to eq("post" => { "id" => 1, "title" => "Hi" }, "count" => 3)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "does NOT unwrap a single ivar — it stays keyed (no magic unwrap)" do
|
|
87
|
+
result = described_class.build({ "post" => B2Fixtures::Post.new(id: 1, title: "Hi", secret: "x") }, strict: true)
|
|
88
|
+
expect(result).to eq("post" => { "id" => 1, "title" => "Hi" })
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe "Serializable policy" do
|
|
93
|
+
it "exposes ONLY ruact_props, never undeclared attributes (no secret leak)" do
|
|
94
|
+
result = described_class.build({ "post" => B2Fixtures::Post.new(id: 1, title: "Hi", secret: "nope") },
|
|
95
|
+
strict: true)
|
|
96
|
+
expect(result.fetch("post")).to eq("id" => 1, "title" => "Hi")
|
|
97
|
+
expect(result.fetch("post")).not_to have_key("secret")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "recurses into a Serializable-valued prop (nested), applying ruact_props at each level" do
|
|
101
|
+
author = B2Fixtures::Author.new(name: "Ada", password_digest: "HASH")
|
|
102
|
+
post = B2Fixtures::AuthoredPost.new(title: "T", author: author)
|
|
103
|
+
result = described_class.build({ "post" => post }, strict: true)
|
|
104
|
+
expect(result.fetch("post")).to eq("title" => "T", "author" => { "name" => "Ada" })
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "serializes an Array of Serializables element-wise" do
|
|
108
|
+
posts = [B2Fixtures::Post.new(id: 1, title: "A", secret: "s"),
|
|
109
|
+
B2Fixtures::Post.new(id: 2, title: "B", secret: "s")]
|
|
110
|
+
result = described_class.build({ "posts" => posts }, strict: true)
|
|
111
|
+
expect(result.fetch("posts")).to eq([{ "id" => 1, "title" => "A" }, { "id" => 2, "title" => "B" }])
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe "primitive pass-through (NOT subject to strict policy)" do
|
|
116
|
+
it "passes scalars through untouched even under strict" do
|
|
117
|
+
result = described_class.build(
|
|
118
|
+
{ "i" => 5, "f" => 1.5, "s" => "x", "t" => true, "n" => nil, "sym" => :ok },
|
|
119
|
+
strict: true
|
|
120
|
+
)
|
|
121
|
+
expect(result).to eq("i" => 5, "f" => 1.5, "s" => "x", "t" => true, "n" => nil, "sym" => :ok)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "passes Time through untouched (Rails render json: handles ISO formatting)" do
|
|
125
|
+
time = Time.utc(2026, 1, 2, 3, 4, 5)
|
|
126
|
+
result = described_class.build({ "at" => time }, strict: true)
|
|
127
|
+
expect(result.fetch("at")).to equal(time)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "stringifies Hash keys and recurses values" do
|
|
131
|
+
assigns = { "meta" => { a: 1, b: B2Fixtures::Post.new(id: 9, title: "N", secret: "s") } }
|
|
132
|
+
result = described_class.build(assigns, strict: true)
|
|
133
|
+
expect(result.fetch("meta")).to eq("a" => 1, "b" => { "id" => 9, "title" => "N" })
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe "strict_serialization policy (AC5)" do
|
|
138
|
+
it "raises Ruact::SerializationError for a non-Serializable object under strict" do
|
|
139
|
+
expect { described_class.build({ "rec" => B2Fixtures::PlainRecord.new }, strict: true) }
|
|
140
|
+
.to raise_error(Ruact::SerializationError, /Cannot serialize B2Fixtures::PlainRecord/)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "falls back to as_json when strict is false" do
|
|
144
|
+
result = described_class.build({ "rec" => B2Fixtures::PlainRecord.new }, strict: false)
|
|
145
|
+
expect(result.fetch("rec")).to eq("id" => 7, "leaked" => "everything")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "raises when as_json returns self (infinite-recursion guard), regardless of strict" do
|
|
149
|
+
expect { described_class.build({ "x" => B2Fixtures::SelfReturningAsJson.new }, strict: false) }
|
|
150
|
+
.to raise_error(Ruact::SerializationError, /as_json returned self/)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "wraps an exception raised inside as_json as Ruact::SerializationError" do
|
|
154
|
+
expect { described_class.build({ "x" => B2Fixtures::RaisingAsJson.new }, strict: false) }
|
|
155
|
+
.to raise_error(Ruact::SerializationError, /as_json raised RuntimeError: boom in as_json/)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
describe "Story 9.4 — .serialize_value (single value, same policy as .build — D6)", :story_9_4 do
|
|
160
|
+
it "serializes a Serializable through ruact_props only" do
|
|
161
|
+
post = B2Fixtures::Post.new(id: 1, title: "Hi", secret: "nope")
|
|
162
|
+
expect(described_class.serialize_value(post, strict: true)).to eq("id" => 1, "title" => "Hi")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "recurses into Arrays of Serializables" do
|
|
166
|
+
posts = [B2Fixtures::Post.new(id: 1, title: "A", secret: "x")]
|
|
167
|
+
expect(described_class.serialize_value(posts, strict: true)).to eq([{ "id" => 1, "title" => "A" }])
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "recurses into Hashes, stringifying keys" do
|
|
171
|
+
value = { total: 2, post: B2Fixtures::Post.new(id: 1, title: "A", secret: "x") }
|
|
172
|
+
expect(described_class.serialize_value(value, strict: true))
|
|
173
|
+
.to eq("total" => 2, "post" => { "id" => 1, "title" => "A" })
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it "passes primitives through untouched" do
|
|
177
|
+
expect(described_class.serialize_value(42, strict: true)).to eq(42)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it "passes nil through (the dispatch controller renders it as JSON null — D6)" do
|
|
181
|
+
expect(described_class.serialize_value(nil, strict: true)).to be_nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "raises Ruact::SerializationError for a non-Serializable under strict" do
|
|
185
|
+
expect { described_class.serialize_value(B2Fixtures::PlainRecord.new, strict: true) }
|
|
186
|
+
.to raise_error(Ruact::SerializationError, /Cannot serialize B2Fixtures::PlainRecord/)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it "falls back to as_json when strict is false" do
|
|
190
|
+
expect(described_class.serialize_value(B2Fixtures::PlainRecord.new, strict: false))
|
|
191
|
+
.to eq("id" => 7, "leaked" => "everything")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it "is the same policy .build applies per ivar (extraction, not a fork)" do
|
|
195
|
+
post = B2Fixtures::Post.new(id: 3, title: "Same", secret: "x")
|
|
196
|
+
expect(described_class.build({ "post" => post }, strict: true))
|
|
197
|
+
.to eq("post" => described_class.serialize_value(post, strict: true))
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|