feat: implement ping command"
This commit is contained in:
1 parent
fb0bbae1f0
commit
8045814b3b
3 files changed
+313
-10
No files matched your search
+10
-1
@@ -1,4 +1,4 @@
|
|||||||
use clap::Parser;
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Parser)]
|
#[derive(Debug, PartialEq, Parser)]
|
||||||
#[command(name = "onx")]
|
#[command(name = "onx")]
|
||||||
@@ -22,4 +22,13 @@ pub struct Cli {
|
|||||||
#[arg(help = "Pretty-print JSON output")]
|
#[arg(help = "Pretty-print JSON output")]
|
||||||
#[arg(default_value_t = false)]
|
#[arg(default_value_t = false)]
|
||||||
pub pretty: bool,
|
pub pretty: bool,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Subcommand)]
|
||||||
|
pub enum Command {
|
||||||
|
#[command(about = "Check connectivity to the Onspring API")]
|
||||||
|
Ping,
|
||||||
}
|
}
|
||||||
+273
-8
@@ -2,9 +2,22 @@ mod cli;
|
|||||||
mod output;
|
mod output;
|
||||||
|
|
||||||
use clap::{Parser, error::ErrorKind};
|
use clap::{Parser, error::ErrorKind};
|
||||||
use cli::Cli;
|
use cli::{Cli, Command};
|
||||||
|
|
||||||
use output::render_error;
|
use onspring::{OnspringClient, OnspringError};
|
||||||
|
use output::{SuccessResponse, render_error, write_json};
|
||||||
|
|
||||||
|
pub type CliResult<T> = Result<T, CliError>;
|
||||||
|
|
||||||
|
pub trait OnspringRunner {
|
||||||
|
fn ping(&self) -> impl std::future::Future<Output = Result<(), OnspringError>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OnspringRunner for OnspringClient {
|
||||||
|
async fn ping(&self) -> Result<(), OnspringError> {
|
||||||
|
self.ping().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct CliError {
|
pub struct CliError {
|
||||||
@@ -35,9 +48,59 @@ impl CliError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type CliResult<T> = Result<T, CliError>;
|
impl From<OnspringError> for CliError {
|
||||||
|
fn from(value: OnspringError) -> Self {
|
||||||
|
match value {
|
||||||
|
OnspringError::InvalidArgument(message) => Self::usage(format!("Invalid request: {message}")),
|
||||||
|
OnspringError::Api {
|
||||||
|
status_code,
|
||||||
|
message,
|
||||||
|
} => {
|
||||||
|
if message.is_empty() {
|
||||||
|
Self::runtime(format!("API request failed with status {status_code}."))
|
||||||
|
} else {
|
||||||
|
Self::runtime(format!("API request failed ({status_code}): {message}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OnspringError::Http(_) => Self::runtime("Network request failed."),
|
||||||
|
OnspringError::Serialization(_) => Self::runtime("Failed to parse API response."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(cli: Cli) -> CliResult<()> {
|
||||||
|
let api_key = cli
|
||||||
|
.api_key
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| CliError::usage("Missing API Key. Set --api-key or ONSPRING_API_KEY."))?;
|
||||||
|
|
||||||
|
let mut builder = OnspringClient::builder(api_key.to_string());
|
||||||
|
|
||||||
|
if let Some(base_url) = &cli.base_url {
|
||||||
|
if base_url.trim().is_empty() {
|
||||||
|
return Err(CliError::usage("Base URL cannot be empty."));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder = builder.base_url(base_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = builder.build();
|
||||||
|
let mut stdout = std::io::stdout();
|
||||||
|
run_with_client(&cli, &client, &mut stdout).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_with_client<C: OnspringRunner, W: std::io::Write>(
|
||||||
|
cli: &Cli,
|
||||||
|
client: &C,
|
||||||
|
writer: &mut W,
|
||||||
|
) -> CliResult<()> {
|
||||||
|
match cli.command {
|
||||||
|
Command::Ping => {
|
||||||
|
client.ping().await?;
|
||||||
|
write_json(writer, &SuccessResponse { ok: true }, cli.pretty)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run(_cli: Cli) -> CliResult<()> {
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,20 +124,60 @@ pub fn render_cli_error(err: &CliError, pretty: bool) -> String {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
struct MockClient {
|
||||||
|
ping_result: Result<(), OnspringError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OnspringRunner for MockClient {
|
||||||
|
async fn ping(&self) -> Result<(), OnspringError> {
|
||||||
|
match &self.ping_result {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(OnspringError::InvalidArgument(msg)) => {
|
||||||
|
Err(OnspringError::InvalidArgument(msg.clone()))
|
||||||
|
}
|
||||||
|
Err(OnspringError::Api {
|
||||||
|
status_code,
|
||||||
|
message,
|
||||||
|
}) => Err(OnspringError::Api {
|
||||||
|
status_code: *status_code,
|
||||||
|
message: message.clone(),
|
||||||
|
}),
|
||||||
|
Err(OnspringError::Serialization(_)) => {
|
||||||
|
let serde_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
|
||||||
|
Err(OnspringError::Serialization(serde_err))
|
||||||
|
}
|
||||||
|
Err(OnspringError::Http(_)) => {
|
||||||
|
Err(OnspringError::InvalidArgument("http error".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_cli_from_when_called_with_valid_program_name_it_should_return_cli() {
|
fn parse_cli_from_when_called_with_valid_subcommand_it_should_return_cli() {
|
||||||
let result = parse_cli_from(["test"]);
|
let result = parse_cli_from(["test", "ping"]);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
result,
|
||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
pretty: false
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_without_subcommand_it_should_return_usage_error() {
|
||||||
|
let result = parse_cli_from(["test"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
assert!(err.message.contains("Usage: test [OPTIONS] <COMMAND>"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_cli_from_when_called_with_unknown_flag_it_should_return_usage_error() {
|
fn parse_cli_from_when_called_with_unknown_flag_it_should_return_usage_error() {
|
||||||
let result = parse_cli_from(["test", "--bogus"]);
|
let result = parse_cli_from(["test", "--bogus"]);
|
||||||
@@ -125,5 +228,167 @@ mod tests {
|
|||||||
r#"{"error":{"code":1,"message":"Something went wrong"}}"#
|
r#"{"error":{"code":1,"message":"Something went wrong"}}"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_error_constructors_should_set_expected_codes_and_messages() {
|
||||||
|
let info = CliError::info("info message");
|
||||||
|
assert_eq!(info.code, 0);
|
||||||
|
assert_eq!(info.message, "info message");
|
||||||
|
|
||||||
|
let runtime = CliError::runtime("runtime message");
|
||||||
|
assert_eq!(runtime.code, 1);
|
||||||
|
assert_eq!(runtime.message, "runtime message");
|
||||||
|
|
||||||
|
let usage = CliError::usage("usage message");
|
||||||
|
assert_eq!(usage.code, 2);
|
||||||
|
assert_eq!(usage.message, "usage message");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_error_from_onspring_error_invalid_argument() {
|
||||||
|
let err = OnspringError::InvalidArgument("bad param".to_string());
|
||||||
|
let cli_err = CliError::from(err);
|
||||||
|
|
||||||
|
assert_eq!(cli_err.code, 2);
|
||||||
|
assert_eq!(cli_err.message, "Invalid request: bad param");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_error_from_onspring_error_api_with_message() {
|
||||||
|
let err = OnspringError::Api {
|
||||||
|
status_code: 404,
|
||||||
|
message: "Resource not found".to_string(),
|
||||||
|
};
|
||||||
|
let cli_err = CliError::from(err);
|
||||||
|
|
||||||
|
assert_eq!(cli_err.code, 1);
|
||||||
|
assert_eq!(
|
||||||
|
cli_err.message,
|
||||||
|
"API request failed (404): Resource not found"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_error_from_onspring_error_api_with_empty_message() {
|
||||||
|
let err = OnspringError::Api {
|
||||||
|
status_code: 500,
|
||||||
|
message: String::new(),
|
||||||
|
};
|
||||||
|
let cli_err = CliError::from(err);
|
||||||
|
|
||||||
|
assert_eq!(cli_err.code, 1);
|
||||||
|
assert_eq!(cli_err.message, "API request failed with status 500.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_error_from_onspring_error_serialization() {
|
||||||
|
let serde_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
|
||||||
|
let err = OnspringError::Serialization(serde_err);
|
||||||
|
let cli_err = CliError::from(err);
|
||||||
|
|
||||||
|
assert_eq!(cli_err.code, 1);
|
||||||
|
assert_eq!(cli_err.message, "Failed to parse API response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn run_when_missing_api_key_it_should_return_usage_error() {
|
||||||
|
let cli = Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = run(cli).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage(
|
||||||
|
"Missing API Key. Set --api-key or ONSPRING_API_KEY."
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn run_when_empty_base_url_it_should_return_usage_error() {
|
||||||
|
let cli = Cli {
|
||||||
|
api_key: Some("test-api-key".to_string()),
|
||||||
|
base_url: Some(" ".to_string()),
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = run(cli).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Err(CliError::usage("Base URL cannot be empty.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn run_with_client_when_ping_succeeds_it_should_write_success_response() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
ping_result: Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let cli = Cli {
|
||||||
|
api_key: Some("key".to_string()),
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = run_with_client(&cli, &mock_client, &mut buffer).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(written.trim(), r#"{"ok":true}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn run_with_client_when_ping_succeeds_pretty_it_should_write_pretty_json() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
ping_result: Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let cli = Cli {
|
||||||
|
api_key: Some("key".to_string()),
|
||||||
|
base_url: None,
|
||||||
|
pretty: true,
|
||||||
|
command: Command::Ping,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = run_with_client(&cli, &mock_client, &mut buffer).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(written.trim(), "{\n \"ok\": true\n}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn run_with_client_when_ping_fails_it_should_return_mapped_cli_error() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
ping_result: Err(OnspringError::InvalidArgument("Invalid Key".to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let cli = Cli {
|
||||||
|
api_key: Some("key".to_string()),
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = run_with_client(&cli, &mock_client, &mut buffer).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Err(CliError::usage("Invalid request: Invalid Key")));
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-1
@@ -1,3 +1,5 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{CliError, CliResult};
|
use crate::{CliError, CliResult};
|
||||||
@@ -13,6 +15,12 @@ struct ErrorEnvelope<'a> {
|
|||||||
pub error: ErrorBody<'a>,
|
pub error: ErrorBody<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SuccessResponse {
|
||||||
|
pub ok: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_json<T: Serialize>(value: &T, pretty: bool) -> CliResult<String> {
|
pub fn render_json<T: Serialize>(value: &T, pretty: bool) -> CliResult<String> {
|
||||||
if pretty {
|
if pretty {
|
||||||
serde_json::to_string_pretty(value).map_err(|_| CliError::runtime("Failed to serialize output"))
|
serde_json::to_string_pretty(value).map_err(|_| CliError::runtime("Failed to serialize output"))
|
||||||
@@ -32,6 +40,16 @@ pub fn render_error(err: &CliError, pretty: bool) -> CliResult<String> {
|
|||||||
render_json(&envelope, pretty)
|
render_json(&envelope, pretty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn write_json<W: Write, T: Serialize>(
|
||||||
|
writer: &mut W,
|
||||||
|
value: &T,
|
||||||
|
pretty: bool,
|
||||||
|
) -> CliResult<()> {
|
||||||
|
let rendered = render_json(value, pretty)?;
|
||||||
|
writeln!(writer, "{rendered}").map_err(|_| CliError::runtime("Failed to write output"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -75,5 +93,16 @@ mod tests {
|
|||||||
));
|
));
|
||||||
assert_eq!(result, expected);
|
assert_eq!(result, expected);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_json_when_called_with_writer_it_should_write_formatted_string() {
|
||||||
|
let data = json!({"ok": true});
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = write_json(&mut buffer, &data, false);
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let output_str = String::from_utf8(buffer).unwrap();
|
||||||
|
assert_eq!(output_str, "{\"ok\":true}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user