use std::collections::BTreeMap;
use serde::Serialize;
use crate::error::{HostError, SchemaCollision};
use crate::identity::{canonical_digest, Digest, HostFingerprint};
use crate::schema::{SchemaDescriptor, SchemaRole};
const COMPOSITION_DIGEST_DOMAIN: &str = "hostbat.composition.schema.v1";
#[derive(Clone, Debug)]
pub struct CompositionSchema {
descriptor: SchemaDescriptor,
owner_module: String,
}
impl CompositionSchema {
#[must_use]
pub fn descriptor(&self) -> &SchemaDescriptor {
&self.descriptor
}
#[must_use]
pub fn owner_module(&self) -> &str {
&self.owner_module
}
}
#[derive(Clone, Debug)]
pub struct HostCompositionManifest {
schemas: Vec<CompositionSchema>,
digest: HostFingerprint,
}
#[derive(Serialize)]
struct CompositionSchemaView<'a> {
id: &'a str,
version: u32,
role: &'a str,
encoding: Digest,
owner_module: &'a str,
}
#[derive(Serialize)]
struct CompositionView<'a> {
domain: &'a str,
schemas: Vec<CompositionSchemaView<'a>>,
}
type SchemaKey = (String, u32, SchemaRole);
#[derive(Default)]
pub(crate) struct CompositionSchemaBuilder {
by_key: BTreeMap<SchemaKey, CompositionSchema>,
}
impl CompositionSchemaBuilder {
pub(crate) fn add(
&mut self,
module: &str,
descriptor: &SchemaDescriptor,
) -> Result<(), HostError> {
let (id, version, role) = descriptor.identity_key();
let key: SchemaKey = (id.to_owned(), version, role);
match self.by_key.get(&key) {
Some(existing) => {
if existing.descriptor.encoding() != descriptor.encoding() {
return Err(HostError::SchemaCollision(Box::new(SchemaCollision {
schema: id.to_owned(),
version,
role: role.to_string(),
first_module: existing.owner_module.clone(),
first_encoding: existing.descriptor.encoding().to_hex(),
second_module: module.to_owned(),
second_encoding: descriptor.encoding().to_hex(),
})));
}
Ok(())
}
None => {
self.by_key.insert(
key,
CompositionSchema {
descriptor: descriptor.clone(),
owner_module: module.to_owned(),
},
);
Ok(())
}
}
}
pub(crate) fn seal(self) -> Result<HostCompositionManifest, HostError> {
let schemas: Vec<CompositionSchema> = self.by_key.into_values().collect();
let view = CompositionView {
domain: COMPOSITION_DIGEST_DOMAIN,
schemas: schemas
.iter()
.map(|entry| {
let (id, version, role) = entry.descriptor.identity_key();
CompositionSchemaView {
id,
version,
role: role.as_str(),
encoding: *entry.descriptor.encoding().bytes(),
owner_module: &entry.owner_module,
}
})
.collect(),
};
let digest = canonical_digest(&view).map(HostFingerprint)?;
Ok(HostCompositionManifest { schemas, digest })
}
}
impl HostCompositionManifest {
pub fn schemas(&self) -> impl Iterator<Item = &CompositionSchema> {
self.schemas.iter()
}
#[must_use]
pub fn len(&self) -> usize {
self.schemas.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.schemas.is_empty()
}
#[must_use]
pub fn digest(&self) -> HostFingerprint {
self.digest
}
#[must_use]
pub fn find(&self, id: &str, version: u32, role: SchemaRole) -> Option<&CompositionSchema> {
self.schemas.iter().find(|entry| {
let (eid, eversion, erole) = entry.descriptor.identity_key();
eid == id && eversion == version && erole == role
})
}
pub fn verify_digest(&self) -> Result<bool, HostError> {
let view = CompositionView {
domain: COMPOSITION_DIGEST_DOMAIN,
schemas: self
.schemas
.iter()
.map(|entry| {
let (id, version, role) = entry.descriptor.identity_key();
CompositionSchemaView {
id,
version,
role: role.as_str(),
encoding: *entry.descriptor.encoding().bytes(),
owner_module: &entry.owner_module,
}
})
.collect(),
};
let recomputed = canonical_digest(&view).map(HostFingerprint)?;
Ok(recomputed == self.digest)
}
}
#[cfg(test)]
#[path = "composition_schema_tests.rs"]
mod composition_schema_tests;