use std::io::Write; use chrono::{DateTime, Utc}; use onspring::{ App, CollectionResponse, CreatedWithIdResponse, Field, FileInfo, FormulaOutputType, ListFieldValue, Multiplicity, PagedResponse, Record, RecordFieldValue, ReportData, ReportInfo, ReportRow, SaveListItemResponse, SaveRecordResponse, ValueType, }; use serde::Serialize; use uuid::Uuid; use crate::{CliError, CliResult}; #[derive(Debug, Serialize)] struct ErrorBody<'a> { pub code: i32, pub message: &'a str, } #[derive(Debug, Serialize)] struct ErrorEnvelope<'a> { pub error: ErrorBody<'a>, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct SuccessResponse { pub ok: bool, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct PagedOutput { pub page_number: Option, pub page_size: Option, pub total_pages: Option, pub total_records: Option, pub items: Option>, } impl From> for PagedOutput where T: From, { fn from(value: PagedResponse) -> Self { Self { page_number: value.page_number, page_size: value.page_size, total_pages: value.total_pages, total_records: value.total_records, items: value .items .map(|items| items.into_iter().map(T::from).collect()), } } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CollectionOutput { pub count: Option, pub items: Option>, } impl From> for CollectionOutput where T: From, { fn from(value: CollectionResponse) -> Self { Self { count: value.count, items: value .items .map(|items| items.into_iter().map(T::from).collect()), } } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct AppOutput { pub href: Option, pub id: i32, pub name: Option, } impl From for AppOutput { fn from(value: App) -> Self { Self { href: value.href, id: value.id, name: value.name, } } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ListFieldValueOutput { pub id: Uuid, pub name: String, pub sort_order: i32, pub numeric_value: Option, pub color: Option, } impl From 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, #[serde(rename = "type")] pub field_type: Option, pub status: Option, pub is_required: bool, pub is_unique: bool, pub multiplicity: Option, pub list_id: Option, pub values: Option>, pub output_type: Option, pub referenced_app_id: Option, } impl From for FieldOutput { fn from(value: Field) -> Self { Self { id: value.id, app_id: value.app_id, name: value.name, field_type: value.field_type, status: value.status, is_required: value.is_required, is_unique: value.is_unique, multiplicity: value.multiplicity, list_id: value.list_id, values: value .values .map(|values| values.into_iter().map(ListFieldValueOutput::from).collect()), output_type: value.output_type, referenced_app_id: value.referenced_app_id, } } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct RecordFieldValueOutput { #[serde(rename = "type")] pub value_type: ValueType, pub field_id: i32, pub value: serde_json::Value, } impl From for RecordFieldValueOutput { fn from(value: RecordFieldValue) -> Self { Self { value_type: value.value_type, field_id: value.field_id, value: value.value, } } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct RecordOutput { pub app_id: i32, pub record_id: i32, pub field_data: Option>, } impl From for RecordOutput { fn from(value: Record) -> Self { Self { app_id: value.app_id, record_id: value.record_id, field_data: value .field_data .map(|fields| fields.into_iter().map(RecordFieldValueOutput::from).collect()), } } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct SaveRecordResponseOutput { pub id: i32, pub warnings: Option>, } impl From for SaveRecordResponseOutput { fn from(value: SaveRecordResponse) -> Self { Self { id: value.id, warnings: value.warnings, } } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FileInfoOutput { #[serde(rename = "type")] pub file_type: Option, pub content_type: Option, pub name: Option, pub created_date: Option>, pub modified_date: Option>, pub owner: Option, pub notes: Option, pub file_href: Option, } impl From for FileInfoOutput { fn from(value: FileInfo) -> Self { Self { file_type: value.file_type, content_type: value.content_type, name: value.name, created_date: value.created_date, modified_date: value.modified_date, owner: value.owner, notes: value.notes, file_href: value.file_href, } } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CreatedWithIdResponseOutput { pub id: i32, } impl From for CreatedWithIdResponseOutput { fn from(value: CreatedWithIdResponse) -> Self { Self { id: value.id } } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct SaveListItemResponseOutput { pub id: Uuid, } impl From for SaveListItemResponseOutput { fn from(value: SaveListItemResponse) -> Self { Self { id: value.id } } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReportInfoOutput { pub app_id: i32, pub id: i32, pub name: Option, pub description: Option, } impl From 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, pub cells: Option>, } impl From 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>, pub rows: Option>, } impl From 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(value: &T, pretty: bool) -> CliResult { if pretty { serde_json::to_string_pretty(value).map_err(|_| CliError::runtime("Failed to serialize output")) } else { serde_json::to_string(value).map_err(|_| CliError::runtime("Failed to serialize output")) } } pub fn render_error(err: &CliError, pretty: bool) -> CliResult { let envelope = ErrorEnvelope { error: ErrorBody { code: err.code, message: &err.message, }, }; render_json(&envelope, pretty) } pub fn write_json( writer: &mut W, value: &T, pretty: bool, ) -> CliResult<()> { let rendered = render_json(value, pretty)?; writeln!(writer, "{rendered}").map_err(|_| CliError::runtime("Failed to write output"))?; Ok(()) } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn render_json_when_called_without_pretty_it_should_print_value() { let data = json!({ "name": "Stevan", }); let result = render_json(&data, false); let expected = Ok(String::from(r#"{"name":"Stevan"}"#)); assert_eq!(result, expected); } #[test] fn render_json_when_called_with_pretty_it_should_pretty_print_value() { let data = json!({ "name": "Stevan", }); let result = render_json(&data, true); let expected = Ok(String::from("{\n \"name\": \"Stevan\"\n}")); assert_eq!(result, expected); } #[test] fn render_error_when_called_with_error_it_should_render_properly() { let error = CliError { code: 1, message: String::from("This is a test"), }; let result = render_error(&error, false); let expected = Ok(String::from( r#"{"error":{"code":1,"message":"This is a test"}}"#, )); assert_eq!(result, expected); } #[test] fn write_json_when_called_with_writer_it_should_write_formatted_string() { let data = json!({"ok": true}); let mut buffer = Vec::new(); let result = write_json(&mut buffer, &data, false); assert_eq!(result, Ok(())); let output_str = String::from_utf8(buffer).unwrap(); assert_eq!(output_str, "{\"ok\":true}\n"); } }