Skip to main content

gingr/
transport.rs

1use crate::{config, endpoint, response};
2use std::fmt;
3
4/// Result type returned by fallible transport operations.
5pub type Result<T> = core::result::Result<T, TransportError>;
6
7#[derive(Debug, thiserror::Error)]
8/// Errors raised while building or sending Gingr transport requests.
9pub enum TransportError {
10    #[error("failed to construct Gingr URL: {0}")]
11    /// Gingr URL could not be built or parsed for transport.
12    Url(#[from] url::ParseError),
13    #[error("HTTP transport is not implemented for this SDK slice")]
14    /// Real HTTP transport is not enabled in this build.
15    HttpNotImplemented,
16}
17
18#[derive(Clone, Debug, PartialEq, Eq)]
19/// HTTP method, path, and parameters for a typed Gingr endpoint request.
20pub struct RequestParts {
21    method: endpoint::Method,
22    path: endpoint::Path,
23    parameters: Vec<(String, String)>,
24    sensitive_parameter_names: Vec<String>,
25}
26
27impl RequestParts {
28    /// Starts a builder that makes each provider parameter explicit before request capture.
29    pub fn builder() -> RequestPartsBuilder {
30        RequestPartsBuilder::default()
31    }
32
33    /// Adds the Gingr API key to outbound request parameters.
34    pub fn with_api_key(mut self, api_key: &config::ApiKey) -> Self {
35        self.parameters
36            .push(("key".to_owned(), api_key.expose_for_transport().to_owned()));
37        self.sensitive_parameter_names.push("key".to_owned());
38        self
39    }
40
41    /// Returns the HTTP method required by this Gingr endpoint.
42    pub fn method(&self) -> endpoint::Method {
43        self.method
44    }
45
46    /// Returns the Gingr API path for this endpoint.
47    pub fn path(&self) -> endpoint::Path {
48        self.path
49    }
50
51    /// Returns query parameters that should be sent on the URL.
52    pub fn query_pairs(&self) -> &[(String, String)] {
53        if self.method == endpoint::Method::Get {
54            &self.parameters
55        } else {
56            &[]
57        }
58    }
59
60    /// Returns form parameters that should be sent in the request body.
61    pub fn form_pairs(&self) -> &[(String, String)] {
62        if self.method == endpoint::Method::Post {
63            &self.parameters
64        } else {
65            &[]
66        }
67    }
68
69    /// Returns a copy safe for logs with sensitive request values removed.
70    pub fn redacted(&self) -> RedactedRequest {
71        RedactedRequest {
72            method: self.method,
73            path: self.path,
74            parameters: self
75                .parameters
76                .iter()
77                .map(|(key, value)| {
78                    let rendered = if self
79                        .sensitive_parameter_names
80                        .iter()
81                        .any(|name| name == key)
82                    {
83                        "<redacted>"
84                    } else {
85                        value
86                    };
87                    (key.clone(), rendered.to_owned())
88                })
89                .collect(),
90        }
91    }
92
93    fn url(&self, base_url: &config::BaseUrl) -> Result<url::Url> {
94        let mut url = base_url.join_path(self.path)?;
95        if self.method == endpoint::Method::Get {
96            url.query_pairs_mut().extend_pairs(self.parameters.iter());
97        }
98        Ok(url)
99    }
100}
101
102#[derive(Clone, Debug, Default, PartialEq, Eq)]
103/// Builder that classifies Gingr endpoint parameters and redaction rules.
104pub struct RequestPartsBuilder {
105    method: Option<endpoint::Method>,
106    path: Option<endpoint::Path>,
107    parameters: Vec<(String, String)>,
108    sensitive_parameter_names: Vec<String>,
109}
110
111impl RequestPartsBuilder {
112    /// Returns the HTTP method required by this Gingr endpoint.
113    pub fn method(mut self, method: endpoint::Method) -> Self {
114        self.method = Some(method);
115        self
116    }
117
118    /// Returns the Gingr API path for this endpoint.
119    pub fn path(mut self, path: endpoint::Path) -> Self {
120        self.path = Some(path);
121        self
122    }
123
124    /// Adds request parameters before they are separated into query or form fields.
125    pub fn parameters(mut self, parameters: Vec<(String, String)>) -> Self {
126        self.parameters = parameters;
127        self
128    }
129
130    /// Marks provider parameters that must be redacted from diagnostics.
131    pub fn sensitive_parameter_names(mut self, names: &'static [&'static str]) -> Self {
132        self.sensitive_parameter_names = names.iter().map(|name| (*name).to_owned()).collect();
133        self
134    }
135
136    /// Finalizes the provider request descriptor after required fields are present and wrappers have validated local invariants.
137    pub fn build(self) -> RequestParts {
138        RequestParts {
139            method: self.method.expect("request method is required"),
140            path: self.path.expect("request path is required"),
141            parameters: self.parameters,
142            sensitive_parameter_names: self.sensitive_parameter_names,
143        }
144    }
145}
146
147#[derive(Clone, Debug, PartialEq, Eq)]
148/// Log-safe view of a Gingr request with sensitive provider parameters removed.
149pub struct RedactedRequest {
150    method: endpoint::Method,
151    path: endpoint::Path,
152    parameters: Vec<(String, String)>,
153}
154
155impl fmt::Display for RedactedRequest {
156    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157        write!(formatter, "{:?} {}", self.method, self.path)?;
158        if !self.parameters.is_empty() {
159            let prefix = match self.method {
160                endpoint::Method::Get => "?",
161                endpoint::Method::Post => " form:",
162            };
163            let params = self
164                .parameters
165                .iter()
166                .map(|(key, value)| format!("{key}={value}"))
167                .collect::<Vec<_>>()
168                .join("&");
169            write!(formatter, "{prefix}{params}")?;
170        }
171        Ok(())
172    }
173}
174
175/// Defines the behavior required from a transport participant in the transport workflow.
176pub trait Transport {
177    /// Fixture callback that receives redacted request parts and returns a raw Gingr response.
178    fn send(&self, config: &config::Client, request: RequestParts) -> Result<response::Raw>;
179}
180
181#[derive(Clone, Debug, Default)]
182/// In-memory Gingr transport used for deterministic tests and fixtures.
183pub struct MockTransport;
184
185impl Transport for MockTransport {
186    fn send(&self, _config: &config::Client, _request: RequestParts) -> Result<response::Raw> {
187        Ok(response::Raw::new(
188            response::HttpStatus::OK,
189            bytes::Bytes::from_static(b"{}"),
190        ))
191    }
192}
193
194#[derive(Clone, Debug, Default)]
195/// Reqwest-backed transport for sending requests to Gingr.
196pub struct HttpTransport;
197
198impl Transport for HttpTransport {
199    fn send(&self, _config: &config::Client, _request: RequestParts) -> Result<response::Raw> {
200        Err(TransportError::HttpNotImplemented)
201    }
202}
203
204#[derive(Clone, Debug)]
205/// Gingr client configuration bundle shared by endpoint builders and transport.
206pub struct Client<T = HttpTransport> {
207    config: config::Client,
208    transport: T,
209}
210
211impl Client<HttpTransport> {
212    /// Constructs this typed Gingr boundary value after the caller has chosen the provider input to trust.
213    pub fn new(config: config::Client) -> Self {
214        Self {
215            config,
216            transport: HttpTransport,
217        }
218    }
219}
220
221impl<T> Client<T> {
222    /// Installs a custom transport implementation, usually for tests.
223    pub fn with_transport(config: config::Client, transport: T) -> Self {
224        Self { config, transport }
225    }
226
227    /// Returns the Gingr client configuration used for requests.
228    pub fn config(&self) -> &config::Client {
229        &self.config
230    }
231
232    /// Returns the raw request parts generated for an endpoint without sending it.
233    pub fn capture_request(&self, request: &impl endpoint::Request) -> Result<RequestParts> {
234        let request = request.request_parts().with_api_key(self.config.api_key());
235        let _ = request.url(self.config.base_url())?;
236        Ok(request)
237    }
238
239    /// Returns a log-safe representation of the generated Gingr request.
240    pub fn redacted_request(&self, request: &impl endpoint::Request) -> Result<RedactedRequest> {
241        self.capture_request(request)
242            .map(|request| request.redacted())
243    }
244}
245
246impl<T: Transport> Client<T> {
247    /// Sends the typed Gingr request through the configured transport.
248    pub fn send(&self, request: &impl endpoint::Request) -> Result<response::Raw> {
249        let request = self.capture_request(request)?;
250        self.transport.send(&self.config, request)
251    }
252}