archunit/common/logging/
event.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use crate::common::LogLevel;

/// Stable vocabulary for records emitted during architecture checks.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum LogEventKind {
    /// A terminal check began.
    StartCheck,
    /// A terminal check completed with a verdict.
    EndCheck,
    /// A check reported detailed execution progress.
    Progress,
    /// A check produced one architecture violation.
    Violation,
    /// A metric value was calculated.
    Metric,
    /// A caller emitted an ordinary debug message.
    Debug,
    /// A caller emitted an ordinary informational message.
    Info,
    /// A caller emitted an ordinary warning.
    Warn,
    /// A caller emitted an error.
    Error,
}

impl LogEventKind {
    /// Returns the stable lowercase event spelling.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::StartCheck => "start check",
            Self::EndCheck => "end check",
            Self::Progress => "log progress",
            Self::Violation => "log violation",
            Self::Metric => "log metric",
            Self::Debug => "debug",
            Self::Info => "info",
            Self::Warn => "warn",
            Self::Error => "error",
        }
    }
}

/// One immutable, single-line logging record before destination-specific timestamping.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogRecord {
    level: LogLevel,
    event: LogEventKind,
    message: String,
}

impl LogRecord {
    pub(super) fn new(level: LogLevel, event: LogEventKind, message: impl AsRef<str>) -> Self {
        Self {
            level,
            event,
            message: sanitize_message(message.as_ref()),
        }
    }

    /// Returns this record's severity.
    #[must_use]
    pub const fn level(&self) -> LogLevel {
        self.level
    }

    /// Returns this record's stable event family.
    #[must_use]
    pub const fn event(&self) -> LogEventKind {
        self.event
    }

    /// Returns the sanitized single-line event details.
    #[must_use]
    pub fn message(&self) -> &str {
        &self.message
    }

    /// Renders the deterministic destination-neutral record.
    #[must_use]
    pub fn render(&self) -> String {
        format!(
            "[{}] {}: {}",
            self.level,
            self.event.as_str(),
            render_message(&self.message)
        )
    }
}

fn sanitize_message(message: &str) -> String {
    message.replace('\r', "\\r").replace('\n', "\\n")
}

fn render_message(message: &str) -> String {
    let mut safe = String::new();
    for character in message.chars() {
        if character.is_control() {
            safe.push_str(&character.escape_default().to_string());
        } else {
            safe.push(character);
        }
    }
    safe
}

#[cfg(test)]
mod tests {
    use super::{LogEventKind, LogRecord};
    use crate::common::LogLevel;

    #[test]
    fn control_characters_cannot_inject_terminal_formatting() {
        let record = LogRecord::new(
            LogLevel::Debug,
            LogEventKind::Progress,
            "src/\x1b[31mfile.rs\t",
        );
        assert!(record.message().contains('\x1b'));
        assert!(!record.render().contains('\x1b'));
        assert!(!record.render().contains('\t'));
        assert!(record.render().contains("file.rs"));
    }

    #[test]
    fn records_use_the_fixed_vocabulary_and_remain_one_line() {
        let record = LogRecord::new(
            LogLevel::Warn,
            LogEventKind::Violation,
            "first line\r\nsecond line",
        );

        assert_eq!(record.level(), LogLevel::Warn);
        assert_eq!(record.event(), LogEventKind::Violation);
        assert_eq!(record.message(), "first line\\r\\nsecond line");
        assert_eq!(
            record.render(),
            "[WARN] log violation: first line\\r\\nsecond line"
        );
    }
}