1use std::{borrow::Cow, fmt};
12
13use base64_codec::codec::Base64Codec;
14use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
15use serde::{Serialize, Serializer, ser::SerializeStruct};
16
17#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct Bytes {
21 pub data: Vec<u8>,
23 pub mime: String,
26}
27
28impl fmt::Display for Bytes {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 writeln!(f, "{} bytes of {}", self.data.len(), self.mime)
31 }
32}
33
34impl Serialize for Bytes {
35 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
36 let mut data = Vec::new();
37 Base64Codec::default().encode_vec(&self.data, &mut data);
38 let data = String::from_utf8(data).unwrap_or_default();
40
41 let mut bytes = serializer.serialize_struct("Bytes", 3)?;
42 bytes.serialize_field("data", &data)?;
43 bytes.serialize_field("size", &self.data.len())?;
44 bytes.serialize_field("mime", &self.mime)?;
45 bytes.end()
46 }
47}
48
49impl JsonSchema for Bytes {
50 fn schema_name() -> Cow<'static, str> {
51 "Bytes".into()
52 }
53
54 fn json_schema(_: &mut SchemaGenerator) -> Schema {
55 json_schema!({
56 "description": "Binary content.",
57 "type": "object",
58 "properties": {
59 "data": {
60 "description": "The content, in base64.",
61 "type": "string",
62 "contentEncoding": "base64",
63 },
64 "size": {
65 "description": "The size of the content, in bytes.",
66 "type": "integer",
67 "format": "uint64",
68 "minimum": 0,
69 },
70 "mime": {
71 "description": "The media type of the content.",
72 "type": "string",
73 },
74 },
75 "required": ["data", "size", "mime"],
76 })
77 }
78}
79
80#[derive(Clone, Debug, Serialize, JsonSchema)]
82#[serde(rename_all = "camelCase")]
83pub struct SavedOutput {
84 pub path: String,
86 pub size: usize,
88}
89
90impl fmt::Display for SavedOutput {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 writeln!(f, "Saved {} bytes to {}", self.size, self.path)
93 }
94}
95
96#[derive(Serialize, JsonSchema)]
102#[serde(rename_all = "camelCase")]
103pub struct Paginated<T> {
104 #[serde(flatten)]
106 pub inner: T,
107 pub next_page: Option<String>,
109}
110
111impl<T: fmt::Display> fmt::Display for Paginated<T> {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 let inner = self.inner.to_string();
114 f.write_str(&inner)?;
115
116 if let Some(cursor) = &self.next_page {
117 if !inner.is_empty() {
118 writeln!(f)?;
119 }
120
121 writeln!(f, "Next page: {cursor}")?;
122 }
123
124 Ok(())
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use serde_json::json;
131
132 use crate::{clap::commands::json_schema::output_schema, output::*};
133
134 #[derive(Serialize, JsonSchema)]
135 struct Rows {
136 rows: Vec<u8>,
137 }
138
139 #[test]
140 fn bytes_print_as_base64_with_their_size() {
141 let bytes = Bytes {
142 data: b"\xff\x00a".to_vec(),
143 mime: "application/octet-stream".into(),
144 };
145
146 assert_eq!(
147 serde_json::to_value(bytes).unwrap(),
148 json!({ "data": "/wBh", "size": 3, "mime": "application/octet-stream" })
149 );
150 }
151
152 #[test]
153 fn a_saved_file_prints_its_path_and_size() {
154 let saved = SavedOutput {
155 path: String::from("/tmp/a.pdf"),
156 size: 42,
157 };
158
159 assert_eq!(saved.to_string(), "Saved 42 bytes to /tmp/a.pdf\n");
160 assert_eq!(
161 serde_json::to_string(&saved).unwrap(),
162 r#"{"path":"/tmp/a.pdf","size":42}"#
163 );
164 }
165
166 #[test]
167 fn a_complete_listing_prints_a_null_cursor() {
168 let page = Paginated {
169 inner: Rows { rows: vec![1] },
170 next_page: None,
171 };
172
173 assert_eq!(
174 serde_json::to_value(page).unwrap(),
175 json!({ "rows": [1], "nextPage": null })
176 );
177 }
178
179 #[test]
180 fn the_text_cursor_follows_a_blank_line() {
181 let page = Paginated {
182 inner: "42\n",
183 next_page: Some(String::from("2")),
184 };
185 assert_eq!(page.to_string(), "42\n\nNext page: 2\n");
186
187 let page = Paginated {
188 inner: "",
189 next_page: Some(String::from("2")),
190 };
191 assert_eq!(page.to_string(), "Next page: 2\n");
192 }
193
194 #[test]
195 fn the_cursor_is_required_in_the_schema() {
196 let schema = output_schema::<Paginated<Rows>>();
197 let required = schema["required"].as_array().unwrap();
198
199 assert!(required.contains(&json!("nextPage")), "{schema}");
200 assert!(required.contains(&json!("rows")), "{schema}");
201 }
202}