Skip to content

Commit

Permalink
feat(meroctl): standardise command output (#888)
Browse files Browse the repository at this point in the history
  • Loading branch information
fbozic authored Oct 31, 2024
1 parent deeea22 commit a2daa55
Show file tree
Hide file tree
Showing 33 changed files with 900 additions and 713 deletions.
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,6 @@ clone_on_ref_ptr = "deny"
empty_enum_variants_with_brackets = "deny"
empty_structs_with_brackets = "deny"
error_impl_error = "deny"
exhaustive_enums = "deny"
exhaustive_structs = "deny"
#expect_used = "deny" TODO: Enable as soon as possible
float_cmp_const = "deny"
fn_to_numeric_cast_any = "deny"
Expand All @@ -236,8 +234,6 @@ lossy_float_literal = "deny"
mem_forget = "deny"
multiple_inherent_impl = "deny"
#panic = "deny" TODO: Enable as soon as possible
print_stderr = "deny"
print_stdout = "deny"
rc_mutex = "deny"
renamed_function_params = "deny"
try_err = "deny"
Expand Down
3 changes: 1 addition & 2 deletions crates/meroctl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ notify.workspace = true
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["io-std", "macros"] }
tokio-tungstenite.workspace = true
tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
url = { workspace = true, features = ["serde"] }

calimero-config = { path = "../config" }
Expand Down
87 changes: 81 additions & 6 deletions crates/meroctl/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
use std::process::ExitCode;

use camino::Utf8PathBuf;
use clap::{Parser, Subcommand};
use const_format::concatcp;
use eyre::Result as EyreResult;
use eyre::Report as EyreReport;
use serde::{Serialize, Serializer};
use thiserror::Error as ThisError;

use crate::defaults;
use crate::output::{Format, Output, Report};

mod app;
mod context;
Expand Down Expand Up @@ -54,14 +59,84 @@ pub struct RootArgs {
/// Name of node
#[arg(short, long, value_name = "NAME")]
pub node_name: String,

#[arg(long, value_name = "FORMAT")]
pub output_format: Format,
}

pub struct Environment {
pub args: RootArgs,
pub output: Output,
}

impl Environment {
pub fn new(args: RootArgs, output: Output) -> Self {
Environment { args, output }
}
}

impl RootCommand {
pub async fn run(self) -> EyreResult<()> {
match self.action {
SubCommands::Context(context) => context.run(self.args).await,
SubCommands::App(application) => application.run(self.args).await,
SubCommands::JsonRpc(jsonrpc) => jsonrpc.run(self.args).await,
pub async fn run(self) -> Result<(), CliError> {
let output = Output::new(self.args.output_format);
let environment = Environment::new(self.args, output);

let result = match self.action {
SubCommands::Context(context) => context.run(&environment).await,
SubCommands::App(application) => application.run(&environment).await,
SubCommands::JsonRpc(jsonrpc) => jsonrpc.run(&environment).await,
};

if let Err(err) = result {
let err = match err.downcast::<ApiError>() {
Ok(err) => CliError::ApiError(err),
Err(err) => CliError::Other(err),
};
environment.output.write(&err);
return Err(err);
}

return Ok(());
}
}

#[derive(Debug, Serialize, ThisError)]
pub enum CliError {
#[error(transparent)]
ApiError(#[from] ApiError),

#[error(transparent)]
Other(
#[from]
#[serde(serialize_with = "serialize_eyre_report")]
EyreReport,
),
}

impl Into<ExitCode> for CliError {
fn into(self) -> ExitCode {
match self {
CliError::ApiError(_) => ExitCode::from(101),
CliError::Other(_) => ExitCode::FAILURE,
}
}
}

impl Report for CliError {
fn report(&self) {
println!("{}", self);
}
}

#[derive(Debug, Serialize, ThisError)]
#[error("{status_code}: {message}")]
pub struct ApiError {
pub status_code: u16,
pub message: String,
}

fn serialize_eyre_report<S>(report: &EyreReport, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&report)
}
25 changes: 20 additions & 5 deletions crates/meroctl/src/cli/app.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use calimero_primitives::application::Application;
use clap::{Parser, Subcommand};
use const_format::concatcp;
use eyre::Result as EyreResult;

use super::RootArgs;
use crate::cli::app::get::GetCommand;
use crate::cli::app::install::InstallCommand;
use crate::cli::app::list::ListCommand;
use crate::cli::Environment;
use crate::output::Report;

mod get;
mod install;
Expand Down Expand Up @@ -38,12 +40,25 @@ pub enum AppSubCommands {
List(ListCommand),
}

impl Report for Application {
fn report(&self) {
println!("id: {}", self.id);
println!("size: {}", self.size);
println!("blobId: {}", self.blob);
println!("source: {}", self.source);
println!("metadata:");
for item in &self.metadata {
println!(" {:?}", item);
}
}
}

impl AppCommand {
pub async fn run(self, args: RootArgs) -> EyreResult<()> {
pub async fn run(self, environment: &Environment) -> EyreResult<()> {
match self.subcommand {
AppSubCommands::Get(get) => get.run(args).await,
AppSubCommands::Install(install) => install.run(args).await,
AppSubCommands::List(list) => list.run(args).await,
AppSubCommands::Get(get) => get.run(environment).await,
AppSubCommands::Install(install) => install.run(environment).await,
AppSubCommands::List(list) => list.run(environment).await,
}
}
}
37 changes: 24 additions & 13 deletions crates/meroctl/src/cli/app/get.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use clap::Parser;
use eyre::{bail, Result as EyreResult};
use calimero_server_primitives::admin::GetApplicationResponse;
use clap::{Parser, ValueEnum};
use eyre::Result as EyreResult;
use reqwest::Client;

use crate::cli::RootArgs;
use crate::common::{fetch_multiaddr, get_response, load_config, multiaddr_to_url, RequestType};
use crate::cli::Environment;
use crate::common::{do_request, fetch_multiaddr, load_config, multiaddr_to_url, RequestType};
use crate::output::Report;

#[derive(Parser, Debug)]
#[command(about = "Fetch application details")]
Expand All @@ -12,17 +14,30 @@ pub struct GetCommand {
pub app_id: String,
}

#[derive(ValueEnum, Debug, Clone)]
pub enum GetValues {
Details,
}

impl Report for GetApplicationResponse {
fn report(&self) {
match self.data.application {
Some(ref application) => application.report(),
None => println!("No application found"),
}
}
}

impl GetCommand {
#[expect(clippy::print_stdout, reason = "Acceptable for CLI")]
pub async fn run(self, args: RootArgs) -> EyreResult<()> {
let config = load_config(&args.home, &args.node_name)?;
pub async fn run(self, environment: &Environment) -> EyreResult<()> {
let config = load_config(&environment.args.home, &environment.args.node_name)?;

let url = multiaddr_to_url(
fetch_multiaddr(&config)?,
&format!("admin-api/dev/applications/{}", self.app_id),
)?;

let response = get_response(
let response: GetApplicationResponse = do_request(
&Client::new(),
url,
None::<()>,
Expand All @@ -31,11 +46,7 @@ impl GetCommand {
)
.await?;

if !response.status().is_success() {
bail!("Request failed with status: {}", response.status())
}

println!("{}", response.text().await?);
environment.output.write(&response);

Ok(())
}
Expand Down
60 changes: 26 additions & 34 deletions crates/meroctl/src/cli/app/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ use camino::Utf8PathBuf;
use clap::Parser;
use eyre::{bail, Result};
use reqwest::Client;
use tracing::info;
use url::Url;

use crate::cli::RootArgs;
use crate::common::{fetch_multiaddr, get_response, load_config, multiaddr_to_url, RequestType};
use crate::cli::Environment;
use crate::common::{do_request, fetch_multiaddr, load_config, multiaddr_to_url, RequestType};
use crate::output::Report;

#[derive(Debug, Parser)]
#[command(about = "Install an application")]
Expand All @@ -28,26 +28,35 @@ pub struct InstallCommand {
pub hash: Option<Hash>,
}

impl Report for InstallApplicationResponse {
fn report(&self) {
println!("id: {}", self.data.application_id);
}
}

impl InstallCommand {
pub async fn run(self, args: RootArgs) -> Result<()> {
let config = load_config(&args.home, &args.node_name)?;
pub async fn run(self, environment: &Environment) -> Result<()> {
let config = load_config(&environment.args.home, &environment.args.node_name)?;
let mut is_dev_installation = false;
let metadata = self.metadata.map(String::into_bytes).unwrap_or_default();

let install_request = if let Some(app_path) = self.path {
let install_dev_request =
InstallDevApplicationRequest::new(app_path.canonicalize_utf8()?, metadata);
let request = if let Some(app_path) = self.path {
is_dev_installation = true;
serde_json::to_value(install_dev_request)?
serde_json::to_value(InstallDevApplicationRequest::new(
app_path.canonicalize_utf8()?,
metadata,
))?
} else if let Some(app_url) = self.url {
let install_request =
InstallApplicationRequest::new(Url::parse(&app_url)?, self.hash, metadata);
serde_json::to_value(install_request)?
serde_json::to_value(InstallApplicationRequest::new(
Url::parse(&app_url)?,
self.hash,
metadata,
))?
} else {
bail!("Either path or url must be provided");
};

let install_url = multiaddr_to_url(
let url = multiaddr_to_url(
fetch_multiaddr(&config)?,
if is_dev_installation {
"admin-api/dev/install-dev-application"
Expand All @@ -56,33 +65,16 @@ impl InstallCommand {
},
)?;

let install_response = get_response(
let response: InstallApplicationResponse = do_request(
&Client::new(),
install_url,
Some(install_request),
url,
Some(request),
&config.identity,
RequestType::Post,
)
.await?;

if !install_response.status().is_success() {
let status = install_response.status();
let error_text = install_response.text().await?;
bail!(
"Application installation failed with status: {}. Error: {}",
status,
error_text
)
}

let body = install_response
.json::<InstallApplicationResponse>()
.await?;

info!(
"Application installed successfully. Application ID: {}",
body.data.application_id
);
environment.output.write(&response);

Ok(())
}
Expand Down
Loading

0 comments on commit a2daa55

Please sign in to comment.