use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Expr, Ident, LitStr, Result, Token, braced};
pub enum DialogStep {
Send(SendStep),
Expect(ExpectStep),
Wait(WaitStep),
Timeout(TimeoutStep),
}
pub struct SendStep {
pub data: LitStr,
pub newline: bool,
}
pub struct ExpectStep {
pub pattern: LitStr,
pub is_regex: bool,
pub timeout: Option<Expr>,
}
pub struct WaitStep {
pub duration: Expr,
}
pub struct TimeoutStep {
pub duration: Expr,
}
impl Parse for DialogStep {
fn parse(input: ParseStream) -> Result<Self> {
let keyword: Ident = input.parse()?;
match keyword.to_string().as_str() {
"send" => {
let data: LitStr = input.parse()?;
Ok(Self::Send(SendStep {
data,
newline: false,
}))
}
"sendln" | "send_line" => {
let data: LitStr = input.parse()?;
Ok(Self::Send(SendStep {
data,
newline: true,
}))
}
"expect" => {
let pattern: LitStr = input.parse()?;
let timeout = if input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
Some(input.parse()?)
} else {
None
};
Ok(Self::Expect(ExpectStep {
pattern,
is_regex: false,
timeout,
}))
}
"expect_re" | "expect_regex" => {
let pattern: LitStr = input.parse()?;
let pattern_str = pattern.value();
if let Err(e) = regex::Regex::new(&pattern_str) {
return Err(syn::Error::new(
pattern.span(),
format!("invalid regex: {e}"),
));
}
let timeout = if input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
Some(input.parse()?)
} else {
None
};
Ok(Self::Expect(ExpectStep {
pattern,
is_regex: true,
timeout,
}))
}
"wait" | "sleep" => {
let duration: Expr = input.parse()?;
Ok(Self::Wait(WaitStep { duration }))
}
"timeout" => {
let duration: Expr = input.parse()?;
Ok(Self::Timeout(TimeoutStep { duration }))
}
other => Err(syn::Error::new(
keyword.span(),
format!("unknown dialog command: {other}"),
)),
}
}
}
pub struct DialogInput {
pub steps: Punctuated<DialogStep, Token![;]>,
}
impl Parse for DialogInput {
fn parse(input: ParseStream) -> Result<Self> {
let steps = if input.peek(syn::token::Brace) {
let content;
braced!(content in input);
Punctuated::parse_terminated(&content)?
} else {
Punctuated::parse_terminated(input)?
};
Ok(Self { steps })
}
}
pub fn expand(input: DialogInput) -> TokenStream {
let steps: Vec<_> = input
.steps
.into_iter()
.map(|step| match step {
DialogStep::Send(send) => {
let data = &send.data;
if send.newline {
quote! {
rust_expect::dialog::DialogStep::SendLine(#data.to_string())
}
} else {
quote! {
rust_expect::dialog::DialogStep::Send(#data.to_string())
}
}
}
DialogStep::Expect(expect) => {
let pattern = &expect.pattern;
let timeout = expect
.timeout
.map_or_else(|| quote! { None }, |t| quote! { Some(#t) });
if expect.is_regex {
quote! {
rust_expect::dialog::DialogStep::ExpectRegex {
pattern: #pattern.to_string(),
timeout: #timeout,
}
}
} else {
quote! {
rust_expect::dialog::DialogStep::Expect {
pattern: #pattern.to_string(),
timeout: #timeout,
}
}
}
}
DialogStep::Wait(wait) => {
let duration = &wait.duration;
quote! {
rust_expect::dialog::DialogStep::Wait(#duration)
}
}
DialogStep::Timeout(timeout) => {
let duration = &timeout.duration;
quote! {
rust_expect::dialog::DialogStep::SetTimeout(#duration)
}
}
})
.collect();
quote! {
rust_expect::dialog::Dialog::new(vec![#(#steps),*])
}
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::*;
#[test]
fn parse_simple_dialog() {
let input: DialogInput = parse_quote! {
expect "login:";
sendln "username"
};
assert_eq!(input.steps.len(), 2);
}
}