Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(meroctl): standardise command output #888

Merged
merged 8 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading