#![forbid(unsafe_code)]
#![deny(missing_docs)]
use std::future::Future;
use std::pin::Pin;
use async_nats::jetstream::kv::{Operation, Store};
use dynamic_config::{AsyncRemoteSource, Error, Fetched, Format};
pub use async_nats::{Client, ConnectOptions};
use futures_util::StreamExt;
pub struct Nats {
store: Store,
key: String,
format: Option<Format>,
server: String,
bucket: String,
}
impl Nats {
pub async fn new(
server: impl Into<String>,
bucket: impl Into<String>,
key: impl Into<String>,
) -> Result<Self, Error> {
Self::with_options(server, bucket, key, ConnectOptions::new()).await
}
pub async fn with_options(
server: impl Into<String>,
bucket: impl Into<String>,
key: impl Into<String>,
options: ConnectOptions,
) -> Result<Self, Error> {
let server = server.into();
let bucket = bucket.into();
let key = key.into();
let client = options
.connect(&server)
.await
.map_err(|error| Error::remote(format!("nats {server}: {error}")))?;
let store = async_nats::jetstream::new(client)
.get_key_value(&bucket)
.await
.map_err(|error| Error::remote(format!("nats {server} bucket {bucket}: {error}")))?;
let format = Format::from_key(&key);
Ok(Self {
store,
key,
format,
server,
bucket,
})
}
pub async fn from_client(
client: Client,
bucket: impl Into<String>,
key: impl Into<String>,
) -> Result<Self, Error> {
let bucket = bucket.into();
let store = async_nats::jetstream::new(client)
.get_key_value(&bucket)
.await
.map_err(|error| Error::remote(format!("nats bucket {bucket}: {error}")))?;
Ok(Self::from_store(store, key))
}
#[must_use]
pub fn from_store(store: Store, key: impl Into<String>) -> Self {
let key = key.into();
let bucket = store.name.clone();
let format = Format::from_key(&key);
Self {
store,
key,
format,
server: "<an existing connection>".to_owned(),
bucket,
}
}
#[must_use]
pub fn with_format(mut self, format: Format) -> Self {
self.format = Some(format);
self
}
pub async fn watch<F>(&self, mut on_change: F) -> Result<(), Error>
where
F: FnMut(Fetched) -> Result<(), Error> + Send,
{
let format = self.format.ok_or_else(|| {
Error::remote(format!(
"{}: the key names no format; call `with_format`",
self.describe()
))
})?;
let mut entries = self.store.watch(&self.key).await.map_err(|error| {
Error::remote(format!("{}: cannot watch: {error}", self.describe()))
})?;
while let Some(entry) = entries.next().await {
let entry = entry.map_err(|error| {
Error::remote(format!("{}: the watch failed: {error}", self.describe()))
})?;
if entry.operation != Operation::Put {
continue;
}
let text = String::from_utf8(entry.value.to_vec()).map_err(|error| {
Error::remote(format!(
"{}: the value is not UTF-8: {error}",
self.describe()
))
})?;
guarded(&mut on_change, Fetched::new(text, format), &self.describe())?;
}
Err(Error::remote(format!(
"{}: the watch ended; the stream was closed",
self.describe()
)))
}
}
impl AsyncRemoteSource for Nats {
fn fetch(&self) -> Pin<Box<dyn Future<Output = Result<Fetched, Error>> + Send + '_>> {
Box::pin(async move {
let format = self.format.ok_or_else(|| {
Error::remote(format!(
"{}: the key names no format; call `with_format`",
self.describe()
))
})?;
let value = self
.store
.get(&self.key)
.await
.map_err(|error| Error::remote(format!("{}: {error}", self.describe())))?
.ok_or_else(|| {
Error::remote(format!("{}: the key holds no value", self.describe()))
})?;
let text = String::from_utf8(value.to_vec()).map_err(|error| {
Error::remote(format!(
"{}: the value is not UTF-8: {error}",
self.describe()
))
})?;
Ok(Fetched::new(text, format))
})
}
fn describe(&self) -> String {
format!(
"nats {} bucket {} key {}",
self.server, self.bucket, self.key
)
}
}
impl std::fmt::Debug for Nats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Nats")
.field("server", &self.server)
.field("bucket", &self.bucket)
.field("key", &self.key)
.field("format", &self.format)
.finish_non_exhaustive()
}
}
fn guarded<F>(on_change: &mut F, document: Fetched, described: &str) -> Result<(), Error>
where
F: FnMut(Fetched) -> Result<(), Error>,
{
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| on_change(document))).unwrap_or_else(
|_| {
Err(Error::remote(format!(
"{described}: the watch callback panicked; the watch is stopped"
)))
},
)
}