Compare commits
2
Commits
30a6826f46
...
56b5abc1d1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56b5abc1d1 | ||
|
|
54346ff20f |
No files matched your search
+547
-9
@@ -1,4 +1,6 @@
|
|||||||
use clap::{Args, Parser, Subcommand};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Parser)]
|
#[derive(Debug, PartialEq, Parser)]
|
||||||
#[command(name = "onx")]
|
#[command(name = "onx")]
|
||||||
@@ -31,11 +33,24 @@ pub struct Cli {
|
|||||||
pub enum Command {
|
pub enum Command {
|
||||||
#[command(about = "Check connectivity to the Onspring API")]
|
#[command(about = "Check connectivity to the Onspring API")]
|
||||||
Ping,
|
Ping,
|
||||||
|
|
||||||
#[command(about = "Perform operations against the apps in the instance")]
|
#[command(about = "Perform operations against the apps in the instance")]
|
||||||
Apps {
|
Apps {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: AppsCommand,
|
command: AppsCommand,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[command(about = "Perform operations against the fields in the instance")]
|
||||||
|
Fields {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: FieldsCommand,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(about = "Perform operations against the records in the instance")]
|
||||||
|
Records {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: RecordsCommand,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Subcommand)]
|
#[derive(Debug, PartialEq, Subcommand)]
|
||||||
@@ -45,22 +60,146 @@ pub enum AppsCommand {
|
|||||||
|
|
||||||
#[command(about = "Get information about an app")]
|
#[command(about = "Get information about an app")]
|
||||||
Get {
|
Get {
|
||||||
#[arg(long)]
|
#[arg(long = "app-id")]
|
||||||
#[arg(short = 'i')]
|
#[arg(short = 'i')]
|
||||||
|
#[arg(alias = "app")]
|
||||||
#[arg(help = "App id to get")]
|
#[arg(help = "App id to get")]
|
||||||
app_id: i32,
|
app_id: i32,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[command(about = "Get information for a batch of apps")]
|
#[command(about = "Get information for a batch of apps")]
|
||||||
BatchGet {
|
BatchGet {
|
||||||
#[arg(long)]
|
#[arg(long = "ids")]
|
||||||
#[arg(value_delimiter = ',')]
|
#[arg(value_delimiter = ',')]
|
||||||
#[arg(short = 'i')]
|
#[arg(short = 'i')]
|
||||||
|
#[arg(alias = "apps")]
|
||||||
#[arg(help = "Comma-separated list of app ids to get")]
|
#[arg(help = "Comma-separated list of app ids to get")]
|
||||||
ids: Vec<i32>,
|
ids: Vec<i32>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Subcommand)]
|
||||||
|
pub enum FieldsCommand {
|
||||||
|
#[command(about = "Get information for a list of fields")]
|
||||||
|
List {
|
||||||
|
#[arg(long = "app-id")]
|
||||||
|
#[arg(short = 'i')]
|
||||||
|
#[arg(alias = "app")]
|
||||||
|
#[arg(help = "App id whose fields will be retrieved")]
|
||||||
|
app_id: i32,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
paging: PagingArgs,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(about = "Get information about a field")]
|
||||||
|
Get {
|
||||||
|
#[arg(long = "field-id")]
|
||||||
|
#[arg(short = 'i')]
|
||||||
|
#[arg(alias = "field")]
|
||||||
|
#[arg(help = "Field id to get")]
|
||||||
|
field_id: i32,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(about = "Get information for a batch of fields")]
|
||||||
|
BatchGet {
|
||||||
|
#[arg(long = "ids")]
|
||||||
|
#[arg(value_delimiter = ',')]
|
||||||
|
#[arg(short = 'i')]
|
||||||
|
#[arg(alias = "fields")]
|
||||||
|
#[arg(help = "Comma-separated list of field ids to get")]
|
||||||
|
ids: Vec<i32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Subcommand)]
|
||||||
|
pub enum RecordsCommand {
|
||||||
|
#[command(about = "Get information for a list of records")]
|
||||||
|
List {
|
||||||
|
#[arg(long = "app")]
|
||||||
|
#[arg(short = 'a')]
|
||||||
|
#[arg(help = "App id whose records will be retrieved")]
|
||||||
|
app_id: i32,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
paging: PagingArgs,
|
||||||
|
|
||||||
|
#[arg(long = "fields")]
|
||||||
|
#[arg(value_delimiter = ',')]
|
||||||
|
#[arg(short = 'f')]
|
||||||
|
#[arg(help = "Comma-separated list of fields whose data will be included in the response")]
|
||||||
|
field_ids: Vec<i32>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
data_format: Option<DataFormatArg>,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(about = "Get information for a record")]
|
||||||
|
Get {
|
||||||
|
#[arg(long = "app")]
|
||||||
|
#[arg(short = 'a')]
|
||||||
|
#[arg(help = "App id for record to be retrieved")]
|
||||||
|
app_id: i32,
|
||||||
|
|
||||||
|
#[arg(long = "record")]
|
||||||
|
#[arg(short = 'r')]
|
||||||
|
#[arg(help = "Record id for record to be retrieved")]
|
||||||
|
record_id: i32,
|
||||||
|
|
||||||
|
#[arg(long = "fields")]
|
||||||
|
#[arg(value_delimiter = ',')]
|
||||||
|
#[arg(short = 'f')]
|
||||||
|
#[arg(help = "Comma-separated list of fields whose data will be included in the response")]
|
||||||
|
field_ids: Vec<i32>,
|
||||||
|
|
||||||
|
#[arg(long = "format")]
|
||||||
|
#[arg(short = 'd')]
|
||||||
|
#[arg(help = "Format of data in response")]
|
||||||
|
data_format: Option<DataFormatArg>,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(about = "Add or update a record")]
|
||||||
|
#[command()]
|
||||||
|
Save {
|
||||||
|
#[command(flatten)]
|
||||||
|
body: BodySource,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(about = "Delete a record")]
|
||||||
|
Delete {
|
||||||
|
#[arg(long = "app")]
|
||||||
|
#[arg(short = 'a')]
|
||||||
|
#[arg(help = "App id for record to delete")]
|
||||||
|
app_id: i32,
|
||||||
|
|
||||||
|
#[arg(long = "record")]
|
||||||
|
#[arg(short = 'r')]
|
||||||
|
#[arg(help = "Record id for record to delete")]
|
||||||
|
record_id: i32,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(about = "Get information for a batch of records")]
|
||||||
|
BatchGet {
|
||||||
|
#[command(flatten)]
|
||||||
|
body: BodySource,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(about = "Get information for a list of records based on a query")]
|
||||||
|
Query {
|
||||||
|
#[command(flatten)]
|
||||||
|
body: BodySource,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
paging: PagingArgs,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(about = "Delete a batch of records")]
|
||||||
|
BatchDelete {
|
||||||
|
#[command(flatten)]
|
||||||
|
body: BodySource,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Args, Default)]
|
#[derive(Debug, PartialEq, Clone, Args, Default)]
|
||||||
pub struct PagingArgs {
|
pub struct PagingArgs {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -74,13 +213,36 @@ pub struct PagingArgs {
|
|||||||
pub page_size: Option<i32>,
|
pub page_size: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Args, Default)]
|
||||||
|
pub struct BodySource {
|
||||||
|
#[arg(long)]
|
||||||
|
#[arg(short = 'j')]
|
||||||
|
#[arg(help = "Inline JSON request body")]
|
||||||
|
pub json: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
#[arg(short = 'f')]
|
||||||
|
#[arg(help = "Path to a JSON request body file")]
|
||||||
|
pub file: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
#[arg(help = "Read JSON request body from stdin")]
|
||||||
|
pub stdin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy, ValueEnum)]
|
||||||
|
pub enum DataFormatArg {
|
||||||
|
Raw,
|
||||||
|
Formatted,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_cli_from<I, T>(args: I) -> crate::error::CliResult<Cli>
|
pub fn parse_cli_from<I, T>(args: I) -> crate::error::CliResult<Cli>
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = T>,
|
I: IntoIterator<Item = T>,
|
||||||
T: Into<std::ffi::OsString> + Clone,
|
T: Into<std::ffi::OsString> + Clone,
|
||||||
{
|
{
|
||||||
use clap::error::ErrorKind;
|
|
||||||
use crate::error::CliError;
|
use crate::error::CliError;
|
||||||
|
use clap::error::ErrorKind;
|
||||||
|
|
||||||
Cli::try_parse_from(args).map_err(|e| match e.kind() {
|
Cli::try_parse_from(args).map_err(|e| match e.kind() {
|
||||||
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => CliError::info(e.to_string()),
|
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => CliError::info(e.to_string()),
|
||||||
@@ -206,16 +368,15 @@ mod tests {
|
|||||||
base_url: None,
|
base_url: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Apps {
|
command: Command::Apps {
|
||||||
command: AppsCommand::BatchGet {
|
command: AppsCommand::BatchGet { ids: vec![1, 2, 3] },
|
||||||
ids: vec![1, 2, 3],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_cli_from_when_called_with_apps_batch_get_without_ids_it_should_return_cli_with_empty_ids() {
|
fn parse_cli_from_when_called_with_apps_batch_get_without_ids_it_should_return_cli_with_empty_ids()
|
||||||
|
{
|
||||||
let result = parse_cli_from(["test", "apps", "batch-get"]);
|
let result = parse_cli_from(["test", "apps", "batch-get"]);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -232,7 +393,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_cli_from_when_called_with_apps_batch_get_invalid_id_format_it_should_return_usage_error() {
|
fn parse_cli_from_when_called_with_apps_batch_get_invalid_id_format_it_should_return_usage_error()
|
||||||
|
{
|
||||||
let result = parse_cli_from(["test", "apps", "batch-get", "--ids", "invalid"]);
|
let result = parse_cli_from(["test", "apps", "batch-get", "--ids", "invalid"]);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
@@ -284,6 +446,165 @@ mod tests {
|
|||||||
assert!(err.message.contains(env!("CARGO_PKG_DESCRIPTION")));
|
assert!(err.message.contains(env!("CARGO_PKG_DESCRIPTION")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_fields_list_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "fields", "list", "--app-id", "10"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Fields {
|
||||||
|
command: FieldsCommand::List {
|
||||||
|
app_id: 10,
|
||||||
|
paging: PagingArgs::default(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_fields_list_paging_args_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from([
|
||||||
|
"test",
|
||||||
|
"fields",
|
||||||
|
"list",
|
||||||
|
"-i",
|
||||||
|
"10",
|
||||||
|
"--page-number",
|
||||||
|
"2",
|
||||||
|
"--page-size",
|
||||||
|
"25",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Fields {
|
||||||
|
command: FieldsCommand::List {
|
||||||
|
app_id: 10,
|
||||||
|
paging: PagingArgs {
|
||||||
|
page_number: Some(2),
|
||||||
|
page_size: Some(25),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_fields_list_missing_app_id_it_should_return_usage_error() {
|
||||||
|
let result = parse_cli_from(["test", "fields", "list"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_fields_get_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "fields", "get", "--field-id", "123"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Fields {
|
||||||
|
command: FieldsCommand::Get { field_id: 123 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_fields_get_short_flag_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "fields", "get", "-i", "123"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Fields {
|
||||||
|
command: FieldsCommand::Get { field_id: 123 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_fields_get_missing_field_id_it_should_return_usage_error() {
|
||||||
|
let result = parse_cli_from(["test", "fields", "get"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_fields_batch_get_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "fields", "batch-get", "--ids", "1,2,3"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Fields {
|
||||||
|
command: FieldsCommand::BatchGet { ids: vec![1, 2, 3] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_fields_batch_get_without_ids_it_should_return_cli_with_empty_ids()
|
||||||
|
{
|
||||||
|
let result = parse_cli_from(["test", "fields", "batch-get"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Fields {
|
||||||
|
command: FieldsCommand::BatchGet { ids: vec![] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_fields_batch_get_invalid_id_format_it_should_return_usage_error()
|
||||||
|
{
|
||||||
|
let result = parse_cli_from(["test", "fields", "batch-get", "--ids", "invalid"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_fields_without_subcommand_it_should_return_usage_error() {
|
||||||
|
let result = parse_cli_from(["test", "fields"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_cli_from_when_called_with_version_flag_it_should_return_info_with_code_0() {
|
fn parse_cli_from_when_called_with_version_flag_it_should_return_info_with_code_0() {
|
||||||
let result = parse_cli_from(["test", "--version"]);
|
let result = parse_cli_from(["test", "--version"]);
|
||||||
@@ -294,5 +615,222 @@ mod tests {
|
|||||||
assert!(err.message.contains(env!("CARGO_PKG_NAME")));
|
assert!(err.message.contains(env!("CARGO_PKG_NAME")));
|
||||||
assert!(err.message.contains(env!("CARGO_PKG_VERSION")));
|
assert!(err.message.contains(env!("CARGO_PKG_VERSION")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_records_list_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "records", "list", "--app", "1"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Records {
|
||||||
|
command: RecordsCommand::List {
|
||||||
|
app_id: 1,
|
||||||
|
paging: PagingArgs::default(),
|
||||||
|
field_ids: vec![],
|
||||||
|
data_format: None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_records_list_all_flags_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from([
|
||||||
|
"test",
|
||||||
|
"records",
|
||||||
|
"list",
|
||||||
|
"-a",
|
||||||
|
"1",
|
||||||
|
"-n",
|
||||||
|
"2",
|
||||||
|
"-s",
|
||||||
|
"10",
|
||||||
|
"-f",
|
||||||
|
"100,200",
|
||||||
|
"--data-format",
|
||||||
|
"raw",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Records {
|
||||||
|
command: RecordsCommand::List {
|
||||||
|
app_id: 1,
|
||||||
|
paging: PagingArgs {
|
||||||
|
page_number: Some(2),
|
||||||
|
page_size: Some(10),
|
||||||
|
},
|
||||||
|
field_ids: vec![100, 200],
|
||||||
|
data_format: Some(DataFormatArg::Raw),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_records_get_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from([
|
||||||
|
"test", "records", "get", "-a", "1", "-r", "10", "-f", "100", "-d", "formatted",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Records {
|
||||||
|
command: RecordsCommand::Get {
|
||||||
|
app_id: 1,
|
||||||
|
record_id: 10,
|
||||||
|
field_ids: vec![100],
|
||||||
|
data_format: Some(DataFormatArg::Formatted),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_records_save_json_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from([
|
||||||
|
"test", "records", "save", "--json", r#"{"appId":1}"#,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Records {
|
||||||
|
command: RecordsCommand::Save {
|
||||||
|
body: BodySource {
|
||||||
|
json: Some(r#"{"appId":1}"#.to_string()),
|
||||||
|
file: None,
|
||||||
|
stdin: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_records_delete_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "records", "delete", "-a", "1", "-r", "10"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Records {
|
||||||
|
command: RecordsCommand::Delete {
|
||||||
|
app_id: 1,
|
||||||
|
record_id: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_records_batch_get_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from([
|
||||||
|
"test", "records", "batch-get", "-f", "input.json",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Records {
|
||||||
|
command: RecordsCommand::BatchGet {
|
||||||
|
body: BodySource {
|
||||||
|
json: None,
|
||||||
|
file: Some(std::path::PathBuf::from("input.json")),
|
||||||
|
stdin: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_records_query_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from([
|
||||||
|
"test", "records", "query", "--stdin", "-n", "1", "-s", "50",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Records {
|
||||||
|
command: RecordsCommand::Query {
|
||||||
|
body: BodySource {
|
||||||
|
json: None,
|
||||||
|
file: None,
|
||||||
|
stdin: true,
|
||||||
|
},
|
||||||
|
paging: PagingArgs {
|
||||||
|
page_number: Some(1),
|
||||||
|
page_size: Some(50),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_records_batch_delete_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from([
|
||||||
|
"test", "records", "batch-delete", "-j", r#"{"appId":1,"recordIds":[10]}"#,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Records {
|
||||||
|
command: RecordsCommand::BatchDelete {
|
||||||
|
body: BodySource {
|
||||||
|
json: Some(r#"{"appId":1,"recordIds":[10]}"#.to_string()),
|
||||||
|
file: None,
|
||||||
|
stdin: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_records_without_subcommand_it_should_return_usage_error() {
|
||||||
|
let result = parse_cli_from(["test", "records"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
+361
-1
@@ -1,5 +1,7 @@
|
|||||||
use onspring::{
|
use onspring::{
|
||||||
App, CollectionResponse, OnspringClient, OnspringError, PagedResponse, PagingRequest,
|
App, BatchDeleteRecordsRequest, BatchGetRecordsRequest, CollectionResponse, DataFormat, Field,
|
||||||
|
OnspringClient, OnspringError, PagedResponse, PagingRequest, QueryRecordsRequest, Record,
|
||||||
|
SaveRecordRequest, SaveRecordResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait OnspringRunner {
|
pub trait OnspringRunner {
|
||||||
@@ -19,6 +21,65 @@ pub trait OnspringRunner {
|
|||||||
&self,
|
&self,
|
||||||
ids: &[i32],
|
ids: &[i32],
|
||||||
) -> impl std::future::Future<Output = Result<CollectionResponse<App>, OnspringError>> + Send;
|
) -> impl std::future::Future<Output = Result<CollectionResponse<App>, OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn list_fields(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> impl std::future::Future<Output = Result<PagedResponse<Field>, OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn get_field(
|
||||||
|
&self,
|
||||||
|
field_id: i32,
|
||||||
|
) -> impl std::future::Future<Output = Result<Field, OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn batch_get_fields(
|
||||||
|
&self,
|
||||||
|
ids: &[i32],
|
||||||
|
) -> impl std::future::Future<Output = Result<CollectionResponse<Field>, OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn list_records(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
field_ids: Option<&[i32]>,
|
||||||
|
data_format: Option<DataFormat>,
|
||||||
|
) -> impl std::future::Future<Output = Result<PagedResponse<Record>, OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn get_record(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
record_id: i32,
|
||||||
|
field_ids: Option<&[i32]>,
|
||||||
|
data_format: Option<DataFormat>,
|
||||||
|
) -> impl std::future::Future<Output = Result<Record, OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn save_record(
|
||||||
|
&self,
|
||||||
|
request: SaveRecordRequest,
|
||||||
|
) -> impl std::future::Future<Output = Result<SaveRecordResponse, OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn delete_record(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
record_id: i32,
|
||||||
|
) -> impl std::future::Future<Output = Result<(), OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn batch_get_records(
|
||||||
|
&self,
|
||||||
|
request: BatchGetRecordsRequest,
|
||||||
|
) -> impl std::future::Future<Output = Result<CollectionResponse<Record>, OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn query_records(
|
||||||
|
&self,
|
||||||
|
request: QueryRecordsRequest,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> impl std::future::Future<Output = Result<PagedResponse<Record>, OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn batch_delete_records(
|
||||||
|
&self,
|
||||||
|
request: BatchDeleteRecordsRequest,
|
||||||
|
) -> impl std::future::Future<Output = Result<(), OnspringError>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OnspringRunner for OnspringClient {
|
impl OnspringRunner for OnspringClient {
|
||||||
@@ -40,6 +101,75 @@ impl OnspringRunner for OnspringClient {
|
|||||||
async fn batch_get_apps(&self, ids: &[i32]) -> Result<CollectionResponse<App>, OnspringError> {
|
async fn batch_get_apps(&self, ids: &[i32]) -> Result<CollectionResponse<App>, OnspringError> {
|
||||||
self.batch_get_apps(ids).await
|
self.batch_get_apps(ids).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_fields(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> Result<PagedResponse<Field>, OnspringError> {
|
||||||
|
self.list_fields(app_id, paging).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_field(&self, field_id: i32) -> Result<Field, OnspringError> {
|
||||||
|
self.get_field(field_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn batch_get_fields(&self, ids: &[i32]) -> Result<CollectionResponse<Field>, OnspringError> {
|
||||||
|
self.batch_get_fields(ids).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_records(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
field_ids: Option<&[i32]>,
|
||||||
|
data_format: Option<DataFormat>,
|
||||||
|
) -> Result<PagedResponse<Record>, OnspringError> {
|
||||||
|
self.list_records(app_id, paging, field_ids, data_format).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_record(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
record_id: i32,
|
||||||
|
field_ids: Option<&[i32]>,
|
||||||
|
data_format: Option<DataFormat>,
|
||||||
|
) -> Result<Record, OnspringError> {
|
||||||
|
self.get_record(app_id, record_id, field_ids, data_format).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_record(
|
||||||
|
&self,
|
||||||
|
request: SaveRecordRequest,
|
||||||
|
) -> Result<SaveRecordResponse, OnspringError> {
|
||||||
|
self.save_record(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_record(&self, app_id: i32, record_id: i32) -> Result<(), OnspringError> {
|
||||||
|
self.delete_record(app_id, record_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn batch_get_records(
|
||||||
|
&self,
|
||||||
|
request: BatchGetRecordsRequest,
|
||||||
|
) -> Result<CollectionResponse<Record>, OnspringError> {
|
||||||
|
self.batch_get_records(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query_records(
|
||||||
|
&self,
|
||||||
|
request: QueryRecordsRequest,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> Result<PagedResponse<Record>, OnspringError> {
|
||||||
|
self.query_records(request, paging).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn batch_delete_records(
|
||||||
|
&self,
|
||||||
|
request: BatchDeleteRecordsRequest,
|
||||||
|
) -> Result<(), OnspringError> {
|
||||||
|
self.batch_delete_records(request).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -55,6 +185,40 @@ pub mod testing {
|
|||||||
pub list_apps_paging: Mutex<Option<Option<PagingRequest>>>,
|
pub list_apps_paging: Mutex<Option<Option<PagingRequest>>>,
|
||||||
pub get_app_id: Mutex<Option<i32>>,
|
pub get_app_id: Mutex<Option<i32>>,
|
||||||
pub batch_get_apps_ids: Mutex<Option<Vec<i32>>>,
|
pub batch_get_apps_ids: Mutex<Option<Vec<i32>>>,
|
||||||
|
|
||||||
|
pub list_fields_result: Result<PagedResponse<Field>, OnspringError>,
|
||||||
|
pub get_field_result: Result<Field, OnspringError>,
|
||||||
|
pub batch_get_fields_result: Result<CollectionResponse<Field>, OnspringError>,
|
||||||
|
pub list_fields_app_id: Mutex<Option<i32>>,
|
||||||
|
pub list_fields_paging: Mutex<Option<Option<PagingRequest>>>,
|
||||||
|
pub get_field_id: Mutex<Option<i32>>,
|
||||||
|
pub batch_get_fields_ids: Mutex<Option<Vec<i32>>>,
|
||||||
|
|
||||||
|
pub list_records_result: Result<PagedResponse<Record>, OnspringError>,
|
||||||
|
pub get_record_result: Result<Record, OnspringError>,
|
||||||
|
pub save_record_result: Result<SaveRecordResponse, OnspringError>,
|
||||||
|
pub delete_record_result: Result<(), OnspringError>,
|
||||||
|
pub batch_get_records_result: Result<CollectionResponse<Record>, OnspringError>,
|
||||||
|
pub query_records_result: Result<PagedResponse<Record>, OnspringError>,
|
||||||
|
pub batch_delete_records_result: Result<(), OnspringError>,
|
||||||
|
|
||||||
|
pub list_records_app_id: Mutex<Option<i32>>,
|
||||||
|
pub list_records_paging: Mutex<Option<Option<PagingRequest>>>,
|
||||||
|
pub list_records_field_ids: Mutex<Option<Option<Vec<i32>>>>,
|
||||||
|
pub list_records_data_format: Mutex<Option<Option<DataFormat>>>,
|
||||||
|
|
||||||
|
pub get_record_app_id: Mutex<Option<i32>>,
|
||||||
|
pub get_record_record_id: Mutex<Option<i32>>,
|
||||||
|
pub get_record_field_ids: Mutex<Option<Option<Vec<i32>>>>,
|
||||||
|
pub get_record_data_format: Mutex<Option<Option<DataFormat>>>,
|
||||||
|
|
||||||
|
pub save_record_request: Mutex<Option<SaveRecordRequest>>,
|
||||||
|
pub delete_record_app_id: Mutex<Option<i32>>,
|
||||||
|
pub delete_record_record_id: Mutex<Option<i32>>,
|
||||||
|
pub batch_get_records_request: Mutex<Option<BatchGetRecordsRequest>>,
|
||||||
|
pub query_records_request: Mutex<Option<QueryRecordsRequest>>,
|
||||||
|
pub query_records_paging: Mutex<Option<Option<PagingRequest>>>,
|
||||||
|
pub batch_delete_records_request: Mutex<Option<BatchDeleteRecordsRequest>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MockClient {
|
impl Default for MockClient {
|
||||||
@@ -80,6 +244,84 @@ pub mod testing {
|
|||||||
list_apps_paging: Mutex::new(None),
|
list_apps_paging: Mutex::new(None),
|
||||||
get_app_id: Mutex::new(None),
|
get_app_id: Mutex::new(None),
|
||||||
batch_get_apps_ids: Mutex::new(None),
|
batch_get_apps_ids: Mutex::new(None),
|
||||||
|
|
||||||
|
list_fields_result: Ok(PagedResponse {
|
||||||
|
page_number: None,
|
||||||
|
page_size: None,
|
||||||
|
total_pages: None,
|
||||||
|
total_records: None,
|
||||||
|
items: None,
|
||||||
|
}),
|
||||||
|
get_field_result: Ok(Field {
|
||||||
|
id: 1,
|
||||||
|
app_id: 1,
|
||||||
|
name: None,
|
||||||
|
field_type: None,
|
||||||
|
status: None,
|
||||||
|
is_required: false,
|
||||||
|
is_unique: false,
|
||||||
|
multiplicity: None,
|
||||||
|
list_id: None,
|
||||||
|
values: None,
|
||||||
|
output_type: None,
|
||||||
|
referenced_app_id: None,
|
||||||
|
}),
|
||||||
|
batch_get_fields_result: Ok(CollectionResponse {
|
||||||
|
count: None,
|
||||||
|
items: None,
|
||||||
|
}),
|
||||||
|
list_fields_app_id: Mutex::new(None),
|
||||||
|
list_fields_paging: Mutex::new(None),
|
||||||
|
get_field_id: Mutex::new(None),
|
||||||
|
batch_get_fields_ids: Mutex::new(None),
|
||||||
|
|
||||||
|
list_records_result: Ok(PagedResponse {
|
||||||
|
page_number: None,
|
||||||
|
page_size: None,
|
||||||
|
total_pages: None,
|
||||||
|
total_records: None,
|
||||||
|
items: None,
|
||||||
|
}),
|
||||||
|
get_record_result: Ok(Record {
|
||||||
|
app_id: 1,
|
||||||
|
record_id: 1,
|
||||||
|
field_data: None,
|
||||||
|
}),
|
||||||
|
save_record_result: Ok(SaveRecordResponse {
|
||||||
|
id: 1,
|
||||||
|
warnings: None,
|
||||||
|
}),
|
||||||
|
delete_record_result: Ok(()),
|
||||||
|
batch_get_records_result: Ok(CollectionResponse {
|
||||||
|
count: None,
|
||||||
|
items: None,
|
||||||
|
}),
|
||||||
|
query_records_result: Ok(PagedResponse {
|
||||||
|
page_number: None,
|
||||||
|
page_size: None,
|
||||||
|
total_pages: None,
|
||||||
|
total_records: None,
|
||||||
|
items: None,
|
||||||
|
}),
|
||||||
|
batch_delete_records_result: Ok(()),
|
||||||
|
|
||||||
|
list_records_app_id: Mutex::new(None),
|
||||||
|
list_records_paging: Mutex::new(None),
|
||||||
|
list_records_field_ids: Mutex::new(None),
|
||||||
|
list_records_data_format: Mutex::new(None),
|
||||||
|
|
||||||
|
get_record_app_id: Mutex::new(None),
|
||||||
|
get_record_record_id: Mutex::new(None),
|
||||||
|
get_record_field_ids: Mutex::new(None),
|
||||||
|
get_record_data_format: Mutex::new(None),
|
||||||
|
|
||||||
|
save_record_request: Mutex::new(None),
|
||||||
|
delete_record_app_id: Mutex::new(None),
|
||||||
|
delete_record_record_id: Mutex::new(None),
|
||||||
|
batch_get_records_request: Mutex::new(None),
|
||||||
|
query_records_request: Mutex::new(None),
|
||||||
|
query_records_paging: Mutex::new(None),
|
||||||
|
batch_delete_records_request: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,5 +378,123 @@ pub mod testing {
|
|||||||
Err(err) => Err(clone_onspring_error(err)),
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_fields(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> Result<PagedResponse<Field>, OnspringError> {
|
||||||
|
*self.list_fields_app_id.lock().unwrap() = Some(app_id);
|
||||||
|
*self.list_fields_paging.lock().unwrap() = Some(paging);
|
||||||
|
match &self.list_fields_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_field(&self, field_id: i32) -> Result<Field, OnspringError> {
|
||||||
|
*self.get_field_id.lock().unwrap() = Some(field_id);
|
||||||
|
match &self.get_field_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn batch_get_fields(&self, ids: &[i32]) -> Result<CollectionResponse<Field>, OnspringError> {
|
||||||
|
*self.batch_get_fields_ids.lock().unwrap() = Some(ids.to_vec());
|
||||||
|
match &self.batch_get_fields_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_records(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
field_ids: Option<&[i32]>,
|
||||||
|
data_format: Option<DataFormat>,
|
||||||
|
) -> Result<PagedResponse<Record>, OnspringError> {
|
||||||
|
*self.list_records_app_id.lock().unwrap() = Some(app_id);
|
||||||
|
*self.list_records_paging.lock().unwrap() = Some(paging);
|
||||||
|
*self.list_records_field_ids.lock().unwrap() = Some(field_ids.map(|ids| ids.to_vec()));
|
||||||
|
*self.list_records_data_format.lock().unwrap() = Some(data_format);
|
||||||
|
match &self.list_records_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_record(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
record_id: i32,
|
||||||
|
field_ids: Option<&[i32]>,
|
||||||
|
data_format: Option<DataFormat>,
|
||||||
|
) -> Result<Record, OnspringError> {
|
||||||
|
*self.get_record_app_id.lock().unwrap() = Some(app_id);
|
||||||
|
*self.get_record_record_id.lock().unwrap() = Some(record_id);
|
||||||
|
*self.get_record_field_ids.lock().unwrap() = Some(field_ids.map(|ids| ids.to_vec()));
|
||||||
|
*self.get_record_data_format.lock().unwrap() = Some(data_format);
|
||||||
|
match &self.get_record_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_record(
|
||||||
|
&self,
|
||||||
|
request: SaveRecordRequest,
|
||||||
|
) -> Result<SaveRecordResponse, OnspringError> {
|
||||||
|
*self.save_record_request.lock().unwrap() = Some(request);
|
||||||
|
match &self.save_record_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_record(&self, app_id: i32, record_id: i32) -> Result<(), OnspringError> {
|
||||||
|
*self.delete_record_app_id.lock().unwrap() = Some(app_id);
|
||||||
|
*self.delete_record_record_id.lock().unwrap() = Some(record_id);
|
||||||
|
match &self.delete_record_result {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn batch_get_records(
|
||||||
|
&self,
|
||||||
|
request: BatchGetRecordsRequest,
|
||||||
|
) -> Result<CollectionResponse<Record>, OnspringError> {
|
||||||
|
*self.batch_get_records_request.lock().unwrap() = Some(request);
|
||||||
|
match &self.batch_get_records_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query_records(
|
||||||
|
&self,
|
||||||
|
request: QueryRecordsRequest,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> Result<PagedResponse<Record>, OnspringError> {
|
||||||
|
*self.query_records_request.lock().unwrap() = Some(request);
|
||||||
|
*self.query_records_paging.lock().unwrap() = Some(paging);
|
||||||
|
match &self.query_records_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn batch_delete_records(
|
||||||
|
&self,
|
||||||
|
request: BatchDeleteRecordsRequest,
|
||||||
|
) -> Result<(), OnspringError> {
|
||||||
|
*self.batch_delete_records_request.lock().unwrap() = Some(request);
|
||||||
|
match &self.batch_delete_records_result {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,539 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::cli::FieldsCommand;
|
||||||
|
use crate::client::OnspringRunner;
|
||||||
|
use crate::error::CliResult;
|
||||||
|
use crate::output::{CollectionOutput, FieldOutput, PagedOutput, write_json};
|
||||||
|
use crate::validation::{paging_request, validate_ids, validate_positive_i32};
|
||||||
|
|
||||||
|
pub async fn handle<C: OnspringRunner, W: Write>(
|
||||||
|
command: &FieldsCommand,
|
||||||
|
client: &C,
|
||||||
|
writer: &mut W,
|
||||||
|
pretty: bool,
|
||||||
|
) -> CliResult<()> {
|
||||||
|
match command {
|
||||||
|
FieldsCommand::List { app_id, paging } => {
|
||||||
|
validate_positive_i32(*app_id, "app_id")?;
|
||||||
|
let paging = paging_request(paging)?;
|
||||||
|
let response = client.list_fields(*app_id, paging).await?;
|
||||||
|
let output: PagedOutput<FieldOutput> = response.into();
|
||||||
|
write_json(writer, &output, pretty)?;
|
||||||
|
}
|
||||||
|
FieldsCommand::Get { field_id } => {
|
||||||
|
validate_positive_i32(*field_id, "field_id")?;
|
||||||
|
let response = client.get_field(*field_id).await?;
|
||||||
|
let output: FieldOutput = response.into();
|
||||||
|
write_json(writer, &output, pretty)?;
|
||||||
|
}
|
||||||
|
FieldsCommand::BatchGet { ids } => {
|
||||||
|
validate_ids(ids, "ids", Some(100))?;
|
||||||
|
let response = client.batch_get_fields(ids).await?;
|
||||||
|
let output: CollectionOutput<FieldOutput> = response.into();
|
||||||
|
write_json(writer, &output, pretty)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use onspring::{
|
||||||
|
CollectionResponse, Field, FormulaOutputType, ListFieldValue, Multiplicity, OnspringError,
|
||||||
|
PagedResponse,
|
||||||
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::cli::PagingArgs;
|
||||||
|
use crate::client::testing::MockClient;
|
||||||
|
use crate::error::CliError;
|
||||||
|
|
||||||
|
fn test_field(id: i32, app_id: i32, name: &str) -> Field {
|
||||||
|
Field {
|
||||||
|
id,
|
||||||
|
app_id,
|
||||||
|
name: Some(name.to_string()),
|
||||||
|
field_type: Some("Text".to_string()),
|
||||||
|
status: Some("Enabled".to_string()),
|
||||||
|
is_required: false,
|
||||||
|
is_unique: false,
|
||||||
|
multiplicity: None,
|
||||||
|
list_id: None,
|
||||||
|
values: None,
|
||||||
|
output_type: None,
|
||||||
|
referenced_app_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_list_succeeds_it_should_write_paged_output() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
list_fields_result: Ok(PagedResponse {
|
||||||
|
page_number: Some(1),
|
||||||
|
page_size: Some(50),
|
||||||
|
total_pages: Some(1),
|
||||||
|
total_records: Some(1),
|
||||||
|
items: Some(vec![test_field(1, 10, "Test Field")]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = FieldsCommand::List {
|
||||||
|
app_id: 10,
|
||||||
|
paging: PagingArgs::default(),
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
written.trim(),
|
||||||
|
r#"{"pageNumber":1,"pageSize":50,"totalPages":1,"totalRecords":1,"items":[{"id":1,"appId":10,"name":"Test Field","type":"Text","status":"Enabled","isRequired":false,"isUnique":false,"multiplicity":null,"listId":null,"values":null,"outputType":null,"referencedAppId":null}]}"#
|
||||||
|
);
|
||||||
|
assert_eq!(*mock_client.list_fields_app_id.lock().unwrap(), Some(10));
|
||||||
|
assert!(
|
||||||
|
mock_client
|
||||||
|
.list_fields_paging
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_list_with_paging_args_it_should_pass_paging_request() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
list_fields_result: Ok(PagedResponse {
|
||||||
|
page_number: Some(2),
|
||||||
|
page_size: Some(10),
|
||||||
|
total_pages: Some(5),
|
||||||
|
total_records: Some(50),
|
||||||
|
items: Some(vec![]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = FieldsCommand::List {
|
||||||
|
app_id: 10,
|
||||||
|
paging: PagingArgs {
|
||||||
|
page_number: Some(2),
|
||||||
|
page_size: Some(10),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
assert_eq!(*mock_client.list_fields_app_id.lock().unwrap(), Some(10));
|
||||||
|
let paging = mock_client
|
||||||
|
.list_fields_paging
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.take()
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(paging.page_number, 2);
|
||||||
|
assert_eq!(paging.page_size, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_list_succeeds_pretty_it_should_write_pretty_json() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
list_fields_result: Ok(PagedResponse {
|
||||||
|
page_number: Some(1),
|
||||||
|
page_size: Some(50),
|
||||||
|
total_pages: Some(1),
|
||||||
|
total_records: Some(1),
|
||||||
|
items: Some(vec![test_field(1, 10, "Test Field")]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = FieldsCommand::List {
|
||||||
|
app_id: 10,
|
||||||
|
paging: PagingArgs::default(),
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, true).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
let expected = concat!(
|
||||||
|
"{\n",
|
||||||
|
" \"pageNumber\": 1,\n",
|
||||||
|
" \"pageSize\": 50,\n",
|
||||||
|
" \"totalPages\": 1,\n",
|
||||||
|
" \"totalRecords\": 1,\n",
|
||||||
|
" \"items\": [\n",
|
||||||
|
" {\n",
|
||||||
|
" \"id\": 1,\n",
|
||||||
|
" \"appId\": 10,\n",
|
||||||
|
" \"name\": \"Test Field\",\n",
|
||||||
|
" \"type\": \"Text\",\n",
|
||||||
|
" \"status\": \"Enabled\",\n",
|
||||||
|
" \"isRequired\": false,\n",
|
||||||
|
" \"isUnique\": false,\n",
|
||||||
|
" \"multiplicity\": null,\n",
|
||||||
|
" \"listId\": null,\n",
|
||||||
|
" \"values\": null,\n",
|
||||||
|
" \"outputType\": null,\n",
|
||||||
|
" \"referencedAppId\": null\n",
|
||||||
|
" }\n",
|
||||||
|
" ]\n",
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(written.trim(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_list_fails_it_should_return_mapped_cli_error() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
list_fields_result: Err(OnspringError::Api {
|
||||||
|
status_code: 401,
|
||||||
|
message: "Unauthorized".to_string(),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = FieldsCommand::List {
|
||||||
|
app_id: 10,
|
||||||
|
paging: PagingArgs::default(),
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::runtime("API request failed (401): Unauthorized"))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_list_has_invalid_app_id_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = FieldsCommand::List {
|
||||||
|
app_id: 0,
|
||||||
|
paging: PagingArgs::default(),
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage("app_id must be greater than 0."))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert!(mock_client.list_fields_app_id.lock().unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_list_has_invalid_paging_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = FieldsCommand::List {
|
||||||
|
app_id: 10,
|
||||||
|
paging: PagingArgs {
|
||||||
|
page_number: Some(0),
|
||||||
|
page_size: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage("page_number must be greater than 0."))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert!(mock_client.list_fields_paging.lock().unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_get_succeeds_it_should_write_field_output() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
get_field_result: Ok(Field {
|
||||||
|
id: 123,
|
||||||
|
app_id: 456,
|
||||||
|
name: Some("Status Field".to_string()),
|
||||||
|
field_type: Some("List".to_string()),
|
||||||
|
status: Some("Enabled".to_string()),
|
||||||
|
is_required: true,
|
||||||
|
is_unique: false,
|
||||||
|
multiplicity: Some(Multiplicity::SingleSelect),
|
||||||
|
list_id: Some(789),
|
||||||
|
values: Some(vec![ListFieldValue {
|
||||||
|
id: Uuid::nil(),
|
||||||
|
name: "Active".to_string(),
|
||||||
|
sort_order: 1,
|
||||||
|
numeric_value: Some(1.0),
|
||||||
|
color: Some("#00ff00".to_string()),
|
||||||
|
}]),
|
||||||
|
output_type: Some(FormulaOutputType::Text),
|
||||||
|
referenced_app_id: Some(999),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = FieldsCommand::Get { field_id: 123 };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
written.trim(),
|
||||||
|
r##"{"id":123,"appId":456,"name":"Status Field","type":"List","status":"Enabled","isRequired":true,"isUnique":false,"multiplicity":"SingleSelect","listId":789,"values":[{"id":"00000000-0000-0000-0000-000000000000","name":"Active","sortOrder":1,"numericValue":1.0,"color":"#00ff00"}],"outputType":"Text","referencedAppId":999}"##
|
||||||
|
);
|
||||||
|
assert_eq!(*mock_client.get_field_id.lock().unwrap(), Some(123));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_get_succeeds_pretty_it_should_write_pretty_json() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
get_field_result: Ok(test_field(123, 456, "Test Field")),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = FieldsCommand::Get { field_id: 123 };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, true).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
let expected = concat!(
|
||||||
|
"{\n",
|
||||||
|
" \"id\": 123,\n",
|
||||||
|
" \"appId\": 456,\n",
|
||||||
|
" \"name\": \"Test Field\",\n",
|
||||||
|
" \"type\": \"Text\",\n",
|
||||||
|
" \"status\": \"Enabled\",\n",
|
||||||
|
" \"isRequired\": false,\n",
|
||||||
|
" \"isUnique\": false,\n",
|
||||||
|
" \"multiplicity\": null,\n",
|
||||||
|
" \"listId\": null,\n",
|
||||||
|
" \"values\": null,\n",
|
||||||
|
" \"outputType\": null,\n",
|
||||||
|
" \"referencedAppId\": null\n",
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(written.trim(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_get_with_invalid_id_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = FieldsCommand::Get { field_id: 0 };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage("field_id must be greater than 0."))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert_eq!(*mock_client.get_field_id.lock().unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_get_fails_it_should_return_mapped_cli_error() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
get_field_result: Err(OnspringError::Api {
|
||||||
|
status_code: 404,
|
||||||
|
message: "Field not found".to_string(),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = FieldsCommand::Get { field_id: 123 };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::runtime("API request failed (404): Field not found"))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_batch_get_succeeds_it_should_write_collection_output() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
batch_get_fields_result: Ok(CollectionResponse {
|
||||||
|
count: Some(2),
|
||||||
|
items: Some(vec![
|
||||||
|
test_field(1, 10, "Field 1"),
|
||||||
|
test_field(2, 10, "Field 2"),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = FieldsCommand::BatchGet { ids: vec![1, 2] };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
written.trim(),
|
||||||
|
r#"{"count":2,"items":[{"id":1,"appId":10,"name":"Field 1","type":"Text","status":"Enabled","isRequired":false,"isUnique":false,"multiplicity":null,"listId":null,"values":null,"outputType":null,"referencedAppId":null},{"id":2,"appId":10,"name":"Field 2","type":"Text","status":"Enabled","isRequired":false,"isUnique":false,"multiplicity":null,"listId":null,"values":null,"outputType":null,"referencedAppId":null}]}"#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*mock_client.batch_get_fields_ids.lock().unwrap(),
|
||||||
|
Some(vec![1, 2])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_batch_get_succeeds_pretty_it_should_write_pretty_json() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
batch_get_fields_result: Ok(CollectionResponse {
|
||||||
|
count: Some(1),
|
||||||
|
items: Some(vec![test_field(1, 10, "Field 1")]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = FieldsCommand::BatchGet { ids: vec![1] };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, true).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let written = String::from_utf8(buffer).unwrap();
|
||||||
|
|
||||||
|
let expected = concat!(
|
||||||
|
"{\n",
|
||||||
|
" \"count\": 1,\n",
|
||||||
|
" \"items\": [\n",
|
||||||
|
" {\n",
|
||||||
|
" \"id\": 1,\n",
|
||||||
|
" \"appId\": 10,\n",
|
||||||
|
" \"name\": \"Field 1\",\n",
|
||||||
|
" \"type\": \"Text\",\n",
|
||||||
|
" \"status\": \"Enabled\",\n",
|
||||||
|
" \"isRequired\": false,\n",
|
||||||
|
" \"isUnique\": false,\n",
|
||||||
|
" \"multiplicity\": null,\n",
|
||||||
|
" \"listId\": null,\n",
|
||||||
|
" \"values\": null,\n",
|
||||||
|
" \"outputType\": null,\n",
|
||||||
|
" \"referencedAppId\": null\n",
|
||||||
|
" }\n",
|
||||||
|
" ]\n",
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(written.trim(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_batch_get_with_empty_ids_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = FieldsCommand::BatchGet { ids: vec![] };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Err(CliError::usage("ids cannot be empty.")));
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert_eq!(*mock_client.batch_get_fields_ids.lock().unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_batch_get_with_invalid_ids_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = FieldsCommand::BatchGet {
|
||||||
|
ids: vec![1, 0, 2],
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage("ids must only contain values > 0."))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert_eq!(*mock_client.batch_get_fields_ids.lock().unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_batch_get_with_too_many_ids_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = FieldsCommand::BatchGet {
|
||||||
|
ids: vec![1; 101],
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage(
|
||||||
|
"ids cannot contain more than 100 values."
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert_eq!(*mock_client.batch_get_fields_ids.lock().unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_fields_batch_get_fails_it_should_return_mapped_cli_error() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
batch_get_fields_result: Err(OnspringError::Api {
|
||||||
|
status_code: 500,
|
||||||
|
message: "".to_string(),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = FieldsCommand::BatchGet { ids: vec![1, 2] };
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::runtime("API request failed with status 500."))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
pub mod apps;
|
pub mod apps;
|
||||||
|
pub mod fields;
|
||||||
pub mod ping;
|
pub mod ping;
|
||||||
|
pub mod records;
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
@@ -15,5 +17,7 @@ pub async fn handle<C: OnspringRunner, W: Write>(
|
|||||||
match &cli.command {
|
match &cli.command {
|
||||||
Command::Ping => ping::handle(client, writer, cli.pretty).await,
|
Command::Ping => ping::handle(client, writer, cli.pretty).await,
|
||||||
Command::Apps { command } => apps::handle(command, client, writer, cli.pretty).await,
|
Command::Apps { command } => apps::handle(command, client, writer, cli.pretty).await,
|
||||||
|
Command::Fields { command } => fields::handle(command, client, writer, cli.pretty).await,
|
||||||
|
Command::Records { command } => records::handle(command, client, writer, cli.pretty).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
+121
-1
@@ -1,7 +1,11 @@
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use onspring::{App, CollectionResponse, PagedResponse};
|
use onspring::{
|
||||||
|
App, CollectionResponse, Field, FormulaOutputType, ListFieldValue, Multiplicity, PagedResponse,
|
||||||
|
Record, RecordFieldValue, SaveRecordResponse, ValueType,
|
||||||
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{CliError, CliResult};
|
use crate::{CliError, CliResult};
|
||||||
|
|
||||||
@@ -88,6 +92,122 @@ impl From<App> for AppOutput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ListFieldValueOutput {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub sort_order: i32,
|
||||||
|
pub numeric_value: Option<f64>,
|
||||||
|
pub color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ListFieldValue> for ListFieldValueOutput {
|
||||||
|
fn from(value: ListFieldValue) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
name: value.name,
|
||||||
|
sort_order: value.sort_order,
|
||||||
|
numeric_value: value.numeric_value,
|
||||||
|
color: value.color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FieldOutput {
|
||||||
|
pub id: i32,
|
||||||
|
pub app_id: i32,
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub field_type: Option<String>,
|
||||||
|
pub status: Option<String>,
|
||||||
|
pub is_required: bool,
|
||||||
|
pub is_unique: bool,
|
||||||
|
pub multiplicity: Option<Multiplicity>,
|
||||||
|
pub list_id: Option<i32>,
|
||||||
|
pub values: Option<Vec<ListFieldValueOutput>>,
|
||||||
|
pub output_type: Option<FormulaOutputType>,
|
||||||
|
pub referenced_app_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Field> for FieldOutput {
|
||||||
|
fn from(value: Field) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
app_id: value.app_id,
|
||||||
|
name: value.name,
|
||||||
|
field_type: value.field_type,
|
||||||
|
status: value.status,
|
||||||
|
is_required: value.is_required,
|
||||||
|
is_unique: value.is_unique,
|
||||||
|
multiplicity: value.multiplicity,
|
||||||
|
list_id: value.list_id,
|
||||||
|
values: value
|
||||||
|
.values
|
||||||
|
.map(|values| values.into_iter().map(ListFieldValueOutput::from).collect()),
|
||||||
|
output_type: value.output_type,
|
||||||
|
referenced_app_id: value.referenced_app_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RecordFieldValueOutput {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub value_type: ValueType,
|
||||||
|
pub field_id: i32,
|
||||||
|
pub value: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RecordFieldValue> for RecordFieldValueOutput {
|
||||||
|
fn from(value: RecordFieldValue) -> Self {
|
||||||
|
Self {
|
||||||
|
value_type: value.value_type,
|
||||||
|
field_id: value.field_id,
|
||||||
|
value: value.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RecordOutput {
|
||||||
|
pub app_id: i32,
|
||||||
|
pub record_id: i32,
|
||||||
|
pub field_data: Option<Vec<RecordFieldValueOutput>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Record> for RecordOutput {
|
||||||
|
fn from(value: Record) -> Self {
|
||||||
|
Self {
|
||||||
|
app_id: value.app_id,
|
||||||
|
record_id: value.record_id,
|
||||||
|
field_data: value
|
||||||
|
.field_data
|
||||||
|
.map(|fields| fields.into_iter().map(RecordFieldValueOutput::from).collect()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SaveRecordResponseOutput {
|
||||||
|
pub id: i32,
|
||||||
|
pub warnings: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SaveRecordResponse> for SaveRecordResponseOutput {
|
||||||
|
fn from(value: SaveRecordResponse) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
warnings: value.warnings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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"))
|
||||||
|
|||||||
+178
-2
@@ -1,6 +1,9 @@
|
|||||||
use onspring::PagingRequest;
|
use std::io::Read;
|
||||||
|
|
||||||
use crate::cli::PagingArgs;
|
use onspring::PagingRequest;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
use crate::cli::{BodySource, PagingArgs};
|
||||||
use crate::error::{CliError, CliResult};
|
use crate::error::{CliError, CliResult};
|
||||||
|
|
||||||
pub fn paging_request(args: &PagingArgs) -> CliResult<Option<PagingRequest>> {
|
pub fn paging_request(args: &PagingArgs) -> CliResult<Option<PagingRequest>> {
|
||||||
@@ -54,6 +57,62 @@ pub fn validate_ids(ids: &[i32], name: &str, max_len: Option<usize>) -> CliResul
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_body<R: Read>(source: &BodySource, mut reader: R) -> CliResult<String> {
|
||||||
|
let count = [
|
||||||
|
source.json.is_some(),
|
||||||
|
source.file.is_some(),
|
||||||
|
source.stdin,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.filter(|&b| b)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return Err(CliError::usage(
|
||||||
|
"A request body is required. Provide --json, --file, or --stdin.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 1 {
|
||||||
|
return Err(CliError::usage(
|
||||||
|
"Only one request body source may be specified (--json, --file, or --stdin).",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref json) = source.json {
|
||||||
|
return Ok(json.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref path) = source.file {
|
||||||
|
return std::fs::read_to_string(path).map_err(|e| {
|
||||||
|
CliError::usage(format!(
|
||||||
|
"Failed to read file '{}': {}",
|
||||||
|
path.display(),
|
||||||
|
e
|
||||||
|
))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if source.stdin {
|
||||||
|
let mut buffer = String::new();
|
||||||
|
reader
|
||||||
|
.read_to_string(&mut buffer)
|
||||||
|
.map_err(|e| CliError::runtime(format!("Failed to read from stdin: {e}")))?;
|
||||||
|
return Ok(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_body<T: DeserializeOwned, R: Read>(
|
||||||
|
source: &BodySource,
|
||||||
|
reader: R,
|
||||||
|
) -> CliResult<T> {
|
||||||
|
let content = read_body(source, reader)?;
|
||||||
|
serde_json::from_str(&content)
|
||||||
|
.map_err(|e| CliError::usage(format!("Invalid JSON request body: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -207,4 +266,121 @@ mod tests {
|
|||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_body_when_no_source_provided_it_should_return_usage_error() {
|
||||||
|
let source = BodySource::default();
|
||||||
|
let result = read_body(&source, std::io::empty());
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage(
|
||||||
|
"A request body is required. Provide --json, --file, or --stdin."
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_body_when_multiple_sources_provided_it_should_return_usage_error() {
|
||||||
|
let source = BodySource {
|
||||||
|
json: Some("{}".to_string()),
|
||||||
|
stdin: true,
|
||||||
|
file: None,
|
||||||
|
};
|
||||||
|
let result = read_body(&source, std::io::empty());
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage(
|
||||||
|
"Only one request body source may be specified (--json, --file, or --stdin)."
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_body_when_json_provided_it_should_return_json_string() {
|
||||||
|
let source = BodySource {
|
||||||
|
json: Some(r#"{"appId":1}"#.to_string()),
|
||||||
|
file: None,
|
||||||
|
stdin: false,
|
||||||
|
};
|
||||||
|
let result = read_body(&source, std::io::empty());
|
||||||
|
assert_eq!(result, Ok(r#"{"appId":1}"#.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_body_when_stdin_provided_it_should_read_from_reader() {
|
||||||
|
let source = BodySource {
|
||||||
|
json: None,
|
||||||
|
file: None,
|
||||||
|
stdin: true,
|
||||||
|
};
|
||||||
|
let input = r#"{"appId":2}"#;
|
||||||
|
let result = read_body(&source, input.as_bytes());
|
||||||
|
assert_eq!(result, Ok(input.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_body_when_file_provided_it_should_read_from_file() {
|
||||||
|
use std::io::Write;
|
||||||
|
let mut temp = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
write!(temp, r#"{{"appId":3}}"#).unwrap();
|
||||||
|
|
||||||
|
let source = BodySource {
|
||||||
|
json: None,
|
||||||
|
file: Some(temp.path().to_path_buf()),
|
||||||
|
stdin: false,
|
||||||
|
};
|
||||||
|
let result = read_body(&source, std::io::empty());
|
||||||
|
assert_eq!(result, Ok(r#"{"appId":3}"#.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_body_when_file_not_found_it_should_return_usage_error() {
|
||||||
|
let source = BodySource {
|
||||||
|
json: None,
|
||||||
|
file: Some(std::path::PathBuf::from("nonexistent_file_path_12345.json")),
|
||||||
|
stdin: false,
|
||||||
|
};
|
||||||
|
let result = read_body(&source, std::io::empty());
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
assert!(err.message.contains("Failed to read file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_body_when_valid_it_should_deserialize() {
|
||||||
|
#[derive(Debug, PartialEq, serde::Deserialize)]
|
||||||
|
struct TestData {
|
||||||
|
app_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = BodySource {
|
||||||
|
json: Some(r#"{"app_id":42}"#.to_string()),
|
||||||
|
file: None,
|
||||||
|
stdin: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result: Result<TestData, _> = parse_body(&source, std::io::empty());
|
||||||
|
assert_eq!(result, Ok(TestData { app_id: 42 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_body_when_invalid_json_it_should_return_usage_error() {
|
||||||
|
#[derive(Debug, PartialEq, serde::Deserialize)]
|
||||||
|
struct TestData {
|
||||||
|
app_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = BodySource {
|
||||||
|
json: Some("invalid json".to_string()),
|
||||||
|
file: None,
|
||||||
|
stdin: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result: Result<TestData, _> = parse_body(&source, std::io::empty());
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
assert!(err.message.contains("Invalid JSON request body"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user