use globset::{Glob, GlobSet, GlobSetBuilder};
use serde::de::value::StrDeserializer;
use serde::de::{DeserializeOwned, Deserializer};
use serde::Deserialize;
use thiserror::Error;
pub const SUPPORTED_VERSION: u32 = 1;
const KNOWN_KEYS: &[&str] = &[
"version",
"schema_epoch",
"canonical",
"authoring",
"contracts",
"name",
"source",
"emit",
"mappings",
"glob",
"require",
"gates",
"suppression_comments",
"protected_paths",
"read_only_paths",
"retrieval_paths",
"retrieval_tool",
"db",
"direction",
"provider",
"rls_tests",
"features",
"git_hooks",
"floor",
"commands",
"run",
"scope",
"inputs",
"install",
"on_stop",
"on_new_read_only",
"reconcile_ignored",
"covers_ignored_of",
];
#[derive(Debug, Error)]
pub enum ManifestError {
#[error("manifest is not valid TOML or violates the schema: {message}")]
Invalid { message: String },
#[error("manifest version {found} is unsupported (this binary supports {supported})")]
UnsupportedVersion { found: u32, supported: u32 },
#[error(
"mapping references undeclared contract '{reference}'; declared contracts: {candidates}"
)]
UnknownContract {
reference: String,
candidates: String,
},
#[error("glob '{glob}' is invalid: {message}")]
BadGlob { glob: String, message: String },
#[error(
"schema_epoch must be a positive integer (a human increments it on \
epoch-sensitive change, R9); found {found}"
)]
NonPositiveEpoch { found: u32 },
#[error(
"[[floor.commands]] declares duplicate name '{name}'; every floor \
command needs a unique name (--skip and covers_ignored_of both \
address commands by name)"
)]
DuplicateFloorCommand { name: String },
#[error(
"floor command '{name}' declares an empty `run` array; a command with \
nothing to run cannot produce a verdict (remove the entry, or give it \
an argv: run = [\"cargo\", \"fmt\", \"--check\"])"
)]
EmptyFloorRun { name: String },
#[error(
"floor command '{name}' declares covers_ignored_of = '{reference}', \
which is not a declared command; declared commands: {candidates}"
)]
UnknownFloorCoverage {
name: String,
reference: String,
candidates: String,
},
#[error(
"floor command '{name}' declares covers_ignored_of = '{name}' — a \
command cannot cover its own ignored tests; the accounting would \
balance while executing nothing new"
)]
SelfFloorCoverage { name: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(transparent)]
pub struct ContractName(String);
impl ContractName {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Contract {
pub name: ContractName,
pub source: String,
pub emit: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Mapping {
pub glob: String,
pub contracts: Vec<ContractName>,
pub require: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Gates {
pub suppression_comments: Option<String>,
#[serde(default)]
pub protected_paths: Vec<String>,
#[serde(default)]
pub read_only_paths: Vec<String>,
#[serde(default)]
pub retrieval_paths: Vec<String>,
pub retrieval_tool: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Db {
pub direction: DbDirection,
pub provider: Option<String>,
pub rls_tests: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DbDirection {
Contract,
Database,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Features {
#[serde(default = "default_enabled")]
pub git_hooks: bool,
}
impl Default for Features {
fn default() -> Self {
Self {
git_hooks: default_enabled(),
}
}
}
fn default_enabled() -> bool {
true
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Floor {
#[serde(default)]
pub commands: Vec<FloorCommand>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FloorScope {
PerFile,
PerCrate,
WholeRepo,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FloorInputs {
#[default]
Repo,
Toolchain,
Network,
Machine,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DisplayOnly<T> {
value: T,
degraded_from: Option<String>,
}
impl<T> DisplayOnly<T> {
#[must_use]
pub fn value(&self) -> &T {
&self.value
}
#[must_use]
pub fn degraded_from(&self) -> Option<&str> {
self.degraded_from.as_deref()
}
}
impl<T: PartialEq> PartialEq<T> for DisplayOnly<T> {
fn eq(&self, other: &T) -> bool {
self.value == *other
}
}
impl<'de, T: DeserializeOwned + Default> Deserialize<'de> for DisplayOnly<T> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
let recognized = T::deserialize(StrDeserializer::<D::Error>::new(&raw));
Ok(match recognized {
Ok(value) => Self {
value,
degraded_from: None,
},
Err(_) => Self {
value: T::default(),
degraded_from: Some(raw),
},
})
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FloorCommand {
pub name: String,
pub run: Vec<String>,
pub scope: FloorScope,
pub inputs: DisplayOnly<FloorInputs>,
pub install: Option<String>,
#[serde(default)]
pub on_stop: bool,
#[serde(default)]
pub on_new_read_only: bool,
#[serde(default)]
pub reconcile_ignored: bool,
pub covers_ignored_of: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawManifest {
version: u32,
schema_epoch: Option<u32>,
canonical: String,
authoring: String,
#[serde(default)]
contracts: Vec<Contract>,
#[serde(default)]
mappings: Vec<Mapping>,
gates: Gates,
db: Option<Db>,
#[serde(default)]
features: Features,
floor: Option<Floor>,
}
pub struct Manifest {
pub version: u32,
pub schema_epoch: u32,
pub canonical: String,
pub authoring: String,
pub contracts: Vec<Contract>,
pub mappings: Vec<Mapping>,
pub gates: Gates,
pub db: Option<Db>,
pub features: Features,
pub floor: Option<Floor>,
mapping_globs: GlobSet,
protected_globs: GlobSet,
read_only_globs: GlobSet,
retrieval_globs: GlobSet,
}
impl std::fmt::Debug for Manifest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Manifest")
.field("version", &self.version)
.field("schema_epoch", &self.schema_epoch)
.field("canonical", &self.canonical)
.field("authoring", &self.authoring)
.field("contracts", &self.contracts)
.field("mappings", &self.mappings)
.field("gates", &self.gates)
.field("db", &self.db)
.field("features", &self.features)
.field("floor", &self.floor)
.finish_non_exhaustive()
}
}
impl Manifest {
pub fn parse(text: &str) -> Result<Self, ManifestError> {
let raw: RawManifest = toml::from_str(text).map_err(|e| enrich_unknown_key(&e))?;
if raw.version != SUPPORTED_VERSION {
return Err(ManifestError::UnsupportedVersion {
found: raw.version,
supported: SUPPORTED_VERSION,
});
}
if let Some(0) = raw.schema_epoch {
return Err(ManifestError::NonPositiveEpoch { found: 0 });
}
resolve_contract_references(&raw)?;
if let Some(floor) = raw.floor.as_ref() {
validate_floor(floor)?;
}
let mapping_globs = build_globset(raw.mappings.iter().map(|m| m.glob.as_str()))?;
let protected_globs = build_globset(raw.gates.protected_paths.iter().map(String::as_str))?;
let read_only_globs = build_globset(raw.gates.read_only_paths.iter().map(String::as_str))?;
let retrieval_globs = build_globset(raw.gates.retrieval_paths.iter().map(String::as_str))?;
Ok(Self {
version: raw.version,
schema_epoch: raw.schema_epoch.unwrap_or(1),
canonical: raw.canonical,
authoring: raw.authoring,
contracts: raw.contracts,
mappings: raw.mappings,
gates: raw.gates,
db: raw.db,
features: raw.features,
floor: raw.floor,
mapping_globs,
protected_globs,
read_only_globs,
retrieval_globs,
})
}
#[must_use]
pub fn mapping_for(&self, path: &str) -> Option<&Mapping> {
self.mapping_globs
.matches(path)
.first()
.map(|&index| &self.mappings[index])
}
#[must_use]
pub fn is_protected(&self, path: &str) -> bool {
self.protected_globs.is_match(path)
}
#[must_use]
pub fn is_read_only(&self, path: &str) -> bool {
self.read_only_globs.is_match(path)
}
#[must_use]
pub fn is_retrieval_gated(&self, path: &str) -> bool {
self.retrieval_globs.is_match(path)
}
#[must_use]
pub fn retrieval_tool(&self) -> Option<&str> {
self.gates.retrieval_tool.as_deref()
}
#[must_use]
pub fn git_hooks_enabled(&self) -> bool {
self.features.git_hooks
}
}
fn resolve_contract_references(raw: &RawManifest) -> Result<(), ManifestError> {
let declared: Vec<&str> = raw.contracts.iter().map(|c| c.name.as_str()).collect();
for mapping in &raw.mappings {
for reference in &mapping.contracts {
if !declared.contains(&reference.as_str()) {
return Err(ManifestError::UnknownContract {
reference: reference.as_str().to_owned(),
candidates: declared.join(", "),
});
}
}
}
Ok(())
}
fn validate_floor(floor: &Floor) -> Result<(), ManifestError> {
let mut seen: Vec<&str> = Vec::with_capacity(floor.commands.len());
for command in &floor.commands {
if seen.contains(&command.name.as_str()) {
return Err(ManifestError::DuplicateFloorCommand {
name: command.name.clone(),
});
}
seen.push(&command.name);
if command.run.is_empty() {
return Err(ManifestError::EmptyFloorRun {
name: command.name.clone(),
});
}
}
for command in &floor.commands {
let Some(reference) = command.covers_ignored_of.as_deref() else {
continue;
};
if reference == command.name {
return Err(ManifestError::SelfFloorCoverage {
name: command.name.clone(),
});
}
if !seen.contains(&reference) {
return Err(ManifestError::UnknownFloorCoverage {
name: command.name.clone(),
reference: reference.to_owned(),
candidates: seen.join(", "),
});
}
}
Ok(())
}
fn build_globset<'a>(globs: impl Iterator<Item = &'a str>) -> Result<GlobSet, ManifestError> {
let mut builder = GlobSetBuilder::new();
for glob in globs {
let compiled = Glob::new(glob).map_err(|error| ManifestError::BadGlob {
glob: glob.to_owned(),
message: error.to_string(),
})?;
builder.add(compiled);
}
builder.build().map_err(|error| ManifestError::BadGlob {
glob: "<combined>".to_owned(),
message: error.to_string(),
})
}
fn enrich_unknown_key(error: &toml::de::Error) -> ManifestError {
let message = error.to_string();
let Some(unknown) = extract_unknown_field(&message) else {
return ManifestError::Invalid { message };
};
let candidates = nearest_keys(&unknown);
if candidates.is_empty() {
return ManifestError::Invalid { message };
}
ManifestError::Invalid {
message: format!("{message}; did you mean: {}?", candidates.join(", ")),
}
}
fn extract_unknown_field(message: &str) -> Option<String> {
let marker = "unknown field `";
let start = message.find(marker)? + marker.len();
let rest = &message[start..];
let end = rest.find('`')?;
Some(rest[..end].to_owned())
}
fn nearest_keys(unknown: &str) -> Vec<&'static str> {
let mut scored: Vec<(usize, &'static str)> = KNOWN_KEYS
.iter()
.map(|&key| (levenshtein(unknown, key), key))
.filter(|&(distance, _)| distance <= 3)
.collect();
scored.sort_unstable();
scored.into_iter().take(3).map(|(_, key)| key).collect()
}
pub(crate) fn levenshtein(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let mut previous: Vec<usize> = (0..=b_chars.len()).collect();
let mut current = vec![0usize; b_chars.len() + 1];
for (i, &a_char) in a_chars.iter().enumerate() {
current[0] = i + 1;
for (j, &b_char) in b_chars.iter().enumerate() {
let substitution = usize::from(a_char != b_char);
current[j + 1] = (previous[j] + substitution)
.min(previous[j + 1] + 1)
.min(current[j] + 1);
}
std::mem::swap(&mut previous, &mut current);
}
previous[b_chars.len()]
}