rs_dapi_client/
request_settings.rs

1//! DAPI client request settings processing.
2
3#[cfg(not(target_arch = "wasm32"))]
4use dapi_grpc::tonic::transport::Certificate;
5use std::time::Duration;
6
7/// Default low-level client timeout
8const DEFAULT_CONNECT_TIMEOUT: Option<Duration> = None;
9const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
10const DEFAULT_RETRIES: usize = 5;
11const DEFAULT_BAN_FAILED_ADDRESS: bool = true;
12
13/// DAPI request settings.
14///
15/// There are four levels of settings where each next level can override all previous ones:
16/// 1. Defaults for this library;
17/// 2. [crate::DapiClient] settings;
18/// 3. [crate::DapiRequest]-specific settings;
19/// 4. settings for an exact request execution call.
20#[derive(Debug, Clone, Copy, Default)]
21pub struct RequestSettings {
22    /// Timeout for establishing a connection.
23    pub connect_timeout: Option<Duration>,
24    /// Timeout for single request (soft limit).
25    ///
26    /// Note that the total maximum time of execution can exceed `(timeout + connect_timeout) * retries`
27    /// as it accounts for internal processing time between retries.
28    pub timeout: Option<Duration>,
29    /// Number of retries in case of failed requests. If max retries reached, the last error is returned.
30    /// 1 means one request and one retry in case of error, etc.
31    pub retries: Option<usize>,
32    /// Ban DAPI address if node not responded or responded with error.
33    pub ban_failed_address: Option<bool>,
34    /// Maximum gRPC response size in bytes (decoding limit).
35    pub max_decoding_message_size: Option<usize>,
36}
37
38impl RequestSettings {
39    /// Create empty [RequestSettings], which means no overrides will be applied.
40    /// Actually does the same as [Default], but it's `const`.
41    pub const fn default() -> Self {
42        RequestSettings {
43            connect_timeout: None,
44            timeout: None,
45            retries: None,
46            ban_failed_address: None,
47            max_decoding_message_size: None,
48        }
49    }
50
51    /// Combines two instances of [RequestSettings] with following rules:
52    /// 1. in case of [Some] and [None] for one field the [Some] variant will remain,
53    /// 2. in case of two [Some] variants, right hand side argument will overwrite the value.
54    pub fn override_by(self, rhs: RequestSettings) -> Self {
55        RequestSettings {
56            connect_timeout: rhs.connect_timeout.or(self.connect_timeout),
57            timeout: rhs.timeout.or(self.timeout),
58            retries: rhs.retries.or(self.retries),
59            ban_failed_address: rhs.ban_failed_address.or(self.ban_failed_address),
60            max_decoding_message_size: rhs
61                .max_decoding_message_size
62                .or(self.max_decoding_message_size),
63        }
64    }
65
66    /// Fill in settings defaults.
67    pub fn finalize(self) -> AppliedRequestSettings {
68        AppliedRequestSettings {
69            connect_timeout: self.connect_timeout.or(DEFAULT_CONNECT_TIMEOUT),
70            timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
71            retries: self.retries.unwrap_or(DEFAULT_RETRIES),
72            ban_failed_address: self
73                .ban_failed_address
74                .unwrap_or(DEFAULT_BAN_FAILED_ADDRESS),
75            max_decoding_message_size: self.max_decoding_message_size,
76            #[cfg(not(target_arch = "wasm32"))]
77            ca_certificate: None,
78        }
79    }
80}
81
82/// DAPI settings ready to use.
83#[derive(Debug, Clone)]
84pub struct AppliedRequestSettings {
85    /// Timeout for establishing a connection.
86    pub connect_timeout: Option<Duration>,
87    /// Timeout for a request.
88    pub timeout: Duration,
89    /// Number of retries until returning the last error.
90    pub retries: usize,
91    /// Ban DAPI address if node not responded or responded with error.
92    pub ban_failed_address: bool,
93    /// Maximum gRPC response size in bytes (decoding limit).
94    pub max_decoding_message_size: Option<usize>,
95    /// Certificate Authority certificate to use for verifying the server's certificate.
96    #[cfg(not(target_arch = "wasm32"))]
97    pub ca_certificate: Option<Certificate>,
98}
99impl AppliedRequestSettings {
100    /// Use provided CA certificate for verifying the server's certificate.
101    ///
102    /// If set to None, the system's default CA certificates will be used.
103    #[cfg(not(target_arch = "wasm32"))]
104    pub fn with_ca_certificate(mut self, ca_cert: Option<Certificate>) -> Self {
105        self.ca_certificate = ca_cert;
106        self
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_request_settings_override_by() {
116        let base = RequestSettings {
117            timeout: Some(Duration::from_secs(5)),
118            retries: Some(3),
119            connect_timeout: Some(Duration::from_secs(2)),
120            ban_failed_address: Some(true),
121            max_decoding_message_size: Some(1024),
122        };
123
124        // Override with partial settings
125        let override_settings = RequestSettings {
126            timeout: Some(Duration::from_secs(10)),
127            retries: None,
128            connect_timeout: None,
129            ban_failed_address: None,
130            max_decoding_message_size: None,
131        };
132
133        let result = base.override_by(override_settings);
134        assert_eq!(result.timeout, Some(Duration::from_secs(10))); // overridden
135        assert_eq!(result.retries, Some(3)); // preserved from base
136        assert_eq!(result.connect_timeout, Some(Duration::from_secs(2))); // preserved
137        assert_eq!(result.ban_failed_address, Some(true)); // preserved
138        assert_eq!(result.max_decoding_message_size, Some(1024)); // preserved
139    }
140
141    #[test]
142    fn test_request_settings_override_by_empty() {
143        let base = RequestSettings {
144            timeout: Some(Duration::from_secs(5)),
145            retries: Some(3),
146            connect_timeout: None,
147            ban_failed_address: None,
148            max_decoding_message_size: None,
149        };
150
151        let result = base.override_by(RequestSettings::default());
152        assert_eq!(result.timeout, Some(Duration::from_secs(5)));
153        assert_eq!(result.retries, Some(3));
154    }
155
156    #[test]
157    fn test_request_settings_finalize_defaults() {
158        let settings = RequestSettings::default();
159        let applied = settings.finalize();
160
161        assert_eq!(applied.connect_timeout, None);
162        assert_eq!(applied.timeout, Duration::from_secs(10));
163        assert_eq!(applied.retries, 5);
164        assert!(applied.ban_failed_address);
165        assert!(applied.max_decoding_message_size.is_none());
166    }
167
168    #[test]
169    fn test_request_settings_finalize_custom() {
170        let settings = RequestSettings {
171            connect_timeout: Some(Duration::from_secs(3)),
172            timeout: Some(Duration::from_secs(30)),
173            retries: Some(10),
174            ban_failed_address: Some(false),
175            max_decoding_message_size: Some(4096),
176        };
177
178        let applied = settings.finalize();
179        assert_eq!(applied.connect_timeout, Some(Duration::from_secs(3)));
180        assert_eq!(applied.timeout, Duration::from_secs(30));
181        assert_eq!(applied.retries, 10);
182        assert!(!applied.ban_failed_address);
183        assert_eq!(applied.max_decoding_message_size, Some(4096));
184    }
185
186    #[cfg(not(target_arch = "wasm32"))]
187    #[test]
188    fn test_applied_settings_with_ca_certificate_none() {
189        let applied = RequestSettings::default().finalize();
190        let result = applied.with_ca_certificate(None);
191        assert!(result.ca_certificate.is_none());
192    }
193
194    #[cfg(not(target_arch = "wasm32"))]
195    #[test]
196    fn test_applied_settings_with_ca_certificate_some() {
197        let applied = RequestSettings::default().finalize();
198        let cert = Certificate::from_pem("fake-pem-data");
199        let result = applied.with_ca_certificate(Some(cert));
200        assert!(result.ca_certificate.is_some());
201    }
202}