use crate::entities;
use crate::errors::TodoErrors;
use crate::serializers::{to_json, to_pretty_json};
use crate::traits::Controller;
use clap::{Args, Command, Parser, Subcommand};
use clap_complete::{generate, Generator, Shell};
use std::io;
pub struct CliState<T>
where
T: Controller,
{
controller: T,
}
impl<T> CliState<T>
where
T: Controller,
{
pub fn new(controller: T) -> Self {
Self { controller }
}
}
pub async fn handle_local<T>(local: Local, state: CliState<T>)
where
T: Controller<
Input = entities::TodoWrite,
Output = entities::TodoRead,
Id = entities::Id,
OptionalInput = entities::TodoUpdate,
>,
{
match local {
Local::Create(Create { title, done }) => {
let todos = title
.into_iter()
.map(|title| entities::TodoWrite::new(title, done))
.collect::<Vec<_>>();
match state.controller.create_batch(todos).await {
Ok(todo) => {
let todo = to_json(&todo).unwrap();
println!("{}", todo);
}
Err(TodoErrors::BatchTooLarge { max_size }) => {
eprintln!("Batch too large, max batch size is {}", max_size);
}
Err(err) => {
eprintln!("Failed to create todo: {:?}", err);
}
}
}
Local::Delete(Delete { id }) => match state.controller.delete(id).await {
Ok(_) => println!("Successfully deleted: {}", id),
Err(err) => {
eprintln!("Failed to delete todo: {:?}", err);
}
},
Local::Get(Get { id, pretty }) => match state.controller.get(id).await {
Ok(todo) => {
let printer = if pretty { to_pretty_json } else { to_json };
let todo = printer(&todo).unwrap();
println!("{}", todo)
}
Err(err) => {
eprintln!("Failed to get todo: {:?}", err);
}
},
Local::List(List {
offset,
limit,
pretty,
}) => match state
.controller
.list(entities::ListRequest { offset, limit })
.await
{
Ok(todos) => {
let printer = if pretty { to_pretty_json } else { to_json };
let todos = printer(&todos).unwrap();
println!("{}", todos)
}
Err(err) => {
eprintln!("Failed to list todos: {:?}", err);
}
},
Local::Update(Update {
id,
title,
done,
undone,
}) => {
let done = if let Some(undone) = undone {
Some(!undone)
} else {
done
};
let todo = entities::TodoUpdate::new(title, done);
match state.controller.update(id, todo).await {
Ok(_) => println!("Successfully updated: {}", id),
Err(err) => {
eprintln!("Failed to update todo: {:?}", err);
}
}
}
Local::Completion(Completion { shell }) => {
print_completions(shell);
}
}
}
#[derive(Parser, Debug)]
#[clap(author, about, version, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(subcommand)]
Serve(Serve),
#[command(flatten)]
Local(Local),
}
#[derive(Subcommand, Debug)]
pub enum Local {
Create(Create),
Delete(Delete),
List(List),
Update(Update),
Get(Get),
Completion(Completion),
}
#[derive(Subcommand, Debug)]
pub enum Serve {
Grpc(GrpcServerAddr),
Http(HttpServerAddr),
}
#[derive(Args, Debug)]
pub struct HttpServerAddr {
#[arg(short, long, default_value_t = 8080)]
#[arg(value_parser = clap::value_parser!(u16).range(1..))]
pub port: u16,
#[arg(short = 'H')]
#[arg(long, default_value = "127.0.0.1")]
#[arg(value_parser = clap::value_parser!(std::net::IpAddr))]
pub host: std::net::IpAddr,
}
#[derive(Args, Debug)]
pub struct GrpcServerAddr {
#[arg(short, long, default_value_t = 50051)]
#[arg(value_parser = clap::value_parser!(u16).range(1..))]
pub port: u16,
#[arg(short = 'H')]
#[arg(long, default_value = "127.0.0.1")]
#[arg(value_parser = clap::value_parser!(std::net::IpAddr))]
pub host: std::net::IpAddr,
}
#[derive(Args, Debug)]
pub struct Create {
#[arg(action = clap::ArgAction::Append)]
pub title: Vec<String>,
#[arg(short, long)]
#[arg(default_value = "false")]
#[arg(action = clap::ArgAction::SetTrue)]
pub done: Option<bool>,
}
#[derive(Args, Debug)]
pub struct Delete {
pub id: entities::Id,
}
#[derive(Args, Debug)]
pub struct List {
#[arg(short, long)]
pub offset: Option<entities::Id>,
#[arg(short, long)]
pub limit: Option<entities::Id>,
#[arg(short, long)]
#[arg(action = clap::ArgAction::SetTrue)]
pub pretty: bool,
}
#[derive(Args, Debug)]
pub struct Update {
pub id: entities::Id,
#[arg(short, long)]
pub title: Option<String>,
#[arg(short, long)]
#[arg(group = "finished")]
#[arg(action = clap::ArgAction::SetTrue)]
pub done: Option<bool>,
#[arg(short, long)]
#[arg(group = "finished")]
#[arg(action = clap::ArgAction::SetTrue)]
pub undone: Option<bool>,
}
#[derive(Args, Debug)]
pub struct Get {
pub id: entities::Id,
#[arg(short, long)]
#[arg(action = clap::ArgAction::SetTrue)]
pub pretty: bool,
}
#[derive(Args, Debug)]
pub struct Completion {
pub shell: Shell,
}
fn generate_completions<G: Generator>(gen: G, cmd: &mut Command) {
generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout());
}
fn print_completions(shell: Shell) {
use clap::CommandFactory;
let mut cmd = Cli::command();
generate_completions(shell, &mut cmd);
}
#[cfg(test)]
mod test {
use super::*;
use clap::CommandFactory;
#[tokio::test]
async fn verify_cli_bare_args() {
Cli::command().debug_assert()
}
#[tokio::test]
async fn create_todo_only_title_provided() {
let args = vec!["todors", "create", "Hello Rust!"];
if let Commands::Local(Local::Create(Create {
title,
done: Some(false),
})) = Cli::parse_from(args).command
{
assert_eq!(title[0], "Hello Rust!");
}
}
#[tokio::test]
async fn delete_todo_by_id() {
let args = vec!["todors", "delete", "1"];
if let Commands::Local(Local::Delete(Delete { id })) = Cli::parse_from(args).command {
assert_eq!(id, 1);
}
}
#[tokio::test]
async fn update_todo_modify_title_only() {
let args = vec!["todors", "update", "1", "--title", "Hello Rust!"];
if let Commands::Local(Local::Update(Update { id, title, .. })) =
Cli::parse_from(args).command
{
assert_eq!(id, 1);
assert_eq!(title.unwrap(), "Hello Rust!");
}
}
#[tokio::test]
async fn update_todo_without_any_modification() {
let args = vec!["todors", "update", "1"];
if let Commands::Local(Local::Update(Update { id, title, .. })) =
Cli::parse_from(args).command
{
assert_eq!(id, 1);
assert_eq!(title, None);
}
}
#[tokio::test]
async fn delete_todo_non_int_id_raises_error() {
let args = vec!["todors", "delete", "foo"];
let c = Cli::try_parse_from(args);
assert!(c.is_err());
}
#[tokio::test]
async fn update_todo_non_int_id_raises_error() {
let args = vec!["todors", "update", "foo", "--title", "Hello Rust!"];
let c = Cli::try_parse_from(args);
assert!(c.is_err());
}
}