dynamic_config/remote/source.rs
1//! What a store hands back, and the two traits a store implements.
2//!
3//! Two traits rather than one because a client is either async to begin
4//! with or it is not, and making the wrong half pretend costs a `block_on`
5//! in somebody's runtime. Both are object-safe: a configuration type holds
6//! one without being generic over it.
7
8use crate::error::Error;
9use crate::source::Format;
10
11/// A document a remote store handed back.
12#[derive(Clone, PartialEq, Eq)]
13pub struct Fetched {
14 /// The document text, in `format`.
15 pub text: String,
16 /// How to parse it.
17 pub format: Format,
18}
19
20impl Fetched {
21 /// A document and the format it is written in.
22 #[must_use]
23 pub fn new(text: impl Into<String>, format: Format) -> Self {
24 Self {
25 text: text.into(),
26 format,
27 }
28 }
29}
30
31// The document is the one thing a `Debug` of this type must never print:
32// a remote store's flagship use case is serving secrets, and `Fetched` is
33// what every watch callback receives — one `tracing::debug!(?document)` away
34// from a log. The length is enough to debug with.
35impl std::fmt::Debug for Fetched {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("Fetched")
38 .field("format", &self.format)
39 .field("bytes", &self.text.len())
40 .finish()
41 }
42}
43
44/// A remote store that can be read without an async runtime.
45///
46/// The right trait for anything with a plain HTTP API — Consul and Vault both
47/// are — because implementing it needs no runtime and using it needs no
48/// runtime either. `fetch` may block; it is called from
49/// `refresh_remote()`, never from `load()`.
50pub trait RemoteSource: Send + Sync + 'static {
51 /// Reads the current document.
52 ///
53 /// # Errors
54 ///
55 /// Whatever going wrong looks like for this store. Use
56 /// [`Error::remote`](crate::Error::remote) so the failure is categorised
57 /// consistently, or [`Error::auth`](crate::Error::auth) for a credential
58 /// the store itself refused — that is the distinction a watch loop backs
59 /// off on rather than stopping.
60 fn fetch(&self) -> Result<Fetched, Error>;
61
62 /// How to name this source in an error or a report.
63 fn describe(&self) -> String;
64}
65
66/// A remote store that is read asynchronously.
67///
68/// The right trait for a client that is async to begin with — etcd speaks gRPC
69/// and NATS is a streaming protocol, so both are. Used through
70/// `refresh_remote_async().await`.
71///
72/// The lifetime-bound boxed future rather than `async fn`: this trait is
73/// object-safe on purpose, so a configuration type can hold one without being
74/// generic over it.
75#[cfg(feature = "async")]
76#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
77pub trait AsyncRemoteSource: Send + Sync + 'static {
78 /// Reads the current document.
79 ///
80 /// # Errors
81 ///
82 /// As [`RemoteSource::fetch`].
83 fn fetch(
84 &self,
85 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Fetched, Error>> + Send + '_>>;
86
87 /// How to name this source in an error or a report.
88 fn describe(&self) -> String;
89}