ruact 0.0.11 → 0.0.12
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/CHANGELOG.md +110 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +14 -4
- data/RELEASING.md +125 -139
- data/lib/ruact/packaging.rb +68 -0
- data/lib/ruact/version.rb +1 -1
- data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
- metadata +6 -107
- data/.codecov.yml +0 -31
- data/.github/workflows/ci.yml +0 -284
- data/.github/workflows/server-functions-bench.yml +0 -54
- data/.rubocop.yml +0 -107
- data/.rubocop_todo.yml +0 -63
- data/Rakefile +0 -10
- data/bench/server_functions_dispatch_bench.rb +0 -276
- data/bench/server_functions_dispatch_bench.results.md +0 -150
- data/docs/internal/README.md +0 -9
- data/docs/internal/decisions/server-functions-api.md +0 -2236
- data/spec/benchmarks/baseline.json +0 -12
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
- data/spec/fixtures/flight/README.md +0 -136
- data/spec/fixtures/flight/array.txt +0 -1
- data/spec/fixtures/flight/as_json_object.txt +0 -2
- data/spec/fixtures/flight/bigint.txt +0 -1
- data/spec/fixtures/flight/boolean_false.txt +0 -1
- data/spec/fixtures/flight/boolean_true.txt +0 -1
- data/spec/fixtures/flight/client_component_with_props.txt +0 -2
- data/spec/fixtures/flight/client_reference.txt +0 -2
- data/spec/fixtures/flight/hash.txt +0 -1
- data/spec/fixtures/flight/infinity.txt +0 -1
- data/spec/fixtures/flight/nan.txt +0 -1
- data/spec/fixtures/flight/negative_infinity.txt +0 -1
- data/spec/fixtures/flight/nil.txt +0 -1
- data/spec/fixtures/flight/number_float.txt +0 -1
- data/spec/fixtures/flight/number_integer.txt +0 -1
- data/spec/fixtures/flight/react_element_no_props.txt +0 -1
- data/spec/fixtures/flight/redirect_row.txt +0 -1
- data/spec/fixtures/flight/serializable_object.txt +0 -2
- data/spec/fixtures/flight/string_basic.txt +0 -1
- data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
- data/spec/fixtures/flight/undefined.txt +0 -1
- data/spec/fixtures/readme/children-error.html.erb +0 -3
- data/spec/fixtures/readme/children-error.txt +0 -1
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
- data/spec/readme_demo_message_spec.rb +0 -67
- data/spec/readme_spec.rb +0 -282
- data/spec/ruact/client_manifest_spec.rb +0 -270
- data/spec/ruact/component_contract_spec.rb +0 -119
- data/spec/ruact/configuration_spec.rb +0 -518
- data/spec/ruact/controller_request_spec.rb +0 -671
- data/spec/ruact/controller_spec.rb +0 -343
- data/spec/ruact/doctor_spec.rb +0 -769
- data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
- data/spec/ruact/erb_preprocessor_spec.rb +0 -361
- data/spec/ruact/errors_spec.rb +0 -93
- data/spec/ruact/flight/renderer_spec.rb +0 -133
- data/spec/ruact/flight/serializer_spec.rb +0 -494
- data/spec/ruact/html_converter_spec.rb +0 -375
- data/spec/ruact/install_generator_spec.rb +0 -1549
- data/spec/ruact/layout_source_spec.rb +0 -108
- data/spec/ruact/manifest_resolver_spec.rb +0 -174
- data/spec/ruact/query_request_spec.rb +0 -706
- data/spec/ruact/query_spec.rb +0 -105
- data/spec/ruact/railtie_spec.rb +0 -155
- data/spec/ruact/render_context_spec.rb +0 -58
- data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
- data/spec/ruact/render_pipeline_spec.rb +0 -928
- data/spec/ruact/scaffold_generator_spec.rb +0 -1849
- data/spec/ruact/serializable_spec.rb +0 -179
- data/spec/ruact/server_bucket_request_spec.rb +0 -785
- data/spec/ruact/server_function_name_spec.rb +0 -53
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
- data/spec/ruact/server_functions/codegen_spec.rb +0 -397
- data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
- data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
- data/spec/ruact/server_functions/introspection_spec.rb +0 -135
- data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
- data/spec/ruact/server_functions/query_context_spec.rb +0 -72
- data/spec/ruact/server_functions/query_source_spec.rb +0 -193
- data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
- data/spec/ruact/server_functions/rake_spec.rb +0 -86
- data/spec/ruact/server_functions/route_source_spec.rb +0 -202
- data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
- data/spec/ruact/server_rescue_request_spec.rb +0 -416
- data/spec/ruact/server_spec.rb +0 -179
- data/spec/ruact/server_upload_request_spec.rb +0 -311
- data/spec/ruact/signed_references_spec.rb +0 -164
- data/spec/ruact/string_distance_spec.rb +0 -38
- data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
- data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
- data/spec/ruact/testing/no_production_load_spec.rb +0 -41
- data/spec/ruact/validation_errors_spec.rb +0 -116
- data/spec/ruact/view_helper_spec.rb +0 -131
- data/spec/spec_helper.rb +0 -77
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +0 -21
- data/spec/support/flight_wire_parser_spec.rb +0 -93
- data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
- data/spec/support/rails_stub.rb +0 -115
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
require "ruact/server_functions/error_suggestion"
|
|
5
|
-
|
|
6
|
-
module Ruact
|
|
7
|
-
module ServerFunctions
|
|
8
|
-
RSpec.describe ErrorSuggestion, :story_8_4 do
|
|
9
|
-
# Test fixtures using class-name spoofing so the module can be exercised
|
|
10
|
-
# without requiring ActiveRecord or ActionController to be loaded
|
|
11
|
-
# (Pitfall #4).
|
|
12
|
-
def error_with_class_name(name)
|
|
13
|
-
klass = Class.new(StandardError) do
|
|
14
|
-
define_singleton_method(:name) { name }
|
|
15
|
-
end
|
|
16
|
-
klass.new("fixture")
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
describe "Story 8.4 — .for returns the NFR30-mandated suggestion" do
|
|
20
|
-
it "returns the RecordInvalid suggestion for ActiveRecord::RecordInvalid" do
|
|
21
|
-
err = error_with_class_name("ActiveRecord::RecordInvalid")
|
|
22
|
-
expect(described_class.for(err))
|
|
23
|
-
.to eq("Validation failed — check the model's `validates` rules")
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
it "returns the CSRF suggestion for ActionController::InvalidAuthenticityToken" do
|
|
27
|
-
err = error_with_class_name("ActionController::InvalidAuthenticityToken")
|
|
28
|
-
expect(described_class.for(err))
|
|
29
|
-
.to eq(
|
|
30
|
-
"CSRF token mismatch — ensure the page was rendered after the most recent server restart and the session cookie is intact"
|
|
31
|
-
)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
it "returns nil for StandardError" do
|
|
35
|
-
expect(described_class.for(StandardError.new("boom"))).to be_nil
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
it "returns nil for RuntimeError" do
|
|
39
|
-
expect(described_class.for(RuntimeError.new("boom"))).to be_nil
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
it "returns nil for ArgumentError" do
|
|
43
|
-
expect(described_class.for(ArgumentError.new("boom"))).to be_nil
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
it "returns nil for a custom user exception class" do
|
|
47
|
-
custom_class = Class.new(StandardError) { define_singleton_method(:name) { "MyApp::PaymentDeclined" } }
|
|
48
|
-
expect(described_class.for(custom_class.new("declined"))).to be_nil
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
describe "Story 8.5 — UploadTooLargeError suggestion", :story_8_5 do
|
|
53
|
-
it "returns the upload-too-large suggestion for Ruact::UploadTooLargeError" do
|
|
54
|
-
err = Ruact::UploadTooLargeError.new(received_bytes: 11_000_000, limit_bytes: 10_485_760)
|
|
55
|
-
expect(described_class.for(err))
|
|
56
|
-
.to eq(
|
|
57
|
-
"Upload exceeded the configured size limit. " \
|
|
58
|
-
"Increase Ruact.config.max_upload_bytes or use Active Storage Direct Upload / " \
|
|
59
|
-
"a presigned S3 URL for large files."
|
|
60
|
-
)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
it "has a SUGGESTIONS entry keyed by the exception class name" do
|
|
64
|
-
expect(described_class::SUGGESTIONS).to have_key("Ruact::UploadTooLargeError")
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
describe "Story 8.4 — SUGGESTIONS constant" do
|
|
69
|
-
it "is frozen so runtime mutation cannot extend the table" do
|
|
70
|
-
expect(described_class::SUGGESTIONS).to be_frozen
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
it "uses class-name strings as keys (not constants)" do
|
|
74
|
-
expect(described_class::SUGGESTIONS.keys).to all(be_a(String))
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
require "active_support/all"
|
|
5
|
-
require "action_dispatch"
|
|
6
|
-
require "ruact/server_functions"
|
|
7
|
-
|
|
8
|
-
module Ruact
|
|
9
|
-
module ServerFunctions
|
|
10
|
-
# Plain query class — only `instance_method(...).parameters` (kwargs) and
|
|
11
|
-
# `.name` (collision origin) are read; no Rails boot needed. Defined at module
|
|
12
|
-
# level (mirrors query_source_spec) so it is not an example-group-leaky
|
|
13
|
-
# constant.
|
|
14
|
-
class CatalogIntrospectQ
|
|
15
|
-
def categories; end
|
|
16
|
-
|
|
17
|
-
def search_users(term:, limit: 10); end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Story 15.3 (FR107) — `ruact:routes --json` introspection. Exercised through
|
|
21
|
-
# a REAL RouteSet carrying both a mutation action and mounted query routes,
|
|
22
|
-
# with the host/query resolvers injected (no controller boot), so the document
|
|
23
|
-
# is proven byte-derived from RouteSource + QuerySource (the same single
|
|
24
|
-
# source of truth codegen consumes) and side-effect-free.
|
|
25
|
-
RSpec.describe Introspection, :story_15_3 do
|
|
26
|
-
let(:query_prefix) { QuerySource::QUERY_CONTROLLER_PREFIX }
|
|
27
|
-
|
|
28
|
-
# A real RouteSet with a mutation action (createPost, updatePost) AND two
|
|
29
|
-
# mounted query GET routes under the query dispatch controller prefix.
|
|
30
|
-
def combined_route_set
|
|
31
|
-
prefix = query_prefix
|
|
32
|
-
rs = ActionDispatch::Routing::RouteSet.new
|
|
33
|
-
rs.draw do
|
|
34
|
-
resources :posts, only: %i[create update]
|
|
35
|
-
get "/q/categories", to: "#{prefix}catalog_introspect_q#categories"
|
|
36
|
-
get "/q/search_users", to: "#{prefix}catalog_introspect_q#search_users"
|
|
37
|
-
end
|
|
38
|
-
rs
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def as_json(route_set)
|
|
42
|
-
described_class.as_json(
|
|
43
|
-
route_set: route_set,
|
|
44
|
-
host_predicate: ->(_controller) { true },
|
|
45
|
-
query_class_for: lambda { |controller|
|
|
46
|
-
controller.start_with?(query_prefix) ? CatalogIntrospectQ : nil
|
|
47
|
-
}
|
|
48
|
-
)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
describe ".as_json" do
|
|
52
|
-
subject(:document) { as_json(combined_route_set) }
|
|
53
|
-
|
|
54
|
-
it "gates the document with the EXPERIMENTAL schema_version (0)", :aggregate_failures do
|
|
55
|
-
expect(document["schema_version"]).to eq(0)
|
|
56
|
-
expect(document["schema_version"]).to eq(described_class::SCHEMA_VERSION)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
it "round-trips through JSON.parse" do
|
|
60
|
-
expect(JSON.parse(JSON.generate(document))).to eq(document)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
it "reports BOTH action and query accessors sorted by name", :aggregate_failures do
|
|
64
|
-
names = document["accessors"].map { |a| a["accessor"] }
|
|
65
|
-
expect(names).to include("createPost", "updatePost", "categories", "searchUsers")
|
|
66
|
-
expect(names).to eq(names.sort)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
it "shapes an action with its required path segments as declared params", :aggregate_failures do
|
|
70
|
-
update = document["accessors"].find { |a| a["accessor"] == "updatePost" }
|
|
71
|
-
expect(update["kind"]).to eq("action")
|
|
72
|
-
expect(update["verb"]).to eq("PATCH")
|
|
73
|
-
expect(update["path"]).to eq("/posts/:id")
|
|
74
|
-
expect(update["segments"]).to eq(["id"])
|
|
75
|
-
expect(update["params"]).to eq([{ "name" => "id", "required" => true }])
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
it "shapes a query with its declared kwargs as params (Story 13.4)", :aggregate_failures do
|
|
79
|
-
search = document["accessors"].find { |a| a["accessor"] == "searchUsers" }
|
|
80
|
-
expect(search["kind"]).to eq("query")
|
|
81
|
-
expect(search["verb"]).to eq("GET")
|
|
82
|
-
expect(search["path"]).to eq("/q/search_users")
|
|
83
|
-
expect(search["segments"]).to eq([])
|
|
84
|
-
expect(search["params"]).to eq(
|
|
85
|
-
[{ "name" => "term", "required" => true }, { "name" => "limit", "required" => false }]
|
|
86
|
-
)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
it "carries exactly the AC2 keys per accessor" do
|
|
90
|
-
document["accessors"].each do |accessor|
|
|
91
|
-
expect(accessor.keys).to contain_exactly("accessor", "kind", "verb", "path", "segments", "params")
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
it "does NOT include a per-accessor component-contract link (D3 = report accessors faithfully)" do
|
|
96
|
-
document["accessors"].each do |accessor|
|
|
97
|
-
expect(accessor).not_to have_key("contract")
|
|
98
|
-
expect(accessor).not_to have_key("has_contract")
|
|
99
|
-
expect(accessor).not_to have_key("contract_exists")
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
describe "side-effect-free (AC2 / AC4d — writes NO bridge or TS)" do
|
|
105
|
-
it "never writes the codegen bridge / TS module", :aggregate_failures do
|
|
106
|
-
allow(SnapshotWriter).to receive(:write_if_changed!)
|
|
107
|
-
allow(Snapshot).to receive(:generate_v2!)
|
|
108
|
-
allow(Codegen).to receive(:generate_ts!)
|
|
109
|
-
allow(File).to receive(:write).and_call_original
|
|
110
|
-
|
|
111
|
-
as_json(combined_route_set)
|
|
112
|
-
|
|
113
|
-
expect(SnapshotWriter).not_to have_received(:write_if_changed!)
|
|
114
|
-
expect(Snapshot).not_to have_received(:generate_v2!)
|
|
115
|
-
expect(Codegen).not_to have_received(:generate_ts!)
|
|
116
|
-
expect(File).not_to have_received(:write)
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
describe "single source of truth (AC2)" do
|
|
121
|
-
it "is byte-derived from ServerFunctions.introspect (same collectors as codegen)" do
|
|
122
|
-
# Both introspection and write_v2_snapshot! flow through .introspect;
|
|
123
|
-
# the document's accessors mirror those entries' identifiers exactly.
|
|
124
|
-
entries = ServerFunctions.introspect(
|
|
125
|
-
route_set: combined_route_set,
|
|
126
|
-
host_predicate: ->(_c) { true },
|
|
127
|
-
query_class_for: ->(c) { c.start_with?(query_prefix) ? CatalogIntrospectQ : nil }
|
|
128
|
-
)
|
|
129
|
-
expect(as_json(combined_route_set)["accessors"].map { |a| a["accessor"] })
|
|
130
|
-
.to eq(entries.map { |e| e["js_identifier"] })
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
|
|
5
|
-
module Ruact
|
|
6
|
-
module ServerFunctions
|
|
7
|
-
RSpec.describe NameBridge, :story_8_0a do
|
|
8
|
-
describe ".to_js_identifier (Story 8.0a — Ruby symbol → JS identifier)" do
|
|
9
|
-
# Six canonical edge cases locked by the Story 8.0 ADR.
|
|
10
|
-
|
|
11
|
-
it "translates standard snake_case to camelCase" do
|
|
12
|
-
expect(described_class.to_js_identifier(:create_post)).to eq("createPost")
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
it "passes a single word through unchanged" do
|
|
16
|
-
expect(described_class.to_js_identifier(:categories)).to eq("categories")
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
it "preserves a single leading underscore" do
|
|
20
|
-
expect(described_class.to_js_identifier(:_internal_dump)).to eq("_internalDump")
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
it "collapses consecutive underscores" do
|
|
24
|
-
expect(described_class.to_js_identifier(:foo__bar)).to eq("fooBar")
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
it "raises ConfigurationError for SCREAMING_SNAKE" do
|
|
28
|
-
expect { described_class.to_js_identifier(:RECALCULATE) }
|
|
29
|
-
.to raise_error(Ruact::ConfigurationError) do |error|
|
|
30
|
-
expect(error.message).to include(":RECALCULATE")
|
|
31
|
-
expect(error.message).to include("/^[a-z_][a-z0-9_]*$/")
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
it "raises ConfigurationError for PascalCase" do
|
|
36
|
-
expect { described_class.to_js_identifier(:CreatePost) }
|
|
37
|
-
.to raise_error(Ruact::ConfigurationError) do |error|
|
|
38
|
-
expect(error.message).to include(":CreatePost")
|
|
39
|
-
expect(error.message).to include("ruact server-function name")
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
describe "additional shape guards" do
|
|
44
|
-
it "rejects symbols starting with a digit" do
|
|
45
|
-
expect { described_class.to_js_identifier(:"1foo") }
|
|
46
|
-
.to raise_error(Ruact::ConfigurationError, /must match/)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
it "rejects symbols containing a dash" do
|
|
50
|
-
expect { described_class.to_js_identifier(:"foo-bar") }
|
|
51
|
-
.to raise_error(Ruact::ConfigurationError, /must match/)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
it "rejects empty string" do
|
|
55
|
-
expect { described_class.to_js_identifier(:"") }
|
|
56
|
-
.to raise_error(Ruact::ConfigurationError, /must match/)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
it "accepts a string argument identical to its symbol form" do
|
|
60
|
-
expect(described_class.to_js_identifier("create_post")).to eq("createPost")
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
it "preserves a leading underscore followed by digits" do
|
|
64
|
-
expect(described_class.to_js_identifier(:_2fa_check)).to eq("_2faCheck")
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
describe "underscore-only symbols (Story 8.0 review patch 2026-05-13)" do
|
|
69
|
-
it "rejects a single underscore" do
|
|
70
|
-
expect { described_class.to_js_identifier(:_) }
|
|
71
|
-
.to raise_error(Ruact::ConfigurationError) do |error|
|
|
72
|
-
expect(error.message).to include(":_")
|
|
73
|
-
expect(error.message).to include("entirely of underscores")
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
it "rejects a run of underscores" do
|
|
78
|
-
expect { described_class.to_js_identifier(:____) }
|
|
79
|
-
.to raise_error(Ruact::ConfigurationError, /entirely of underscores/)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
it "still accepts a leading underscore followed by alphanumeric" do
|
|
83
|
-
expect(described_class.to_js_identifier(:_x)).to eq("_x")
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
describe "JS reserved words (Story 8.0 review patch 2026-05-13)" do
|
|
88
|
-
# Spot-check a handful of representative classes: keyword (`class`),
|
|
89
|
-
# module-level reserved (`export`), strict-mode reserved (`let`),
|
|
90
|
-
# contextually-reserved (`await`, `async`), literal (`true`).
|
|
91
|
-
it "rejects :class" do
|
|
92
|
-
expect { described_class.to_js_identifier(:class) }
|
|
93
|
-
.to raise_error(Ruact::ConfigurationError) do |error|
|
|
94
|
-
expect(error.message).to include(":class")
|
|
95
|
-
expect(error.message).to include("JS reserved word")
|
|
96
|
-
expect(error.message).to include('"class"')
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
it "rejects :export" do
|
|
101
|
-
expect { described_class.to_js_identifier(:export) }
|
|
102
|
-
.to raise_error(Ruact::ConfigurationError, /JS reserved word.*"export"/)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
it "rejects :let, :await, :async, :true (representative coverage)" do
|
|
106
|
-
%i[let await async true].each do |sym|
|
|
107
|
-
expect { described_class.to_js_identifier(sym) }
|
|
108
|
-
.to raise_error(Ruact::ConfigurationError, /JS reserved word/),
|
|
109
|
-
"expected :#{sym} to raise as a reserved word"
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
it "rejects :eval and :arguments (strict-mode invalid binding names — " \
|
|
114
|
-
"Story 8.0 Re-run patch 2026-05-13)" do
|
|
115
|
-
# ES module code runs in strict mode, where `eval` and `arguments`
|
|
116
|
-
# cannot be used as identifier names. The 8.0a codegen emits a
|
|
117
|
-
# `"type": "module"` file, so these guards apply unconditionally.
|
|
118
|
-
%i[eval arguments].each do |sym|
|
|
119
|
-
expect { described_class.to_js_identifier(sym) }
|
|
120
|
-
.to raise_error(Ruact::ConfigurationError, /JS reserved word/),
|
|
121
|
-
"expected :#{sym} to raise as strict-mode invalid binding name"
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
it "accepts multi-word symbols whose camelCased output is not reserved" do
|
|
126
|
-
# No real Ruby snake_case maps to a single reserved word post-
|
|
127
|
-
# camelCasing (reserved words are themselves single-word), but the
|
|
128
|
-
# check happens AFTER camelCasing so a hypothetical degenerate
|
|
129
|
-
# input is still caught.
|
|
130
|
-
expect { described_class.to_js_identifier(:cl_ass) }
|
|
131
|
-
.not_to raise_error # "clAss" is not reserved — sanity guard
|
|
132
|
-
expect(described_class.to_js_identifier(:cl_ass)).to eq("clAss")
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
it "ACCEPTS reserved words that survive only with the leading-underscore prefix" do
|
|
136
|
-
# `:_class` → "_class" — not reserved (the underscore is a literal
|
|
137
|
-
# character); intentional escape hatch for devs whose domain
|
|
138
|
-
# vocabulary collides with JS keywords.
|
|
139
|
-
expect(described_class.to_js_identifier(:_class)).to eq("_class")
|
|
140
|
-
expect(described_class.to_js_identifier(:_export)).to eq("_export")
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
it "ACCEPTS the suffix-shaped escape hint from the error message" do
|
|
144
|
-
# The error message suggests `:class_action`; assert that hint
|
|
145
|
-
# produces a valid identifier — guards against a regression where
|
|
146
|
-
# the suggested fix would also fail validation.
|
|
147
|
-
expect(described_class.to_js_identifier(:class_action)).to eq("classAction")
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
describe "Ruact-reserved names (Story 8.2 review patch R2 — 2026-05-17)", :story_8_2 do
|
|
152
|
-
# The codegen unconditionally re-exports certain runtime helpers
|
|
153
|
-
# from `@/.ruact/server-functions` (e.g. `revalidate`). A server
|
|
154
|
-
# function named `revalidate` would emit `export const revalidate`
|
|
155
|
-
# next to the helper re-export and crash at module load with a
|
|
156
|
-
# duplicate-export error. Reject at route-draw instead.
|
|
157
|
-
it "rejects :revalidate because it collides with the unconditional helper re-export" do
|
|
158
|
-
expect { described_class.to_js_identifier(:revalidate) }
|
|
159
|
-
.to raise_error(Ruact::ConfigurationError) do |error|
|
|
160
|
-
expect(error.message).to include(":revalidate")
|
|
161
|
-
expect(error.message).to include("duplicate export")
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
it "accepts :revalidate_post (suffix escape hatch — same convention as JS reserved-word path)" do
|
|
166
|
-
expect(described_class.to_js_identifier(:revalidate_post)).to eq("revalidatePost")
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
it "accepts :_revalidate (leading-underscore escape hatch)" do
|
|
170
|
-
expect(described_class.to_js_identifier(:_revalidate)).to eq("_revalidate")
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
it "rejects :_make_server_function because it collides with the codegen's runtime import" do
|
|
174
|
-
expect { described_class.to_js_identifier(:_make_server_function) }
|
|
175
|
-
.to raise_error(Ruact::ConfigurationError) do |error|
|
|
176
|
-
expect(error.message).to include(":_make_server_function")
|
|
177
|
-
expect(error.message).to include("duplicate export")
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
it "accepts :_make_ref now that the demolished v1 runtime export is no longer reserved (Story 9.9)" do
|
|
182
|
-
expect(described_class.to_js_identifier(:_make_ref)).to eq("_makeRef")
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Story 9.5 — `_makeQuery` (the v2 query import) and `useQuery` (the
|
|
186
|
-
# query hook re-export) are new top-level bindings in the generated
|
|
187
|
-
# module; a query/action method mapping to either would redeclare the
|
|
188
|
-
# import / duplicate the export and crash at module load.
|
|
189
|
-
it "Story 9.5 — rejects :use_query because it collides with the useQuery re-export" do
|
|
190
|
-
expect { described_class.to_js_identifier(:use_query) }
|
|
191
|
-
.to raise_error(Ruact::ConfigurationError) do |error|
|
|
192
|
-
expect(error.message).to include(":use_query")
|
|
193
|
-
expect(error.message).to include("duplicate export")
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
it "Story 9.5 — rejects :_make_query because it collides with the codegen's runtime import" do
|
|
198
|
-
expect { described_class.to_js_identifier(:_make_query) }
|
|
199
|
-
.to raise_error(Ruact::ConfigurationError) do |error|
|
|
200
|
-
expect(error.message).to include(":_make_query")
|
|
201
|
-
expect(error.message).to include("duplicate export")
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
it "Story 9.5 — accepts :use_query_results (suffix escape hatch keeps working)" do
|
|
206
|
-
expect(described_class.to_js_identifier(:use_query_results)).to eq("useQueryResults")
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
end
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Story 9.4 — unit spec for the per-request query context (D3). The context
|
|
4
|
-
# wraps the DISPATCHING controller instance and delegates to it: because the
|
|
5
|
-
# internal dispatch controller inherits `Ruact.config.query_parent_controller`,
|
|
6
|
-
# `controller.current_user` IS the host's own method (Devise / Pundit /
|
|
7
|
-
# hand-rolled). No Rails boot needed here — a plain stub controller suffices.
|
|
8
|
-
require "spec_helper"
|
|
9
|
-
|
|
10
|
-
module Ruact
|
|
11
|
-
module ServerFunctions
|
|
12
|
-
RSpec.describe QueryContext, :story_9_4 do
|
|
13
|
-
describe "Story 9.4 — delegation to the dispatching controller (AC3 / D3)" do
|
|
14
|
-
let(:controller_class) do
|
|
15
|
-
Class.new do
|
|
16
|
-
def params = { "q" => "x" }
|
|
17
|
-
def request = :the_request
|
|
18
|
-
def session = { "sid" => 1 }
|
|
19
|
-
|
|
20
|
-
def current_user
|
|
21
|
-
{ "id" => 7 }
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
subject(:context) { described_class.new(controller: controller_class.new) }
|
|
27
|
-
|
|
28
|
-
it "delegates params" do
|
|
29
|
-
expect(context.params).to eq("q" => "x")
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
it "delegates request" do
|
|
33
|
-
expect(context.request).to eq(:the_request)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
it "delegates session" do
|
|
37
|
-
expect(context.session).to eq("sid" => 1)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
it "delegates current_user to the host's own method" do
|
|
41
|
-
expect(context.current_user).to eq("id" => 7)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
describe "Story 9.4 — current_user reaches a PRIVATE host helper too" do
|
|
46
|
-
let(:controller_class) do
|
|
47
|
-
Class.new do
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def current_user
|
|
51
|
-
:private_user
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
it "resolves it (hand-rolled apps commonly define current_user private)" do
|
|
57
|
-
context = described_class.new(controller: controller_class.new)
|
|
58
|
-
expect(context.current_user).to eq(:private_user)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
describe "Story 9.4 — clear error when the host defines no current_user (D3)" do
|
|
63
|
-
it "raises NoMethodError naming the parent controller and the fix" do
|
|
64
|
-
context = described_class.new(controller: Class.new.new)
|
|
65
|
-
expect { context.current_user }.to raise_error(NoMethodError, /current_user/) do |error|
|
|
66
|
-
expect(error.message).to include("query_parent_controller")
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|