Skip to content

Commit d48fe8d

Browse files
committed
Refactor commands, validators, and REPL builder to support async commands
1 parent 01ba437 commit d48fe8d

File tree

11 files changed

+1033
-525
lines changed

11 files changed

+1033
-525
lines changed

Cargo.toml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
[package]
2-
name = "easy-repl"
2+
name = "mini-async-repl"
33
version = "0.2.1"
4-
authors = ["Jędrzej Boczar <[email protected]>"]
5-
edition = "2021"
4+
authors = ["Martin Verzilli <[email protected]", "Jędrzej Boczar <[email protected]>"]
5+
edition = "2023"
66
license = "MIT OR Apache-2.0"
7-
description = "An easy to use REPL, ideal when there is a need to crate an ad-hoc shell"
8-
repository = "https://github.com/jedrzejboczar/easy-repl"
7+
description = "An async-first REPL"
8+
repository = "https://github.com/manastech/mini-async-repl"
99
keywords = [
1010
"repl",
1111
"cli",
1212
"shell",
1313
"interactive",
1414
"interpreter",
15+
"async",
1516
]
1617
categories = ["command-line-interface"]
1718

@@ -23,3 +24,4 @@ anyhow = "1.0"
2324
textwrap = "0.15"
2425
trie-rs = "0.1"
2526
shell-words = "1.0"
27+
tokio = { version = "1.34.0", features = ["macros", "rt", "rt-multi-thread"] }

examples/errors.rs

Lines changed: 178 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,195 @@
11
use std::time::Instant;
22

33
use anyhow::{self, Context};
4-
use easy_repl::{command, CommandStatus, Critical, Repl};
4+
use easy_repl::{
5+
command::{
6+
ExecuteCommand,
7+
NewCommand,
8+
CommandArgInfo,
9+
CommandArgType,
10+
Validator,
11+
ArgsError,
12+
Critical,
13+
},
14+
CommandStatus,
15+
Repl,
16+
};
17+
use std::pin::Pin;
18+
use std::future::Future;
519

6-
// this could be any funcion returining Result with an error implementing Error
7-
// here for simplicity we make use of the Other variant of std::io::Error
8-
fn may_throw(description: String) -> Result<(), std::io::Error> {
9-
Err(std::io::Error::new(std::io::ErrorKind::Other, description))
20+
struct OkCommandHandler {}
21+
impl OkCommandHandler {
22+
pub fn new() -> Self {
23+
Self {}
24+
}
25+
async fn handle_command(&mut self) -> anyhow::Result<CommandStatus> {
26+
Ok(CommandStatus::Done)
27+
}
28+
async fn resolved(result: Result<(), ArgsError>) -> Result<CommandStatus, anyhow::Error> {
29+
match result {
30+
Ok(_) => Ok(CommandStatus::Done),
31+
Err(e) => Err(e.into()),
32+
}
33+
}
34+
}
35+
impl ExecuteCommand for OkCommandHandler {
36+
fn execute(&mut self, args: Vec<String>) -> Pin<Box<dyn Future<Output = anyhow::Result<CommandStatus>> + '_>> {
37+
let valid = Validator::validate(args.clone(), vec![
38+
CommandArgInfo::new_with_name(CommandArgType::String, "name"),
39+
]);
40+
if let Err(e) = valid {
41+
return Box::pin(OkCommandHandler::resolved(Err(e)));
42+
}
43+
Box::pin(self.handle_command())
44+
}
45+
}
46+
47+
struct RecoverableErrorHandler {}
48+
impl RecoverableErrorHandler {
49+
pub fn new() -> Self {
50+
Self {}
51+
}
52+
async fn handle_command(&mut self, text: String) -> anyhow::Result<CommandStatus> {
53+
Self::may_throw(text)?;
54+
Ok(CommandStatus::Done)
55+
}
56+
async fn resolved(result: Result<(), ArgsError>) -> Result<CommandStatus, anyhow::Error> {
57+
match result {
58+
Ok(_) => Ok(CommandStatus::Done),
59+
Err(e) => Err(e.into()),
60+
}
61+
}
62+
// this could be any function returning Result with an error implementing Error
63+
// here for simplicity we make use of the Other variant of std::io::Error
64+
fn may_throw(description: String) -> Result<(), std::io::Error> {
65+
Err(std::io::Error::new(std::io::ErrorKind::Other, description))
66+
}
67+
}
68+
impl ExecuteCommand for RecoverableErrorHandler {
69+
fn execute(&mut self, args: Vec<String>) -> Pin<Box<dyn Future<Output = anyhow::Result<CommandStatus>> + '_>> {
70+
let valid = Validator::validate(args.clone(), vec![CommandArgInfo::new_with_name(CommandArgType::String, "text")]);
71+
if let Err(e) = valid {
72+
return Box::pin(RecoverableErrorHandler::resolved(Err(e)));
73+
}
74+
Box::pin(self.handle_command(args[0].clone()))
75+
}
76+
}
77+
78+
struct CriticalErrorHandler {}
79+
impl CriticalErrorHandler {
80+
pub fn new() -> Self {
81+
Self {}
82+
}
83+
async fn handle_command(&mut self, text: String) -> anyhow::Result<CommandStatus> {
84+
// Short notation using the Critical trait
85+
Self::may_throw(text).into_critical()?;
86+
// More explicitly it could be:
87+
// if let Err(err) = may_throw(text) {
88+
// Err(easy_repl::CriticalError::Critical(err.into()))?;
89+
// }
90+
// or even:
91+
// if let Err(err) = may_throw(text) {
92+
// return Err(easy_repl::CriticalError::Critical(err.into())).into();
93+
// }
94+
Ok(CommandStatus::Done)
95+
}
96+
async fn resolved(result: Result<(), ArgsError>) -> Result<CommandStatus, anyhow::Error> {
97+
match result {
98+
Ok(_) => Ok(CommandStatus::Done),
99+
Err(e) => Err(e.into()),
100+
}
101+
}
102+
// this could be any function returning Result with an error implementing Error
103+
// here for simplicity we make use of the Other variant of std::io::Error
104+
fn may_throw(description: String) -> Result<(), std::io::Error> {
105+
Err(std::io::Error::new(std::io::ErrorKind::Other, description))
106+
}
107+
}
108+
impl ExecuteCommand for CriticalErrorHandler {
109+
fn execute(&mut self, args: Vec<String>) -> Pin<Box<dyn Future<Output = anyhow::Result<CommandStatus>> + '_>> {
110+
let valid = Validator::validate(args.clone(), vec![CommandArgInfo::new_with_name(CommandArgType::String, "text")]);
111+
if let Err(e) = valid {
112+
return Box::pin(CriticalErrorHandler::resolved(Err(e)));
113+
}
114+
Box::pin(self.handle_command(args[0].clone()))
115+
}
10116
}
11117

12-
fn main() -> anyhow::Result<()> {
118+
struct RouletteErrorHandler {
119+
start: Instant,
120+
}
121+
impl RouletteErrorHandler {
122+
pub fn new(start: Instant) -> Self {
123+
Self { start }
124+
}
125+
async fn handle_command(&mut self) -> anyhow::Result<CommandStatus> {
126+
let ns = Instant::now().duration_since(self.start).as_nanos();
127+
let cylinder = ns % 6;
128+
match cylinder {
129+
0 => Self::may_throw("Bang!".into()).into_critical()?,
130+
1..=2 => Self::may_throw("Blank cartridge?".into())?,
131+
_ => (),
132+
}
133+
Ok(CommandStatus::Done)
134+
135+
}
136+
async fn resolved(result: Result<(), ArgsError>) -> Result<CommandStatus, anyhow::Error> {
137+
match result {
138+
Ok(_) => Ok(CommandStatus::Done),
139+
Err(e) => Err(e.into()),
140+
}
141+
}
142+
// this could be any function returning Result with an error implementing Error
143+
// here for simplicity we make use of the Other variant of std::io::Error
144+
fn may_throw(description: String) -> Result<(), std::io::Error> {
145+
Err(std::io::Error::new(std::io::ErrorKind::Other, description))
146+
}
147+
}
148+
impl ExecuteCommand for RouletteErrorHandler {
149+
fn execute(&mut self, args: Vec<String>) -> Pin<Box<dyn Future<Output = anyhow::Result<CommandStatus>> + '_>> {
150+
let valid = Validator::validate(args.clone(), vec![]);
151+
if let Err(e) = valid {
152+
return Box::pin(RouletteErrorHandler::resolved(Err(e)));
153+
}
154+
Box::pin(self.handle_command())
155+
}
156+
}
157+
158+
#[tokio::main]
159+
async fn main() -> anyhow::Result<()> {
13160
let start = Instant::now();
14161

15162
#[rustfmt::skip]
16163
let mut repl = Repl::builder()
17-
.add("ok", command! {
18-
"Run a command that just succeeds",
19-
() => || Ok(CommandStatus::Done)
164+
.add("ok", NewCommand {
165+
description: "Run a command that just succeeds".into(),
166+
args_info: vec![],
167+
handler: Box::new(OkCommandHandler::new()),
20168
})
21-
.add("error", command! {
22-
"Command with recoverable error handled by the REPL",
23-
(text:String) => |text| {
24-
may_throw(text)?;
25-
Ok(CommandStatus::Done)
26-
},
169+
.add("error", NewCommand {
170+
description: "Command with recoverable error handled by the REPL".into(),
171+
args_info: vec![CommandArgInfo::new_with_name(CommandArgType::String, "text")],
172+
handler: Box::new(RecoverableErrorHandler::new()),
27173
})
28-
.add("critical", command! {
29-
"Command returns a critical error that must be handled outside of REPL",
30-
(text:String) => |text| {
31-
// Short notation using the Critical trait
32-
may_throw(text).into_critical()?;
33-
// More explicitly it could be:
34-
// if let Err(err) = may_throw(text) {
35-
// Err(easy_repl::CriticalError::Critical(err.into()))?;
36-
// }
37-
// or even:
38-
// if let Err(err) = may_throw(text) {
39-
// return Err(easy_repl::CriticalError::Critical(err.into())).into();
40-
// }
41-
Ok(CommandStatus::Done)
42-
},
174+
.add("critical", NewCommand {
175+
description: "Command returns a critical error that must be handled outside of REPL".into(),
176+
args_info: vec![CommandArgInfo::new_with_name(CommandArgType::String, "text")],
177+
handler: Box::new(CriticalErrorHandler::new()),
43178
})
44-
.add("roulette", command! {
45-
"Feeling lucky?",
46-
() => || {
47-
let ns = Instant::now().duration_since(start).as_nanos();
48-
let cylinder = ns % 6;
49-
match cylinder {
50-
0 => may_throw("Bang!".into()).into_critical()?,
51-
1..=2 => may_throw("Blank cartridge?".into())?,
52-
_ => (),
53-
}
54-
Ok(CommandStatus::Done)
55-
},
179+
.add("roulette", NewCommand {
180+
description: "Feeling lucky?".into(),
181+
args_info: vec![],
182+
handler: Box::new(RouletteErrorHandler::new(Instant::now())),
56183
})
57184
.build()
58185
.context("Failed to create repl")?;
59186

60-
repl.run().context("Critical REPL error")?;
61-
62-
Ok(())
63-
}
187+
let repl_res = repl.run().await;
188+
match repl_res {
189+
Ok(_) => Ok(()),
190+
Err(e) => {
191+
println!("Repl halted. Quitting.");
192+
Ok(())
193+
}
194+
}
195+
}

examples/from_str.rs

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,107 @@ use std::net::IpAddr;
22
use std::path::PathBuf;
33

44
use anyhow::{self, Context};
5-
use easy_repl::{command, CommandStatus, Repl};
5+
use easy_repl::{
6+
command::{
7+
ExecuteCommand,
8+
NewCommand,
9+
CommandArgInfo,
10+
CommandArgType,
11+
Validator,
12+
ArgsError,
13+
},
14+
CommandStatus,
15+
Repl,
16+
};
17+
use std::pin::Pin;
18+
use std::future::Future;
619

7-
fn main() -> anyhow::Result<()> {
20+
struct LsCommandHandler {}
21+
impl LsCommandHandler {
22+
pub fn new() -> Self {
23+
Self {}
24+
}
25+
async fn handle_command(&mut self, dir: PathBuf) -> anyhow::Result<CommandStatus> {
26+
for entry in dir.read_dir()? {
27+
println!("{}", entry?.path().to_string_lossy());
28+
}
29+
Ok(CommandStatus::Done)
30+
}
31+
async fn resolved(result: Result<(), ArgsError>) -> Result<CommandStatus, anyhow::Error> {
32+
match result {
33+
Ok(_) => Ok(CommandStatus::Done),
34+
Err(e) => Err(e.into()),
35+
}
36+
}
37+
}
38+
impl ExecuteCommand for LsCommandHandler {
39+
fn execute(&mut self, args: Vec<String>) -> Pin<Box<dyn Future<Output = anyhow::Result<CommandStatus>> + '_>> {
40+
let valid = Validator::validate(args.clone(), vec![
41+
CommandArgInfo::new_with_name(CommandArgType::Custom, "dir"),
42+
]);
43+
if let Err(e) = valid {
44+
return Box::pin(LsCommandHandler::resolved(Err(e)));
45+
}
46+
47+
let dir_buf: PathBuf = args[0].clone().into();
48+
Box::pin(self.handle_command(dir_buf))
49+
}
50+
}
51+
52+
struct IpAddrCommandHandler {}
53+
impl IpAddrCommandHandler {
54+
pub fn new() -> Self {
55+
Self {}
56+
}
57+
async fn handle_command(&mut self, ip: IpAddr) -> anyhow::Result<CommandStatus> {
58+
println!("{}", ip);
59+
Ok(CommandStatus::Done)
60+
}
61+
async fn resolved(result: Result<(), ArgsError>) -> Result<CommandStatus, anyhow::Error> {
62+
match result {
63+
Ok(_) => Ok(CommandStatus::Done),
64+
Err(e) => Err(e.into()),
65+
}
66+
}
67+
}
68+
impl ExecuteCommand for IpAddrCommandHandler {
69+
fn execute(&mut self, args: Vec<String>) -> Pin<Box<dyn Future<Output = anyhow::Result<CommandStatus>> + '_>> {
70+
let valid = Validator::validate(args.clone(), vec![
71+
CommandArgInfo::new_with_name(CommandArgType::Custom, "ip"),
72+
]);
73+
if let Err(e) = valid {
74+
return Box::pin(IpAddrCommandHandler::resolved(Err(e)));
75+
}
76+
77+
let ip = args[0].parse();
78+
79+
match ip {
80+
Ok(ip) => Box::pin(self.handle_command(ip)),
81+
Err(e) => Box::pin(IpAddrCommandHandler::resolved(Err(ArgsError::WrongArgumentValue {
82+
argument: args[0].clone(),
83+
error: e.to_string(),
84+
})))
85+
}
86+
}
87+
}
88+
89+
90+
#[tokio::main]
91+
async fn main() -> anyhow::Result<()> {
892
#[rustfmt::skip]
993
let mut repl = Repl::builder()
10-
.add("ls", command! {
11-
"List files in a directory",
12-
(dir: PathBuf) => |dir: PathBuf| {
13-
for entry in dir.read_dir()? {
14-
println!("{}", entry?.path().to_string_lossy());
15-
}
16-
Ok(CommandStatus::Done)
17-
}
94+
.add("ls", NewCommand {
95+
description: "List files in a directory".into(),
96+
args_info: vec![CommandArgInfo::new_with_name(CommandArgType::Custom, "dir")],
97+
handler: Box::new(LsCommandHandler::new()),
1898
})
19-
.add("ipaddr", command! {
20-
"Just parse and print the given IP address",
21-
(ip: IpAddr) => |ip: IpAddr| {
22-
println!("{}", ip);
23-
Ok(CommandStatus::Done)
24-
}
99+
.add("ipaddr", NewCommand {
100+
description: "Just parse and print the given IP address".into(),
101+
args_info: vec![CommandArgInfo::new_with_name(CommandArgType::Custom, "ip")],
102+
handler: Box::new(IpAddrCommandHandler::new()),
25103
})
26104
.build()
27105
.context("Failed to create repl")?;
28106

29-
repl.run().context("Critical REPL error")
107+
repl.run().await.context("Critical REPL error")
30108
}

0 commit comments

Comments
 (0)