1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
//! Which trusty-analyze RPC method one `/api/analyze/…` request becomes (#6155).
//!
//! Why: trusty-analyze answers framed JSON-RPC on a Unix socket and the SPA
//! speaks HTTP paths. A socket has no paths, so the translation has to be
//! written down, and writing it down is what makes the mapped surface
//! reviewable: a request this table does not name is refused with `501` rather
//! than forwarded somewhere approximate.
//!
//! What: [`map_request`] turns a method, a path, a query string and a body into
//! one [`Call`]. The table covers every endpoint the dashboard at
//! `/tools/analyze/` calls — `crates/trusty-console/ui-analyze/src/lib/api.js`,
//! all of it, and nothing else.
//!
//! ## The path shapes are the SPA's, unchanged
//!
//! Each row's left-hand side is the path trusty-analyze's retired axum router
//! answered, minus the `/api/analyze/` prefix the console mounts this bridge at.
//! Keeping them means the SPA needed no rewrite: `base.js` reads the injected
//! `window.__ANALYZE_BASE__` and every `api.js` path resolves against it.
//!
//! ## One row renames a parameter, and it is the only one
//!
//! `api.js` sends `?top_k=` to `complexity_hotspots`, whose request type spells
//! that field `top_n` (`analysis::HotspotsRequest`). The retired axum route did
//! the rename in its query extractor; this table does it here, and
//! `the_hotspot_top_k_query_becomes_the_daemons_top_n` is what holds it. Every
//! other query key already matches its `params` field.
//!
//! ## No stream row exists, deliberately
//!
//! The SPA used to open an `EventSource` on `/sse`. #6287 deleted that route,
//! the `AnalyzerEvent` broadcast behind it, and put no streaming RPC method in
//! their place — `service::rpc::METHODS` has none. Bridging it would mean adding
//! a daemon-side endpoint, so the subscription was removed from the SPA instead
//! (`ui-analyze/src/lib/state.svelte.js`) and this table has no `Call::Stream`.
//!
//! ## Query values are coerced, and that is visible when it is wrong
//!
//! A query string is all text; the RPC params are typed. `top_k=20` has to
//! become `20` or the call answers `invalid_params`. [`query_json`] coerces the
//! two boolean literals and any integer and leaves everything else a string. A
//! string-valued parameter whose value is literally `true` or a bare integer
//! would be coerced wrongly — and would then be REFUSED by the daemon's own
//! deserialiser rather than silently mis-read, which is why the coercion is safe
//! to make blind.
//!
//! Test: `maps_every_endpoint_the_spa_calls`, `refuses_an_unmapped_path`,
//! `query_json_coerces_integers`, `query_json_is_an_empty_object_for_no_query`.
use Method;
use ;
use ;
/// What one mapped request asks the daemon for.
///
/// Why an enum with one variant rather than a bare `(method, params)` pair: the
/// search and memory bridges both carry a `Stream` arm, and the three modules
/// are read side by side. Keeping the shape means an analyze stream — if the
/// daemon ever grows one — is a variant, not a signature change through
/// `routes.rs`.
pub
/// Turn one `/api/analyze/…` request into the call it stands for.
///
/// Why the `Err` is a sentence rather than a code: it is rendered into the `501`
/// body an operator reads, and naming the unmapped path is what makes the gap
/// actionable.
///
/// What: `path` is the sub-path with no leading slash — what axum's `{*path}`
/// captures, already percent-decoded. `body` is the raw request body, read only
/// by the one row that carries one (`POST /facts`).
///
/// # Errors
///
/// A path and method pair this table does not name, a fact id that is not a
/// number, and a body that is not JSON.
///
/// Test: `maps_every_endpoint_the_spa_calls`, `refuses_an_unmapped_path`,
/// `a_fact_delete_with_a_non_numeric_id_is_refused`.
pub
/// Merge an index id into the query-derived params object.
///
/// Why: `GET /indexes/{id}/clusters?k=8&method=bow` carries half its arguments
/// in the path and half in the query, and the RPC method takes one flat object.
/// Test: `maps_every_endpoint_the_spa_calls`.
/// Parse a request body as JSON, reading an empty body as absent.
///
/// Why `Null` and not `{}` for an empty body: `facts::UpsertFactRequest`
/// requires four fields, so an empty POST must be refused. `null` is what its
/// derived `Deserialize` refuses, which is the correct refusal to inherit —
/// naming the missing field rather than inventing one here.
///
/// # Errors
///
/// A non-empty body that is not JSON.
///
/// Test: `body_json_reads_an_empty_body_as_absent`,
/// `refuses_a_body_that_is_not_json`.
/// Turn a query string into the typed JSON object the RPC params expect.
///
/// Why coercion rather than passing strings through: see the module docs — the
/// daemon's params are typed and a query string is not. Why it is safe to do
/// blind: a wrongly-coerced value is refused by the daemon's own deserialiser
/// with `invalid_params`, which this bridge surfaces as `400`. Nothing is read
/// as a different valid value.
/// What: `true`/`false` become booleans, anything parsing as an `i64` becomes a
/// number, everything else stays a string. A repeated key keeps its LAST value,
/// matching what `serde_urlencoded` does for a non-sequence field.
/// Test: `query_json_coerces_integers`,
/// `query_json_is_an_empty_object_for_no_query`.