mod error;
mod middleware;
mod policy;
pub use error::Error;
pub use keel_macros::wrap;
pub use middleware::KeelMiddleware;
use keel_core::Engine;
use keel_core_api::KeelError;
use std::path::Path;
use std::sync::OnceLock;
static ENGINE: OnceLock<Engine> = OnceLock::new();
pub fn init() -> Result<(), KeelError> {
init_from(std::env::current_dir().unwrap_or_default())
}
pub fn init_from(dir: impl AsRef<Path>) -> Result<(), KeelError> {
if ENGINE.get().is_some() {
return Ok(());
}
let policy = policy::load(dir.as_ref())?;
let engine = Engine::new();
engine.configure(&policy)?;
let _ = ENGINE.set(engine);
Ok(())
}
#[must_use]
pub fn is_initialized() -> bool {
ENGINE.get().is_some()
}
fn engine() -> &'static Engine {
ENGINE.get_or_init(|| {
let dir = std::env::current_dir().unwrap_or_default();
let policy = policy::load(&dir).unwrap_or_else(|_| serde_json::json!({}));
let engine = Engine::new();
let _ = engine.configure(&policy);
engine
})
}
pub fn report() -> serde_json::Value {
engine().report()
}
#[doc(hidden)]
pub mod __private {
pub use serde;
pub use serde_json;
use crate::Error;
use keel_core_api::{AttemptResult, ENVELOPE_VERSION, ErrorClass, Request};
use serde::{Serialize, de::DeserializeOwned};
use std::future::Future;
pub async fn wrap_call<T, E, F, Fut>(
target: &str,
op: &str,
idempotent: bool,
mut make_attempt: F,
) -> Result<T, Error<E>>
where
T: Serialize + DeserializeOwned + Send + 'static,
E: std::error::Error + Send + Sync + 'static,
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
{
let request = Request {
v: ENVELOPE_VERSION,
target: target.to_owned(),
op: op.to_owned(),
idempotent,
args_hash: None,
};
let mut live_ok: Option<T> = None;
let mut live_err: Option<E> = None;
let outcome = crate::engine()
.execute(&request, async |_attempt: u32| match make_attempt().await {
Ok(value) => match serde_json::to_value(&value) {
Ok(payload) => {
live_ok = Some(value);
AttemptResult::Ok { payload }
}
Err(err) => AttemptResult::Error {
class: ErrorClass::Other,
http_status: None,
retry_after_ms: None,
message: format!(
"keel: #[keel::wrap]'d function's result failed to serialize to \
JSON (needed for the cache path): {err}"
),
original: None,
},
},
Err(err) => {
let message = err.to_string();
live_err = Some(err);
AttemptResult::Error {
class: ErrorClass::Other,
http_status: None,
retry_after_ms: None,
message,
original: None,
}
}
})
.await;
if outcome.result == "ok" {
if outcome.from_cache {
let payload = outcome.payload.unwrap_or(serde_json::Value::Null);
return serde_json::from_value(payload).map_err(|err| {
Error::Keel(keel_core_api::OutcomeError {
code: keel_core_api::ErrorCode::Internal,
class: ErrorClass::Other,
http_status: None,
message: format!(
"keel: cached payload for target {target:?} failed to deserialize: \
{err}"
),
original: None,
})
});
}
return Ok(
live_ok.expect("engine reported a non-cached success without running the effect")
);
}
match live_err {
Some(err) => Err(Error::Original(err)),
None => {
Err(Error::Keel(outcome.error.expect(
"engine reported an error outcome without an OutcomeError",
)))
}
}
}
}