Skip to main content

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 SSE-stream reconnect policy.
29    #[serde(default)]
30    pub sse_reconnect: Option<ReconnectSettings>,
31
32    /// Reject plaintext `http://` endpoints.
33    ///
34    /// Defaults to `false` (the in-mesh convention). Without this knob a
35    /// discovery-resolved client always got the default, so a consumer talking
36    /// to an endpoint outside a trusted boundary had no way to demand TLS —
37    /// while forwarding a tenant bearer token over it.
38    #[serde(default)]
39    pub require_tls: Option<bool>,
40
41    /// Platform-plane credential source forwarded onto the built
42    /// [`ClientConfig`](crate::runtime::config::ClientConfig). Injected by the
43    /// runtime's proxy-wiring phase, never from config (`#[serde(skip)]`); gated
44    /// on `runtime-client` since the type lives there.
45    #[cfg(feature = "runtime-client")]
46    #[serde(skip)]
47    pub internal_token_provider: Option<crate::runtime::config::InternalTokenProvider>,
48}
49
50/// Deserializable mirror of
51/// [`RetryConfig`](crate::runtime::config::RetryConfig). All fields optional;
52/// missing values keep the runtime default.
53#[derive(Clone, Debug, Default, Deserialize)]
54pub struct RetrySettings {
55    pub max_attempts: Option<u32>,
56    #[serde(default, with = "toolkit_utils::humantime_serde::option")]
57    pub base_delay: Option<Duration>,
58    #[serde(default, with = "toolkit_utils::humantime_serde::option")]
59    pub max_delay: Option<Duration>,
60    pub multiplier: Option<f64>,
61}
62
63/// Deserializable mirror of
64/// [`ReconnectConfig`](crate::runtime::config::ReconnectConfig).
65#[derive(Clone, Debug, Default, Deserialize)]
66pub struct ReconnectSettings {
67    pub max_attempts: Option<u32>,
68    #[serde(default, with = "toolkit_utils::humantime_serde::option")]
69    pub base_delay: Option<Duration>,
70    #[serde(default, with = "toolkit_utils::humantime_serde::option")]
71    pub max_delay: Option<Duration>,
72}
73
74/// Transport choice + endpoint + tuning for one provided contract.
75///
76/// Read by `#[toolkit::provides]` from
77/// `gears.<gear>.config.client_wiring.<contract_snake>`. If the key is
78/// absent the wiring defaults to [`ClientWiring::Local`].
79#[derive(Clone, Debug, Default, Deserialize)]
80#[serde(rename_all = "lowercase", tag = "transport")]
81pub enum ClientWiring {
82    /// In-process. The provider gear's local factory is invoked.
83    #[default]
84    Local,
85    /// Generated REST client points at `endpoint`.
86    Rest {
87        endpoint: String,
88        #[serde(default, flatten)]
89        tuning: ClientTuning,
90    },
91    /// Generated gRPC client connects to `endpoint`.
92    Grpc {
93        endpoint: String,
94        #[serde(default, flatten)]
95        tuning: ClientTuning,
96    },
97}
98
99#[cfg(feature = "runtime-client")]
100impl ClientTuning {
101    /// Apply tuning overrides onto a fresh [`ClientConfig`] built from `endpoint`.
102    #[must_use]
103    pub fn apply_to(&self, endpoint: impl Into<String>) -> crate::runtime::config::ClientConfig {
104        use crate::runtime::config::{ClientConfig, ReconnectConfig, RetryConfig};
105
106        let mut cfg = ClientConfig::new(endpoint);
107        if let Some(timeout) = self.timeout {
108            cfg = cfg.with_timeout(timeout);
109        }
110        if let Some(ref r) = self.retry {
111            let base = cfg.retry.clone();
112            cfg = cfg.with_retry(RetryConfig {
113                max_attempts: r.max_attempts.unwrap_or(base.max_attempts),
114                base_delay: r.base_delay.unwrap_or(base.base_delay),
115                max_delay: r.max_delay.unwrap_or(base.max_delay),
116                multiplier: r.multiplier.unwrap_or(base.multiplier),
117            });
118        }
119        if let Some(ref s) = self.sse_reconnect {
120            let base = cfg.sse_reconnect.clone();
121            cfg = cfg.with_sse_reconnect(ReconnectConfig {
122                max_attempts: s.max_attempts.unwrap_or(base.max_attempts),
123                base_delay: s.base_delay.unwrap_or(base.base_delay),
124                max_delay: s.max_delay.unwrap_or(base.max_delay),
125            });
126        }
127        if let Some(require_tls) = self.require_tls {
128            cfg = cfg.with_require_tls(require_tls);
129        }
130        cfg = cfg.with_internal_token_provider(self.internal_token_provider.clone());
131        cfg
132    }
133
134    /// Attach the platform-plane credential source forwarded onto the built
135    /// [`ClientConfig`](crate::runtime::config::ClientConfig). Used by the
136    /// proxy-wiring phase to thread the process credential into a
137    /// directory-resolving (`#[toolkit::consumes]`) client.
138    #[must_use]
139    pub fn with_internal_token_provider(
140        mut self,
141        provider: Option<crate::runtime::config::InternalTokenProvider>,
142    ) -> Self {
143        self.internal_token_provider = provider;
144        self
145    }
146}