lightstreamer_rs/config/mod.rs
1//! Everything a client needs to be told before it can open a session.
2//!
3//! Configuration arrives through typed, checked values — never through the
4//! environment. This is a library: reading `LS_PASSWORD` out of the process
5//! environment behind the caller's back would make the crate's behaviour
6//! depend on something the caller did not write down, and would put a secret
7//! somewhere this crate has no business looking. Credentials are supplied by
8//! the caller, are never logged, and never appear in an error.
9//!
10//! # Shape of the API
11//!
12//! ```
13//! use lightstreamer_rs::{AdapterSet, ClientConfig, Credentials, ServerAddress};
14//!
15//! let config = ClientConfig::builder(ServerAddress::try_new("https://push.lightstreamer.com")?)
16//! .with_adapter_set(AdapterSet::try_new("DEMO")?)
17//! .with_credentials(Credentials::anonymous())
18//! .build()?;
19//!
20//! assert_eq!(config.address().as_str(), "https://push.lightstreamer.com");
21//! # Ok::<(), lightstreamer_rs::ConfigError>(())
22//! ```
23
24mod address;
25mod credentials;
26mod options;
27
28use std::time::Duration;
29
30pub use address::ServerAddress;
31pub use credentials::{AdapterSet, Credentials};
32pub use options::{ConnectionOptions, RetryPolicy, Transport};
33
34/// The longest any timing in this crate may be set to: 365 days.
35///
36/// A choice of this crate, and an upper bound rather than a recommendation —
37/// every sensible value is orders of magnitude below it. It exists because a
38/// duration is added to an [`Instant`](std::time::Instant) to make a deadline,
39/// and that addition is not total: past the runtime timer's own range the
40/// arithmetic overflows, and an overflowed deadline is an expired one. A
41/// timeout of [`Duration::MAX`] would then mean "give up at once" — the exact
42/// opposite of what it says. Refusing the value at configuration time is the
43/// only place that inversion can be caught while it is still a caller's
44/// mistake rather than a client that retries in a loop.
45pub const MAX_TIMING: Duration = Duration::from_secs(365 * 24 * 60 * 60);
46
47/// A configuration value that cannot be used as given.
48///
49/// Every variant names the one thing that is wrong and, where it helps, the
50/// offending value. No variant ever carries a password: a credential that
51/// reached an error message would end up in whatever log the caller writes the
52/// error to.
53#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
54#[non_exhaustive]
55pub enum ConfigError {
56 /// The server address was empty or only whitespace.
57 #[error("server address is empty")]
58 EmptyServerAddress,
59
60 /// The server address has no `scheme://` prefix.
61 ///
62 /// The scheme is required rather than guessed, because defaulting to TLS
63 /// or to plain text would each be wrong half the time and neither would be
64 /// visible to the caller.
65 #[error(
66 "server address `{address}` has no scheme: expected ws://, wss://, http:// or https://"
67 )]
68 MissingScheme {
69 /// The address as supplied.
70 address: String,
71 },
72
73 /// The server address names a scheme this crate cannot speak.
74 #[error("unsupported scheme `{scheme}`: expected ws, wss, http or https")]
75 UnsupportedScheme {
76 /// The scheme as supplied, lowercased.
77 scheme: String,
78 },
79
80 /// The server address has a scheme but no host after it.
81 #[error("server address `{address}` has no host")]
82 MissingHost {
83 /// The address as supplied.
84 address: String,
85 },
86
87 /// The server address carries a `user:password@` prefix.
88 ///
89 /// Refused rather than stripped, and deliberately reported without
90 /// quoting the address: an address that reached an error message would
91 /// take the credential in it into whatever log the error is written to.
92 /// Supply credentials with [`Credentials`] instead, where this crate
93 /// knows to redact them.
94 #[error("server address must not contain credentials; use Credentials instead")]
95 AddressHasUserinfo,
96
97 /// The host part of the server address is not a host.
98 #[error(
99 "`{host}` is not a host: an IPv6 literal must be bracketed, and no host may contain whitespace"
100 )]
101 InvalidHost {
102 /// The host as supplied.
103 host: String,
104 },
105
106 /// The port part of the server address is not a port.
107 #[error("`{port}` is not a port: expected a number from 1 to 65535")]
108 InvalidPort {
109 /// The port as supplied.
110 port: String,
111 },
112
113 /// What follows the host in the server address is not a usable base path.
114 ///
115 /// TLCP fixes the endpoint path [`docs/spec/01-foundations.md` §6.2.1], so
116 /// a query, a fragment or a `..` segment either means something this crate
117 /// would silently drop or aims the connection at a different resource.
118 #[error("`{path}` is not a base path: no query, fragment or `..` segment is allowed")]
119 InvalidAddressPath {
120 /// The path as supplied.
121 path: String,
122 },
123
124 /// The Adapter Set name was empty or only whitespace.
125 ///
126 /// To request the server's `DEFAULT` Adapter Set, leave the name unset
127 /// instead: naming the empty string is a different request, and not one a
128 /// caller means [`docs/spec/03-requests.md` §2.1].
129 #[error("adapter set name is empty; leave it unset to use the server's DEFAULT")]
130 EmptyAdapterSet,
131
132 /// The Adapter Set name contains an ASCII control character.
133 #[error("adapter set name contains a control character")]
134 AdapterSetControlCharacter,
135
136 /// A subscription named no items.
137 #[error("subscription names no items")]
138 EmptyItemGroup,
139
140 /// An item group contains an ASCII control character.
141 #[error("item group contains a control character")]
142 ItemGroupControlCharacter,
143
144 /// An item name in a list contains whitespace, which would split it into
145 /// two items once the list is joined.
146 #[error("item name `{name}` contains whitespace; item names are separated by spaces")]
147 ItemNameHasWhitespace {
148 /// The offending name.
149 name: String,
150 },
151
152 /// A subscription named no fields.
153 #[error("subscription names no fields")]
154 EmptyFieldSchema,
155
156 /// A field schema contains an ASCII control character.
157 #[error("field schema contains a control character")]
158 FieldSchemaControlCharacter,
159
160 /// A field name in a list contains whitespace, which would split it into
161 /// two fields once the list is joined.
162 #[error("field name `{name}` contains whitespace; field names are separated by spaces")]
163 FieldNameHasWhitespace {
164 /// The offending name.
165 name: String,
166 },
167
168 /// A message sequence name is not a legal identifier.
169 #[error("`{name}` is not a sequence name: expected letters, digits and underscores")]
170 InvalidSequenceName {
171 /// The name as supplied.
172 name: String,
173 },
174
175 /// A frequency limit is not a decimal number with a dot separator.
176 #[error("`{value}` is not a frequency: expected a decimal number such as `2` or `0.5`")]
177 InvalidFrequency {
178 /// The text as supplied.
179 value: String,
180 },
181
182 /// A snapshot length was asked for in a mode that does not admit one.
183 ///
184 /// `LS_snapshot` as a number of events is "admitted only if `LS_mode` is
185 /// `DISTINCT`" [`docs/spec/03-requests.md` §6.1].
186 #[error("a snapshot length is admitted only with SubscriptionMode::Distinct, not {mode}")]
187 SnapshotLengthNeedsDistinct {
188 /// The mode the subscription was built with.
189 mode: &'static str,
190 },
191
192 /// A snapshot was asked for in [`SubscriptionMode::Raw`](crate::SubscriptionMode::Raw),
193 /// which keeps no item state and so has none to send
194 /// [`docs/spec/03-requests.md` §6.1].
195 #[error("a snapshot is not available with SubscriptionMode::Raw")]
196 SnapshotNeedsAStatefulMode,
197
198 /// A maximum frequency was asked for in
199 /// [`SubscriptionMode::Raw`](crate::SubscriptionMode::Raw), which the
200 /// server does not filter [`docs/spec/03-requests.md` §6.1].
201 #[error("a maximum frequency is not applied with SubscriptionMode::Raw")]
202 FrequencyNeedsAFilteredMode,
203
204 /// A buffer size was asked for in a mode the server does not buffer.
205 ///
206 /// `LS_requested_buffer_size` is considered "only if `LS_mode` is `MERGE`
207 /// or `DISTINCT`" [`docs/spec/03-requests.md` §6.1].
208 #[error("a buffer size applies only to SubscriptionMode::Merge or ::Distinct, not {mode}")]
209 BufferSizeNeedsMergeOrDistinct {
210 /// The mode the subscription was built with.
211 mode: &'static str,
212 },
213
214 /// A buffer size was asked for alongside an unfiltered frequency, which
215 /// the server ignores [`docs/spec/03-requests.md` §6.1].
216 #[error("a buffer size is ignored with MaxFrequency::Unfiltered; set one or the other")]
217 BufferSizeWithUnfilteredFrequency,
218
219 /// A [`Message::fire_and_forget`](crate::Message::fire_and_forget) message
220 /// was placed in a sequence.
221 ///
222 /// The two are inseparable in the protocol: `LS_msg_prog` is "mandatory
223 /// whenever `LS_sequence` is specified" and a fire-and-forget message is
224 /// precisely one with no progressive [`docs/spec/03-requests.md` §12.1].
225 #[error("a fire-and-forget message cannot be placed in a sequence: it carries no progressive")]
226 SequencedMessageNeedsAProgressive,
227
228 /// A maximum wait was set on a message that is in no sequence, where the
229 /// server ignores it [`docs/spec/03-requests.md` §12.1].
230 #[error("a maximum wait applies only to a message in a sequence")]
231 MaxWaitNeedsASequence,
232
233 /// A duration that must be positive was zero.
234 #[error("`{field}` must be greater than zero")]
235 ZeroDuration {
236 /// Which knob, by the name of its builder method.
237 field: &'static str,
238 },
239
240 /// A duration is longer than this crate's timers are defined for.
241 ///
242 /// Either it does not fit the whole number of milliseconds the wire
243 /// carries, or it exceeds [`MAX_TIMING`]. Both are refused here rather
244 /// than clamped: a deadline that overflowed would become an *immediate*
245 /// one, inverting the very setting that produced it.
246 #[error("`{field}` is longer than the supported maximum of {MAX_TIMING:?}")]
247 DurationTooLarge {
248 /// Which knob, by the name of its builder method.
249 field: &'static str,
250 },
251
252 /// The reconnection ceiling is below the first reconnection delay, so the
253 /// schedule could never be honoured.
254 #[error("retry max_delay ({max:?}) is below initial_delay ({initial:?})")]
255 RetryCeilingBelowInitial {
256 /// The configured first delay.
257 initial: Duration,
258 /// The configured ceiling.
259 max: Duration,
260 },
261}
262
263/// Everything needed to open a session, checked and ready to use.
264///
265/// Build one with [`ClientConfig::builder`]; the fields are private so a
266/// configuration that failed a check cannot exist. Hand it to
267/// [`Client::connect`](crate::Client::connect).
268///
269/// # Examples
270///
271/// ```
272/// use std::time::Duration;
273/// use lightstreamer_rs::{
274/// AdapterSet, ClientConfig, ConnectionOptions, Credentials, ServerAddress,
275/// };
276///
277/// let config = ClientConfig::builder(ServerAddress::try_new("wss://push.example.com")?)
278/// .with_adapter_set(AdapterSet::try_new("MY_ADAPTERS")?)
279/// .with_credentials(Credentials::new("alice", "hunter2"))
280/// .with_options(ConnectionOptions::default().with_open_timeout(Duration::from_secs(5)))
281/// .build()?;
282///
283/// assert_eq!(config.adapter_set().map(AdapterSet::as_str), Some("MY_ADAPTERS"));
284/// # Ok::<(), lightstreamer_rs::ConfigError>(())
285/// ```
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct ClientConfig {
288 address: ServerAddress,
289 adapter_set: Option<AdapterSet>,
290 credentials: Credentials,
291 transport: Transport,
292 options: ConnectionOptions,
293}
294
295impl ClientConfig {
296 /// Starts building a configuration for a server.
297 ///
298 /// The address is the only value with no sensible default, so it is taken
299 /// here rather than left to be forgotten.
300 #[must_use = "builders do nothing unless .build() is called"]
301 pub fn builder(address: ServerAddress) -> ClientConfigBuilder {
302 ClientConfigBuilder {
303 address,
304 adapter_set: None,
305 credentials: Credentials::anonymous(),
306 transport: Transport::default(),
307 options: ConnectionOptions::default(),
308 }
309 }
310
311 /// The server this configuration points at.
312 #[must_use]
313 #[inline]
314 pub const fn address(&self) -> &ServerAddress {
315 &self.address
316 }
317
318 /// The Adapter Set that will serve the session, or `None` to let the
319 /// server use its `DEFAULT`.
320 #[must_use]
321 #[inline]
322 pub const fn adapter_set(&self) -> Option<&AdapterSet> {
323 self.adapter_set.as_ref()
324 }
325
326 /// The credentials the session will be created with.
327 #[must_use]
328 #[inline]
329 pub const fn credentials(&self) -> &Credentials {
330 &self.credentials
331 }
332
333 /// The transport that will carry the session.
334 #[must_use]
335 #[inline]
336 pub const fn transport(&self) -> Transport {
337 self.transport
338 }
339
340 /// The timeouts, limits and reconnection policy.
341 #[must_use]
342 #[inline]
343 pub const fn options(&self) -> &ConnectionOptions {
344 &self.options
345 }
346
347 /// Takes the configuration apart for the layer that will run it.
348 ///
349 /// This module is a **leaf**: it validates what the caller wrote and knows
350 /// nothing about the state machine, the wire, or the transports — it cannot
351 /// even name them. The translation from this vocabulary into theirs
352 /// therefore lives on the other side of the boundary, in
353 /// `SessionOptions::from_client_config`, and this is the one accessor it
354 /// needs.
355 pub(crate) fn into_parts(self) -> (Option<AdapterSet>, Credentials, ConnectionOptions) {
356 (self.adapter_set, self.credentials, self.options)
357 }
358}
359
360/// Assembles a [`ClientConfig`], checking it once at the end.
361///
362/// The checks that involve more than one value — a reconnection ceiling below
363/// its own first delay, for instance — cannot be made one setter at a time, so
364/// [`ClientConfigBuilder::build`] is where all of them happen and where the
365/// typed error comes from.
366#[derive(Debug, Clone)]
367#[must_use = "builders do nothing unless .build() is called"]
368pub struct ClientConfigBuilder {
369 address: ServerAddress,
370 adapter_set: Option<AdapterSet>,
371 credentials: Credentials,
372 transport: Transport,
373 options: ConnectionOptions,
374}
375
376impl ClientConfigBuilder {
377 /// Names the Adapter Set that will serve the session.
378 ///
379 /// Left unset, the server uses an Adapter Set named `DEFAULT`
380 /// [`docs/spec/03-requests.md` §2.1].
381 #[must_use = "builders do nothing unless .build() is called"]
382 pub fn with_adapter_set(mut self, adapter_set: AdapterSet) -> Self {
383 self.adapter_set = Some(adapter_set);
384 self
385 }
386
387 /// Supplies the credentials the session is created with.
388 ///
389 /// Defaults to [`Credentials::anonymous`].
390 #[must_use = "builders do nothing unless .build() is called"]
391 pub fn with_credentials(mut self, credentials: Credentials) -> Self {
392 self.credentials = credentials;
393 self
394 }
395
396 /// Forces a particular transport.
397 ///
398 /// Defaults to [`Transport::WebSocket`]. The two HTTP variants —
399 /// [`Transport::HttpStreaming`] and [`Transport::HttpPolling`] — are also
400 /// implemented, for a network path that mangles WebSocket upgrades.
401 #[must_use = "builders do nothing unless .build() is called"]
402 pub const fn with_transport(mut self, transport: Transport) -> Self {
403 self.transport = transport;
404 self
405 }
406
407 /// Sets the timeouts, limits and reconnection policy.
408 #[must_use = "builders do nothing unless .build() is called"]
409 pub fn with_options(mut self, options: ConnectionOptions) -> Self {
410 self.options = options;
411 self
412 }
413
414 /// Checks everything and produces the configuration.
415 ///
416 /// # Errors
417 ///
418 /// - [`ConfigError::ZeroDuration`] if `open_timeout`, or a keepalive hint
419 /// or inactivity commitment that was set, is zero, or if the first retry
420 /// delay is zero. A zero `open_timeout` would abandon every attempt
421 /// before it started; a zero retry delay would reconnect in a hot loop.
422 /// - [`ConfigError::DurationTooLarge`] if any duration exceeds
423 /// [`MAX_TIMING`], or if a value that travels as a whole number of
424 /// milliseconds does not fit one. Every timing is checked, including the
425 /// keepalive slack and both retry delays: an overflowed deadline is an
426 /// expired one, so a duration too large to add to an instant would
427 /// silently become the opposite of what it says.
428 /// - [`ConfigError::RetryCeilingBelowInitial`] if the reconnection ceiling
429 /// is below the first delay.
430 ///
431 /// A zero keepalive *slack* is accepted: it means "allow the server not
432 /// one millisecond past the interval it promised", which is aggressive but
433 /// coherent.
434 ///
435 /// # Examples
436 ///
437 /// ```
438 /// use std::time::Duration;
439 /// use lightstreamer_rs::{
440 /// ClientConfig, ConfigError, ConnectionOptions, ServerAddress,
441 /// };
442 ///
443 /// let rejected = ClientConfig::builder(ServerAddress::try_new("wss://push.example.com")?)
444 /// .with_options(ConnectionOptions::default().with_open_timeout(Duration::ZERO))
445 /// .build();
446 ///
447 /// assert!(matches!(rejected, Err(ConfigError::ZeroDuration { field: "open_timeout" })));
448 /// # Ok::<(), ConfigError>(())
449 /// ```
450 pub fn build(self) -> Result<ClientConfig, ConfigError> {
451 require_positive("open_timeout", self.options.open_timeout())?;
452 require_within_maximum("open_timeout", self.options.open_timeout())?;
453 require_within_maximum("keepalive_slack", self.options.keepalive_slack())?;
454
455 if let Some(hint) = self.options.keepalive_hint() {
456 require_positive("keepalive_hint", hint)?;
457 require_within_maximum("keepalive_hint", hint)?;
458 require_millis("keepalive_hint", hint)?;
459 }
460 if let Some(commitment) = self.options.inactivity_commitment() {
461 require_positive("inactivity_commitment", commitment)?;
462 require_within_maximum("inactivity_commitment", commitment)?;
463 require_millis("inactivity_commitment", commitment)?;
464 }
465
466 let retry = self.options.retry();
467 require_positive("retry.initial_delay", retry.initial_delay())?;
468 require_within_maximum("retry.initial_delay", retry.initial_delay())?;
469 require_within_maximum("retry.max_delay", retry.max_delay())?;
470 if retry.max_delay() < retry.initial_delay() {
471 return Err(ConfigError::RetryCeilingBelowInitial {
472 initial: retry.initial_delay(),
473 max: retry.max_delay(),
474 });
475 }
476
477 Ok(ClientConfig {
478 address: self.address,
479 adapter_set: self.adapter_set,
480 credentials: self.credentials,
481 transport: self.transport,
482 options: self.options,
483 })
484 }
485}
486
487/// Rejects a zero duration where a positive one is required.
488#[cold]
489#[inline(never)]
490fn zero_duration(field: &'static str) -> ConfigError {
491 ConfigError::ZeroDuration { field }
492}
493
494/// Checks that a duration is greater than zero.
495fn require_positive(field: &'static str, value: Duration) -> Result<(), ConfigError> {
496 if value.is_zero() {
497 return Err(zero_duration(field));
498 }
499 Ok(())
500}
501
502/// Checks that a duration is one this crate's timers are defined for.
503fn require_within_maximum(field: &'static str, value: Duration) -> Result<(), ConfigError> {
504 if value > MAX_TIMING {
505 return Err(ConfigError::DurationTooLarge { field });
506 }
507 Ok(())
508}
509
510/// Checks that a duration fits the whole number of milliseconds the wire
511/// carries.
512fn require_millis(field: &'static str, value: Duration) -> Result<(), ConfigError> {
513 u64::try_from(value.as_millis())
514 .map(|_| ())
515 .map_err(|_| ConfigError::DurationTooLarge { field })
516}
517
518#[cfg(test)]
519mod tests {
520
521 use super::*;
522
523 fn address() -> ServerAddress {
524 match ServerAddress::try_new("wss://push.example.com") {
525 Ok(address) => address,
526 Err(error) => unreachable!("the fixture address is valid: {error}"),
527 }
528 }
529
530 #[test]
531 fn test_config_builds_with_only_an_address() {
532 match ClientConfig::builder(address()).build() {
533 Ok(config) => {
534 assert_eq!(config.adapter_set(), None);
535 assert_eq!(config.transport(), Transport::WebSocket);
536 assert_eq!(config.credentials().user(), None);
537 }
538 Err(error) => panic!("a minimal configuration was rejected: {error}"),
539 }
540 }
541
542 #[test]
543 fn test_config_keeps_what_it_was_given() {
544 let adapters = match AdapterSet::try_new("DEMO") {
545 Ok(set) => set,
546 Err(error) => unreachable!("the fixture adapter set is valid: {error}"),
547 };
548 let built = ClientConfig::builder(address())
549 .with_adapter_set(adapters)
550 .with_credentials(Credentials::new("alice", "hunter2"))
551 .with_options(ConnectionOptions::default().with_send_sync(false))
552 .build();
553
554 match built {
555 Ok(config) => {
556 assert_eq!(config.adapter_set().map(AdapterSet::as_str), Some("DEMO"));
557 assert_eq!(config.credentials().user(), Some("alice"));
558 assert!(config.credentials().has_password());
559 assert!(!config.options().send_sync());
560 }
561 Err(error) => panic!("rejected: {error}"),
562 }
563 }
564
565 #[test]
566 fn test_config_rejects_a_zero_open_timeout() {
567 let built = ClientConfig::builder(address())
568 .with_options(ConnectionOptions::default().with_open_timeout(Duration::ZERO))
569 .build();
570 assert!(matches!(
571 built,
572 Err(ConfigError::ZeroDuration {
573 field: "open_timeout"
574 })
575 ));
576 }
577
578 #[test]
579 fn test_config_rejects_a_zero_keepalive_hint() {
580 let built = ClientConfig::builder(address())
581 .with_options(ConnectionOptions::default().with_keepalive_hint(Some(Duration::ZERO)))
582 .build();
583 assert!(matches!(
584 built,
585 Err(ConfigError::ZeroDuration {
586 field: "keepalive_hint"
587 })
588 ));
589 }
590
591 #[test]
592 fn test_config_rejects_a_zero_inactivity_commitment() {
593 let built = ClientConfig::builder(address())
594 .with_options(
595 ConnectionOptions::default().with_inactivity_commitment(Some(Duration::ZERO)),
596 )
597 .build();
598 assert!(matches!(
599 built,
600 Err(ConfigError::ZeroDuration {
601 field: "inactivity_commitment"
602 })
603 ));
604 }
605
606 #[test]
607 fn test_config_rejects_a_duration_that_cannot_be_expressed_in_millis() {
608 let built = ClientConfig::builder(address())
609 .with_options(ConnectionOptions::default().with_keepalive_hint(Some(Duration::MAX)))
610 .build();
611 assert!(matches!(
612 built,
613 Err(ConfigError::DurationTooLarge {
614 field: "keepalive_hint"
615 })
616 ));
617 }
618
619 #[test]
620 fn test_config_rejects_a_zero_first_retry_delay() {
621 let retry = RetryPolicy::default().with_initial_delay(Duration::ZERO);
622 let built = ClientConfig::builder(address())
623 .with_options(ConnectionOptions::default().with_retry(retry))
624 .build();
625 assert!(matches!(
626 built,
627 Err(ConfigError::ZeroDuration {
628 field: "retry.initial_delay"
629 })
630 ));
631 }
632
633 #[test]
634 fn test_config_rejects_a_retry_ceiling_below_its_first_delay() {
635 let retry = RetryPolicy::default()
636 .with_initial_delay(Duration::from_secs(10))
637 .with_max_delay(Duration::from_secs(1));
638 let built = ClientConfig::builder(address())
639 .with_options(ConnectionOptions::default().with_retry(retry))
640 .build();
641 assert!(matches!(
642 built,
643 Err(ConfigError::RetryCeilingBelowInitial { .. })
644 ));
645 }
646
647 #[test]
648 fn test_config_accepts_an_equal_retry_ceiling_and_first_delay() {
649 let retry = RetryPolicy::default()
650 .with_initial_delay(Duration::from_secs(2))
651 .with_max_delay(Duration::from_secs(2));
652 let built = ClientConfig::builder(address())
653 .with_options(ConnectionOptions::default().with_retry(retry))
654 .build();
655 assert!(built.is_ok());
656 }
657
658 // -----------------------------------------------------------------------
659 // A-08: no timing may be large enough to overflow a deadline
660 // -----------------------------------------------------------------------
661
662 /// Applies one duration to every timing knob in turn.
663 fn with_each_timing(value: Duration) -> Vec<(&'static str, Result<ClientConfig, ConfigError>)> {
664 let retry_initial = RetryPolicy::default()
665 .with_initial_delay(value)
666 .with_max_delay(value);
667 // The ceiling is checked against the first delay too, so that one is
668 // pinned at the smallest legal value while the ceiling varies.
669 let retry_max = RetryPolicy::default()
670 .with_initial_delay(Duration::from_nanos(1))
671 .with_max_delay(value);
672 vec![
673 (
674 "open_timeout",
675 ClientConfig::builder(address())
676 .with_options(ConnectionOptions::default().with_open_timeout(value))
677 .build(),
678 ),
679 (
680 "keepalive_slack",
681 ClientConfig::builder(address())
682 .with_options(ConnectionOptions::default().with_keepalive_slack(value))
683 .build(),
684 ),
685 (
686 "keepalive_hint",
687 ClientConfig::builder(address())
688 .with_options(ConnectionOptions::default().with_keepalive_hint(Some(value)))
689 .build(),
690 ),
691 (
692 "inactivity_commitment",
693 ClientConfig::builder(address())
694 .with_options(
695 ConnectionOptions::default().with_inactivity_commitment(Some(value)),
696 )
697 .build(),
698 ),
699 (
700 "retry.initial_delay",
701 ClientConfig::builder(address())
702 .with_options(ConnectionOptions::default().with_retry(retry_initial))
703 .build(),
704 ),
705 (
706 "retry.max_delay",
707 ClientConfig::builder(address())
708 .with_options(ConnectionOptions::default().with_retry(retry_max))
709 .build(),
710 ),
711 ]
712 }
713
714 #[test]
715 fn test_config_accepts_every_timing_at_the_supported_maximum() {
716 for (field, built) in with_each_timing(MAX_TIMING) {
717 assert!(built.is_ok(), "{field} was rejected at the maximum");
718 }
719 }
720
721 #[test]
722 fn test_config_rejects_every_timing_one_step_past_the_maximum() {
723 let over = match MAX_TIMING.checked_add(Duration::from_nanos(1)) {
724 Some(over) => over,
725 None => Duration::MAX,
726 };
727 for (field, built) in with_each_timing(over) {
728 assert!(
729 matches!(built, Err(ConfigError::DurationTooLarge { field: named }) if named == field),
730 "{field} was accepted one step past the maximum"
731 );
732 }
733 }
734
735 #[test]
736 fn test_config_rejects_every_timing_at_duration_max() {
737 // The value that would otherwise overflow the deadline arithmetic and
738 // become an *immediate* one.
739 for (field, built) in with_each_timing(Duration::MAX) {
740 assert!(
741 matches!(built, Err(ConfigError::DurationTooLarge { field: named }) if named == field),
742 "{field} accepted Duration::MAX"
743 );
744 }
745 }
746
747 #[test]
748 fn test_config_accepts_every_timing_at_its_smallest_usable_value() {
749 // One nanosecond is aggressive but coherent everywhere it is allowed;
750 // zero is separately refused where it would invert behaviour.
751 for (field, built) in with_each_timing(Duration::from_nanos(1)) {
752 assert!(built.is_ok(), "{field} was rejected at one nanosecond");
753 }
754 }
755
756 #[test]
757 fn test_config_accepts_a_zero_keepalive_slack() {
758 let built = ClientConfig::builder(address())
759 .with_options(ConnectionOptions::default().with_keepalive_slack(Duration::ZERO))
760 .build();
761 assert!(built.is_ok());
762 }
763
764 #[test]
765 fn test_config_accepts_unlimited_retries() {
766 let retry = RetryPolicy::default().with_max_attempts(None);
767 let built = ClientConfig::builder(address())
768 .with_options(ConnectionOptions::default().with_retry(retry))
769 .build();
770 assert!(built.is_ok());
771 }
772}