mod error;
mod openapi;
mod part;
use std::{borrow::Cow, collections::BTreeMap, sync::Arc};
use axum::{handler::Handler, routing::MethodRouter};
use crate::{
Site,
callables::{self},
commands::{self, CommandRegistry},
embed, emitters,
routes::{self},
services::{ServiceRegistry},
signals::{self, SignalRegistry},
tasks::{TaskRegistry},
};
use openapi::DocEngine;
pub use error::BundleError;
pub use openapi::OpenApiConf;
pub use part::{BundlePart, asset_dir, bundle, command, cron, periodic, pgnotify, route, service, signal};
pub use uxar_macros::{
asset_dir, bundle, cron, flow, periodic, pgnotify, route, service, signal, task,
};
pub use {
crate::routes::RouteConf,
crate::apidocs::{ApiMeta, DocViewer},
emitters::CronConf,
emitters::PeriodicConf,
emitters::PgNotifyConf,
signals::SignalConf,
};
pub trait IntoBundle {
fn into_bundle(self) -> Bundle;
}
impl IntoBundle for Bundle {
fn into_bundle(self) -> Bundle {
self
}
}
impl IntoBundle for axum::Router<Site> {
fn into_bundle(self) -> Bundle {
Bundle {
inner_router: self,
..Bundle::new()
}
}
}
pub struct Bundle {
pub(super) inner_router: routes::AxumRouter<Site>,
pub(crate) ops: BTreeMap<uuid::Uuid, crate::callables::Operation>,
pub(crate) name_index: BTreeMap<String, uuid::Uuid>,
pub(super) id: uuid::Uuid,
label: Option<String>,
pub(crate) signals: SignalRegistry,
pub(crate) emitters: emitters::EmitterRegistry,
pub(super) errors: Vec<BundleError>,
pub(crate) tasks: TaskRegistry,
pub(crate) asset_dirs: Vec<embed::Dir>,
pub(crate) services: ServiceRegistry,
pub(crate) commands: CommandRegistry,
pub(crate) doc_engine: DocEngine,
}
impl Bundle {
pub fn new() -> Self {
Self {
id: uuid::Uuid::new_v4(),
label: None,
inner_router: routes::AxumRouter::new(),
ops: BTreeMap::new(),
name_index: BTreeMap::new(),
signals: SignalRegistry::new(),
emitters: emitters::EmitterRegistry::new(),
errors: Vec::new(),
tasks: TaskRegistry::new(),
asset_dirs: Vec::new(),
services: ServiceRegistry::new(),
commands: CommandRegistry::new(),
doc_engine: DocEngine::new(),
}
}
pub fn id(&self) -> uuid::Uuid {
self.id
}
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn validate(&self) -> Result<(), BundleError> {
if !self.errors.is_empty() {
return Err(BundleError::ErrorList(self.errors.clone()));
}
Ok(())
}
pub fn to_router(&self) -> routes::AxumRouter<Site> {
self.inner_router.clone()
}
pub fn iter_operations(&self) -> impl Iterator<Item = &crate::callables::Operation> {
self.ops.values()
}
pub fn with_tags(mut self, tags: impl IntoIterator<Item = impl Into<Cow<'static, str>>>) -> Self {
let tags: Vec<Cow<'static, str>> = tags.into_iter().map(|t| t.into()).collect();
for op in self.ops.values_mut() {
op.tags.extend(tags.iter().cloned());
}
self
}
pub fn merge<B: IntoBundle>(mut self, other: B) -> Self {
let other = other.into_bundle();
let router = match self.absorb(other) {
Ok(r) => r,
Err(e) => {
self.errors.push(e);
return self;
}
};
self.inner_router = self.inner_router.merge(router);
self
}
pub fn with_prefix(mut self, path: &str) -> Self {
debug_assert!(path.starts_with('/'), "prefix must start with '/'");
debug_assert!(!path.ends_with('/'), "prefix must not end with '/'");
for op in self.ops.values_mut() {
op.nest(path);
}
self.inner_router = routes::AxumRouter::new().nest(path, self.inner_router);
self
}
pub fn layer<L>(mut self, layer: L) -> Self
where
L: tower::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
L::Service: tower::Service<axum::http::Request<axum::body::Body>>
+ Clone
+ Send
+ Sync
+ 'static,
<L::Service as tower::Service<axum::http::Request<axum::body::Body>>>::Response:
axum::response::IntoResponse + 'static,
<L::Service as tower::Service<axum::http::Request<axum::body::Body>>>::Error:
Into<std::convert::Infallible> + 'static,
<L::Service as tower::Service<axum::http::Request<axum::body::Body>>>::Future:
Send + 'static,
{
self.inner_router = self.inner_router.layer(layer);
self
}
pub(crate) fn with_router_unchecked(mut self, router: routes::AxumRouter<Site>) -> Self {
self.inner_router = router;
self
}
pub fn reverse(&self, name: &str, args: &[(&str, &str)]) -> Option<String> {
let id = self.name_index.get(name)?;
let op = self.ops.get(id)?;
let mut path = op.path.to_string();
for (k, v) in args {
let placeholder = format!("{{{k}}}");
if path.contains(&placeholder) {
path = path.replace(&placeholder, v);
}
}
debug_assert!(
!path.contains('{'),
"reverse('{}') called with missing args; remaining: {path}",
name
);
Some(path)
}
fn absorb(&mut self, other: Bundle) -> Result<axum::Router<Site>, BundleError> {
self.ops.extend(other.ops);
self.name_index.extend(other.name_index);
self.signals.merge(other.signals);
self.asset_dirs.extend(other.asset_dirs);
if let Err(e) = self.emitters.merge(other.emitters) {
return Err(BundleError::Emitter(Arc::new(e)));
}
if let Err(e) = self.services.merge(other.services) {
return Err(BundleError::Service(Arc::new(e)));
}
if let Err(e) = self.tasks.merge(other.tasks) {
return Err(BundleError::Task(Arc::new(e)));
}
if let Err(e) = self.commands.merge(other.commands) {
return Err(BundleError::Command(Arc::new(e)));
}
self.doc_engine.merge(other.doc_engine);
Ok(other.inner_router)
}
}
impl Default for Bundle {
fn default() -> Self {
Self::new()
}
}