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}