Skip to main content

thunder/wire/
config.rs

1//! Protocol configuration (SPEC-002) — the declarative description of how
2//! **one application** uses the shared wire. Pure data: the codec never
3//! depends on it; `thunder::client` / `thunder::server` drive their
4//! behavior from it.
5//!
6//! # Thunder ships one standard and zero product knowledge
7//!
8//! There are no named configurations here — no `synap()`, no `nexus()`, no
9//! registry of products. Thunder was born from three products' RPC
10//! implementations, but a protocol library that must serve implementations
11//! which do not exist yet cannot ship a hardcoded list of the ones that did.
12//!
13//! Instead: [`Config::standard()`] is **the** family standard, and every
14//! dimension is a knob. An application that matches the standard writes its
15//! identity and nothing else:
16//!
17//! ```
18//! use thunder::Config;
19//!
20//! let config = Config::standard().scheme("myapp").port(9000);
21//! ```
22//!
23//! An application that still diverges says so **in its own repository**,
24//! where that knowledge belongs:
25//!
26//! ```
27//! use thunder::wire::config::{Handshake, HelloStyle, PushPolicy};
28//! use thunder::Config;
29//!
30//! // A deployment whose RPC path authenticates via AUTH and has no HELLO
31//! // handler, and which ships a push-producing command.
32//! let config = Config::standard()
33//!     .scheme("legacy")
34//!     .port(15501)
35//!     .handshake(Handshake::AuthCommand)
36//!     .hello_style(HelloStyle::NotUsed)
37//!     .push(PushPolicy::Enabled);
38//! ```
39//!
40//! Convergence is therefore visible and per-application: delete overrides
41//! until only `scheme` and `port` remain. Nobody waits on a Thunder release
42//! for a row in a registry, and Thunder never carries behavior it does not
43//! own.
44//!
45//! The standard's values are pinned to `conformance/standard.yaml` by a
46//! test in every language, so the four implementations can never disagree
47//! about what "standard" means — the one guarantee the old per-product
48//! registry legitimately provided.
49
50use crate::wire::DEFAULT_MAX_FRAME_BYTES;
51
52/// Handshake style (PRO-001).
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum Handshake {
55    /// No RPC-layer handshake at all: the connection is usable immediately.
56    None,
57    /// `HELLO` optional; `AUTH [api_key]` / `[user, pass]` / `[password]`;
58    /// pre-auth allowlist `PING/HELLO/AUTH/QUIT`.
59    ///
60    /// Whether a deployment *enforces* credentials is its own config
61    /// (`ListenerConfig::auth_required`), not a protocol dialect: a client
62    /// with no credentials configured simply sends no `AUTH`, which is
63    /// correct against an open deployment (PRO-001a).
64    AuthCommand,
65    /// `HELLO` must be the first frame, carrying credentials. **The
66    /// standard** — see [`Config::standard`].
67    HelloMandatory,
68}
69
70/// HELLO payload style (PRO-001).
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum HelloStyle {
73    /// The application has no `HELLO` command.
74    NotUsed,
75    /// `HELLO` with **no arguments**; the reply is a metadata Map
76    /// `{server, version, proto, id, authenticated}`. Credentials travel
77    /// via `AUTH`, never inside the HELLO.
78    ArgLess,
79    /// Map with `version`, `token` | `api_key`, `client_name`; the reply
80    /// carries `proto` and `capabilities`. **The standard** — the only
81    /// style that negotiates a version and advertises capabilities, which
82    /// is what an evolving protocol needs.
83    MapPayload,
84}
85
86/// Server-push policy (PRO-001).
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum PushPolicy {
89    /// `PUSH_ID` reserved: servers refuse it from clients and never emit
90    /// it. **The standard** — emitting push is a capability an application
91    /// opts into by shipping a push-producing command.
92    Reserved,
93    /// Push frames flow to the client's push hook.
94    Enabled,
95}
96
97/// Which error-string prefix conventions the client parses (PRO-014).
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum ErrorConvention {
100    /// No prefix parsing.
101    None,
102    /// `ERR` / `NOAUTH` / `WRONGPASS` / `NOPERM` prefixes.
103    Resp3Prefixes,
104    /// Leading `"[<code>] "` machine-readable code.
105    BracketCode,
106    /// Both conventions composed. **The standard** — a strict superset, so
107    /// it parses either grammar and needs no negotiation.
108    Both,
109}
110
111/// Transport-security policy (PRO-001).
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum TlsPolicy {
114    /// Plain TCP. **The standard default** — TLS is an additive capability
115    /// a deployment turns on, never a dialect.
116    Off,
117    /// TLS available behind configuration (rustls).
118    Optional,
119    /// Config keys reserved; not wired yet.
120    Reserved,
121}
122
123/// One application's protocol configuration (PRO-001).
124///
125/// Configs are **data, never behavior**: no config may alter wire bytes
126/// (PRO-003) — it selects among behaviors Thunder already implements.
127/// Construct with [`Config::standard`] and the builder, or as a plain
128/// struct literal; both are supported and neither requires a Thunder
129/// release.
130#[derive(Debug, Clone, PartialEq)]
131pub struct Config {
132    /// URL scheme the endpoint parser registers for this application
133    /// (PRO-012). Identity — Thunder has no default for it.
134    pub scheme: &'static str,
135    /// Default RPC port for the scheme (PRO-012). Identity — Thunder has
136    /// no default for it.
137    pub default_port: u16,
138    /// Handshake style.
139    pub handshake: Handshake,
140    /// HELLO payload style.
141    pub hello_style: HelloStyle,
142    /// Server-push policy.
143    pub push: PushPolicy,
144    /// Frame cap (WIRE-020).
145    pub max_frame_bytes: usize,
146    /// Per-connection in-flight request bound (CLT-012 / SRV-003).
147    pub max_in_flight: usize,
148    /// Error-string conventions the client parses.
149    pub error_codes: ErrorConvention,
150    /// Transport-security policy.
151    pub tls: TlsPolicy,
152}
153
154impl Config {
155    /// **The** family standard (pinned by `conformance/standard.yaml`).
156    ///
157    /// Mandatory `HELLO` map with `proto` negotiation and a capabilities
158    /// reply; the `[CODE]` error superset; 64 MiB frames; 256 in-flight;
159    /// push reserved; TLS off.
160    ///
161    /// `scheme` is `""` and `default_port` is `0` — identity is the
162    /// application's to supply, and a `Config` that never sets them is only
163    /// usable with an explicit `host:port` endpoint.
164    pub const fn standard() -> Self {
165        Self {
166            scheme: "",
167            default_port: 0,
168            handshake: Handshake::HelloMandatory,
169            hello_style: HelloStyle::MapPayload,
170            push: PushPolicy::Reserved,
171            max_frame_bytes: DEFAULT_MAX_FRAME_BYTES,
172            max_in_flight: 256,
173            error_codes: ErrorConvention::Both,
174            tls: TlsPolicy::Off,
175        }
176    }
177
178    /// Set the URL scheme this application answers on (PRO-012).
179    #[must_use]
180    pub const fn scheme(mut self, scheme: &'static str) -> Self {
181        self.scheme = scheme;
182        self
183    }
184
185    /// Set the default RPC port for the scheme (PRO-012).
186    #[must_use]
187    pub const fn port(mut self, port: u16) -> Self {
188        self.default_port = port;
189        self
190    }
191
192    /// Override the handshake style.
193    #[must_use]
194    pub const fn handshake(mut self, handshake: Handshake) -> Self {
195        self.handshake = handshake;
196        self
197    }
198
199    /// Override the HELLO payload style.
200    #[must_use]
201    pub const fn hello_style(mut self, hello_style: HelloStyle) -> Self {
202        self.hello_style = hello_style;
203        self
204    }
205
206    /// Override the server-push policy.
207    #[must_use]
208    pub const fn push(mut self, push: PushPolicy) -> Self {
209        self.push = push;
210        self
211    }
212
213    /// Override the frame cap (WIRE-020).
214    #[must_use]
215    pub const fn max_frame_bytes(mut self, max_frame_bytes: usize) -> Self {
216        self.max_frame_bytes = max_frame_bytes;
217        self
218    }
219
220    /// Override the per-connection in-flight bound.
221    #[must_use]
222    pub const fn max_in_flight(mut self, max_in_flight: usize) -> Self {
223        self.max_in_flight = max_in_flight;
224        self
225    }
226
227    /// Override the error-string conventions parsed.
228    #[must_use]
229    pub const fn error_codes(mut self, error_codes: ErrorConvention) -> Self {
230        self.error_codes = error_codes;
231        self
232    }
233
234    /// Override the transport-security policy.
235    #[must_use]
236    pub const fn tls(mut self, tls: TlsPolicy) -> Self {
237        self.tls = tls;
238        self
239    }
240}
241
242impl Default for Config {
243    /// The standard (see [`Config::standard`]).
244    fn default() -> Self {
245        Self::standard()
246    }
247}