feat: added reports command and added support for json config
This commit is contained in:
1 parent
c83708a727
commit
df07eec5fa
7 files changed
+1123
-12
No files matched your search
+202
-3
@@ -10,16 +10,19 @@ use uuid::Uuid;
|
|||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
#[arg(short = 'k')]
|
#[arg(short = 'k')]
|
||||||
#[arg(env = "ONSPRING_API_KEY")]
|
|
||||||
#[arg(help = "The API key for the Onspring instance")]
|
#[arg(help = "The API key for the Onspring instance")]
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
#[arg(short = 'u')]
|
#[arg(short = 'u')]
|
||||||
#[arg(env = "ONSPRING_BASE_URL")]
|
|
||||||
#[arg(help = "The base URL for the Onspring API")]
|
#[arg(help = "The base URL for the Onspring API")]
|
||||||
pub base_url: Option<String>,
|
pub base_url: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
#[arg(short = 'c')]
|
||||||
|
#[arg(help = "Path to a configuration JSON file")]
|
||||||
|
pub config: Option<PathBuf>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
#[arg(short = 'p')]
|
#[arg(short = 'p')]
|
||||||
#[arg(help = "Pretty-print JSON output")]
|
#[arg(help = "Pretty-print JSON output")]
|
||||||
@@ -64,6 +67,12 @@ pub enum Command {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: ListsCommand,
|
command: ListsCommand,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[command(about = "Perform operations against the reports in the instance")]
|
||||||
|
Reports {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: ReportsCommand,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Subcommand)]
|
#[derive(Debug, PartialEq, Subcommand)]
|
||||||
@@ -338,6 +347,38 @@ pub enum ListsCommand {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Subcommand)]
|
||||||
|
pub enum ReportsCommand {
|
||||||
|
#[command(about = "Get the data for a specific report")]
|
||||||
|
Get {
|
||||||
|
#[arg(long = "report")]
|
||||||
|
#[arg(short = 'r')]
|
||||||
|
#[arg(help = "Report id whose data should be retrieved")]
|
||||||
|
report_id: i32,
|
||||||
|
|
||||||
|
#[arg(long = "format")]
|
||||||
|
#[arg(short = 'f')]
|
||||||
|
#[arg(help = "Format of the report data to be retrieved")]
|
||||||
|
data_format: Option<DataFormatArg>,
|
||||||
|
|
||||||
|
#[arg(long = "type")]
|
||||||
|
#[arg(short = 't')]
|
||||||
|
#[arg(help = "Type of the report data to be retrieved")]
|
||||||
|
data_type: Option<ReportDataTypeArg>,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(about = "List the reports available in an app or survey")]
|
||||||
|
List {
|
||||||
|
#[arg(long = "app")]
|
||||||
|
#[arg(short = 'a')]
|
||||||
|
#[arg(help = "The id of the app whose reports should be listed")]
|
||||||
|
app_id: i32,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
paging: PagingArgs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Args, Default)]
|
#[derive(Debug, PartialEq, Clone, Args, Default)]
|
||||||
pub struct PagingArgs {
|
pub struct PagingArgs {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -374,6 +415,12 @@ pub enum DataFormatArg {
|
|||||||
Formatted,
|
Formatted,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy, ValueEnum)]
|
||||||
|
pub enum ReportDataTypeArg {
|
||||||
|
ReportData,
|
||||||
|
ChartData,
|
||||||
|
}
|
||||||
|
|
||||||
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>,
|
||||||
@@ -401,6 +448,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Ping,
|
command: Command::Ping,
|
||||||
})
|
})
|
||||||
@@ -416,6 +464,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Apps {
|
command: Command::Apps {
|
||||||
command: AppsCommand::List(PagingArgs::default()),
|
command: AppsCommand::List(PagingArgs::default()),
|
||||||
@@ -441,6 +490,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Apps {
|
command: Command::Apps {
|
||||||
command: AppsCommand::List(PagingArgs {
|
command: AppsCommand::List(PagingArgs {
|
||||||
@@ -461,6 +511,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Apps {
|
command: Command::Apps {
|
||||||
command: AppsCommand::Get { app_id: 123 },
|
command: AppsCommand::Get { app_id: 123 },
|
||||||
@@ -478,6 +529,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Apps {
|
command: Command::Apps {
|
||||||
command: AppsCommand::Get { app_id: 123 },
|
command: AppsCommand::Get { app_id: 123 },
|
||||||
@@ -504,6 +556,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Apps {
|
command: Command::Apps {
|
||||||
command: AppsCommand::BatchGet { ids: vec![1, 2, 3] },
|
command: AppsCommand::BatchGet { ids: vec![1, 2, 3] },
|
||||||
@@ -522,6 +575,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Apps {
|
command: Command::Apps {
|
||||||
command: AppsCommand::BatchGet { ids: vec![] },
|
command: AppsCommand::BatchGet { ids: vec![] },
|
||||||
@@ -593,6 +647,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Fields {
|
command: Command::Fields {
|
||||||
command: FieldsCommand::List {
|
command: FieldsCommand::List {
|
||||||
@@ -623,6 +678,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Fields {
|
command: Command::Fields {
|
||||||
command: FieldsCommand::List {
|
command: FieldsCommand::List {
|
||||||
@@ -655,6 +711,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Fields {
|
command: Command::Fields {
|
||||||
command: FieldsCommand::Get { field_id: 123 },
|
command: FieldsCommand::Get { field_id: 123 },
|
||||||
@@ -672,6 +729,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Fields {
|
command: Command::Fields {
|
||||||
command: FieldsCommand::Get { field_id: 123 },
|
command: FieldsCommand::Get { field_id: 123 },
|
||||||
@@ -698,6 +756,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Fields {
|
command: Command::Fields {
|
||||||
command: FieldsCommand::BatchGet { ids: vec![1, 2, 3] },
|
command: FieldsCommand::BatchGet { ids: vec![1, 2, 3] },
|
||||||
@@ -716,6 +775,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Fields {
|
command: Command::Fields {
|
||||||
command: FieldsCommand::BatchGet { ids: vec![] },
|
command: FieldsCommand::BatchGet { ids: vec![] },
|
||||||
@@ -763,6 +823,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Records {
|
command: Command::Records {
|
||||||
command: RecordsCommand::List {
|
command: RecordsCommand::List {
|
||||||
@@ -799,6 +860,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Records {
|
command: Command::Records {
|
||||||
command: RecordsCommand::List {
|
command: RecordsCommand::List {
|
||||||
@@ -836,6 +898,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Records {
|
command: Command::Records {
|
||||||
command: RecordsCommand::Get {
|
command: RecordsCommand::Get {
|
||||||
@@ -858,6 +921,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Records {
|
command: Command::Records {
|
||||||
command: RecordsCommand::Save {
|
command: RecordsCommand::Save {
|
||||||
@@ -881,6 +945,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Records {
|
command: Command::Records {
|
||||||
command: RecordsCommand::Delete {
|
command: RecordsCommand::Delete {
|
||||||
@@ -901,6 +966,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Records {
|
command: Command::Records {
|
||||||
command: RecordsCommand::BatchGet {
|
command: RecordsCommand::BatchGet {
|
||||||
@@ -924,6 +990,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Records {
|
command: Command::Records {
|
||||||
command: RecordsCommand::Query {
|
command: RecordsCommand::Query {
|
||||||
@@ -957,6 +1024,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Records {
|
command: Command::Records {
|
||||||
command: RecordsCommand::BatchDelete {
|
command: RecordsCommand::BatchDelete {
|
||||||
@@ -991,6 +1059,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Files {
|
command: Command::Files {
|
||||||
command: FilesCommand::Info {
|
command: FilesCommand::Info {
|
||||||
@@ -1014,6 +1083,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Files {
|
command: Command::Files {
|
||||||
command: FilesCommand::Get {
|
command: FilesCommand::Get {
|
||||||
@@ -1054,6 +1124,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Files {
|
command: Command::Files {
|
||||||
command: FilesCommand::Upload {
|
command: FilesCommand::Upload {
|
||||||
@@ -1082,6 +1153,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Files {
|
command: Command::Files {
|
||||||
command: FilesCommand::Delete {
|
command: FilesCommand::Delete {
|
||||||
@@ -1106,7 +1178,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parse_cli_from_when_called_with_lists_save_item_it_should_return_cli() {
|
fn parse_cli_from_when_called_with_lists_save_item_it_should_return_cli() {
|
||||||
let result = parse_cli_from([
|
let result = parse_cli_from([
|
||||||
"test", "lists", "save-item", "-l", "1", "-j", r#"{"name":"Item 1"}"#,
|
"test",
|
||||||
|
"lists",
|
||||||
|
"save-item",
|
||||||
|
"-l",
|
||||||
|
"1",
|
||||||
|
"-j",
|
||||||
|
r#"{"name":"Item 1"}"#,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1114,6 +1192,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Lists {
|
command: Command::Lists {
|
||||||
command: ListsCommand::SaveItem {
|
command: ListsCommand::SaveItem {
|
||||||
@@ -1146,6 +1225,7 @@ mod tests {
|
|||||||
Ok(Cli {
|
Ok(Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Lists {
|
command: Command::Lists {
|
||||||
command: ListsCommand::DeleteItem {
|
command: ListsCommand::DeleteItem {
|
||||||
@@ -1165,4 +1245,123 @@ mod tests {
|
|||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
assert_eq!(err.code, 2);
|
assert_eq!(err.code, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_reports_get_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "reports", "get", "-r", "123"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
config: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Reports {
|
||||||
|
command: ReportsCommand::Get {
|
||||||
|
report_id: 123,
|
||||||
|
data_format: None,
|
||||||
|
data_type: None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_reports_get_with_options_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from([
|
||||||
|
"test",
|
||||||
|
"reports",
|
||||||
|
"get",
|
||||||
|
"--report",
|
||||||
|
"456",
|
||||||
|
"--format",
|
||||||
|
"formatted",
|
||||||
|
"--type",
|
||||||
|
"chart-data",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
config: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Reports {
|
||||||
|
command: ReportsCommand::Get {
|
||||||
|
report_id: 456,
|
||||||
|
data_format: Some(DataFormatArg::Formatted),
|
||||||
|
data_type: Some(ReportDataTypeArg::ChartData),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_reports_list_it_should_return_cli() {
|
||||||
|
let result = parse_cli_from(["test", "reports", "list", "-a", "10", "-n", "2", "-s", "25"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
config: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Reports {
|
||||||
|
command: ReportsCommand::List {
|
||||||
|
app_id: 10,
|
||||||
|
paging: PagingArgs {
|
||||||
|
page_number: Some(2),
|
||||||
|
page_size: Some(25),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_reports_without_subcommand_it_should_return_usage_error() {
|
||||||
|
let result = parse_cli_from(["test", "reports"]);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_config_flag_it_should_return_cli_with_config_path() {
|
||||||
|
let result = parse_cli_from(["test", "--config", "custom/config.json", "ping"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
config: Some(PathBuf::from("custom/config.json")),
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_cli_from_when_called_with_short_config_flag_it_should_return_cli_with_config_path() {
|
||||||
|
let result = parse_cli_from(["test", "-c", "custom/config.json", "ping"]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
config: Some(PathBuf::from("custom/config.json")),
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+90
-2
@@ -1,8 +1,9 @@
|
|||||||
use onspring::{
|
use onspring::{
|
||||||
App, BatchDeleteRecordsRequest, BatchGetRecordsRequest, CollectionResponse,
|
App, BatchDeleteRecordsRequest, BatchGetRecordsRequest, CollectionResponse,
|
||||||
CreatedWithIdResponse, DataFormat, Field, FileInfo, FileResponse, OnspringClient, OnspringError,
|
CreatedWithIdResponse, DataFormat, Field, FileInfo, FileResponse, OnspringClient, OnspringError,
|
||||||
PagedResponse, PagingRequest, QueryRecordsRequest, Record, SaveFileRequest, SaveListItemRequest,
|
PagedResponse, PagingRequest, QueryRecordsRequest, Record, ReportData, ReportDataType,
|
||||||
SaveListItemResponse, SaveRecordRequest, SaveRecordResponse,
|
ReportInfo, SaveFileRequest, SaveListItemRequest, SaveListItemResponse, SaveRecordRequest,
|
||||||
|
SaveRecordResponse,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -120,6 +121,19 @@ pub trait OnspringRunner {
|
|||||||
list_id: i32,
|
list_id: i32,
|
||||||
item_id: Uuid,
|
item_id: Uuid,
|
||||||
) -> impl std::future::Future<Output = Result<(), OnspringError>> + Send;
|
) -> impl std::future::Future<Output = Result<(), OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn get_report(
|
||||||
|
&self,
|
||||||
|
report_id: i32,
|
||||||
|
data_format: Option<DataFormat>,
|
||||||
|
data_type: Option<ReportDataType>,
|
||||||
|
) -> impl std::future::Future<Output = Result<ReportData, OnspringError>> + Send;
|
||||||
|
|
||||||
|
fn list_reports(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> impl std::future::Future<Output = Result<PagedResponse<ReportInfo>, OnspringError>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OnspringRunner for OnspringClient {
|
impl OnspringRunner for OnspringClient {
|
||||||
@@ -256,6 +270,23 @@ impl OnspringRunner for OnspringClient {
|
|||||||
async fn delete_list_item(&self, list_id: i32, item_id: Uuid) -> Result<(), OnspringError> {
|
async fn delete_list_item(&self, list_id: i32, item_id: Uuid) -> Result<(), OnspringError> {
|
||||||
self.delete_list_item(list_id, item_id).await
|
self.delete_list_item(list_id, item_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_report(
|
||||||
|
&self,
|
||||||
|
report_id: i32,
|
||||||
|
data_format: Option<DataFormat>,
|
||||||
|
data_type: Option<ReportDataType>,
|
||||||
|
) -> Result<ReportData, OnspringError> {
|
||||||
|
self.get_report(report_id, data_format, data_type).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_reports(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> Result<PagedResponse<ReportInfo>, OnspringError> {
|
||||||
|
self.list_reports(app_id, paging).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -333,6 +364,16 @@ pub mod testing {
|
|||||||
|
|
||||||
pub delete_list_item_list_id: Mutex<Option<i32>>,
|
pub delete_list_item_list_id: Mutex<Option<i32>>,
|
||||||
pub delete_list_item_item_id: Mutex<Option<Uuid>>,
|
pub delete_list_item_item_id: Mutex<Option<Uuid>>,
|
||||||
|
|
||||||
|
pub get_report_result: Result<ReportData, OnspringError>,
|
||||||
|
pub list_reports_result: Result<PagedResponse<ReportInfo>, OnspringError>,
|
||||||
|
|
||||||
|
pub get_report_report_id: Mutex<Option<i32>>,
|
||||||
|
pub get_report_data_format: Mutex<Option<Option<DataFormat>>>,
|
||||||
|
pub get_report_data_type: Mutex<Option<Option<ReportDataType>>>,
|
||||||
|
|
||||||
|
pub list_reports_app_id: Mutex<Option<i32>>,
|
||||||
|
pub list_reports_paging: Mutex<Option<Option<PagingRequest>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MockClient {
|
impl Default for MockClient {
|
||||||
@@ -479,6 +520,25 @@ pub mod testing {
|
|||||||
|
|
||||||
delete_list_item_list_id: Mutex::new(None),
|
delete_list_item_list_id: Mutex::new(None),
|
||||||
delete_list_item_item_id: Mutex::new(None),
|
delete_list_item_item_id: Mutex::new(None),
|
||||||
|
|
||||||
|
get_report_result: Ok(ReportData {
|
||||||
|
columns: None,
|
||||||
|
rows: None,
|
||||||
|
}),
|
||||||
|
list_reports_result: Ok(PagedResponse {
|
||||||
|
page_number: None,
|
||||||
|
page_size: None,
|
||||||
|
total_pages: None,
|
||||||
|
total_records: None,
|
||||||
|
items: None,
|
||||||
|
}),
|
||||||
|
|
||||||
|
get_report_report_id: Mutex::new(None),
|
||||||
|
get_report_data_format: Mutex::new(None),
|
||||||
|
get_report_data_type: Mutex::new(None),
|
||||||
|
|
||||||
|
list_reports_app_id: Mutex::new(None),
|
||||||
|
list_reports_paging: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -731,5 +791,33 @@ pub mod testing {
|
|||||||
Err(err) => Err(clone_onspring_error(err)),
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_report(
|
||||||
|
&self,
|
||||||
|
report_id: i32,
|
||||||
|
data_format: Option<DataFormat>,
|
||||||
|
data_type: Option<ReportDataType>,
|
||||||
|
) -> Result<ReportData, OnspringError> {
|
||||||
|
*self.get_report_report_id.lock().unwrap() = Some(report_id);
|
||||||
|
*self.get_report_data_format.lock().unwrap() = Some(data_format);
|
||||||
|
*self.get_report_data_type.lock().unwrap() = Some(data_type);
|
||||||
|
match &self.get_report_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_reports(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> Result<PagedResponse<ReportInfo>, OnspringError> {
|
||||||
|
*self.list_reports_app_id.lock().unwrap() = Some(app_id);
|
||||||
|
*self.list_reports_paging.lock().unwrap() = Some(paging);
|
||||||
|
match &self.list_reports_result {
|
||||||
|
Ok(res) => Ok(res.clone()),
|
||||||
|
Err(err) => Err(clone_onspring_error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ pub mod files;
|
|||||||
pub mod lists;
|
pub mod lists;
|
||||||
pub mod ping;
|
pub mod ping;
|
||||||
pub mod records;
|
pub mod records;
|
||||||
|
pub mod reports;
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
@@ -23,5 +24,6 @@ pub async fn handle<C: OnspringRunner, W: Write>(
|
|||||||
Command::Records { command } => records::handle(command, client, writer, cli.pretty).await,
|
Command::Records { command } => records::handle(command, client, writer, cli.pretty).await,
|
||||||
Command::Files { command } => files::handle(command, client, writer, cli.pretty).await,
|
Command::Files { command } => files::handle(command, client, writer, cli.pretty).await,
|
||||||
Command::Lists { command } => lists::handle(command, client, writer, cli.pretty).await,
|
Command::Lists { command } => lists::handle(command, client, writer, cli.pretty).await,
|
||||||
|
Command::Reports { command } => reports::handle(command, client, writer, cli.pretty).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use onspring::{DataFormat, ReportDataType};
|
||||||
|
|
||||||
|
use crate::cli::{ReportDataTypeArg, ReportsCommand};
|
||||||
|
use crate::client::OnspringRunner;
|
||||||
|
use crate::error::CliResult;
|
||||||
|
use crate::output::{PagedOutput, ReportDataOutput, ReportInfoOutput, write_json};
|
||||||
|
use crate::validation::{paging_request, validate_positive_i32};
|
||||||
|
|
||||||
|
impl From<ReportDataTypeArg> for ReportDataType {
|
||||||
|
fn from(value: ReportDataTypeArg) -> Self {
|
||||||
|
match value {
|
||||||
|
ReportDataTypeArg::ReportData => ReportDataType::ReportData,
|
||||||
|
ReportDataTypeArg::ChartData => ReportDataType::ChartData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle<C: OnspringRunner, W: Write>(
|
||||||
|
command: &ReportsCommand,
|
||||||
|
client: &C,
|
||||||
|
writer: &mut W,
|
||||||
|
pretty: bool,
|
||||||
|
) -> CliResult<()> {
|
||||||
|
match command {
|
||||||
|
ReportsCommand::Get {
|
||||||
|
report_id,
|
||||||
|
data_format,
|
||||||
|
data_type,
|
||||||
|
} => {
|
||||||
|
validate_positive_i32(*report_id, "report_id")?;
|
||||||
|
let data_format = data_format.map(DataFormat::from);
|
||||||
|
let data_type = data_type.map(ReportDataType::from);
|
||||||
|
let response = client.get_report(*report_id, data_format, data_type).await?;
|
||||||
|
let output: ReportDataOutput = response.into();
|
||||||
|
write_json(writer, &output, pretty)?;
|
||||||
|
}
|
||||||
|
ReportsCommand::List { app_id, paging } => {
|
||||||
|
validate_positive_i32(*app_id, "app_id")?;
|
||||||
|
let paging = paging_request(paging)?;
|
||||||
|
let response = client.list_reports(*app_id, paging).await?;
|
||||||
|
let output: PagedOutput<ReportInfoOutput> = response.into();
|
||||||
|
write_json(writer, &output, pretty)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use onspring::{OnspringError, PagedResponse, ReportData, ReportInfo, ReportRow};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::cli::{DataFormatArg, PagingArgs};
|
||||||
|
use crate::client::testing::MockClient;
|
||||||
|
use crate::error::CliError;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_get_succeeds_it_should_write_report_data_output() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
get_report_result: Ok(ReportData {
|
||||||
|
columns: Some(vec!["Col1".to_string(), "Col2".to_string()]),
|
||||||
|
rows: Some(vec![ReportRow {
|
||||||
|
record_id: Some(10),
|
||||||
|
cells: Some(vec![json!("Val1"), json!(42)]),
|
||||||
|
}]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = ReportsCommand::Get {
|
||||||
|
report_id: 123,
|
||||||
|
data_format: None,
|
||||||
|
data_type: None,
|
||||||
|
};
|
||||||
|
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#"{"columns":["Col1","Col2"],"rows":[{"recordId":10,"cells":["Val1",42]}]}"#
|
||||||
|
);
|
||||||
|
assert_eq!(*mock_client.get_report_report_id.lock().unwrap(), Some(123));
|
||||||
|
assert_eq!(
|
||||||
|
*mock_client.get_report_data_format.lock().unwrap(),
|
||||||
|
Some(None)
|
||||||
|
);
|
||||||
|
assert_eq!(*mock_client.get_report_data_type.lock().unwrap(), Some(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_get_with_format_and_type_it_should_pass_mapped_arguments() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
get_report_result: Ok(ReportData {
|
||||||
|
columns: Some(vec!["Header".to_string()]),
|
||||||
|
rows: Some(vec![]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = ReportsCommand::Get {
|
||||||
|
report_id: 456,
|
||||||
|
data_format: Some(DataFormatArg::Formatted),
|
||||||
|
data_type: Some(ReportDataTypeArg::ChartData),
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
assert_eq!(*mock_client.get_report_report_id.lock().unwrap(), Some(456));
|
||||||
|
assert_eq!(
|
||||||
|
*mock_client.get_report_data_format.lock().unwrap(),
|
||||||
|
Some(Some(DataFormat::Formatted))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*mock_client.get_report_data_type.lock().unwrap(),
|
||||||
|
Some(Some(ReportDataType::ChartData))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_get_with_raw_and_report_data_it_should_pass_mapped_arguments() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
get_report_result: Ok(ReportData {
|
||||||
|
columns: None,
|
||||||
|
rows: None,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = ReportsCommand::Get {
|
||||||
|
report_id: 789,
|
||||||
|
data_format: Some(DataFormatArg::Raw),
|
||||||
|
data_type: Some(ReportDataTypeArg::ReportData),
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
assert_eq!(*mock_client.get_report_report_id.lock().unwrap(), Some(789));
|
||||||
|
assert_eq!(
|
||||||
|
*mock_client.get_report_data_format.lock().unwrap(),
|
||||||
|
Some(Some(DataFormat::Raw))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*mock_client.get_report_data_type.lock().unwrap(),
|
||||||
|
Some(Some(ReportDataType::ReportData))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_get_succeeds_pretty_it_should_write_pretty_json() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
get_report_result: Ok(ReportData {
|
||||||
|
columns: Some(vec!["Col".to_string()]),
|
||||||
|
rows: Some(vec![ReportRow {
|
||||||
|
record_id: Some(1),
|
||||||
|
cells: Some(vec![json!("Val")]),
|
||||||
|
}]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = ReportsCommand::Get {
|
||||||
|
report_id: 1,
|
||||||
|
data_format: None,
|
||||||
|
data_type: None,
|
||||||
|
};
|
||||||
|
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",
|
||||||
|
" \"columns\": [\n",
|
||||||
|
" \"Col\"\n",
|
||||||
|
" ],\n",
|
||||||
|
" \"rows\": [\n",
|
||||||
|
" {\n",
|
||||||
|
" \"recordId\": 1,\n",
|
||||||
|
" \"cells\": [\n",
|
||||||
|
" \"Val\"\n",
|
||||||
|
" ]\n",
|
||||||
|
" }\n",
|
||||||
|
" ]\n",
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(written.trim(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_get_has_invalid_report_id_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = ReportsCommand::Get {
|
||||||
|
report_id: 0,
|
||||||
|
data_format: None,
|
||||||
|
data_type: None,
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage("report_id must be greater than 0."))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert!(mock_client.get_report_report_id.lock().unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_get_has_negative_report_id_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = ReportsCommand::Get {
|
||||||
|
report_id: -5,
|
||||||
|
data_format: None,
|
||||||
|
data_type: None,
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Err(CliError::usage("report_id must be greater than 0."))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
assert!(mock_client.get_report_report_id.lock().unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_get_fails_it_should_return_mapped_cli_error() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
get_report_result: Err(OnspringError::Api {
|
||||||
|
status_code: 404,
|
||||||
|
message: "Report not found".to_string(),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = ReportsCommand::Get {
|
||||||
|
report_id: 999,
|
||||||
|
data_format: None,
|
||||||
|
data_type: None,
|
||||||
|
};
|
||||||
|
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): Report not found"))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_list_succeeds_it_should_write_paged_output() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
list_reports_result: Ok(PagedResponse {
|
||||||
|
page_number: Some(1),
|
||||||
|
page_size: Some(50),
|
||||||
|
total_pages: Some(1),
|
||||||
|
total_records: Some(1),
|
||||||
|
items: Some(vec![ReportInfo {
|
||||||
|
app_id: 10,
|
||||||
|
id: 100,
|
||||||
|
name: Some("Open Tasks".to_string()),
|
||||||
|
description: Some("Report of open tasks".to_string()),
|
||||||
|
}]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = ReportsCommand::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":[{"appId":10,"id":100,"name":"Open Tasks","description":"Report of open tasks"}]}"#
|
||||||
|
);
|
||||||
|
assert_eq!(*mock_client.list_reports_app_id.lock().unwrap(), Some(10));
|
||||||
|
assert!(
|
||||||
|
mock_client
|
||||||
|
.list_reports_paging
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_list_with_paging_args_it_should_pass_paging_request() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
list_reports_result: Ok(PagedResponse {
|
||||||
|
page_number: Some(2),
|
||||||
|
page_size: Some(15),
|
||||||
|
total_pages: Some(3),
|
||||||
|
total_records: Some(40),
|
||||||
|
items: Some(vec![]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = ReportsCommand::List {
|
||||||
|
app_id: 10,
|
||||||
|
paging: PagingArgs {
|
||||||
|
page_number: Some(2),
|
||||||
|
page_size: Some(15),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let result = handle(&command, &mock_client, &mut buffer, false).await;
|
||||||
|
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
|
||||||
|
let paging = mock_client
|
||||||
|
.list_reports_paging
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.take()
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(paging.page_number, 2);
|
||||||
|
assert_eq!(paging.page_size, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_list_succeeds_pretty_it_should_write_pretty_json() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
list_reports_result: Ok(PagedResponse {
|
||||||
|
page_number: Some(1),
|
||||||
|
page_size: Some(50),
|
||||||
|
total_pages: Some(1),
|
||||||
|
total_records: Some(1),
|
||||||
|
items: Some(vec![ReportInfo {
|
||||||
|
app_id: 10,
|
||||||
|
id: 100,
|
||||||
|
name: Some("Open Tasks".to_string()),
|
||||||
|
description: None,
|
||||||
|
}]),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = ReportsCommand::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",
|
||||||
|
" \"appId\": 10,\n",
|
||||||
|
" \"id\": 100,\n",
|
||||||
|
" \"name\": \"Open Tasks\",\n",
|
||||||
|
" \"description\": null\n",
|
||||||
|
" }\n",
|
||||||
|
" ]\n",
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(written.trim(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_list_has_invalid_app_id_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = ReportsCommand::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_reports_app_id.lock().unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_list_has_invalid_paging_it_should_return_usage_error() {
|
||||||
|
let mock_client = MockClient::default();
|
||||||
|
|
||||||
|
let command = ReportsCommand::List {
|
||||||
|
app_id: 10,
|
||||||
|
paging: PagingArgs {
|
||||||
|
page_number: Some(-1),
|
||||||
|
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_reports_paging.lock().unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handle_when_reports_list_fails_it_should_return_mapped_cli_error() {
|
||||||
|
let mock_client = MockClient {
|
||||||
|
list_reports_result: Err(OnspringError::Api {
|
||||||
|
status_code: 403,
|
||||||
|
message: "Forbidden".to_string(),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = ReportsCommand::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 (403): Forbidden"))
|
||||||
|
);
|
||||||
|
assert!(buffer.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::cli::Cli;
|
||||||
|
use crate::error::{CliError, CliResult};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ConfigFile {
|
||||||
|
pub api_key: Option<String>,
|
||||||
|
pub base_url: Option<String>,
|
||||||
|
pub pretty: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct ResolvedConfig {
|
||||||
|
pub api_key: Option<String>,
|
||||||
|
pub base_url: Option<String>,
|
||||||
|
pub pretty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_config_path() -> Option<PathBuf> {
|
||||||
|
let home = std::env::var("HOME")
|
||||||
|
.ok()
|
||||||
|
.or_else(|| std::env::var("USERPROFILE").ok())?;
|
||||||
|
|
||||||
|
Some(PathBuf::from(home).join(".config").join("onx").join("config.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config_file(path: &Path, is_explicit: bool) -> CliResult<Option<ConfigFile>> {
|
||||||
|
if !path.exists() {
|
||||||
|
if is_explicit {
|
||||||
|
return Err(CliError::usage(format!(
|
||||||
|
"Config file not found: {}",
|
||||||
|
path.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = fs::read_to_string(path).map_err(|err| {
|
||||||
|
CliError::runtime(format!(
|
||||||
|
"Failed to read config file {}: {err}",
|
||||||
|
path.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let config: ConfigFile = serde_json::from_str(&contents).map_err(|err| {
|
||||||
|
CliError::runtime(format!(
|
||||||
|
"Failed to parse config file {}: {err}",
|
||||||
|
path.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Some(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_config(cli: &Cli, config_file: Option<&ConfigFile>) -> ResolvedConfig {
|
||||||
|
let config_file_default = ConfigFile::default();
|
||||||
|
let file = config_file.unwrap_or(&config_file_default);
|
||||||
|
|
||||||
|
// Precedence: CLI Flag > Config File > Environment Variable
|
||||||
|
let api_key = cli
|
||||||
|
.api_key
|
||||||
|
.clone()
|
||||||
|
.or_else(|| file.api_key.clone())
|
||||||
|
.or_else(|| std::env::var("ONSPRING_API_KEY").ok());
|
||||||
|
|
||||||
|
let base_url = cli
|
||||||
|
.base_url
|
||||||
|
.clone()
|
||||||
|
.or_else(|| file.base_url.clone())
|
||||||
|
.or_else(|| std::env::var("ONSPRING_BASE_URL").ok());
|
||||||
|
|
||||||
|
let pretty = if cli.pretty {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
file.pretty.unwrap_or(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
ResolvedConfig {
|
||||||
|
api_key,
|
||||||
|
base_url,
|
||||||
|
pretty,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cli::Command;
|
||||||
|
use std::io::Write;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_file_when_file_exists_it_should_parse_camel_case_json() {
|
||||||
|
let mut file = NamedTempFile::new().unwrap();
|
||||||
|
writeln!(
|
||||||
|
file,
|
||||||
|
r#"{{"apiKey":"file-key","baseUrl":"https://config.onspring.com","pretty":true}}"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let loaded = load_config_file(file.path(), true).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
loaded,
|
||||||
|
Some(ConfigFile {
|
||||||
|
api_key: Some("file-key".to_string()),
|
||||||
|
base_url: Some("https://config.onspring.com".to_string()),
|
||||||
|
pretty: Some(true),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_file_when_not_explicit_and_file_missing_it_should_return_none() {
|
||||||
|
let missing_path = PathBuf::from("non_existent_config_file_path_12345.json");
|
||||||
|
let loaded = load_config_file(&missing_path, false).unwrap();
|
||||||
|
assert_eq!(loaded, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_file_when_explicit_and_file_missing_it_should_return_usage_error() {
|
||||||
|
let missing_path = PathBuf::from("non_existent_config_file_path_12345.json");
|
||||||
|
let err = load_config_file(&missing_path, true).unwrap_err();
|
||||||
|
assert_eq!(err.code, 2);
|
||||||
|
assert!(err.message.contains("Config file not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_file_when_invalid_json_it_should_return_runtime_error() {
|
||||||
|
let mut file = NamedTempFile::new().unwrap();
|
||||||
|
writeln!(file, "not json").unwrap();
|
||||||
|
|
||||||
|
let err = load_config_file(file.path(), true).unwrap_err();
|
||||||
|
assert_eq!(err.code, 1);
|
||||||
|
assert!(err.message.contains("Failed to parse config file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_config_path_when_home_or_userprofile_set_it_should_resolve_path() {
|
||||||
|
let path = default_config_path();
|
||||||
|
assert!(path.is_some());
|
||||||
|
let p = path.unwrap();
|
||||||
|
assert!(p.ends_with(".config/onx/config.json") || p.ends_with(r#".config\onx\config.json"#));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_config_when_cli_flag_provided_it_should_take_precedence() {
|
||||||
|
let cli = Cli {
|
||||||
|
api_key: Some("cli-key".to_string()),
|
||||||
|
base_url: Some("https://cli.onspring.com".to_string()),
|
||||||
|
config: None,
|
||||||
|
pretty: true,
|
||||||
|
command: Command::Ping,
|
||||||
|
};
|
||||||
|
let file = ConfigFile {
|
||||||
|
api_key: Some("file-key".to_string()),
|
||||||
|
base_url: Some("https://file.onspring.com".to_string()),
|
||||||
|
pretty: Some(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resolved = resolve_config(&cli, Some(&file));
|
||||||
|
assert_eq!(
|
||||||
|
resolved,
|
||||||
|
ResolvedConfig {
|
||||||
|
api_key: Some("cli-key".to_string()),
|
||||||
|
base_url: Some("https://cli.onspring.com".to_string()),
|
||||||
|
pretty: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_config_when_cli_flag_absent_it_should_use_config_file() {
|
||||||
|
let cli = Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
config: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
|
};
|
||||||
|
let file = ConfigFile {
|
||||||
|
api_key: Some("file-key".to_string()),
|
||||||
|
base_url: Some("https://file.onspring.com".to_string()),
|
||||||
|
pretty: Some(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resolved = resolve_config(&cli, Some(&file));
|
||||||
|
assert_eq!(
|
||||||
|
resolved,
|
||||||
|
ResolvedConfig {
|
||||||
|
api_key: Some("file-key".to_string()),
|
||||||
|
base_url: Some("https://file.onspring.com".to_string()),
|
||||||
|
pretty: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_config_when_no_file_it_should_default_pretty_to_false() {
|
||||||
|
let cli = Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
config: None,
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
|
};
|
||||||
|
|
||||||
|
let resolved = resolve_config(&cli, None);
|
||||||
|
assert!(!resolved.pretty);
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
-5
@@ -1,6 +1,7 @@
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod client;
|
mod client;
|
||||||
mod commands;
|
mod commands;
|
||||||
|
mod config;
|
||||||
mod error;
|
mod error;
|
||||||
mod output;
|
mod output;
|
||||||
mod validation;
|
mod validation;
|
||||||
@@ -9,17 +10,28 @@ use onspring::OnspringClient;
|
|||||||
|
|
||||||
pub use cli::{Cli, Command, parse_cli_from};
|
pub use cli::{Cli, Command, parse_cli_from};
|
||||||
pub use client::OnspringRunner;
|
pub use client::OnspringRunner;
|
||||||
|
pub use config::{ConfigFile, ResolvedConfig, default_config_path, load_config_file, resolve_config};
|
||||||
pub use error::{CliError, CliResult, render_cli_error};
|
pub use error::{CliError, CliResult, render_cli_error};
|
||||||
|
|
||||||
pub async fn run(cli: Cli) -> CliResult<()> {
|
pub async fn run(mut cli: Cli) -> CliResult<()> {
|
||||||
let api_key = cli
|
let config_file = if let Some(config_path) = &cli.config {
|
||||||
|
load_config_file(config_path, true)?
|
||||||
|
} else if let Some(default_path) = default_config_path() {
|
||||||
|
load_config_file(&default_path, false)?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let resolved = resolve_config(&cli, config_file.as_ref());
|
||||||
|
|
||||||
|
let api_key = resolved
|
||||||
.api_key
|
.api_key
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.ok_or_else(|| CliError::usage("Missing API Key. Set --api-key or ONSPRING_API_KEY."))?;
|
.ok_or_else(|| CliError::usage("Missing API Key. Set --api-key, config file, or ONSPRING_API_KEY."))?;
|
||||||
|
|
||||||
let mut builder = OnspringClient::builder(api_key.to_string());
|
let mut builder = OnspringClient::builder(api_key.to_string());
|
||||||
|
|
||||||
if let Some(base_url) = &cli.base_url {
|
if let Some(base_url) = &resolved.base_url {
|
||||||
if base_url.trim().is_empty() {
|
if base_url.trim().is_empty() {
|
||||||
return Err(CliError::usage("Base URL cannot be empty."));
|
return Err(CliError::usage("Base URL cannot be empty."));
|
||||||
}
|
}
|
||||||
@@ -27,6 +39,8 @@ pub async fn run(cli: Cli) -> CliResult<()> {
|
|||||||
builder = builder.base_url(base_url);
|
builder = builder.base_url(base_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cli.pretty = resolved.pretty;
|
||||||
|
|
||||||
let client = builder.build();
|
let client = builder.build();
|
||||||
let mut stdout = std::io::stdout();
|
let mut stdout = std::io::stdout();
|
||||||
run_with_client(&cli, &client, &mut stdout).await
|
run_with_client(&cli, &client, &mut stdout).await
|
||||||
@@ -43,12 +57,15 @@ pub async fn run_with_client<C: OnspringRunner, W: std::io::Write>(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::io::Write;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn run_when_missing_api_key_it_should_return_usage_error() {
|
async fn run_when_missing_api_key_it_should_return_usage_error() {
|
||||||
let cli = Cli {
|
let cli = Cli {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Ping,
|
command: Command::Ping,
|
||||||
};
|
};
|
||||||
@@ -58,16 +75,59 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
result,
|
||||||
Err(CliError::usage(
|
Err(CliError::usage(
|
||||||
"Missing API Key. Set --api-key or ONSPRING_API_KEY."
|
"Missing API Key. Set --api-key, config file, or ONSPRING_API_KEY."
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn run_when_api_key_in_config_file_it_should_succeed() {
|
||||||
|
let mut config_file = NamedTempFile::new().unwrap();
|
||||||
|
writeln!(config_file, r#"{{"apiKey":"config-key"}}"#).unwrap();
|
||||||
|
|
||||||
|
let cli = Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
config: Some(config_file.path().to_path_buf()),
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
|
};
|
||||||
|
|
||||||
|
// run builds the client with config-key and calls ping
|
||||||
|
// Since OnspringClient builder does not hit network until execute,
|
||||||
|
// calling run will fail at runtime network request or succeed if network is mocked,
|
||||||
|
// but here run creates an OnspringClient and executes ping against real endpoint (network error).
|
||||||
|
let result = run(cli).await;
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn run_when_empty_base_url_in_config_file_it_should_return_usage_error() {
|
||||||
|
let mut config_file = NamedTempFile::new().unwrap();
|
||||||
|
writeln!(
|
||||||
|
config_file,
|
||||||
|
r#"{{"apiKey":"config-key","baseUrl":" "}}"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let cli = Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
config: Some(config_file.path().to_path_buf()),
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = run(cli).await;
|
||||||
|
assert_eq!(result, Err(CliError::usage("Base URL cannot be empty.")));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn run_when_empty_base_url_it_should_return_usage_error() {
|
async fn run_when_empty_base_url_it_should_return_usage_error() {
|
||||||
let cli = Cli {
|
let cli = Cli {
|
||||||
api_key: Some("test-api-key".to_string()),
|
api_key: Some("test-api-key".to_string()),
|
||||||
base_url: Some(" ".to_string()),
|
base_url: Some(" ".to_string()),
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Ping,
|
command: Command::Ping,
|
||||||
};
|
};
|
||||||
@@ -77,12 +137,33 @@ mod tests {
|
|||||||
assert_eq!(result, Err(CliError::usage("Base URL cannot be empty.")));
|
assert_eq!(result, Err(CliError::usage("Base URL cannot be empty.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn run_when_config_file_has_invalid_json_it_should_return_runtime_error() {
|
||||||
|
let mut config_file = NamedTempFile::new().unwrap();
|
||||||
|
writeln!(config_file, "invalid-json").unwrap();
|
||||||
|
|
||||||
|
let cli = Cli {
|
||||||
|
api_key: None,
|
||||||
|
base_url: None,
|
||||||
|
config: Some(config_file.path().to_path_buf()),
|
||||||
|
pretty: false,
|
||||||
|
command: Command::Ping,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = run(cli).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code, 1);
|
||||||
|
assert!(err.message.contains("Failed to parse config file"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn run_with_client_it_should_delegate_to_commands() {
|
async fn run_with_client_it_should_delegate_to_commands() {
|
||||||
let mock_client = client::testing::MockClient::default();
|
let mock_client = client::testing::MockClient::default();
|
||||||
let cli = Cli {
|
let cli = Cli {
|
||||||
api_key: Some("key".to_string()),
|
api_key: Some("key".to_string()),
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
config: None,
|
||||||
pretty: false,
|
pretty: false,
|
||||||
command: Command::Ping,
|
command: Command::Ping,
|
||||||
};
|
};
|
||||||
|
|||||||
+56
-2
@@ -3,8 +3,8 @@ use std::io::Write;
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use onspring::{
|
use onspring::{
|
||||||
App, CollectionResponse, CreatedWithIdResponse, Field, FileInfo, FormulaOutputType,
|
App, CollectionResponse, CreatedWithIdResponse, Field, FileInfo, FormulaOutputType,
|
||||||
ListFieldValue, Multiplicity, PagedResponse, Record, RecordFieldValue, SaveListItemResponse,
|
ListFieldValue, Multiplicity, PagedResponse, Record, RecordFieldValue, ReportData, ReportInfo,
|
||||||
SaveRecordResponse, ValueType,
|
ReportRow, SaveListItemResponse, SaveRecordResponse, ValueType,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -263,6 +263,60 @@ impl From<SaveListItemResponse> for SaveListItemResponseOutput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ReportInfoOutput {
|
||||||
|
pub app_id: i32,
|
||||||
|
pub id: i32,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReportInfo> for ReportInfoOutput {
|
||||||
|
fn from(value: ReportInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
app_id: value.app_id,
|
||||||
|
id: value.id,
|
||||||
|
name: value.name,
|
||||||
|
description: value.description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ReportRowOutput {
|
||||||
|
pub record_id: Option<i32>,
|
||||||
|
pub cells: Option<Vec<serde_json::Value>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReportRow> for ReportRowOutput {
|
||||||
|
fn from(value: ReportRow) -> Self {
|
||||||
|
Self {
|
||||||
|
record_id: value.record_id,
|
||||||
|
cells: value.cells,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ReportDataOutput {
|
||||||
|
pub columns: Option<Vec<String>>,
|
||||||
|
pub rows: Option<Vec<ReportRowOutput>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReportData> for ReportDataOutput {
|
||||||
|
fn from(value: ReportData) -> Self {
|
||||||
|
Self {
|
||||||
|
columns: value.columns,
|
||||||
|
rows: value
|
||||||
|
.rows
|
||||||
|
.map(|rows| rows.into_iter().map(ReportRowOutput::from).collect()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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"))
|
||||||
|
|||||||
Reference in new issue
Block a user