1use crate::{aoc::Verdict, puzzle::Part};
8use colored::Colorize as _;
9use std::io::{self, Write as _};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Event {
14 Data(String),
16 Raw(String),
18 Answer {
20 part: Part,
22 answer: String,
24 verdict: Option<Verdict>,
26 },
27 Warning(String),
29}
30
31pub trait Reporter {
33 fn report(&mut self, event: Event);
35
36 fn data(&mut self, text: &str) {
38 self.report(Event::Data(text.to_owned()));
39 }
40
41 fn raw(&mut self, text: &str) {
43 self.report(Event::Raw(text.to_owned()));
44 }
45
46 fn answer(&mut self, part: Part, answer: &str, verdict: Option<Verdict>) {
48 self.report(Event::Answer {
49 part,
50 answer: answer.to_owned(),
51 verdict,
52 });
53 }
54
55 fn warn(&mut self, text: &str) {
57 self.report(Event::Warning(text.to_owned()));
58 }
59}
60
61#[derive(Debug, Default, Clone, Copy)]
63pub struct TermReporter;
64
65impl Reporter for TermReporter {
66 fn report(&mut self, event: Event) {
67 match event {
68 Event::Data(text) => println!("{text}"),
69 Event::Raw(text) => {
70 print!("{text}");
71 if !text.ends_with('\n') {
72 println!();
73 }
74 let _ = io::stdout().flush();
75 }
76 Event::Answer {
77 part,
78 answer,
79 verdict,
80 } => match &verdict {
81 None => println!("{answer}"),
82 Some(Verdict::Correct) => println!("{}", answer.green()),
83 Some(verdict) => {
84 let coloured = if verdict.is_judged() {
88 answer.red()
89 } else {
90 answer.yellow()
91 };
92
93 println!("{coloured}");
94 eprintln!("{part}: {verdict}");
95 }
96 },
97 Event::Warning(text) => eprintln!("{} {text}", "warning:".yellow().bold()),
98 }
99 }
100}
101
102#[cfg(test)]
103pub(crate) mod recording {
104 use super::{Event, Reporter};
105
106 #[derive(Debug, Default)]
107 pub(crate) struct RecordingReporter {
108 pub(crate) events: Vec<Event>,
109 }
110
111 impl RecordingReporter {
112 pub(crate) fn new() -> Self {
113 Self::default()
114 }
115
116 pub(crate) fn raw_output(&self) -> String {
117 self.events
118 .iter()
119 .filter_map(|event| match event {
120 Event::Raw(text) => Some(text.as_str()),
121 _ => None,
122 })
123 .collect()
124 }
125
126 pub(crate) fn answers(&self) -> Vec<Event> {
127 self.events
128 .iter()
129 .filter(|event| matches!(event, Event::Answer { .. }))
130 .cloned()
131 .collect()
132 }
133
134 pub(crate) fn warnings(&self) -> Vec<&str> {
135 self.events
136 .iter()
137 .filter_map(|event| match event {
138 Event::Warning(text) => Some(text.as_str()),
139 _ => None,
140 })
141 .collect()
142 }
143 }
144
145 impl Reporter for RecordingReporter {
146 fn report(&mut self, event: Event) {
147 self.events.push(event);
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::{recording::RecordingReporter, *};
155
156 #[test]
157 fn records_every_kind_of_event() {
158 let mut reporter = RecordingReporter::new();
159
160 reporter.data("/aoc/2024/day07/rust");
161 reporter.raw("hello\n");
162 reporter.answer(Part::One, "1227", Some(Verdict::Correct));
163 reporter.warn("no cookie");
164
165 assert_eq!(reporter.events.len(), 4);
166 assert_eq!(reporter.raw_output(), "hello\n");
167 assert_eq!(reporter.warnings(), ["no cookie"]);
168 assert_eq!(
169 reporter.events[0],
170 Event::Data("/aoc/2024/day07/rust".to_owned())
171 );
172 assert_eq!(
173 reporter.events[2],
174 Event::Answer {
175 part: Part::One,
176 answer: "1227".to_owned(),
177 verdict: Some(Verdict::Correct),
178 }
179 );
180 }
181}