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    pub auth_required: bool,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum OpKind {
53    /// One input, one output. The common case — every CRUD verb and
54    /// every non-streaming procedure.
55    Unary,
56    /// One input, a finite sequence of outputs. Used for `@stream`
57    /// procedures and (future) streamed `list`. Terminates server-side.
58    Sequence,
59    /// One input, an open-ended sequence of outputs ended only by
60    /// client cancellation or disconnect. WebSocket-only — see §3.4
61    /// of the design doc. Fire-and-forget: no cursors, no replay
62    /// buffer.
63    Subscription,
64}
65
66impl OpKind {
67    pub const fn as_str(&self) -> &'static str {
68        match self {
69            OpKind::Unary => "unary",
70            OpKind::Sequence => "sequence",
71            OpKind::Subscription => "subscription",
72        }
73    }
74}
75
76/// Canonical string assembled by the envelope signing path:
77/// `METHOD\nPATH\nQUERY\nCONTENT-TYPE\nbody-hex`. Both seal and verify
78/// reconstruct the same string from the same inputs.
79pub fn canonical_request_string(
80    method: &str,
81    path: &str,
82    canonical_query: Option<&str>,
83    content_type: Option<&str>,
84    body: &[u8],
85) -> String {
86    let query = canonical_query.unwrap_or_default();
87    let content_type = content_type.unwrap_or_default();
88    let body_hex = body
89        .iter()
90        .map(|byte| format!("{byte:02x}"))
91        .collect::<String>();
92    format!("{method}\n{path}\n{query}\n{content_type}\n{body_hex}")
93}