use std::collections::BTreeMap;
use syncbat::{AdmissionGuard, Handler, OperationDescriptor};
use crate::descriptor::{GuardDescriptor, HookDescriptor, HookPhase, JobDescriptor};
use batpak::event::EventKind;
use crate::error::HostError;
use crate::event_payload_binding::EventPayloadBinding;
use crate::manifest::{HostModuleManifest, SealedParts};
use crate::schema::SchemaDescriptor;
use crate::subscription::SubscriptionDescriptor;
type BoxedHandler = Box<dyn Handler + 'static>;
type BoxedGuard = Box<dyn AdmissionGuard + 'static>;
pub(crate) type BoxedHook = Box<dyn LifecycleHook + 'static>;
pub(crate) type BoxedJob = Box<dyn JobBody + 'static>;
pub trait LifecycleHook {
fn run(&self) -> Result<(), String>;
}
impl<F> LifecycleHook for F
where
F: Fn() -> Result<(), String>,
{
fn run(&self) -> Result<(), String> {
self()
}
}
pub trait JobBody: Send + Sync {
fn make(&self) -> Box<dyn FnOnce() + Send + 'static>;
}
impl<F> JobBody for F
where
F: Fn() -> Box<dyn FnOnce() + Send + 'static> + Send + Sync,
{
fn make(&self) -> Box<dyn FnOnce() + Send + 'static> {
self()
}
}
pub struct HostModule {
manifest: HostModuleManifest,
handlers: BTreeMap<String, BoxedHandler>,
guard: Option<BoxedGuard>,
hooks: Vec<(HookDescriptor, BoxedHook)>,
jobs: BTreeMap<String, BoxedJob>,
}
pub(crate) struct HostModuleParts {
pub(crate) manifest: HostModuleManifest,
pub(crate) handlers: BTreeMap<String, BoxedHandler>,
pub(crate) guard: Option<BoxedGuard>,
pub(crate) hooks: Vec<(HookDescriptor, BoxedHook)>,
pub(crate) jobs: BTreeMap<String, BoxedJob>,
}
impl HostModule {
#[must_use]
pub fn builder(id: impl Into<String>, version: u32) -> HostModuleBuilder {
HostModuleBuilder::new(id, version)
}
#[must_use]
pub fn manifest(&self) -> &HostModuleManifest {
&self.manifest
}
pub(crate) fn into_parts(self) -> HostModuleParts {
HostModuleParts {
manifest: self.manifest,
handlers: self.handlers,
guard: self.guard,
hooks: self.hooks,
jobs: self.jobs,
}
}
#[cfg(any(test, gauntlet_red_fixture))]
pub(crate) fn tamper_manifest_for_fixture(&mut self) {
self.manifest.corrupt_digest_for_fixture();
}
}
pub struct HostModuleBuilder {
id: String,
version: u32,
operations: BTreeMap<String, (OperationDescriptor, BoxedHandler)>,
receipt_namespaces: Vec<String>,
guard: Option<(GuardDescriptor, BoxedGuard)>,
hooks: Vec<(HookDescriptor, BoxedHook)>,
jobs: BTreeMap<String, (JobDescriptor, BoxedJob)>,
schemas: BTreeMap<(String, u32, crate::schema::SchemaRole), SchemaDescriptor>,
subscriptions: BTreeMap<String, SubscriptionDescriptor>,
event_payload_bindings: BTreeMap<u16, EventPayloadBinding>,
}
impl HostModuleBuilder {
#[must_use]
pub fn new(id: impl Into<String>, version: u32) -> Self {
Self {
id: id.into(),
version,
operations: BTreeMap::new(),
receipt_namespaces: Vec::new(),
guard: None,
hooks: Vec::new(),
jobs: BTreeMap::new(),
schemas: BTreeMap::new(),
subscriptions: BTreeMap::new(),
event_payload_bindings: BTreeMap::new(),
}
}
pub fn operation<H>(
mut self,
descriptor: OperationDescriptor,
handler: H,
) -> Result<Self, HostError>
where
H: Handler + 'static,
{
descriptor
.validate()
.map_err(|error| HostError::coherence(&self.id, error.to_string()))?;
let name = descriptor.name().to_owned();
if self.operations.contains_key(&name) {
return Err(HostError::coherence(
&self.id,
format!("operation {name:?} is declared twice"),
));
}
self.operations
.insert(name, (descriptor, Box::new(handler)));
Ok(self)
}
pub fn guard<G>(mut self, descriptor: GuardDescriptor, guard: G) -> Result<Self, HostError>
where
G: AdmissionGuard + 'static,
{
if self.guard.is_some() {
return Err(HostError::coherence(
&self.id,
"a module mounts at most one guard",
));
}
self.guard = Some((descriptor, Box::new(guard)));
Ok(self)
}
pub fn receipt_namespace(mut self, namespace: impl Into<String>) -> Result<Self, HostError> {
let namespace = namespace.into();
if self.receipt_namespaces.contains(&namespace) {
return Err(HostError::coherence(
&self.id,
format!("receipt namespace {namespace:?} is declared twice"),
));
}
self.receipt_namespaces.push(namespace);
Ok(self)
}
pub fn schema(mut self, descriptor: SchemaDescriptor) -> Result<Self, HostError> {
let (id, version, role) = descriptor.identity_key();
let key = (id.to_owned(), version, role);
if self.schemas.contains_key(&key) {
return Err(HostError::SchemaInvalid {
schema: id.to_owned(),
detail: format!(
"schema {id:?} {version} ({role}) is declared twice in module {:?}",
self.id
),
});
}
self.schemas.insert(key, descriptor);
Ok(self)
}
pub fn subscription(mut self, descriptor: SubscriptionDescriptor) -> Result<Self, HostError> {
let id = descriptor.id().as_str().to_owned();
if self.subscriptions.contains_key(&id) {
return Err(HostError::SubscriptionDuplicateWithinModule {
module: self.id.clone(),
id,
});
}
self.subscriptions.insert(id, descriptor);
Ok(self)
}
pub fn bind_event_payload(
mut self,
kind: EventKind,
payload_schema_ref: impl Into<String>,
) -> Result<Self, HostError> {
let binding = EventPayloadBinding::new(kind, payload_schema_ref)?;
let kind_raw = binding.kind_raw();
if self.event_payload_bindings.contains_key(&kind_raw) {
return Err(HostError::EventPayloadBindingDuplicateWithinModule {
module: self.id.clone(),
kind: kind_raw,
});
}
self.event_payload_bindings.insert(kind_raw, binding);
Ok(self)
}
pub fn hook<H>(mut self, phase: HookPhase, name: impl Into<String>, order: u32, hook: H) -> Self
where
H: LifecycleHook + 'static,
{
self.hooks
.push((HookDescriptor::new(phase, name, order), Box::new(hook)));
self
}
pub fn job<J>(mut self, kind: impl Into<String>, body: J) -> Result<Self, HostError>
where
J: JobBody + 'static,
{
let kind = kind.into();
if self.jobs.contains_key(&kind) {
return Err(HostError::coherence(
&self.id,
format!("supervised-job kind {kind:?} is declared twice"),
));
}
self.jobs
.insert(kind.clone(), (JobDescriptor::new(kind), Box::new(body)));
Ok(self)
}
pub fn build(self) -> Result<HostModule, HostError> {
validate_module_id(&self.id)?;
if self.operations.is_empty()
&& self.hooks.is_empty()
&& self.jobs.is_empty()
&& self.guard.is_none()
&& self.schemas.is_empty()
&& self.subscriptions.is_empty()
&& self.event_payload_bindings.is_empty()
{
return Err(HostError::coherence(
&self.id,
"module declares no operations, hooks, jobs, guard, schemas, subscriptions, or event payload bindings",
));
}
let mut handlers = BTreeMap::new();
let mut operation_descriptors = Vec::with_capacity(self.operations.len());
for (name, (descriptor, handler)) in self.operations {
operation_descriptors.push(descriptor);
handlers.insert(name, handler);
}
let mut job_descriptors = Vec::with_capacity(self.jobs.len());
let mut jobs = BTreeMap::new();
for (kind, (descriptor, body)) in self.jobs {
job_descriptors.push(descriptor);
jobs.insert(kind, body);
}
let (guard_descriptor, guard_impl) = match self.guard {
Some((descriptor, guard)) => (Some(descriptor), Some(guard)),
None => (None, None),
};
let mut hooks = self.hooks;
hooks.sort_by(|(a, _), (b, _)| a.order_key().cmp(&b.order_key()));
reject_hook_order_collisions(&self.id, &hooks)?;
let hook_descriptors: Vec<HookDescriptor> = hooks
.iter()
.map(|(descriptor, _)| descriptor.clone())
.collect();
let mut receipt_namespaces = self.receipt_namespaces;
receipt_namespaces.sort();
let schemas: Vec<SchemaDescriptor> = self.schemas.into_values().collect();
let subscriptions: Vec<SubscriptionDescriptor> = self.subscriptions.into_values().collect();
let event_payload_bindings: Vec<EventPayloadBinding> =
self.event_payload_bindings.into_values().collect();
let manifest = HostModuleManifest::seal(SealedParts {
id: self.id,
version: self.version,
operations: operation_descriptors,
receipt_namespaces,
guard: guard_descriptor,
hooks: hook_descriptors,
jobs: job_descriptors,
schemas,
subscriptions,
event_payload_bindings,
})?;
Ok(HostModule {
manifest,
handlers,
guard: guard_impl,
hooks,
jobs,
})
}
}
fn reject_hook_order_collisions(
module: &str,
hooks: &[(HookDescriptor, BoxedHook)],
) -> Result<(), HostError> {
for window in hooks.windows(2) {
let [(a, _), (b, _)] = window else { continue };
if a.phase == b.phase && a.order == b.order {
return Err(HostError::hook_order_collision(module, a.phase, a.order));
}
}
Ok(())
}
fn validate_module_id(id: &str) -> Result<(), HostError> {
let reject = |detail: &str| {
Err(HostError::coherence(
id,
format!("invalid module id: {detail}"),
))
};
if id.is_empty() {
return reject("empty");
}
if id.len() > 128 {
return reject("longer than 128 bytes");
}
if id.starts_with('.') || id.ends_with('.') {
return reject("leading or trailing '.'");
}
if id.contains("..") {
return reject("doubled '.'");
}
if !id
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'-'))
{
return reject("characters outside [a-z0-9._-]");
}
Ok(())
}