tencent_sdk/client/
config.rs

1use crate::Error;
2use std::{fmt, time::Duration};
3use url::Url;
4
5pub(crate) const DEFAULT_USER_AGENT: &str = concat!("tencent-sdk/", env!("CARGO_PKG_VERSION"));
6pub(crate) const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
7pub(crate) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
8pub(crate) const DEFAULT_BODY_SNIPPET_MAX_BYTES: usize = 4096;
9pub(crate) const DEFAULT_RETRY_BASE_DELAY: Duration = Duration::from_millis(50);
10
11#[derive(Debug, Clone, Copy, Eq, PartialEq)]
12#[non_exhaustive]
13pub enum EndpointMode {
14    /// Resolve `service` into `{service}.{base_host}`.
15    ServiceSubdomain,
16    /// Always use `base_host` directly (useful for testing against a mock server).
17    FixedHost,
18}
19
20#[derive(Debug, Clone, Default)]
21pub struct RequestOptions {
22    pub(crate) timeout: Option<Duration>,
23    pub(crate) capture_body_snippet: Option<bool>,
24    pub(crate) idempotency_key: Option<IdempotencyKey>,
25}
26
27impl RequestOptions {
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    pub fn timeout(mut self, timeout: Duration) -> Self {
33        self.timeout = Some(timeout);
34        self
35    }
36
37    pub fn capture_body_snippet(mut self, enabled: bool) -> Self {
38        self.capture_body_snippet = Some(enabled);
39        self
40    }
41
42    pub fn idempotency_key(mut self, key: impl Into<IdempotencyKey>) -> Self {
43        self.idempotency_key = Some(key.into());
44        self
45    }
46}
47
48#[derive(Clone, Eq, PartialEq)]
49pub struct IdempotencyKey(String);
50
51impl IdempotencyKey {
52    pub fn new(value: impl Into<String>) -> Self {
53        Self(value.into())
54    }
55
56    pub fn as_str(&self) -> &str {
57        &self.0
58    }
59}
60
61impl fmt::Debug for IdempotencyKey {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        f.write_str("IdempotencyKey([redacted])")
64    }
65}
66
67impl From<String> for IdempotencyKey {
68    fn from(value: String) -> Self {
69        Self(value)
70    }
71}
72
73impl From<&str> for IdempotencyKey {
74    fn from(value: &str) -> Self {
75        Self(value.to_string())
76    }
77}
78
79#[derive(Debug, Clone)]
80pub(crate) struct RetryConfig {
81    pub(crate) max_retries: usize,
82    pub(crate) base_delay: Duration,
83}
84
85#[derive(Debug, Clone)]
86pub(crate) struct RequestDefaults {
87    pub(crate) timeout: Duration,
88    pub(crate) capture_body_snippet: bool,
89    pub(crate) body_snippet_max_bytes: usize,
90}
91
92#[derive(Debug, Clone)]
93pub(crate) struct EndpointConfig {
94    pub(crate) scheme: String,
95    pub(crate) host: String,
96    pub(crate) port: Option<u16>,
97    pub(crate) mode: EndpointMode,
98}
99
100impl EndpointConfig {
101    pub(crate) fn from_base_url(base_url: &str, mode: EndpointMode) -> Result<Self, Error> {
102        let url = Url::parse(base_url)
103            .map_err(|source| Error::invalid_base_url(base_url.to_string(), Box::new(source)))?;
104
105        let scheme = url.scheme();
106        if scheme != "http" && scheme != "https" {
107            let source = std::io::Error::new(
108                std::io::ErrorKind::InvalidInput,
109                "base url scheme must be http or https",
110            );
111            return Err(Error::invalid_base_url(
112                base_url.to_string(),
113                Box::new(source),
114            ));
115        }
116
117        if !url.username().is_empty() || url.password().is_some() {
118            let source = std::io::Error::new(
119                std::io::ErrorKind::InvalidInput,
120                "base url must not include credentials",
121            );
122            return Err(Error::invalid_base_url(
123                base_url.to_string(),
124                Box::new(source),
125            ));
126        }
127
128        if url.fragment().is_some() {
129            let source = std::io::Error::new(
130                std::io::ErrorKind::InvalidInput,
131                "base url must not include a fragment",
132            );
133            return Err(Error::invalid_base_url(
134                base_url.to_string(),
135                Box::new(source),
136            ));
137        }
138
139        let host = url.host_str().ok_or_else(|| {
140            let source = std::io::Error::new(
141                std::io::ErrorKind::InvalidInput,
142                "base url must include a host",
143            );
144            Error::invalid_base_url(base_url.to_string(), Box::new(source))
145        })?;
146
147        if !(url.path().is_empty() || url.path() == "/") || url.query().is_some() {
148            let source = std::io::Error::new(
149                std::io::ErrorKind::InvalidInput,
150                "base url must not include a path or query",
151            );
152            return Err(Error::invalid_base_url(
153                base_url.to_string(),
154                Box::new(source),
155            ));
156        }
157
158        Ok(Self {
159            scheme: scheme.to_string(),
160            host: host.to_string(),
161            port: url.port(),
162            mode,
163        })
164    }
165
166    pub(crate) fn authority_for_service(&self, service: &str) -> String {
167        let base_host = match self.mode {
168            EndpointMode::ServiceSubdomain => format!("{service}.{}", self.host),
169            EndpointMode::FixedHost => self.host.clone(),
170        };
171
172        match self.port {
173            Some(port) => format!("{base_host}:{port}"),
174            None => base_host,
175        }
176    }
177}