use marque_ism::{IsmAttributes, Span};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::SystemTime;
pub use marque_ism::{DocumentPosition, MarkingType, Zone};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RuleId(&'static str);
impl RuleId {
#[inline]
pub const fn new(id: &'static str) -> Self {
Self(id)
}
#[inline]
pub const fn as_str(&self) -> &'static str {
self.0
}
}
impl std::fmt::Display for RuleId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Severity {
Off,
Warn,
Error,
Fix,
}
impl Severity {
pub fn parse_config(s: &str) -> Option<Self> {
match s {
"off" => Some(Self::Off),
"warn" => Some(Self::Warn),
"error" => Some(Self::Error),
"fix" => Some(Self::Fix),
_ => None,
}
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Off => "off",
Self::Warn => "warn",
Self::Error => "error",
Self::Fix => "fix",
}
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct RuleContext {
pub marking_type: MarkingType,
pub zone: Option<Zone>,
pub position: Option<DocumentPosition>,
pub page_context: Option<std::sync::Arc<marque_ism::PageContext>>,
pub corrections: Option<Arc<HashMap<String, String>>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FixSource {
BuiltinRule,
CorrectionsMap,
MigrationTable,
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct FixProposal {
pub rule: RuleId,
pub source: FixSource,
pub span: Span,
pub original: Box<str>,
pub replacement: Box<str>,
pub confidence: f32,
pub migration_ref: Option<&'static str>,
}
impl FixProposal {
pub fn new(
rule: RuleId,
source: FixSource,
span: Span,
original: impl Into<Box<str>>,
replacement: impl Into<Box<str>>,
confidence: f32,
migration_ref: Option<&'static str>,
) -> Self {
assert!(
(0.0..=1.0).contains(&confidence) && !confidence.is_nan(),
"FixProposal confidence must be in [0.0, 1.0] and not NaN, got {confidence}"
);
Self {
rule,
source,
span,
original: original.into(),
replacement: replacement.into(),
confidence,
migration_ref,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct AppliedFix {
pub proposal: FixProposal,
pub timestamp: SystemTime,
pub classifier_id: Option<Arc<str>>,
pub dry_run: bool,
pub input: Option<Arc<str>>,
}
impl AppliedFix {
#[doc(hidden)]
pub fn __engine_promote(
proposal: FixProposal,
timestamp: SystemTime,
classifier_id: Option<Arc<str>>,
dry_run: bool,
input: Option<Arc<str>>,
) -> Self {
Self {
proposal,
timestamp,
classifier_id,
dry_run,
input,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub rule: RuleId,
pub severity: Severity,
pub span: Span,
pub message: Box<str>,
pub citation: &'static str,
pub fix: Option<FixProposal>,
}
impl Diagnostic {
pub fn new(
rule: RuleId,
severity: Severity,
span: Span,
message: impl Into<Box<str>>,
citation: &'static str,
fix: Option<FixProposal>,
) -> Self {
Self {
rule,
severity,
span,
message: message.into(),
citation,
fix,
}
}
}
pub trait Rule: Send + Sync {
fn id(&self) -> RuleId;
fn name(&self) -> &'static str;
fn default_severity(&self) -> Severity;
fn check(&self, attrs: &IsmAttributes, ctx: &RuleContext) -> Vec<Diagnostic>;
}
pub trait RuleSet: Send + Sync {
fn rules(&self) -> &[Box<dyn Rule>];
fn schema_version(&self) -> &'static str;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rule_id_round_trip() {
let r = RuleId::new("E001");
assert_eq!(r.as_str(), "E001");
assert_eq!(r.to_string(), "E001");
}
#[test]
fn severity_parse_config_accepts_known_values() {
assert_eq!(Severity::parse_config("off"), Some(Severity::Off));
assert_eq!(Severity::parse_config("warn"), Some(Severity::Warn));
assert_eq!(Severity::parse_config("error"), Some(Severity::Error));
assert_eq!(Severity::parse_config("fix"), Some(Severity::Fix));
}
#[test]
fn severity_parse_config_is_case_sensitive() {
assert_eq!(Severity::parse_config("OFF"), None);
assert_eq!(Severity::parse_config("Warn"), None);
}
#[test]
fn severity_parse_config_rejects_unknown_strings() {
assert_eq!(Severity::parse_config("err"), None);
assert_eq!(Severity::parse_config("disable"), None);
assert_eq!(Severity::parse_config(""), None);
}
#[test]
fn severity_display_round_trips() {
for s in [
Severity::Off,
Severity::Warn,
Severity::Error,
Severity::Fix,
] {
assert_eq!(Severity::parse_config(s.as_str()), Some(s));
assert_eq!(s.to_string(), s.as_str());
}
}
#[test]
fn severity_ord_off_is_lowest() {
assert!(Severity::Off < Severity::Warn);
assert!(Severity::Warn < Severity::Error);
assert!(Severity::Error < Severity::Fix);
}
#[test]
fn fix_proposal_new_accepts_boundary_confidence() {
let _zero = FixProposal::new(
RuleId::new("E001"),
FixSource::BuiltinRule,
Span::new(0, 0),
"x",
"y",
0.0,
None,
);
let _one = FixProposal::new(
RuleId::new("E001"),
FixSource::BuiltinRule,
Span::new(0, 0),
"x",
"y",
1.0,
None,
);
}
#[test]
#[should_panic(expected = "FixProposal confidence")]
fn fix_proposal_new_panics_on_negative_confidence() {
let _ = FixProposal::new(
RuleId::new("E001"),
FixSource::BuiltinRule,
Span::new(0, 0),
"x",
"y",
-0.1,
None,
);
}
#[test]
#[should_panic(expected = "FixProposal confidence")]
fn fix_proposal_new_panics_on_above_one_confidence() {
let _ = FixProposal::new(
RuleId::new("E001"),
FixSource::BuiltinRule,
Span::new(0, 0),
"x",
"y",
1.5,
None,
);
}
#[test]
#[should_panic(expected = "FixProposal confidence")]
fn fix_proposal_new_panics_on_nan_confidence() {
let _ = FixProposal::new(
RuleId::new("E001"),
FixSource::BuiltinRule,
Span::new(0, 0),
"x",
"y",
f32::NAN,
None,
);
}
}