Skip to main content

cratestack_core/
transport.rs

1//! Transport-binding wire shapes shared by every generator (REST,
2//! RPC) and every server emitter.
3
4/// Wire-level capabilities for one route under a REST binding.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct RouteTransportCapabilities {
7    pub request_types: &'static [&'static str],
8    pub response_types: &'static [&'static str],
9    pub default_response_type: &'static str,
10    pub supports_sequence_response: bool,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct RouteTransportDescriptor {
15    pub name: &'static str,
16    pub method: &'static str,
17    pub path: &'static str,
18    pub capabilities: RouteTransportCapabilities,
19}
20
21/// Wire-shape of a single op in a `transport rpc` schema. See
22/// `docs/design/rpc-transport.md` for the full design — in short, an
23/// op is the dispatch unit shared by every RPC binding (HTTP unary,
24/// HTTP batch, HTTP stream, WebSocket). The macro emits one
25/// `OpDescriptor` per CRUD verb and per procedure when
26/// `Schema.transport == TransportStyle::Rpc`.
27///
28/// REST schemas continue to emit [`RouteTransportDescriptor`] instead;
29/// nothing emits both.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct OpDescriptor {
32    /// Stable dotted id, e.g. `"model.User.list"` or
33    /// `"procedure.publishPost"`. This is the only dispatch key —
34    /// same string appears in URLs (`POST /rpc/:op_id`), in
35    /// batch/WS `Request.op` fields, and in generated client SDK
36    /// call sites.
37    pub op_id: &'static str,
38    pub kind: OpKind,
39    /// Schema-level name of the input type (e.g. `"PublishPostInput"`).
40    /// Empty string when the op takes no input.
41    pub input_ty: &'static str,
42    /// Schema-level name of the output type. Empty string when the
43    /// op returns nothing (e.g. `delete` with no echo).
44    pub output_ty: &'static str,
45    /// Whether the op can be safely retried without an idempotency
46    /// key. True for reads and pure procedures; false for mutations.
47    pub idempotent_by_default: bool,
48    /// Whether the dispatcher should treat this op as participating in
49    /// rate limiting. `true` for every op by default; `false` only for a
50    /// procedure marked `@no_rate_limit` in a schema that declares
51    /// `extension rate_limit { }` (`docs/design/extensions.md` §5) — model
52    /// CRUD ops have no opt-out today and are always `true`. This is
53    /// participation only: it carries no burst/refill/window numbers, and
54    /// changes nothing about whether `RateLimitLayer` is actually wired up
55    /// at runtime, mirroring how `idempotent_by_default` above describes a
56    /// fact about the op rather than configuring anything.
57    pub rate_limited_by_default: bool,
58    pub auth_required: bool,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum OpKind {
63    /// One input, one output. The common case — every CRUD verb and
64    /// every non-streaming procedure.
65    Unary,
66    /// One input, a finite sequence of outputs. Used for `@stream`
67    /// procedures and (future) streamed `list`. Terminates server-side.
68    Sequence,
69    /// No input, an open-ended sequence of outputs ended only by
70    /// backpressure overflow or client disconnect. Emitted for
71    /// `model.<X>.subscribe` when a model declares `@@subscribe`.
72    /// Dispatched today via SSE (`GET /rpc/subscribe/{op_id}`, design
73    /// doc §3.4a) — the recommended first binding per issue #183's
74    /// spike decision; WebSocket (§3.4) remains speced but unbuilt,
75    /// gated on a real bidirectional/high-multiplexing need. Both
76    /// bindings share the same fire-and-forget semantics: no cursors,
77    /// no replay buffer.
78    Subscription,
79}
80
81impl OpKind {
82    pub const fn as_str(&self) -> &'static str {
83        match self {
84            OpKind::Unary => "unary",
85            OpKind::Sequence => "sequence",
86            OpKind::Subscription => "subscription",
87        }
88    }
89}
90
91/// Canonical string assembled by the envelope signing path:
92/// `METHOD\nPATH\nQUERY\nCONTENT-TYPE\nbody-hex`. Both seal and verify
93/// reconstruct the same string from the same inputs.
94pub fn canonical_request_string(
95    method: &str,
96    path: &str,
97    canonical_query: Option<&str>,
98    content_type: Option<&str>,
99    body: &[u8],
100) -> String {
101    let query = canonical_query.unwrap_or_default();
102    let content_type = content_type.unwrap_or_default();
103    let body_hex = body
104        .iter()
105        .map(|byte| format!("{byte:02x}"))
106        .collect::<String>();
107    format!("{method}\n{path}\n{query}\n{content_type}\n{body_hex}")
108}