mod command;
mod config;
mod empty;
mod frontmatter;
mod marker;
mod print;
mod wait;
use super::{
execute_command, util, AsciiCast, ErrorType, ExecutionContext, FrontMatterState, ParseContext,
};
pub use command::CommandInstruction;
pub use config::ConfigInstruction;
pub use empty::EmptyInstruction;
pub use frontmatter::FrontMatterInstruction;
pub use marker::MarkerInstruction;
pub use print::PrintInstruction;
pub use wait::WaitInstruction;
pub trait InstructionTrait: std::fmt::Debug {
fn parse(s: &str, context: &mut ParseContext) -> Result<Self, ErrorType>
where
Self: Sized;
fn execute(
&self,
context: &mut ExecutionContext,
cast: &mut AsciiCast<impl std::io::Write>,
) -> Result<(), ErrorType>;
}
#[derive(Debug)]
pub enum Instruction {
Config(ConfigInstruction),
Print(PrintInstruction),
Marker(MarkerInstruction),
Empty(EmptyInstruction),
Command(CommandInstruction),
Wait(WaitInstruction),
FrontMatter(FrontMatterInstruction),
}
impl InstructionTrait for Instruction {
fn parse(s: &str, context: &mut ParseContext) -> Result<Self, ErrorType> {
let s = s.trim();
let Some(first) = s.chars().next() else {
return Ok(Self::Empty(EmptyInstruction::new()));
};
let trimmed = s[1..].trim().to_string();
context.start = first;
match first {
'@' => Ok(Self::Config(ConfigInstruction::parse(&trimmed, context)?)),
'%' => Ok(Self::Print(PrintInstruction::parse(&trimmed, context)?)),
'!' => Ok(Self::Marker(MarkerInstruction::parse(&trimmed, context)?)),
'#' => Ok(Self::Empty(EmptyInstruction::new())),
'$' | '>' => Ok(Self::Command(CommandInstruction::parse(&trimmed, context)?)),
'~' => Ok(Self::Wait(WaitInstruction::parse(&trimmed, context)?)),
_ => Ok(Self::FrontMatter(FrontMatterInstruction::parse(
s, context,
)?)),
}
}
fn execute(
&self,
context: &mut ExecutionContext,
cast: &mut AsciiCast<impl std::io::Write>,
) -> Result<(), ErrorType> {
match self {
Self::Config(instruction) => instruction.execute(context, cast),
Self::Print(instruction) => instruction.execute(context, cast),
Self::Marker(instruction) => instruction.execute(context, cast),
Self::Empty(instruction) => instruction.execute(context, cast),
Self::Command(instruction) => instruction.execute(context, cast),
Self::Wait(instruction) => instruction.execute(context, cast),
Self::FrontMatter(instruction) => instruction.execute(context, cast),
}
}
}
#[cfg(test)]
impl PartialEq for Instruction {
fn eq(&self, other: &Self) -> bool {
format!("{self:?}") == format!("{other:?}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn instruction_with_space() {
let mut context = ParseContext::new();
let instructions: [(&str, Instruction); 10] = [
(
" @interval 2ms",
Instruction::Config(
ConfigInstruction::parse("interval 2ms", &mut context).unwrap(),
),
),
(
" %print",
Instruction::Print(PrintInstruction::parse("print", &mut context).unwrap()),
),
(
" !marker",
Instruction::Marker(MarkerInstruction::parse("marker", &mut context).unwrap()),
),
(" #comment", Instruction::Empty(EmptyInstruction::new())),
(
" $echo \"Hello, World!\"",
Instruction::Command(
CommandInstruction::parse(
"echo \"Hello, World!\"",
&mut context.with_start('$'),
)
.unwrap(),
),
),
(
" @ interval 2ms",
Instruction::Config(
ConfigInstruction::parse("interval 2ms", &mut context).unwrap(),
),
),
(
"% print",
Instruction::Print(PrintInstruction::parse("print", &mut context).unwrap()),
),
(
"! marker",
Instruction::Marker(MarkerInstruction::parse("marker", &mut context).unwrap()),
),
("# comment", Instruction::Empty(EmptyInstruction::new())),
(
"$ echo \"Hello, World!\"",
Instruction::Command(
CommandInstruction::parse(
"echo \"Hello, World!\"",
&mut context.with_start('$'),
)
.unwrap(),
),
),
];
for (input, expected) in &instructions {
assert_eq!(&Instruction::parse(input, &mut context).unwrap(), expected);
}
}
#[test]
fn instruction_without_space() {
let mut context = ParseContext::new();
let instructions: [(&str, Instruction); 5] = [
(
"@interval 2ms",
Instruction::Config(
ConfigInstruction::parse("interval 2ms", &mut context).unwrap(),
),
),
(
"%print",
Instruction::Print(PrintInstruction::parse("print", &mut context).unwrap()),
),
(
"!marker",
Instruction::Marker(MarkerInstruction::parse("marker", &mut context).unwrap()),
),
("#comment", Instruction::Empty(EmptyInstruction::new())),
(
"$echo \"Hello, World!\"",
Instruction::Command(
CommandInstruction::parse(
"echo \"Hello, World!\"",
&mut context.with_start('$'),
)
.unwrap(),
),
),
];
for (input, expected) in &instructions {
assert_eq!(&Instruction::parse(input, &mut context).unwrap(), expected);
}
}
#[test]
fn empty_instruction() {
let empty_lines = ["", " ", "\t", "\t ", " \t", "\n", "\r\n", "# some comment"];
let mut context = ParseContext::new();
let expected = Instruction::Empty(EmptyInstruction::new());
for line in &empty_lines {
assert_eq!(&Instruction::parse(line, &mut context).unwrap(), &expected);
}
}
#[test]
fn invalid_instruction() {
let unknown_instructions = ["invalid", "&", "^"];
let mut context = ParseContext::new();
for line in &unknown_instructions {
let err = Instruction::parse(line, &mut context).unwrap_err();
assert!(
matches!(err, ErrorType::UnknownInstruction,),
"Expected UnknownInstruction, got {err:?}"
);
}
let malformed_instructions = ["@", "@@"];
for line in &malformed_instructions {
let err = Instruction::parse(line, &mut context).unwrap_err();
assert!(
matches!(err, ErrorType::MalformedInstruction,),
"Expected MalformedInstruction, got {err:?}"
);
}
}
}