Files
onx/src/validation.rs
T

372 lines
9.5 KiB
Rust
Raw Normal View History

2026-09-14 19:53:56 -05:00
use std::io::Read;
2026-09-04 18:58:22 -05:00
2026-09-14 19:53:56 -05:00
use onspring::PagingRequest;
use serde::de::DeserializeOwned;
use crate::cli::{BodySource, PagingArgs};
2026-09-04 18:58:22 -05:00
use crate::error::{CliError, CliResult};
pub fn paging_request(args: &PagingArgs) -> CliResult<Option<PagingRequest>> {
if args.page_number.is_none() && args.page_size.is_none() {
return Ok(None);
}
let page_number = args.page_number.unwrap_or(1);
let page_size = args.page_size.unwrap_or(50);
if page_number <= 0 {
return Err(CliError::usage("page_number must be greater than 0."));
}
if page_size <= 0 {
return Err(CliError::usage("page_size must be greater than 0."));
}
Ok(Some(PagingRequest {
page_number,
page_size,
}))
}
pub fn validate_positive_i32(value: i32, field: &str) -> CliResult<()> {
if value <= 0 {
return Err(CliError::usage(format!("{field} must be greater than 0.")));
}
Ok(())
}
pub fn validate_ids(ids: &[i32], name: &str, max_len: Option<usize>) -> CliResult<()> {
if ids.is_empty() {
return Err(CliError::usage(format!("{name} cannot be empty.")));
}
if let Some(max_len) = max_len
&& ids.len() > max_len
{
return Err(CliError::usage(format!(
"{name} cannot contain more than {max_len} values."
)));
}
if ids.iter().any(|id| *id <= 0) {
return Err(CliError::usage(format!(
"{name} must only contain values > 0."
)));
}
Ok(())
}
2026-09-14 19:53:56 -05:00
pub fn read_body<R: Read>(source: &BodySource, mut reader: R) -> CliResult<String> {
2026-09-14 20:40:21 -05:00
let count = [source.json.is_some(), source.file.is_some(), source.stdin]
.into_iter()
.filter(|&b| b)
.count();
2026-09-14 19:53:56 -05:00
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 {
2026-09-14 20:40:21 -05:00
return std::fs::read_to_string(path)
.map_err(|e| CliError::usage(format!("Failed to read file '{}': {}", path.display(), e)));
2026-09-14 19:53:56 -05:00
}
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!()
}
2026-09-14 20:40:21 -05:00
pub fn parse_body<T: DeserializeOwned, R: Read>(source: &BodySource, reader: R) -> CliResult<T> {
2026-09-14 19:53:56 -05:00
let content = read_body(source, reader)?;
serde_json::from_str(&content)
.map_err(|e| CliError::usage(format!("Invalid JSON request body: {e}")))
}
2026-09-04 18:58:22 -05:00
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paging_request_when_no_args_it_should_return_none() {
let args = PagingArgs {
page_number: None,
page_size: None,
};
let result = paging_request(&args);
assert!(matches!(result, Ok(None)));
}
#[test]
fn paging_request_when_page_number_provided_it_should_default_page_size() {
let args = PagingArgs {
page_number: Some(3),
page_size: None,
};
let result = paging_request(&args).unwrap();
let paging = result.unwrap();
assert_eq!(paging.page_number, 3);
assert_eq!(paging.page_size, 50);
}
#[test]
fn paging_request_when_page_size_provided_it_should_default_page_number() {
let args = PagingArgs {
page_number: None,
page_size: Some(25),
};
let result = paging_request(&args).unwrap();
let paging = result.unwrap();
assert_eq!(paging.page_number, 1);
assert_eq!(paging.page_size, 25);
}
#[test]
fn paging_request_when_both_args_provided_it_should_return_paging_request() {
let args = PagingArgs {
page_number: Some(2),
page_size: Some(10),
};
let result = paging_request(&args).unwrap();
let paging = result.unwrap();
assert_eq!(paging.page_number, 2);
assert_eq!(paging.page_size, 10);
}
#[test]
fn paging_request_when_page_number_zero_or_negative_it_should_return_usage_error() {
let args_zero = PagingArgs {
page_number: Some(0),
page_size: Some(10),
};
assert_eq!(
paging_request(&args_zero).unwrap_err(),
CliError::usage("page_number must be greater than 0.")
);
let args_neg = PagingArgs {
page_number: Some(-1),
page_size: Some(10),
};
assert_eq!(
paging_request(&args_neg).unwrap_err(),
CliError::usage("page_number must be greater than 0.")
);
}
#[test]
fn paging_request_when_page_size_zero_or_negative_it_should_return_usage_error() {
let args_zero = PagingArgs {
page_number: Some(1),
page_size: Some(0),
};
assert_eq!(
paging_request(&args_zero).unwrap_err(),
CliError::usage("page_size must be greater than 0.")
);
let args_neg = PagingArgs {
page_number: Some(1),
page_size: Some(-5),
};
assert_eq!(
paging_request(&args_neg).unwrap_err(),
CliError::usage("page_size must be greater than 0.")
);
}
#[test]
fn validate_positive_i32_when_valid_it_should_return_ok() {
assert_eq!(validate_positive_i32(1, "app_id"), Ok(()));
assert_eq!(validate_positive_i32(100, "app_id"), Ok(()));
}
#[test]
fn validate_positive_i32_when_zero_or_negative_it_should_return_usage_error() {
assert_eq!(
validate_positive_i32(0, "app_id"),
Err(CliError::usage("app_id must be greater than 0."))
);
assert_eq!(
validate_positive_i32(-1, "app_id"),
Err(CliError::usage("app_id must be greater than 0."))
);
}
#[test]
fn validate_ids_when_valid_it_should_return_ok() {
assert_eq!(validate_ids(&[1, 2, 3], "ids", Some(100)), Ok(()));
}
#[test]
fn validate_ids_when_empty_it_should_return_usage_error() {
assert_eq!(
validate_ids(&[], "ids", Some(100)),
Err(CliError::usage("ids cannot be empty."))
);
}
#[test]
fn validate_ids_when_contains_zero_or_negative_it_should_return_usage_error() {
assert_eq!(
validate_ids(&[1, 0, 3], "ids", Some(100)),
Err(CliError::usage("ids must only contain values > 0."))
);
assert_eq!(
validate_ids(&[1, -2, 3], "ids", Some(100)),
Err(CliError::usage("ids must only contain values > 0."))
);
}
#[test]
fn validate_ids_when_exceeds_max_len_it_should_return_usage_error() {
let ids = vec![1; 101];
assert_eq!(
validate_ids(&ids, "ids", Some(100)),
2026-09-14 20:40:21 -05:00
Err(CliError::usage("ids cannot contain more than 100 values."))
2026-09-04 18:58:22 -05:00
);
}
2026-09-14 19:53:56 -05:00
#[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"));
}
2026-09-04 18:58:22 -05:00
}