use std::{
any::{Any, TypeId},
cell::{Cell, OnceCell},
fmt,
rc::Rc,
time::Duration,
};
use lenso_kernel::{
ActivateContext, CancellationToken, DeactivateContext, PluginDependencies, PluginFuture,
PluginLifecycle, RuntimeFailure,
};
#[derive(Clone, Debug)]
pub struct LifecycleContext {
cancellation: CancellationToken,
remaining_budget: Option<Duration>,
}
impl LifecycleContext {
fn constructing(context: &ActivateContext) -> Self {
Self {
cancellation: context.cancellation(),
remaining_budget: None,
}
}
fn stopping(context: &DeactivateContext) -> Self {
Self {
cancellation: context.cancellation(),
remaining_budget: context.remaining_budget(),
}
}
pub fn cancellation(&self) -> CancellationToken {
self.cancellation.clone()
}
pub const fn remaining_budget(&self) -> Option<Duration> {
self.remaining_budget
}
}
#[derive(Clone, Debug)]
pub struct ConstructionContext {
configuration: String,
dependencies: PluginDependencies,
lifecycle: LifecycleContext,
}
impl ConstructionContext {
fn new(configuration: String, context: &ActivateContext) -> Self {
Self {
configuration,
dependencies: context.dependencies().clone(),
lifecycle: LifecycleContext::constructing(context),
}
}
pub fn configuration(&self) -> &str {
&self.configuration
}
pub const fn dependencies(&self) -> &PluginDependencies {
&self.dependencies
}
pub const fn lifecycle(&self) -> &LifecycleContext {
&self.lifecycle
}
}
pub type ConstructionFuture<T> =
futures::future::LocalBoxFuture<'static, Result<Rc<T>, RuntimeFailure>>;
pub type ErasedConstructionFuture =
futures::future::LocalBoxFuture<'static, Result<Rc<dyn Any>, RuntimeFailure>>;
pub type ErasedStop = fn(Rc<dyn Any>, LifecycleContext) -> PluginFuture;
#[derive(Clone, Copy)]
pub struct LinkedPluginConstruction {
plugin_type: fn() -> TypeId,
custom: bool,
construct: fn(ConstructionContext) -> ErasedConstructionFuture,
stop: Option<ErasedStop>,
}
impl LinkedPluginConstruction {
#[doc(hidden)]
pub const fn new(
plugin_type: fn() -> TypeId,
custom: bool,
construct: fn(ConstructionContext) -> ErasedConstructionFuture,
stop: Option<ErasedStop>,
) -> Self {
Self {
plugin_type,
custom,
construct,
stop,
}
}
}
impl fmt::Debug for LinkedPluginConstruction {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("LinkedPluginConstruction")
.field("custom", &self.custom)
.finish_non_exhaustive()
}
}
inventory::collect!(LinkedPluginConstruction);
type Constructor<T> = Rc<dyn Fn(ConstructionContext) -> ConstructionFuture<T>>;
type Stopper<T> = Rc<dyn Fn(Rc<T>, LifecycleContext) -> PluginFuture>;
pub struct PluginObject<T> {
value: Rc<OnceCell<Rc<T>>>,
}
impl<T> PluginObject<T> {
pub fn empty() -> Self {
Self {
value: Rc::new(OnceCell::new()),
}
}
pub fn from_value(value: Rc<T>) -> Self {
let object = Self::empty();
assert!(
object.value.set(value).is_ok(),
"a new Plugin object cell is empty"
);
object
}
fn install(&self, value: Rc<T>) -> Result<(), RuntimeFailure> {
self.value
.set(value)
.map_err(|_| RuntimeFailure::InvalidResolvedPlan {
detail: "Plugin object was constructed more than once".to_owned(),
})
}
pub fn get(&self) -> Result<Rc<T>, RuntimeFailure> {
self.value
.get()
.cloned()
.ok_or(RuntimeFailure::AdmissionClosed)
}
}
impl<T> Clone for PluginObject<T> {
fn clone(&self) -> Self {
Self {
value: self.value.clone(),
}
}
}
impl<T> fmt::Debug for PluginObject<T> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PluginObject")
.field("constructed", &self.value.get().is_some())
.finish()
}
}
pub struct CompleteObjectLifecycle<T> {
object: PluginObject<T>,
configuration: String,
constructor: Constructor<T>,
stopper: Option<Stopper<T>>,
construction_started: Cell<bool>,
stop_attempted: Cell<bool>,
}
impl<T> CompleteObjectLifecycle<T> {
pub fn new(
object: PluginObject<T>,
configuration: impl Into<String>,
constructor: impl Fn(ConstructionContext) -> ConstructionFuture<T> + 'static,
) -> Self {
Self {
object,
configuration: configuration.into(),
constructor: Rc::new(constructor),
stopper: None,
construction_started: Cell::new(false),
stop_attempted: Cell::new(false),
}
}
pub fn linked(
object: PluginObject<T>,
configuration: impl Into<String>,
) -> Result<Self, RuntimeFailure>
where
T: Any,
{
let mut defaults = Vec::new();
let mut customs = Vec::new();
for linked in inventory::iter::<LinkedPluginConstruction> {
if (linked.plugin_type)() == TypeId::of::<T>() {
if linked.custom {
customs.push(linked);
} else {
defaults.push(linked);
}
}
}
let selected = match (customs.as_slice(), defaults.as_slice()) {
([custom], _) => *custom,
([], [default]) => *default,
([], []) => {
return Err(RuntimeFailure::InvalidResolvedPlan {
detail: "Plugin type has no linked constructor".to_owned(),
});
}
(customs, _) if customs.len() > 1 => {
return Err(RuntimeFailure::InvalidResolvedPlan {
detail: "Plugin type has multiple custom constructors".to_owned(),
});
}
_ => {
return Err(RuntimeFailure::InvalidResolvedPlan {
detail: "Plugin type has multiple default constructors".to_owned(),
});
}
};
let constructor = selected.construct;
let lifecycle = Self::new(object, configuration, move |context| {
let erased = constructor(context);
Box::pin(async move {
erased
.await?
.downcast::<T>()
.map_err(|_| RuntimeFailure::InvalidResolvedPlan {
detail: "linked Plugin constructor returned the wrong type".to_owned(),
})
})
});
Ok(if let Some(stop) = selected.stop {
lifecycle.with_stop(move |object, context| {
let object: Rc<dyn Any> = object;
stop(object, context)
})
} else {
lifecycle
})
}
#[must_use]
pub fn with_stop(
mut self,
stopper: impl Fn(Rc<T>, LifecycleContext) -> PluginFuture + 'static,
) -> Self {
self.stopper = Some(Rc::new(stopper));
self
}
}
impl<T> fmt::Debug for CompleteObjectLifecycle<T> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CompleteObjectLifecycle")
.field("object", &self.object)
.field("has_stop", &self.stopper.is_some())
.finish_non_exhaustive()
}
}
impl<T: 'static> PluginLifecycle for CompleteObjectLifecycle<T> {
fn construct(&self, context: ActivateContext) -> PluginFuture {
if self.construction_started.replace(true) {
return Box::pin(futures::future::ready(Err(
RuntimeFailure::InvalidResolvedPlan {
detail: "Plugin construction was attempted more than once".to_owned(),
},
)));
}
let object = self.object.clone();
let construction = (self.constructor)(ConstructionContext::new(
self.configuration.clone(),
&context,
));
let cancellation = context.cancellation();
Box::pin(async move {
if cancellation.is_cancelled() {
return Err(RuntimeFailure::AdmissionClosed);
}
let value = construction.await?;
object.install(value)?;
if cancellation.is_cancelled() {
return Err(RuntimeFailure::AdmissionClosed);
}
Ok(())
})
}
fn deactivate(&self, context: DeactivateContext) -> PluginFuture {
if self.stop_attempted.replace(true) {
return Box::pin(futures::future::ready(Ok(())));
}
let Some(stopper) = self.stopper.clone() else {
return Box::pin(futures::future::ready(Ok(())));
};
let Ok(object) = self.object.get() else {
return Box::pin(futures::future::ready(Ok(())));
};
stopper(object, LifecycleContext::stopping(&context))
}
}