use crate::model::EffectiveDefaults;
use serde::Serialize;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MatchDecision {
Match,
Exclude,
NoMatch,
}
impl fmt::Display for MatchDecision {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Match => "MATCH",
Self::Exclude => "BLOCK",
Self::NoMatch => "NO_MATCH",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case", tag = "component", content = "name")]
pub enum UrlComponent {
Path,
Query,
QueryItem(String),
Fragment,
}
impl fmt::Display for UrlComponent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Path => f.write_str("path"),
Self::Query => f.write_str("query"),
Self::QueryItem(name) => write!(f, "query[{name}]"),
Self::Fragment => f.write_str("fragment"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ComponentReason {
Unconstrained,
Exact,
Wildcard,
Substitution,
PatternMismatch,
CaseMismatch,
MissingQueryItem,
UnsupportedPredicate,
}
impl ComponentReason {
#[must_use]
pub fn is_match(self) -> bool {
matches!(
self,
Self::Unconstrained
| Self::Exact
| Self::Wildcard
| Self::Substitution
| Self::UnsupportedPredicate
)
}
}
impl fmt::Display for ComponentReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Unconstrained => "not constrained by this rule",
Self::Exact => "literal match",
Self::Wildcard => "wildcard match",
Self::Substitution => "substitution match",
Self::PatternMismatch => "pattern did not match",
Self::CaseMismatch => "differs only by letter case",
Self::MissingQueryItem => "query item is missing",
Self::UnsupportedPredicate => {
"predicate is not a string, so the whole query dictionary is ignored"
}
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ComponentTrace {
#[serde(flatten)]
pub component: UrlComponent,
pub pattern: Option<String>,
pub input: String,
pub matched: bool,
pub reason: ComponentReason,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RuleTrace {
pub detail_index: usize,
pub rule_index: usize,
pub legacy: bool,
pub exclude: bool,
pub comment: Option<String>,
pub effective: EffectiveDefaults,
pub components: Vec<ComponentTrace>,
pub matched: bool,
}
impl RuleTrace {
#[must_use]
pub fn matched_component_count(&self) -> usize {
self.components
.iter()
.filter(|component| {
component.matched && component.reason != ComponentReason::Unconstrained
})
.count()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DetailTrace {
pub index: usize,
pub app_ids: Vec<String>,
pub applies: bool,
pub rules: Vec<RuleTrace>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case", tag = "stop")]
#[non_exhaustive]
pub enum StopReason {
Matched,
Excluded,
NoAppLinksSection,
NoApplicableDetail,
NoRuleMatched,
HostMismatch {
expected: String,
actual: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MatchTrace {
pub details: Vec<DetailTrace>,
pub selected_detail: Option<usize>,
pub selected_rule: Option<usize>,
pub stop_reason: StopReason,
pub closest_failure: Option<RuleTrace>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MatchResult {
pub decision: MatchDecision,
pub domain: String,
pub app_id: String,
pub url: String,
pub trace: MatchTrace,
pub notes: Vec<String>,
}
impl MatchResult {
#[must_use]
pub fn is_match(&self) -> bool {
self.decision == MatchDecision::Match
}
#[must_use]
pub fn selected_rule(&self) -> Option<&RuleTrace> {
let detail = self.trace.selected_detail?;
let rule = self.trace.selected_rule?;
self.trace
.details
.iter()
.find(|entry| entry.index == detail)?
.rules
.iter()
.find(|candidate| candidate.rule_index == rule)
}
}
impl fmt::Display for MatchResult {
#[allow(clippy::too_many_lines)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}", self.decision)?;
writeln!(f)?;
writeln!(f, "application: {}", self.app_id)?;
writeln!(f, "domain: {}", self.domain)?;
writeln!(f, "url: {}", self.url)?;
if let Some(rule) = self.selected_rule() {
writeln!(f)?;
writeln!(f, "detail: #{}", rule.detail_index)?;
writeln!(
f,
"rule: #{}{}",
rule.rule_index,
if rule.legacy { " (legacy paths)" } else { "" }
)?;
if let Some(comment) = &rule.comment {
writeln!(f, "comment: {comment}")?;
}
writeln!(f)?;
write_components(f, rule)?;
writeln!(f)?;
writeln!(
f,
"effective settings:\n caseSensitive = {}\n percentEncoded = {}",
rule.effective.case_sensitive, rule.effective.percent_encoded
)?;
}
writeln!(f)?;
writeln!(f, "reason:")?;
match &self.trace.stop_reason {
StopReason::Matched => {
writeln!(f, " every component this rule specifies matched")?;
}
StopReason::Excluded => {
writeln!(
f,
" the first matching rule sets exclude: true, so matching stopped"
)?;
}
StopReason::NoAppLinksSection => {
writeln!(f, " the document has no applinks section")?;
}
StopReason::NoApplicableDetail => {
writeln!(f, " no applinks.details entry lists {}", self.app_id)?;
}
StopReason::NoRuleMatched => {
writeln!(
f,
" the entries that apply to {} have no rule matching this URL",
self.app_id
)?;
}
StopReason::HostMismatch { expected, actual } => {
writeln!(
f,
" the URL host is {actual}, but this document was served for {expected}"
)?;
}
}
if let Some(closest) = &self.trace.closest_failure {
writeln!(f)?;
writeln!(
f,
"closest failure:\n detail #{}, rule #{}",
closest.detail_index, closest.rule_index
)?;
write_components(f, closest)?;
}
for note in &self.notes {
writeln!(f, "\nnote: {note}")?;
}
Ok(())
}
}
fn write_components(f: &mut fmt::Formatter<'_>, rule: &RuleTrace) -> fmt::Result {
for component in &rule.components {
if component.reason == ComponentReason::Unconstrained {
continue;
}
let mark = if component.matched { "ok " } else { "FAIL" };
writeln!(f, " [{mark}] {}", component.component)?;
writeln!(f, " url: {}", component.input)?;
if let Some(pattern) = &component.pattern {
writeln!(f, " pattern: {pattern}")?;
}
writeln!(f, " {}", component.reason)?;
}
Ok(())
}