use serde::Serialize;
use syncbat::OperationDescriptor;
use crate::descriptor::{GuardDescriptor, HookDescriptor, JobDescriptor};
use crate::error::HostError;
use crate::event_payload_binding::EventPayloadBinding;
use crate::identity::{canonical_digest, ModuleDigest};
use crate::schema::{SchemaDescriptor, SchemaManifestView};
use crate::subscription::SubscriptionDescriptor;
const MODULE_DIGEST_DOMAIN: &str = "hostbat.module.v1";
#[derive(Clone, Debug)]
pub struct HostModuleManifest {
id: String,
version: u32,
operations: Vec<OperationDescriptor>,
receipt_namespaces: Vec<String>,
guard: Option<GuardDescriptor>,
hooks: Vec<HookDescriptor>,
jobs: Vec<JobDescriptor>,
schemas: Vec<SchemaDescriptor>,
subscriptions: Vec<SubscriptionDescriptor>,
event_payload_bindings: Vec<EventPayloadBinding>,
digest: ModuleDigest,
}
#[derive(Serialize)]
struct OperationView<'a> {
name: &'a str,
effect: &'a str,
input_schema_ref: &'a str,
output_schema_ref: &'a str,
receipt_kind: &'a str,
title: Option<&'a str>,
}
impl<'a> From<&'a OperationDescriptor> for OperationView<'a> {
fn from(descriptor: &'a OperationDescriptor) -> Self {
Self {
name: descriptor.name(),
effect: descriptor.effect.as_str(),
input_schema_ref: descriptor.input_schema_ref(),
output_schema_ref: descriptor.output_schema_ref(),
receipt_kind: descriptor.receipt_kind(),
title: descriptor.title(),
}
}
}
#[derive(Serialize)]
struct ManifestView<'a> {
domain: &'a str,
id: &'a str,
version: u32,
operations: Vec<OperationView<'a>>,
receipt_namespaces: &'a [String],
guard: Option<&'a GuardDescriptor>,
hooks: &'a [HookDescriptor],
jobs: &'a [JobDescriptor],
schemas: Vec<SchemaManifestView<'a>>,
subscriptions: &'a [SubscriptionDescriptor],
event_payload_bindings: Vec<crate::event_payload_binding::EventPayloadBindingView<'a>>,
}
struct ManifestParts<'a> {
id: &'a str,
version: u32,
operations: &'a [OperationDescriptor],
receipt_namespaces: &'a [String],
guard: Option<&'a GuardDescriptor>,
hooks: &'a [HookDescriptor],
jobs: &'a [JobDescriptor],
schemas: &'a [SchemaDescriptor],
subscriptions: &'a [SubscriptionDescriptor],
event_payload_bindings: &'a [EventPayloadBinding],
}
fn compute_digest(parts: &ManifestParts<'_>) -> Result<ModuleDigest, HostError> {
let view = ManifestView {
domain: MODULE_DIGEST_DOMAIN,
id: parts.id,
version: parts.version,
operations: parts.operations.iter().map(OperationView::from).collect(),
receipt_namespaces: parts.receipt_namespaces,
guard: parts.guard,
hooks: parts.hooks,
jobs: parts.jobs,
schemas: parts
.schemas
.iter()
.map(SchemaDescriptor::manifest_view)
.collect(),
subscriptions: parts.subscriptions,
event_payload_bindings: parts
.event_payload_bindings
.iter()
.map(EventPayloadBinding::manifest_view)
.collect(),
};
canonical_digest(&view).map(ModuleDigest)
}
pub(crate) struct SealedParts {
pub(crate) id: String,
pub(crate) version: u32,
pub(crate) operations: Vec<OperationDescriptor>,
pub(crate) receipt_namespaces: Vec<String>,
pub(crate) guard: Option<GuardDescriptor>,
pub(crate) hooks: Vec<HookDescriptor>,
pub(crate) jobs: Vec<JobDescriptor>,
pub(crate) schemas: Vec<SchemaDescriptor>,
pub(crate) subscriptions: Vec<SubscriptionDescriptor>,
pub(crate) event_payload_bindings: Vec<EventPayloadBinding>,
}
impl HostModuleManifest {
pub(crate) fn seal(parts: SealedParts) -> Result<Self, HostError> {
let SealedParts {
id,
version,
operations,
receipt_namespaces,
guard,
hooks,
jobs,
schemas,
subscriptions,
event_payload_bindings,
} = parts;
let digest = compute_digest(&ManifestParts {
id: &id,
version,
operations: &operations,
receipt_namespaces: &receipt_namespaces,
guard: guard.as_ref(),
hooks: &hooks,
jobs: &jobs,
schemas: &schemas,
subscriptions: &subscriptions,
event_payload_bindings: &event_payload_bindings,
})?;
Ok(Self {
id,
version,
operations,
receipt_namespaces,
guard,
hooks,
jobs,
schemas,
subscriptions,
event_payload_bindings,
digest,
})
}
pub fn verify_hash(&self) -> Result<bool, HostError> {
let recomputed = compute_digest(&ManifestParts {
id: &self.id,
version: self.version,
operations: &self.operations,
receipt_namespaces: &self.receipt_namespaces,
guard: self.guard.as_ref(),
hooks: &self.hooks,
jobs: &self.jobs,
schemas: &self.schemas,
subscriptions: &self.subscriptions,
event_payload_bindings: &self.event_payload_bindings,
})?;
Ok(recomputed == self.digest)
}
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
#[must_use]
pub fn version(&self) -> u32 {
self.version
}
#[must_use]
pub fn digest(&self) -> ModuleDigest {
self.digest
}
pub fn operations(&self) -> impl Iterator<Item = &OperationDescriptor> {
self.operations.iter()
}
pub fn receipt_namespaces(&self) -> impl Iterator<Item = &str> {
self.receipt_namespaces.iter().map(String::as_str)
}
#[must_use]
pub fn guard(&self) -> Option<&GuardDescriptor> {
self.guard.as_ref()
}
pub fn hooks(&self) -> impl Iterator<Item = &HookDescriptor> {
self.hooks.iter()
}
pub fn jobs(&self) -> impl Iterator<Item = &JobDescriptor> {
self.jobs.iter()
}
pub fn schemas(&self) -> impl Iterator<Item = &SchemaDescriptor> {
self.schemas.iter()
}
pub fn subscriptions(&self) -> impl Iterator<Item = &SubscriptionDescriptor> {
self.subscriptions.iter()
}
pub fn event_payload_bindings(&self) -> impl Iterator<Item = &EventPayloadBinding> {
self.event_payload_bindings.iter()
}
#[cfg(any(test, gauntlet_red_fixture))]
pub(crate) fn corrupt_digest_for_fixture(&mut self) {
let mut bytes = self.digest.0;
bytes[0] ^= 0xff;
self.digest = ModuleDigest(bytes);
}
}