use std::fmt::Write as _;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TranscriptLine {
Alert {
title: String,
message: String,
icon: i32,
button: i32,
},
Beep(i32),
Response {
question: String,
title: String,
default_value: String,
label: String,
password: bool,
},
MailMsg {
ui: bool,
to: String,
cc: String,
bcc: String,
subject: String,
body: String,
},
Print {
ui: bool,
start: i32,
end: i32,
silent: bool,
shrink_to_fit: bool,
print_as_image: bool,
reverse: bool,
annotations: bool,
},
SubmitForm {
url: String,
data: Vec<u8>,
},
GotoPage(i32),
NamedAction(String),
ConsolePrintln(String),
FunctionAlert {
caller: String,
message: String,
},
}
pub const DEFAULT_ALERT_TITLE: &str = "Alert";
impl TranscriptLine {
#[must_use]
pub fn render(&self) -> Option<String> {
let mut out = String::new();
match self {
TranscriptLine::Alert {
title,
message,
icon,
button,
} => {
if *icon != 0 || *button != 0 {
let _ = write!(out, "{title}[icon={icon},type={button}]: {message}");
} else {
let _ = write!(out, "{title}: {message}");
}
}
TranscriptLine::FunctionAlert { caller, message } => {
let _ = write!(out, "{caller}[icon=3,type=0]: {message}");
}
TranscriptLine::Beep(kind) => {
let _ = write!(out, "BEEP!!! {kind}");
}
TranscriptLine::Response {
question,
title,
default_value,
label,
password,
} => {
let _ = write!(
out,
"{title}: {question}, defaultValue={default_value}, label={label}, \
isPassword={}, length=2048",
u8::from(*password)
);
}
TranscriptLine::MailMsg {
ui,
to,
cc,
bcc,
subject,
body,
} => {
let _ = write!(
out,
"Mail Msg: {}, to={to}, cc={cc}, bcc={bcc}, subject={subject}, body={body}",
u8::from(*ui)
);
}
TranscriptLine::Print {
ui,
start,
end,
silent,
shrink_to_fit,
print_as_image,
reverse,
annotations,
} => {
let _ = write!(
out,
"Doc Print: {}, {start}, {end}, {}, {}, {}, {}, {}",
u8::from(*ui),
u8::from(*silent),
u8::from(*shrink_to_fit),
u8::from(*print_as_image),
u8::from(*reverse),
u8::from(*annotations)
);
}
TranscriptLine::SubmitForm { url, data } => {
let _ = writeln!(
out,
"Doc Submit Form: url={url} + {} data bytes:",
data.len()
);
for byte in data {
let _ = write!(out, " {byte:02x}");
}
}
TranscriptLine::GotoPage(page) => {
let _ = write!(out, "Goto Page: {page}");
}
TranscriptLine::NamedAction(name) => {
let _ = write!(out, "Execute named action: {name}");
}
TranscriptLine::ConsolePrintln(_) => return None,
}
Some(out)
}
#[must_use]
pub fn alert(message: impl Into<String>) -> TranscriptLine {
TranscriptLine::Alert {
title: DEFAULT_ALERT_TITLE.to_string(),
message: message.into(),
icon: 0,
button: 0,
}
}
}
#[must_use]
pub fn render(lines: &[TranscriptLine]) -> String {
let mut out = String::new();
for line in lines {
if let Some(rendered) = line.render() {
out.push_str(&rendered);
out.push('\n');
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_default_alert_is_prefixed_with_the_literal_alert() {
assert_eq!(
TranscriptLine::alert("Hello").render().as_deref(),
Some("Alert: Hello")
);
}
#[test]
fn a_titled_alert_prints_its_own_title() {
let line = TranscriptLine::Alert {
title: "Warning".to_string(),
message: "careful".to_string(),
icon: 0,
button: 0,
};
assert_eq!(line.render().as_deref(), Some("Warning: careful"));
}
#[test]
fn a_decorated_alert_prints_both_numbers() {
let with_icon = TranscriptLine::Alert {
title: "Alert".to_string(),
message: "m".to_string(),
icon: 3,
button: 0,
};
assert_eq!(
with_icon.render().as_deref(),
Some("Alert[icon=3,type=0]: m")
);
let with_type = TranscriptLine::Alert {
title: "Alert".to_string(),
message: "m".to_string(),
icon: 0,
button: 2,
};
assert_eq!(
with_type.render().as_deref(),
Some("Alert[icon=0,type=2]: m")
);
}
#[test]
fn a_function_alert_is_titled_by_its_caller() {
let line = TranscriptLine::FunctionAlert {
caller: "AFNumber_Keystroke".to_string(),
message: "The input value is invalid.".to_string(),
};
assert_eq!(
line.render().as_deref(),
Some("AFNumber_Keystroke[icon=3,type=0]: The input value is invalid.")
);
}
#[test]
fn console_output_is_discarded_as_upstream_discards_it() {
assert_eq!(
TranscriptLine::ConsolePrintln("x".to_string()).render(),
None
);
let rendered = render(&[
TranscriptLine::alert("one"),
TranscriptLine::ConsolePrintln("hidden".to_string()),
TranscriptLine::alert("two"),
]);
assert_eq!(rendered, "Alert: one\nAlert: two\n");
}
#[test]
fn the_other_seven_shapes_render_their_golden_form() {
assert_eq!(
TranscriptLine::Beep(2).render().as_deref(),
Some("BEEP!!! 2")
);
assert_eq!(
TranscriptLine::GotoPage(4).render().as_deref(),
Some("Goto Page: 4")
);
assert_eq!(
TranscriptLine::NamedAction("Print".to_string())
.render()
.as_deref(),
Some("Execute named action: Print")
);
assert_eq!(
TranscriptLine::Response {
question: "q".to_string(),
title: "t".to_string(),
default_value: String::new(),
label: String::new(),
password: false,
}
.render()
.as_deref(),
Some("t: q, defaultValue=, label=, isPassword=0, length=2048")
);
assert_eq!(
TranscriptLine::MailMsg {
ui: true,
to: String::new(),
cc: String::new(),
bcc: String::new(),
subject: String::new(),
body: String::new(),
}
.render()
.as_deref(),
Some("Mail Msg: 1, to=, cc=, bcc=, subject=, body=")
);
assert_eq!(
TranscriptLine::Print {
ui: false,
start: 0,
end: 0,
silent: false,
shrink_to_fit: false,
print_as_image: false,
reverse: false,
annotations: false,
}
.render()
.as_deref(),
Some("Doc Print: 0, 0, 0, 0, 0, 0, 0, 0")
);
}
#[test]
fn a_submitted_form_dumps_every_byte_on_one_line() {
let line = TranscriptLine::SubmitForm {
url: "https://example.com".to_string(),
data: b"name=Tralfaz&age=12".to_vec(),
};
let rendered = line.render().unwrap_or_default();
let mut lines = rendered.lines();
assert_eq!(
lines.next(),
Some("Doc Submit Form: url=https://example.com + 19 data bytes:")
);
assert_eq!(
lines.next(),
Some(" 6e 61 6d 65 3d 54 72 61 6c 66 61 7a 26 61 67 65 3d 31 32")
);
assert_eq!(lines.next(), None);
}
}