toolkit_contract/wiring.rs
1//! `ClientWiring` — typed config schema consumed by `#[toolkit::provides]`.
2//!
3//! Lives outside the feature-gated `runtime` module so the deserialization
4//! itself is always available: any module loaded into the host must be able
5//! to parse its wiring config regardless of which transport features its
6//! provider SDK compiled in. The actual conversion to a runtime
7//! [`ClientConfig`](crate::runtime::config::ClientConfig) is gated on
8//! `runtime-client`.
9
10use std::time::Duration;
11
12use serde::Deserialize;
13
14/// Fine-tuning knobs forwarded to the transport client when a remote
15/// transport is selected. All fields are optional — missing values fall
16/// back to the SDK defaults baked into
17/// [`ClientConfig`](crate::runtime::config::ClientConfig).
18#[derive(Clone, Debug, Default, Deserialize)]
19pub struct ClientTuning {
20 /// Per-call request deadline (e.g., `"5s"`, `"500ms"`).
21 #[serde(default, with = "toolkit_utils::humantime_serde::option")]
22 pub timeout: Option<Duration>,
23
24 /// Override for the retry policy applied to `#[retryable]` methods.
25 #[serde(default)]
26 pub retry: Option<RetrySettings>,
27
28 /// Override for the stream reconnect policy, of any framing.
29 ///
30 /// `alias = "sse_reconnect"` keeps already-deployed config files working:
31 /// this struct has no `rename_all`, so the Rust field name *is* the JSON
32 /// key, and the field was called `sse_reconnect` before the policy covered
33 /// framings other than SSE.
34 ///
35 /// Supplying **both** `stream_reconnect` and `sse_reconnect` is rejected as
36 /// a duplicate field — the two name the same field — so a config that adds
37 /// the new key without removing the legacy one fails loudly at parse time
38 /// rather than silently honouring one and dropping the other. This holds
39 /// even though the field is `#[serde(flatten)]`-ed into [`ClientWiring`].
40 #[serde(default, alias = "sse_reconnect")]
41 pub stream_reconnect: Option<ReconnectSettings>,
42
43 /// Reject plaintext `http://` endpoints.
44 ///
45 /// Defaults to `false` (the in-mesh convention). Without this knob a
46 /// discovery-resolved client always got the default, so a consumer talking
47 /// to an endpoint outside a trusted boundary had no way to demand TLS —
48 /// while forwarding a tenant bearer token over it.
49 #[serde(default)]
50 pub require_tls: Option<bool>,
51
52 /// Platform-plane credential source forwarded onto the built
53 /// [`ClientConfig`](crate::runtime::config::ClientConfig). Injected by the
54 /// runtime's proxy-wiring phase, never from config (`#[serde(skip)]`); gated
55 /// on `runtime-client` since the type lives there.
56 #[cfg(feature = "runtime-client")]
57 #[serde(skip)]
58 pub internal_token_provider: Option<crate::runtime::config::InternalTokenProvider>,
59}
60
61/// Deserializable mirror of
62/// [`RetryConfig`](crate::runtime::config::RetryConfig). All fields optional;
63/// missing values keep the runtime default.
64#[derive(Clone, Debug, Default, Deserialize)]
65pub struct RetrySettings {
66 pub max_attempts: Option<u32>,
67 #[serde(default, with = "toolkit_utils::humantime_serde::option")]
68 pub base_delay: Option<Duration>,
69 #[serde(default, with = "toolkit_utils::humantime_serde::option")]
70 pub max_delay: Option<Duration>,
71 pub multiplier: Option<f64>,
72}
73
74/// Deserializable mirror of
75/// [`ReconnectConfig`](crate::runtime::config::ReconnectConfig).
76#[derive(Clone, Debug, Default, Deserialize)]
77pub struct ReconnectSettings {
78 pub max_attempts: Option<u32>,
79 #[serde(default, with = "toolkit_utils::humantime_serde::option")]
80 pub base_delay: Option<Duration>,
81 #[serde(default, with = "toolkit_utils::humantime_serde::option")]
82 pub max_delay: Option<Duration>,
83}
84
85/// Transport choice + endpoint + tuning for one provided contract.
86///
87/// Read by `#[toolkit::provides]` from
88/// `gears.<gear>.config.client_wiring.<contract_snake>`. If the key is
89/// absent the wiring defaults to [`ClientWiring::Local`].
90#[derive(Clone, Debug, Default, Deserialize)]
91#[serde(rename_all = "lowercase", tag = "transport")]
92pub enum ClientWiring {
93 /// In-process. The provider gear's local factory is invoked.
94 #[default]
95 Local,
96 /// Generated REST client points at `endpoint`.
97 Rest {
98 endpoint: String,
99 #[serde(default, flatten)]
100 tuning: ClientTuning,
101 },
102 /// Generated gRPC client connects to `endpoint`.
103 Grpc {
104 endpoint: String,
105 #[serde(default, flatten)]
106 tuning: ClientTuning,
107 },
108}
109
110#[cfg(feature = "runtime-client")]
111impl ClientTuning {
112 /// Apply tuning overrides onto a fresh [`ClientConfig`] built from `endpoint`.
113 #[must_use]
114 pub fn apply_to(&self, endpoint: impl Into<String>) -> crate::runtime::config::ClientConfig {
115 use crate::runtime::config::{ClientConfig, ReconnectConfig, RetryConfig};
116
117 let mut cfg = ClientConfig::new(endpoint);
118 if let Some(timeout) = self.timeout {
119 cfg = cfg.with_timeout(timeout);
120 }
121 if let Some(ref r) = self.retry {
122 let base = cfg.retry.clone();
123 cfg = cfg.with_retry(RetryConfig {
124 max_attempts: r.max_attempts.unwrap_or(base.max_attempts),
125 base_delay: r.base_delay.unwrap_or(base.base_delay),
126 max_delay: r.max_delay.unwrap_or(base.max_delay),
127 multiplier: r.multiplier.unwrap_or(base.multiplier),
128 });
129 }
130 if let Some(ref s) = self.stream_reconnect {
131 let base = cfg.stream_reconnect.clone();
132 cfg = cfg.with_stream_reconnect(ReconnectConfig {
133 max_attempts: s.max_attempts.unwrap_or(base.max_attempts),
134 base_delay: s.base_delay.unwrap_or(base.base_delay),
135 max_delay: s.max_delay.unwrap_or(base.max_delay),
136 // Not exposed as wiring keys yet; inherit the base policy.
137 min_healthy_uptime: base.min_healthy_uptime,
138 max_total_reopens: base.max_total_reopens,
139 });
140 }
141 if let Some(require_tls) = self.require_tls {
142 cfg = cfg.with_require_tls(require_tls);
143 }
144 cfg = cfg.with_internal_token_provider(self.internal_token_provider.clone());
145 cfg
146 }
147
148 /// Attach the platform-plane credential source forwarded onto the built
149 /// [`ClientConfig`](crate::runtime::config::ClientConfig). Used by the
150 /// proxy-wiring phase to thread the process credential into a
151 /// directory-resolving (`#[toolkit::consumes]`) client.
152 #[must_use]
153 pub fn with_internal_token_provider(
154 mut self,
155 provider: Option<crate::runtime::config::InternalTokenProvider>,
156 ) -> Self {
157 self.internal_token_provider = provider;
158 self
159 }
160}