use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, Weak};
use std::time::Duration;
use crate::error::{Error, ErrorKind};
use crate::source::Format;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fetched {
pub text: String,
pub format: Format,
}
impl Fetched {
pub fn new(text: impl Into<String>, format: Format) -> Self {
Self {
text: text.into(),
format,
}
}
}
pub trait RemoteSource: Send + Sync + 'static {
fn fetch(&self) -> Result<Fetched, Error>;
fn describe(&self) -> String;
}
#[cfg(feature = "async")]
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
pub trait AsyncRemoteSource: Send + Sync + 'static {
fn fetch(
&self,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Fetched, Error>> + Send + '_>>;
fn describe(&self) -> String;
}
#[derive(Default)]
pub struct Remote {
source: Mutex<Option<Kind>>,
fetched: Mutex<Option<Fetched>>,
}
#[derive(Clone)]
enum Kind {
Blocking(Arc<dyn RemoteSource>),
#[cfg(feature = "async")]
Asynchronous(Arc<dyn AsyncRemoteSource>),
}
impl Remote {
#[must_use]
pub const fn new() -> Self {
Self {
source: Mutex::new(None),
fetched: Mutex::new(None),
}
}
pub fn set(&self, source: impl RemoteSource) {
*self.source_slot() = Some(Kind::Blocking(Arc::new(source)));
*self.fetched_slot() = None;
}
#[cfg(feature = "async")]
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
pub fn set_async(&self, source: impl AsyncRemoteSource) {
*self.source_slot() = Some(Kind::Asynchronous(Arc::new(source)));
*self.fetched_slot() = None;
}
pub fn refresh(&self) -> Result<(), Error> {
let fetched = {
let source = self.source_slot();
match source.as_ref() {
Some(Kind::Blocking(source)) => source.fetch()?,
#[cfg(feature = "async")]
Some(Kind::Asynchronous(source)) => {
return Err(Error::new(
ErrorKind::Remote,
format!(
"`{}` is an async source; refresh it with `refresh_remote_async`",
source.describe()
),
))
}
None => return Err(none_installed()),
}
};
*self.fetched_slot() = Some(fetched);
Ok(())
}
#[cfg(feature = "async")]
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
pub async fn refresh_async(&self) -> Result<(), Error> {
let source = self.source_slot().clone();
let fetched = match source {
Some(Kind::Blocking(source)) => source.fetch()?,
Some(Kind::Asynchronous(source)) => source.fetch().await?,
None => return Err(none_installed()),
};
*self.fetched_slot() = Some(fetched);
Ok(())
}
pub fn install(&self, document: Fetched) {
*self.fetched_slot() = Some(document);
}
pub fn document(&self) -> Option<Fetched> {
self.fetched_slot().clone()
}
pub fn is_configured(&self) -> bool {
self.source_slot().is_some()
}
pub fn describe(&self) -> Option<String> {
self.source_slot().as_ref().map(|source| match source {
Kind::Blocking(source) => source.describe(),
#[cfg(feature = "async")]
Kind::Asynchronous(source) => source.describe(),
})
}
pub fn clear(&self) {
*self.fetched_slot() = None;
}
fn source_slot(&self) -> std::sync::MutexGuard<'_, Option<Kind>> {
self.source
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn fetched_slot(&self) -> std::sync::MutexGuard<'_, Option<Fetched>> {
self.fetched
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
impl std::fmt::Debug for Remote {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Remote")
.field("source", &self.describe())
.field("fetched", &self.document().is_some())
.finish()
}
}
fn none_installed() -> Error {
Error::new(
ErrorKind::Remote,
"no remote source is installed; call `set_remote` first",
)
}
#[must_use = "dropping the handle stops the watch; bind it, or call `.detach()` \
to watch for the rest of the process"]
#[derive(Debug)]
pub struct RemoteWatch {
running: Arc<AtomicBool>,
}
impl RemoteWatch {
pub fn new() -> Self {
Self {
running: Arc::new(AtomicBool::new(true)),
}
}
#[must_use]
pub fn watching(&self) -> Watching {
Watching {
running: Arc::downgrade(&self.running),
}
}
pub fn stop(&self) {
self.running.store(false, Ordering::Release);
}
#[must_use]
pub fn is_stopped(&self) -> bool {
!self.running.load(Ordering::Acquire)
}
pub fn detach(self) {
std::mem::forget(self);
}
}
impl Default for RemoteWatch {
fn default() -> Self {
Self::new()
}
}
impl Drop for RemoteWatch {
fn drop(&mut self) {
self.stop();
}
}
#[derive(Debug, Clone)]
pub struct Watching {
running: Weak<AtomicBool>,
}
impl Watching {
#[must_use]
pub fn keep_going(&self) -> bool {
self.running
.upgrade()
.is_some_and(|running| running.load(Ordering::Acquire))
}
pub fn sleep_for(&self, total: Duration) {
const SLICE: Duration = Duration::from_millis(250);
let mut slept = Duration::ZERO;
while slept < total && self.keep_going() {
std::thread::sleep(SLICE.min(total - slept));
slept += SLICE;
}
}
#[must_use]
pub fn forever() -> Self {
let running = Box::leak(Box::new(Arc::new(AtomicBool::new(true))));
Self {
running: Arc::downgrade(running),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Fake(&'static str);
impl RemoteSource for Fake {
fn fetch(&self) -> Result<Fetched, Error> {
Ok(Fetched::new(self.0, Format::Json))
}
fn describe(&self) -> String {
"a fake store".to_owned()
}
}
struct Broken;
impl RemoteSource for Broken {
fn fetch(&self) -> Result<Fetched, Error> {
Err(Error::remote("the store is unreachable"))
}
fn describe(&self) -> String {
"a broken store".to_owned()
}
}
#[test]
fn nothing_is_fetched_until_it_is_asked_for() {
let remote = Remote::new();
remote.set(Fake(r#"{"db": {"host": "a"}}"#));
assert!(remote.is_configured());
assert!(
remote.document().is_none(),
"installing a source must not reach the network"
);
remote.refresh().unwrap();
assert!(remote.document().is_some());
}
struct Flaky(std::sync::atomic::AtomicBool);
impl RemoteSource for Flaky {
fn fetch(&self) -> Result<Fetched, Error> {
if self.0.swap(true, Ordering::SeqCst) {
return Err(Error::remote("the store went away"));
}
Fake(r#"{"db": {"host": "a"}}"#).fetch()
}
fn describe(&self) -> String {
"a store that answers once".to_owned()
}
}
#[test]
fn a_failed_fetch_leaves_the_previous_document_alone() {
let remote = Remote::new();
remote.set(Flaky(std::sync::atomic::AtomicBool::new(false)));
remote.refresh().unwrap();
let before = remote.document();
assert!(before.is_some(), "the first fetch succeeds");
let error = remote.refresh().unwrap_err();
assert!(error.to_string().contains("went away"), "{error}");
assert_eq!(remote.document(), before);
}
#[test]
fn a_broken_store_reports_rather_than_pretending() {
let remote = Remote::new();
remote.set(Broken);
let error = remote.refresh().unwrap_err();
assert_eq!(error.kind(), ErrorKind::Remote);
assert!(error.to_string().contains("unreachable"), "{error}");
}
#[test]
fn refreshing_with_no_source_says_so() {
let error = Remote::new().refresh().unwrap_err();
assert!(error.to_string().contains("set_remote"), "{error}");
}
#[test]
fn replacing_the_source_drops_the_old_document() {
let remote = Remote::new();
remote.set(Fake(r#"{"db": {"host": "a"}}"#));
remote.refresh().unwrap();
remote.set(Fake(r#"{"db": {"host": "b"}}"#));
assert!(
remote.document().is_none(),
"a new source answering with the old store's values would be a puzzle"
);
}
}