use std::collections::BTreeMap;
use sealmap_model::{Codebase, SourcePath, SymbolId};
use super::check::{Resolution, resolve};
use super::lock::{GENERATOR, Lock, SymbolSeal, TopicSeal, is_date};
use super::topic::{citations, topic_hash, topic_id, with_pointer};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signature {
pub reviewer: String,
pub model: String,
pub date: String,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SignError {
#[error("{0}: no front matter with an `id:`")]
NoTopicId(SourcePath),
#[error("{0}: topic id `{1}` is empty or contains control characters")]
BadTopicId(SourcePath, String),
#[error("topic id `{id}` is already sealed for {other}")]
IdTaken {
id: String,
other: SourcePath,
},
#[error("line {line}: `{text}` is not a canonical sym: id")]
Malformed {
line: u32,
text: String,
},
#[error("line {line}: `{id}` cannot be sealed: {why}")]
Unsealable {
line: u32,
id: SymbolId,
why: String,
},
#[error("{0}")]
BadSignature(String),
#[error("{0}")]
Family(#[from] crate::route::RouteError),
}
pub fn sign(
lock: &mut Lock,
file: &SourcePath,
text: &str,
codebase: &Codebase,
lock_name: &str,
who: &Signature,
) -> Result<String, SignError> {
if who.reviewer.trim().is_empty() || who.model.trim().is_empty() {
return Err(SignError::BadSignature("reviewer and model must not be empty".into()));
}
if !is_date(&who.date) {
return Err(SignError::BadSignature(format!("date `{}` is not YYYY-MM-DD", who.date)));
}
if crate::route::Route::parse(&who.reviewer)?.rungs.len() != 1 {
return Err(SignError::BadSignature(format!(
"reviewer `{}` must be exactly one `host:model` (the model that gave the verdict), not a route",
who.reviewer.trim()
)));
}
crate::route::cross_family(&who.model, &who.reviewer)?;
let id = topic_id(text).ok_or_else(|| SignError::NoTopicId(file.clone()))?;
if !super::lock::is_topic_id(&id) {
return Err(SignError::BadTopicId(file.clone(), id));
}
if let Some(other) = lock.topics.get(&id).filter(|t| &t.file != file) {
return Err(SignError::IdTaken { id, other: other.file.clone() });
}
let mut symbols = BTreeMap::new();
for c in citations(text) {
let sym = c.id.map_err(|_| SignError::Malformed { line: c.line, text: c.text.clone() })?;
let unsealable = |why: String| SignError::Unsealable { line: c.line, id: sym.clone(), why };
match resolve(codebase, &sym, None) {
Resolution::Found { sig, body, .. } if !sig.is_unset() && !body.is_unset() => {
symbols.insert(sym, SymbolSeal { sig, body });
}
Resolution::Found { .. } => return Err(unsealable("it has no fingerprints".into())),
Resolution::Unparsable { file } => return Err(unsealable(format!("{file} does not parse"))),
Resolution::Absent { .. } => return Err(unsealable("it is not in the model".into())),
}
}
let sealed_text = with_pointer(text, lock_name).ok_or_else(|| SignError::NoTopicId(file.clone()))?;
lock.topics.retain(|_, t| &t.file != file);
lock.generator = GENERATOR.into();
lock.insert(TopicSeal {
id,
file: file.clone(),
topic_hash: topic_hash(&sealed_text),
reviewer: who.reviewer.clone(),
model: who.model.clone(),
date: who.date.clone(),
symbols,
});
Ok(sealed_text)
}