use serde::Serialize;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Warning,
Error,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Info => "info",
Self::Warning => "warning",
Self::Error => "error",
})
}
}
macro_rules! diagnostic_codes {
($($variant:ident => ($code:literal, $severity:ident, $title:literal),)*) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[non_exhaustive]
#[serde(into = "&'static str")]
pub enum DiagnosticCode {
$(
#[doc = $title]
$variant,
)*
}
impl DiagnosticCode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
$(Self::$variant => $code,)*
}
}
#[must_use]
pub const fn default_severity(self) -> Severity {
match self {
$(Self::$variant => Severity::$severity,)*
}
}
#[must_use]
pub const fn title(self) -> &'static str {
match self {
$(Self::$variant => $title,)*
}
}
#[must_use]
pub const fn all() -> &'static [DiagnosticCode] {
&[$(Self::$variant,)*]
}
}
impl From<DiagnosticCode> for &'static str {
fn from(code: DiagnosticCode) -> Self {
code.as_str()
}
}
};
}
diagnostic_codes! {
InvalidJson => ("AASA001", Error, "payload is not valid JSON"),
RootNotObject => ("AASA002", Error, "root value is not a JSON object"),
FieldTypeMismatch => ("AASA004", Error, "field has an unexpected JSON type"),
NoRecognizedService => ("AASA100", Warning, "no recognized Associated Domains service section"),
UnknownTopLevelKey => ("AASA101", Info, "unrecognized top-level key"),
DetailMissingAppId => ("AASA110", Error, "details entry has neither appID nor appIDs"),
DetailHasBothAppIdForms => ("AASA111", Warning, "details entry declares both appID and appIDs"),
MixedComponentsAndPaths => ("AASA120", Warning, "details entry mixes modern components with legacy paths"),
LegacyDetailsDictionary => ("AASA121", Warning, "details uses the legacy dictionary form"),
LegacyAppsKeyNonEmpty => ("AASA122", Warning, "legacy applinks.apps array is not empty"),
EmptyAppIdentifier => ("AASA130", Error, "empty application identifier"),
SuspiciousAppIdentifier => ("AASA131", Warning, "application identifier is not in `<TeamID>.<BundleID>` form"),
MalformedSubstitutionName => ("AASA140", Error, "substitution variable name contains $, ( or )"),
RecursiveSubstitutionValue => ("AASA141", Error, "substitution value references another substitution variable"),
UnknownSubstitutionVariable => ("AASA142", Error, "pattern references an undefined substitution variable"),
EmptySubstitutionList => ("AASA143", Warning, "substitution variable has no values and can never match"),
SubstitutionShadowsPredefined => ("AASA144", Warning, "substitution variable shadows a predefined Apple variable"),
UnsupportedQueryPredicate => ("AASA150", Error, "query predicate value is not a string"),
UnterminatedSubstitutionReference => ("AASA151", Error, "pattern contains an unterminated $( reference"),
DuplicateAppIdentifier => ("AASA160", Warning, "application identifier is listed more than once"),
DocumentTooLarge => ("AASA170", Error, "payload exceeds the configured size limit"),
EmptyComponentRule => ("AASA180", Warning, "component rule constrains nothing and matches every URL"),
UnreachableRule => ("AASA190", Warning, "rule is unreachable because an earlier rule always matches"),
DefaultsContainsPatternKeys => ("AASA192", Info, "defaults object carries pattern keys with undocumented behavior"),
NoDetails => ("AASA193", Warning, "applinks declares no details, so no app can open this domain"),
EmptyPatternAlternative => ("AASA194", Warning, "substitution value is empty"),
SignedPayload => ("AASA200", Warning, "the file is CMS-signed; the signature was not verified"),
}
impl fmt::Display for DiagnosticCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Diagnostic {
pub code: DiagnosticCode,
pub severity: Severity,
pub path: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
}
impl Diagnostic {
pub(crate) fn new(
code: DiagnosticCode,
path: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
code,
severity: code.default_severity(),
path: path.into(),
message: message.into(),
help: None,
}
}
pub(crate) fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} [{}] {}: {}",
self.severity, self.code, self.path, self.message
)?;
if let Some(help) = &self.help {
write!(f, "\n help: {help}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ValidationReport {
diagnostics: Vec<Diagnostic>,
}
impl ValidationReport {
pub(crate) fn from_diagnostics(mut diagnostics: Vec<Diagnostic>) -> Self {
diagnostics.sort_by(|a, b| {
b.severity
.cmp(&a.severity)
.then_with(|| a.path.cmp(&b.path))
.then_with(|| a.code.cmp(&b.code))
});
Self { diagnostics }
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
#[must_use]
pub fn errors(&self) -> Vec<&Diagnostic> {
self.iter_severity(Severity::Error)
}
#[must_use]
pub fn warnings(&self) -> Vec<&Diagnostic> {
self.iter_severity(Severity::Warning)
}
#[must_use]
pub fn infos(&self) -> Vec<&Diagnostic> {
self.iter_severity(Severity::Info)
}
fn iter_severity(&self, severity: Severity) -> Vec<&Diagnostic> {
self.diagnostics
.iter()
.filter(|diagnostic| diagnostic.severity == severity)
.collect()
}
#[must_use]
pub fn has_errors(&self) -> bool {
self.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == Severity::Error)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.diagnostics.is_empty()
}
#[must_use]
pub fn contains(&self, code: DiagnosticCode) -> bool {
self.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == code)
}
}
impl fmt::Display for ValidationReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.diagnostics.is_empty() {
return f.write_str("no diagnostics");
}
for (index, diagnostic) in self.diagnostics.iter().enumerate() {
if index > 0 {
f.write_str("\n")?;
}
write!(f, "{diagnostic}")?;
}
Ok(())
}
}