feat: implement field commands

This commit is contained in:
Stevan Freeborn committed 2026-09-11 22:13:22 -05:00
1 parent 30a6826f46
commit 54346ff20f
5 files changed
+907 -9

No files matched your search

+201 -7
View File
@@ -36,6 +36,11 @@ pub enum Command {
#[command(subcommand)]
command: AppsCommand,
},
#[command(about = "Perform operations against the fields in the instance")]
Fields {
#[command(subcommand)]
command: FieldsCommand,
},
}
#[derive(Debug, PartialEq, Subcommand)]
@@ -61,6 +66,37 @@ pub enum AppsCommand {
},
}
#[derive(Debug, PartialEq, Subcommand)]
pub enum FieldsCommand {
#[command(about = "Get information for a list of fields")]
List {
#[arg(long)]
#[arg(short = 'i')]
#[arg(help = "App id to get")]
app_id: i32,
#[command(flatten)]
paging: PagingArgs,
},
#[command(about = "Get information about a field")]
Get {
#[arg(long)]
#[arg(short = 'i')]
#[arg(help = "Field id to get")]
field_id: i32,
},
#[command(about = "Get information for a batch of fields")]
BatchGet {
#[arg(long)]
#[arg(value_delimiter = ',')]
#[arg(short = 'i')]
#[arg(help = "Comma-separated list of field ids to get")]
ids: Vec<i32>,
},
}
#[derive(Debug, PartialEq, Clone, Args, Default)]
pub struct PagingArgs {
#[arg(long)]
@@ -79,8 +115,8 @@ where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
use clap::error::ErrorKind;
use crate::error::CliError;
use clap::error::ErrorKind;
Cli::try_parse_from(args).map_err(|e| match e.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => CliError::info(e.to_string()),
@@ -206,16 +242,15 @@ mod tests {
base_url: None,
pretty: false,
command: Command::Apps {
command: AppsCommand::BatchGet {
ids: vec![1, 2, 3],
},
command: AppsCommand::BatchGet { ids: vec![1, 2, 3] },
},
})
);
}
#[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"]);
assert_eq!(
@@ -232,7 +267,8 @@ mod tests {
}
#[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"]);
assert!(result.is_err());
@@ -284,6 +320,165 @@ mod tests {
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]
fn parse_cli_from_when_called_with_version_flag_it_should_return_info_with_code_0() {
let result = parse_cli_from(["test", "--version"]);
@@ -295,4 +490,3 @@ mod tests {
assert!(err.message.contains(env!("CARGO_PKG_VERSION")));
}
}
+100 -1
View File
@@ -1,5 +1,5 @@
use onspring::{
App, CollectionResponse, OnspringClient, OnspringError, PagedResponse, PagingRequest,
App, CollectionResponse, Field, OnspringClient, OnspringError, PagedResponse, PagingRequest,
};
pub trait OnspringRunner {
@@ -19,6 +19,22 @@ pub trait OnspringRunner {
&self,
ids: &[i32],
) -> 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;
}
impl OnspringRunner for OnspringClient {
@@ -40,6 +56,22 @@ impl OnspringRunner for OnspringClient {
async fn batch_get_apps(&self, ids: &[i32]) -> Result<CollectionResponse<App>, OnspringError> {
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
}
}
#[cfg(test)]
@@ -55,6 +87,14 @@ pub mod testing {
pub list_apps_paging: Mutex<Option<Option<PagingRequest>>>,
pub get_app_id: Mutex<Option<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>>>,
}
impl Default for MockClient {
@@ -80,6 +120,36 @@ pub mod testing {
list_apps_paging: Mutex::new(None),
get_app_id: 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),
}
}
}
@@ -136,5 +206,34 @@ pub mod testing {
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)),
}
}
}
}
+539
View File
@@ -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());
}
}
+2
View File
@@ -1,5 +1,6 @@
pub mod apps;
pub mod ping;
pub mod fields;
use std::io::Write;
@@ -15,5 +16,6 @@ pub async fn handle<C: OnspringRunner, W: Write>(
match &cli.command {
Command::Ping => ping::handle(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,
}
}
+65 -1
View File
@@ -1,7 +1,10 @@
use std::io::Write;
use onspring::{App, CollectionResponse, PagedResponse};
use onspring::{
App, CollectionResponse, Field, FormulaOutputType, ListFieldValue, Multiplicity, PagedResponse,
};
use serde::Serialize;
use uuid::Uuid;
use crate::{CliError, CliResult};
@@ -88,6 +91,67 @@ 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,
}
}
}
pub fn render_json<T: Serialize>(value: &T, pretty: bool) -> CliResult<String> {
if pretty {
serde_json::to_string_pretty(value).map_err(|_| CliError::runtime("Failed to serialize output"))