use std::fmt;
use std::num::NonZeroU64;
use crate::codec::{self, Files};
use crate::convert::v1::export::Export;
use crate::convert::v1::{ConvertError, Finding, convert};
use crate::identity::{Key, LabelPrefix};
use crate::lint::{RuleId, Severity};
use crate::yaml::{self, Value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reported {
pub key: Key,
pub rule: Option<RuleId>,
pub severity: Severity,
pub message: String,
pub term: Option<String>,
pub body_line: Option<usize>,
}
impl Reported {
#[must_use]
pub fn of(finding: &Finding) -> Self {
let plain = |key: Key, rule, severity, message: &str| Self {
key,
rule,
severity,
message: message.to_owned(),
term: None,
body_line: None,
};
match finding {
Finding::StatementAsText { requirement } => plain(
requirement.key(),
Some(RuleId::V2002),
Severity::Info,
"The statement is stored as text, without slots.",
),
Finding::UnresolvedReference { node, reference } => Self {
term: Some(reference.to_string()),
..plain(
node.key(),
Some(RuleId::V2003),
Severity::Warning,
"The reference names no element.",
)
},
Finding::HeadingInBody { node, line } => Self {
body_line: Some(*line),
..plain(
node.key(),
Some(RuleId::V2001),
Severity::Info,
"The body holds a heading; use a Section.",
)
},
Finding::InternalUnderExternal {
component,
ancestor,
} => plain(
component.key(),
None,
Severity::Info,
&format!(
"v1 scopes this Component internally beneath the external Component \
{ancestor}; it now takes that Component's boundary."
),
),
}
}
}
#[derive(Debug)]
pub struct Imported {
pub files: Files,
pub sidecar: Vec<u8>,
pub findings: Vec<Finding>,
pub derived: Vec<LabelPrefix>,
}
#[derive(Debug, thiserror::Error)]
pub enum ImportError {
#[error("{0}")]
Export(String),
#[error(transparent)]
Convert(#[from] ConvertError),
#[error("{0}")]
Counters(CountersError),
#[error("the counter {value} for `{}` is not above the highest label seen, {highest}", prefix.as_str())]
CounterTooLow {
prefix: LabelPrefix,
value: NonZeroU64,
highest: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CountersError {
Yaml(yaml::Error),
NotAMapping,
UnknownPrefix(String),
NotPositive {
prefix: String,
value: String,
},
}
impl fmt::Display for CountersError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Yaml(error) => error.fmt(formatter),
Self::NotAMapping => formatter.write_str("the counters are not a mapping"),
Self::UnknownPrefix(prefix) => {
write!(formatter, "`{prefix}` is not a label prefix of the Format")
}
Self::NotPositive { prefix, value } => {
write!(
formatter,
"the counter for `{prefix}` is `{value}`, not a positive integer"
)
}
}
}
}
pub fn read_counters(bytes: &[u8]) -> Result<Vec<(LabelPrefix, NonZeroU64)>, CountersError> {
let root = yaml::read(bytes).map_err(CountersError::Yaml)?;
let Some(root) = root else {
return Ok(Vec::new());
};
let Value::Mapping(entries) = root.value else {
return Err(CountersError::NotAMapping);
};
entries
.into_iter()
.map(|entry| {
let prefix = LabelPrefix::parse(&entry.key)
.ok_or_else(|| CountersError::UnknownPrefix(entry.key.clone()))?;
let text = match &entry.value.value {
Value::Scalar(scalar) => scalar.text.clone(),
_ => String::new(),
};
let value = text
.parse::<NonZeroU64>()
.ok()
.filter(|_| text.bytes().all(|byte| byte.is_ascii_digit()))
.ok_or(CountersError::NotPositive {
prefix: entry.key,
value: text,
})?;
Ok((prefix, value))
})
.collect()
}
pub fn import(export: &str, counters: Option<&[u8]>) -> Result<Imported, ImportError> {
let export =
Export::from_json(export).map_err(|error| ImportError::Export(error.to_string()))?;
let given = counters
.map(read_counters)
.transpose()
.map_err(ImportError::Counters)?
.unwrap_or_default();
let mut conversion = convert(&export)?;
let seen = conversion.document.counters.clone();
for &(prefix, value) in &given {
let next = seen.next(prefix);
if value < next {
return Err(ImportError::CounterTooLow {
prefix,
value,
highest: next.get() - 1,
});
}
conversion.document.counters.set(prefix, value);
}
let derived = LabelPrefix::ALL
.into_iter()
.filter(|prefix| !given.iter().any(|(named, _)| named == prefix))
.collect();
Ok(Imported {
files: codec::write(&conversion.document),
sidecar: conversion.sidecar.to_json().into_bytes(),
findings: conversion.findings,
derived,
})
}