uxar 0.1.5

Opinionated Rust web framework built on Axum for Postgres-backed JSON APIs
Documentation
use std::sync::Arc;

use crate::{
    Site,
    callables::{self},
    commands::{self},
    embed, emitters,
    services::{Agent, Service, ServiceBuildContext, ServiceHandler},
    signals::{self, SignalConf},
};

use super::{Bundle, BundleError};

pub(super) enum BundlePartInner {
    Route(axum::routing::MethodRouter<Site>, crate::callables::Operation),
    Emitter(emitters::Emitter),
    Task(crate::tasks::TaskService),
    Signal(signals::Signaller),
    Error(BundleError),
    AssetDir(embed::Dir),
    Command(commands::Command),
    Service(ServiceHandler),
}

/// A single registerable piece of a bundle: a route, emitter, signal, service, etc.
///
/// Constructed by the free functions in this module (`route`, `cron`, `signal`, …)
/// or by the proc-macro equivalents. Call `.patch(PatchOp)` to amend metadata.
pub struct BundlePart {
    pub(super) part: BundlePartInner,
    pub(super) operation: Option<crate::callables::Operation>,
}

impl BundlePart {
    /// Amends the operation metadata for this part (name, description, arg names, etc.).
    pub fn patch(mut self, f: callables::PatchOp) -> Self {
        if let Some(op) = &mut self.operation {
            f.apply(op);
        } else if let BundlePartInner::Route(_, ref mut op) = self.part {
            f.apply(op);
        }
        self
    }
}

// ---------------------------------------------------------------------------
// Bundle injection
// ---------------------------------------------------------------------------

impl Bundle {
    pub(super) fn add_part(mut self, part: BundlePart) -> Self {
        // Non-route parts contribute an operation to the ops store (no name_index entry
        // since reversal is only meaningful for HTTP routes).
        if !matches!(&part.part, BundlePartInner::Route(..)) {
            if let Some(mut op) = part.operation {
                op.bundle_id = Some(self.id);
                self.ops.insert(op.id, op);
            }
        }
        match part.part {
            BundlePartInner::Route(router, mut op) => {
                op.bundle_id = Some(self.id);
                self = self.register_route(router, op);
            }
            BundlePartInner::Emitter(em) => {
                if let Err(e) = self.emitters.register(em) {
                    self.errors.push(BundleError::Emitter(Arc::new(e)));
                }
            }
            BundlePartInner::Signal(sig) => {
                self.signals.register(sig);
            }
            BundlePartInner::Error(e) => {
                self.errors.push(e);
            }
            BundlePartInner::AssetDir(d) => {
                self.asset_dirs.push(d);
            }
            BundlePartInner::Service(entry) => {
                if let Err(e) = self.services.register(entry) {
                    self.errors.push(BundleError::Service(Arc::new(e)));
                }
            }
            BundlePartInner::Task(ts) => {
                if let Err(e) = self.tasks.register(ts) {
                    self.errors.push(BundleError::Task(Arc::new(e)));
                }
            }
            BundlePartInner::Command(cmd) => {
                if let Err(e) = self.commands.register(cmd) {
                    self.errors.push(BundleError::Command(Arc::new(e)));
                }
            }
        }
        self
    }

    /// Registers an HTTP route and its operation metadata.
    pub(super) fn register_route(
        mut self,
        router: axum::routing::MethodRouter<Site>,
        op: crate::callables::Operation,
    ) -> Self {
        self.inner_router = self.inner_router.route(op.path.as_ref(), router);
        let id = op.id;
        let name = op.name.clone();
        self.ops.insert(id, op);
        self.name_index.insert(name, id);
        self
    }
}

// ---------------------------------------------------------------------------
// Free constructor functions
// ---------------------------------------------------------------------------

/// Creates a route part from a handler function and its routing configuration.
pub fn route<H, T, Args>(handler: H, meta: crate::routes::RouteConf) -> BundlePart
where
    H: axum::handler::Handler<T, Site>
        + callables::Specable<Args>
        + Clone
        + Send
        + Sync
        + 'static,
    T: 'static,
    Args: callables::IntoArgSpecs + 'static,
{
    let spec = callables::CallSpec::new(&handler);
    let mut op =
        crate::callables::Operation::from_specs(crate::callables::OperationKind::Route, &spec);
    op.path = meta.path.clone().into();
    op.name = meta.name.clone().into();
    op.methods = meta.methods.clone().into();
    op = op.with_conf(&meta);

    let router = axum::routing::on(meta.methods.into(), handler);
    BundlePart {
        operation: None,
        part: BundlePartInner::Route(router, op),
    }
}

/// Creates a cron-scheduled emitter part.
pub fn cron<O, H, Args>(handler: H, options: emitters::CronConf) -> BundlePart
where
    O: callables::Payloadable,
    Args: callables::FromContext<emitters::EmitterContext>
        + callables::IntoArgSpecs
        + Send
        + 'static,
    H: callables::Specable<Args, Output = callables::Payload<O>> + Send + Sync + 'static,
{
    match emitters::cron(handler, options) {
        Ok(em) => BundlePart {
            operation: Some(em.operation()),
            part: BundlePartInner::Emitter(em),
        },
        Err(e) => BundlePart {
            operation: None,
            part: BundlePartInner::Error(BundleError::Emitter(Arc::new(e))),
        },
    }
}

/// Creates a time-interval emitter part.
pub fn periodic<O, H, Args>(handler: H, options: emitters::PeriodicConf) -> BundlePart
where
    O: callables::Payloadable,
    Args: callables::FromContext<emitters::EmitterContext>
        + callables::IntoArgSpecs
        + Send
        + 'static,
    H: callables::Specable<Args, Output = callables::Payload<O>> + Send + Sync + 'static,
{
    match emitters::periodic(handler, options) {
        Ok(em) => BundlePart {
            operation: Some(em.operation()),
            part: BundlePartInner::Emitter(em),
        },
        Err(e) => BundlePart {
            operation: None,
            part: BundlePartInner::Error(BundleError::Emitter(Arc::new(e))),
        },
    }
}

/// Creates a Postgres NOTIFY listener emitter part.
pub fn pgnotify<O, H, Args>(handler: H, options: emitters::PgNotifyConf) -> BundlePart
where
    O: callables::Payloadable,
    Args: callables::FromContext<emitters::EmitterContext>
        + callables::IntoArgSpecs
        + Send
        + 'static,
    H: callables::Specable<Args, Output = callables::Payload<O>> + Send + Sync + 'static,
{
    match emitters::pgnotify(handler, options) {
        Ok(em) => BundlePart {
            operation: Some(em.operation()),
            part: BundlePartInner::Emitter(em),
        },
        Err(e) => BundlePart {
            operation: None,
            part: BundlePartInner::Error(BundleError::Emitter(Arc::new(e))),
        },
    }
}

/// Creates a signal handler part.
pub fn signal<T, H, Args>(handler: H, options: SignalConf) -> BundlePart
where
    T: callables::Payloadable,
    H: callables::Specable<Args, Output = ()> + Send + Sync + 'static,
    Args: callables::FromContext<signals::SignalContext>
        + callables::IntoArgSpecs
        + callables::HasPayload<T>
        + Send
        + 'static,
{
    let sig = crate::signals::signal::<T, H, Args>(handler, options);
    let op = sig.operation();
    BundlePart {
        operation: Some(op),
        part: BundlePartInner::Signal(sig),
    }
}

/// Creates a background service part.
pub fn service<T, H, Args>(handler: H) -> BundlePart
where
    T: Service,
    H: callables::Specable<Args, Output = Agent<T>> + Send + Sync + 'static,
    Args: callables::FromContext<ServiceBuildContext> + callables::IntoArgSpecs + Send + 'static,
{
    let entry = ServiceHandler::new(handler);
    let op = entry.operation();
    BundlePart {
        part: BundlePartInner::Service(entry),
        operation: Some(op),
    }
}

/// Creates a CLI command part.
pub fn command<T, H, Args>(handler: H, conf: commands::CommandConf) -> BundlePart
where
    T: callables::Payloadable,
    H: callables::Specable<Args, Output = Result<(), commands::CommandError>>
        + Send
        + Sync
        + 'static,
    Args: callables::FromContext<commands::CommandContext>
        + callables::IntoArgSpecs
        + callables::HasPayload<T>
        + Send
        + 'static,
{
    let cmd = commands::command(handler, conf);
    let op = cmd.operation();
    BundlePart {
        part: BundlePartInner::Command(cmd),
        operation: Some(op),
    }
}

/// Creates a static asset directory part.
pub fn asset_dir(dir: embed::Dir) -> BundlePart {
    BundlePart {
        operation: None,
        part: BundlePartInner::AssetDir(dir),
    }
}

/// Builds a [`Bundle`] from an iterable of [`BundlePart`]s.
///
/// This is the primary way to construct a bundle. Parts are registered in
/// iteration order.
pub fn bundle(parts: impl IntoIterator<Item = BundlePart>) -> Bundle {
    parts.into_iter().fold(Bundle::new(), Bundle::add_part)
}