diff --git a/embedded-cli-macros/src/command/mod.rs b/embedded-cli-macros/src/command/mod.rs index f564617..903c6eb 100644 --- a/embedded-cli-macros/src/command/mod.rs +++ b/embedded-cli-macros/src/command/mod.rs @@ -24,7 +24,7 @@ struct ServiceAttrs { skip_from_raw: bool, } -pub fn derive_command(input: DeriveInput) -> Result { +pub fn derive_command(input: DeriveInput, is_async: bool) -> Result { let opts = ServiceAttrs::from_derive_input(&input)?; let DeriveInput { vis, @@ -67,7 +67,7 @@ pub fn derive_command(input: DeriveInput) -> Result { } else { parse::derive_from_raw(&target, &commands)? }; - let impl_processor = processor::impl_processor(&vis, &target)?; + let impl_processor = processor::impl_processor(&vis, &target, is_async)?; let output = quote! { #derive_autocomplete diff --git a/embedded-cli-macros/src/group/mod.rs b/embedded-cli-macros/src/group/mod.rs index ba965f1..db18a99 100644 --- a/embedded-cli-macros/src/group/mod.rs +++ b/embedded-cli-macros/src/group/mod.rs @@ -9,7 +9,7 @@ use self::command_group::CommandGroup; mod command_group; -pub fn derive_command_group(input: DeriveInput) -> Result { +pub fn derive_command_group(input: DeriveInput, is_async: bool) -> Result { let DeriveInput { vis, ident, @@ -37,7 +37,7 @@ pub fn derive_command_group(input: DeriveInput) -> Result { let derive_autocomplete = derive_autocomplete(&target, &groups); let derive_help = derive_help(&target, &groups); let derive_from_raw = derive_from_raw(&target, &groups); - let impl_processor = processor::impl_processor(&vis, &target)?; + let impl_processor = processor::impl_processor(&vis, &target, is_async)?; let output = quote! { #derive_autocomplete diff --git a/embedded-cli-macros/src/lib.rs b/embedded-cli-macros/src/lib.rs index a2db1cd..ce6e55d 100644 --- a/embedded-cli-macros/src/lib.rs +++ b/embedded-cli-macros/src/lib.rs @@ -11,7 +11,7 @@ mod utils; pub fn derive_command(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); - let output = match command::derive_command(input) { + let output = match command::derive_command(input, false) { Ok(output) => output, Err(e) => return e.write_errors().into(), }; @@ -32,7 +32,49 @@ pub fn derive_command(input: TokenStream) -> TokenStream { pub fn derive_command_group(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); - let output = match group::derive_command_group(input) { + let output = match group::derive_command_group(input, false) { + Ok(output) => output, + Err(e) => return e.write_errors().into(), + }; + + // wrap with anonymous scope + quote! { + const _: () = { + extern crate embedded_cli as _cli; + use _cli::__private::io as _io; + + #output + }; + } + .into() +} + +#[proc_macro_derive(CommandAsync, attributes(command, arg))] +pub fn derive_command_async(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input); + + let output = match command::derive_command(input, true) { + Ok(output) => output, + Err(e) => return e.write_errors().into(), + }; + + // wrap with anonymous scope + quote! { + const _: () = { + extern crate embedded_cli as _cli; + use _cli::__private::io as _io; + + #output + }; + } + .into() +} + +#[proc_macro_derive(CommandGroupAsync, attributes(group))] +pub fn derive_command_group_async(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input); + + let output = match group::derive_command_group(input, true) { Ok(output) => output, Err(e) => return e.write_errors().into(), }; diff --git a/embedded-cli-macros/src/processor.rs b/embedded-cli-macros/src/processor.rs index 78c5e43..b853a71 100644 --- a/embedded-cli-macros/src/processor.rs +++ b/embedded-cli-macros/src/processor.rs @@ -5,50 +5,100 @@ use syn::Visibility; use crate::utils::TargetType; -pub fn impl_processor(vis: &Visibility, target: &TargetType) -> Result { +pub fn impl_processor( + vis: &Visibility, + target: &TargetType, + is_async: bool, +) -> Result { let ident = target.ident(); let named_lifetime = target.named_lifetime(); let unnamed_lifetime = target.unnamed_lifetime(); - let output = quote! { - - impl #named_lifetime #ident #named_lifetime { - #vis fn processor< - W: _io::Write, - E: _io::Error, - F: FnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, - >( - f: F, - ) -> impl _cli::service::CommandProcessor { - struct Processor< + let output = if is_async { + quote! { + + impl #named_lifetime #ident #named_lifetime { + #vis fn processor< W: _io::Write, E: _io::Error, - F: FnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, - > { + F: AsyncFnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, + >( f: F, - _ph: core::marker::PhantomData<(W, E)>, + ) -> impl _cli::service::AsyncCommandProcessor { + struct Processor< + W: _io::Write, + E: _io::Error, + F: AsyncFnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, + > { + f: F, + _ph: core::marker::PhantomData<(W, E)>, + } + + impl< + W: _io::Write, + E: _io::Error, + F: AsyncFnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, + > _cli::service::AsyncCommandProcessor for Processor + { + async fn process<'a>( + &mut self, + cli: &mut _cli::cli::CliHandle<'_, W, E>, + raw: _cli::command::RawCommand<'a>, + ) -> Result<(), _cli::service::ProcessError<'a, E>> { + let cmd = <#ident #unnamed_lifetime as _cli::service::FromRaw>::parse(raw)?; + (self.f)(cli, cmd).await?; + Ok(()) + } + } + + Processor { + f, + _ph: core::marker::PhantomData, + } } + } + } + } else { + quote! { - impl< + impl #named_lifetime #ident #named_lifetime { + #vis fn processor< + W: _io::Write, + E: _io::Error, + F: FnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, + >( + f: F, + ) -> impl _cli::service::CommandProcessor { + struct Processor< W: _io::Write, E: _io::Error, F: FnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, - > _cli::service::CommandProcessor for Processor - { - fn process<'a>( - &mut self, - cli: &mut _cli::cli::CliHandle<'_, W, E>, - raw: _cli::command::RawCommand<'a>, - ) -> Result<(), _cli::service::ProcessError<'a, E>> { - let cmd = <#ident #unnamed_lifetime as _cli::service::FromRaw>::parse(raw)?; - (self.f)(cli, cmd)?; - Ok(()) + > { + f: F, + _ph: core::marker::PhantomData<(W, E)>, + } + + impl< + W: _io::Write, + E: _io::Error, + F: FnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, + > _cli::service::CommandProcessor for Processor + { + fn process<'a>( + &mut self, + cli: &mut _cli::cli::CliHandle<'_, W, E>, + raw: _cli::command::RawCommand<'a>, + ) -> Result<(), _cli::service::ProcessError<'a, E>> { + let cmd = <#ident #unnamed_lifetime as _cli::service::FromRaw>::parse(raw)?; + (self.f)(cli, cmd)?; + Ok(()) + } } - } - Processor { - f, - _ph: core::marker::PhantomData, + Processor { + f, + _ph: core::marker::PhantomData, + } } } } diff --git a/embedded-cli/Cargo.toml b/embedded-cli/Cargo.toml index 0af3143..17bb545 100644 --- a/embedded-cli/Cargo.toml +++ b/embedded-cli/Cargo.toml @@ -30,3 +30,4 @@ ufmt = "0.2.0" [dev-dependencies] regex = "1.10.4" rstest = "0.19.0" +tokio = "1.52.3" diff --git a/embedded-cli/src/cli.rs b/embedded-cli/src/cli.rs index f2d91b7..c3caf53 100644 --- a/embedded-cli/src/cli.rs +++ b/embedded-cli/src/cli.rs @@ -1,6 +1,11 @@ pub use crate::builder::CliBuilder; -use core::fmt::Debug; +use core::{ + fmt::Debug, + future::Future, + pin::pin, + task::{Context, Poll, Waker}, +}; #[cfg(not(feature = "history"))] use core::marker::PhantomData; @@ -12,7 +17,10 @@ use crate::{ command::RawCommand, editor::Editor, input::{ControlInput, Input, InputGenerator}, - service::{Autocomplete, CommandProcessor, Help, ParseError, ProcessError}, + service::{ + AsyncCommandProcessor, Autocomplete, CommandProcessor, Help, ParseError, ProcessError, + SyncToAsyncAdapter, + }, token::Tokens, utils, writer::{WriteExt, Writer}, @@ -161,19 +169,40 @@ where &mut self, b: u8, processor: &mut P, + ) -> Result<(), E> { + let mut cx = Context::from_waker(Waker::noop()); + match pin!(self.process_byte_async::(b, &mut SyncToAsyncAdapter::new(processor))) + .poll(&mut cx) + { + Poll::Ready(res) => res, + Poll::Pending => unreachable!(), + } + } + + /// Each call to process byte can be done with different + /// command set and/or command processor. + /// In process callback you can change some outside state + /// so next calls will use different processor + pub async fn process_byte_async>( + &mut self, + b: u8, + processor: &mut P, ) -> Result<(), E> { if let (Some(mut editor), Some(mut input_generator)) = (self.editor.take(), self.input_generator.take()) { - let result = input_generator - .accept(b) - .map(|input| match input { + let result = if let Some(input) = input_generator.accept(b) { + Some(match input { Input::Control(control) => { self.on_control_input::(&mut editor, control, processor) + .await } Input::Char(text) => self.on_text_input(&mut editor, text), }) - .unwrap_or(Ok(())); + } else { + None + } + .unwrap_or(Ok(())); self.editor = Some(editor); self.input_generator = Some(input_generator); @@ -244,7 +273,7 @@ where Ok(()) } - fn on_control_input>( + async fn on_control_input>( &mut self, editor: &mut Editor, control: ControlInput, @@ -259,7 +288,7 @@ where let text = editor.text_mut(); let tokens = Tokens::new(text); - self.process_input::(tokens, processor)?; + self.process_input::(tokens, processor).await?; editor.clear(); @@ -354,7 +383,7 @@ where Ok(()) } - fn process_command>( + async fn process_command>( &mut self, command: RawCommand<'_>, handler: &mut P, @@ -362,7 +391,7 @@ where let cli_writer = Writer::new(&mut self.writer); let mut handle = CliHandle::new(cli_writer); - let res = handler.process(&mut handle, command); + let res = handler.process(&mut handle, command).await; if let Some(prompt) = handle.new_prompt { self.prompt = prompt; @@ -380,7 +409,7 @@ where } #[allow(clippy::extra_unused_type_parameters)] - fn process_input>( + async fn process_input>( &mut self, tokens: Tokens<'_>, handler: &mut P, @@ -391,7 +420,7 @@ where return self.process_help::(request); } - self.process_command(command, handler)?; + self.process_command(command, handler).await?; }; Ok(()) diff --git a/embedded-cli/src/lib.rs b/embedded-cli/src/lib.rs index 59c8582..58afe17 100644 --- a/embedded-cli/src/lib.rs +++ b/embedded-cli/src/lib.rs @@ -25,7 +25,7 @@ pub mod writer; /// Macro available if embedded-cli is built with `features = ["macros"]`. #[cfg(feature = "embedded-cli-macros")] -pub use embedded_cli_macros::{Command, CommandGroup}; +pub use embedded_cli_macros::{Command, CommandAsync, CommandGroup, CommandGroupAsync}; // Used by generated code. Not public API. #[doc(hidden)] diff --git a/embedded-cli/src/service.rs b/embedded-cli/src/service.rs index 870e9c3..fdfb143 100644 --- a/embedded-cli/src/service.rs +++ b/embedded-cli/src/service.rs @@ -1,3 +1,5 @@ +use core::marker::PhantomData; + use embedded_io::Write; use crate::{arguments::FromArgumentError, cli::CliHandle, command::RawCommand}; @@ -139,3 +141,57 @@ where self(cli, command) } } + +pub(crate) struct SyncToAsyncAdapter< + 'a, + W: Write, + E: embedded_io::Error, + P: CommandProcessor, +>(&'a mut P, PhantomData<(W, E)>); + +impl<'a, W: Write, E: embedded_io::Error, P: CommandProcessor> + SyncToAsyncAdapter<'a, W, E, P> +{ + pub const fn new(p: &'a mut P) -> Self { + Self(p, PhantomData) + } +} + +impl<'b, W: Write, E: embedded_io::Error, P: CommandProcessor> + AsyncCommandProcessor for SyncToAsyncAdapter<'b, W, E, P> +{ + async fn process<'a>( + &mut self, + cli: &mut CliHandle<'_, W, E>, + raw: RawCommand<'a>, + ) -> Result<(), ProcessError<'a, E>> { + self.0.process(cli, raw) + } +} + +#[allow(async_fn_in_trait)] +pub trait AsyncCommandProcessor, E: embedded_io::Error> { + async fn process<'a>( + &mut self, + cli: &mut CliHandle<'_, W, E>, + raw: RawCommand<'a>, + ) -> Result<(), ProcessError<'a, E>>; +} + +impl AsyncCommandProcessor for F +where + W: Write, + E: embedded_io::Error, + F: for<'a> AsyncFnMut( + &mut CliHandle<'_, W, E>, + RawCommand<'a>, + ) -> Result<(), ProcessError<'a, E>>, +{ + async fn process<'a>( + &mut self, + cli: &mut CliHandle<'_, W, E>, + command: RawCommand<'a>, + ) -> Result<(), ProcessError<'a, E>> { + self(cli, command).await + } +}