use std::{borrow::Cow, fmt};
use base64_codec::codec::Base64Codec;
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::{Serialize, Serializer, ser::SerializeStruct};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Bytes {
pub data: Vec<u8>,
pub mime: String,
}
impl fmt::Display for Bytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{} bytes of {}", self.data.len(), self.mime)
}
}
impl Serialize for Bytes {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut data = Vec::new();
Base64Codec::default().encode_vec(&self.data, &mut data);
let data = String::from_utf8(data).unwrap_or_default();
let mut bytes = serializer.serialize_struct("Bytes", 3)?;
bytes.serialize_field("data", &data)?;
bytes.serialize_field("size", &self.data.len())?;
bytes.serialize_field("mime", &self.mime)?;
bytes.end()
}
}
impl JsonSchema for Bytes {
fn schema_name() -> Cow<'static, str> {
"Bytes".into()
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
json_schema!({
"description": "Binary content.",
"type": "object",
"properties": {
"data": {
"description": "The content, in base64.",
"type": "string",
"contentEncoding": "base64",
},
"size": {
"description": "The size of the content, in bytes.",
"type": "integer",
"format": "uint64",
"minimum": 0,
},
"mime": {
"description": "The media type of the content.",
"type": "string",
},
},
"required": ["data", "size", "mime"],
})
}
}
#[derive(Clone, Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct SavedOutput {
pub path: String,
pub size: usize,
}
impl fmt::Display for SavedOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Saved {} bytes to {}", self.size, self.path)
}
}
#[derive(Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Paginated<T> {
#[serde(flatten)]
pub inner: T,
pub next_page: Option<String>,
}
impl<T: fmt::Display> fmt::Display for Paginated<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let inner = self.inner.to_string();
f.write_str(&inner)?;
if let Some(cursor) = &self.next_page {
if !inner.is_empty() {
writeln!(f)?;
}
writeln!(f, "Next page: {cursor}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::{clap::commands::json_schema::output_schema, output::*};
#[derive(Serialize, JsonSchema)]
struct Rows {
rows: Vec<u8>,
}
#[test]
fn bytes_print_as_base64_with_their_size() {
let bytes = Bytes {
data: b"\xff\x00a".to_vec(),
mime: "application/octet-stream".into(),
};
assert_eq!(
serde_json::to_value(bytes).unwrap(),
json!({ "data": "/wBh", "size": 3, "mime": "application/octet-stream" })
);
}
#[test]
fn a_saved_file_prints_its_path_and_size() {
let saved = SavedOutput {
path: String::from("/tmp/a.pdf"),
size: 42,
};
assert_eq!(saved.to_string(), "Saved 42 bytes to /tmp/a.pdf\n");
assert_eq!(
serde_json::to_string(&saved).unwrap(),
r#"{"path":"/tmp/a.pdf","size":42}"#
);
}
#[test]
fn a_complete_listing_prints_a_null_cursor() {
let page = Paginated {
inner: Rows { rows: vec![1] },
next_page: None,
};
assert_eq!(
serde_json::to_value(page).unwrap(),
json!({ "rows": [1], "nextPage": null })
);
}
#[test]
fn the_text_cursor_follows_a_blank_line() {
let page = Paginated {
inner: "42\n",
next_page: Some(String::from("2")),
};
assert_eq!(page.to_string(), "42\n\nNext page: 2\n");
let page = Paginated {
inner: "",
next_page: Some(String::from("2")),
};
assert_eq!(page.to_string(), "Next page: 2\n");
}
#[test]
fn the_cursor_is_required_in_the_schema() {
let schema = output_schema::<Paginated<Rows>>();
let required = schema["required"].as_array().unwrap();
assert!(required.contains(&json!("nextPage")), "{schema}");
assert!(required.contains(&json!("rows")), "{schema}");
}
}