1use std::{
2 collections::BTreeMap,
3 fmt,
4 sync::{Arc, OnceLock, RwLock},
5};
6
7use serde_json::Value;
8
9use super::Envelope;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct TableColumn {
14 pub field: String,
16 pub header: String,
18}
19
20impl TableColumn {
21 #[must_use]
23 pub fn new(field: impl Into<String>, header: impl Into<String>) -> Self {
24 Self {
25 field: field.into(),
26 header: header.into(),
27 }
28 }
29}
30
31#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct HumanViewDef {
34 pub schema_id: String,
36 pub columns: Vec<TableColumn>,
38}
39
40impl HumanViewDef {
41 #[must_use]
43 pub fn new(schema_id: impl Into<String>, columns: impl Into<Vec<TableColumn>>) -> Self {
44 Self {
45 schema_id: schema_id.into(),
46 columns: columns.into(),
47 }
48 }
49}
50
51pub type HumanViewFn = Arc<dyn Fn(&Value) -> String + Send + Sync>;
53
54#[derive(Clone)]
56pub struct HumanViewRenderer {
57 render: HumanViewFn,
58}
59
60impl HumanViewRenderer {
61 #[must_use]
63 pub fn new(render: impl Fn(&Value) -> String + Send + Sync + 'static) -> Self {
64 Self {
65 render: Arc::new(render),
66 }
67 }
68
69 #[must_use]
71 pub fn render(&self, data: &Value) -> String {
72 (self.render)(data)
73 }
74}
75
76impl fmt::Debug for HumanViewRenderer {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 formatter
79 .debug_struct("HumanViewRenderer")
80 .finish_non_exhaustive()
81 }
82}
83
84#[derive(Clone, Debug, Default)]
86pub struct HumanViewRegistry {
87 by_schema_id: BTreeMap<String, Vec<TableColumn>>,
88 custom_by_schema_id: BTreeMap<String, HumanViewRenderer>,
89}
90
91impl HumanViewRegistry {
92 #[must_use]
94 pub fn new() -> Self {
95 Self::default()
96 }
97
98 pub fn register(&mut self, view: HumanViewDef) {
100 self.by_schema_id.insert(view.schema_id, view.columns);
101 }
102
103 pub fn register_func(
105 &mut self,
106 schema_id: impl Into<String>,
107 render: impl Fn(&Value) -> String + Send + Sync + 'static,
108 ) {
109 self.custom_by_schema_id
110 .insert(schema_id.into(), HumanViewRenderer::new(render));
111 }
112
113 pub fn merge(&mut self, other: &Self) {
115 self.by_schema_id.extend(other.by_schema_id.clone());
116 self.custom_by_schema_id
117 .extend(other.custom_by_schema_id.clone());
118 }
119
120 #[must_use]
122 pub fn columns(&self, schema_id: &str) -> Option<&[TableColumn]> {
123 self.by_schema_id.get(schema_id).map(Vec::as_slice)
124 }
125
126 #[must_use]
128 pub fn custom(&self, schema_id: &str) -> Option<&HumanViewRenderer> {
129 self.custom_by_schema_id.get(schema_id)
130 }
131}
132
133static GLOBAL_HUMAN_VIEW_REGISTRY: OnceLock<RwLock<HumanViewRegistry>> = OnceLock::new();
134
135fn global_human_view_registry() -> &'static RwLock<HumanViewRegistry> {
136 GLOBAL_HUMAN_VIEW_REGISTRY.get_or_init(|| RwLock::new(HumanViewRegistry::new()))
137}
138
139pub fn register_global_human_view(view: HumanViewDef) {
141 let mut registry = global_human_view_registry()
142 .write()
143 .unwrap_or_else(|poisoned| poisoned.into_inner());
144 registry.register(view);
145}
146
147pub fn register_global_human_view_func(
149 schema_id: impl Into<String>,
150 render: impl Fn(&Value) -> String + Send + Sync + 'static,
151) {
152 let mut registry = global_human_view_registry()
153 .write()
154 .unwrap_or_else(|poisoned| poisoned.into_inner());
155 registry.register_func(schema_id, render);
156}
157
158#[must_use]
160pub fn lookup_global_human_view_columns(schema_id: &str) -> Option<Vec<TableColumn>> {
161 global_human_view_registry()
162 .read()
163 .unwrap_or_else(|poisoned| poisoned.into_inner())
164 .columns(schema_id)
165 .map(<[TableColumn]>::to_vec)
166}
167
168#[must_use]
170pub fn lookup_global_human_view_func(schema_id: &str) -> Option<HumanViewRenderer> {
171 global_human_view_registry()
172 .read()
173 .unwrap_or_else(|poisoned| poisoned.into_inner())
174 .custom(schema_id)
175 .cloned()
176}
177
178#[must_use]
180pub fn global_human_view_registry_snapshot() -> HumanViewRegistry {
181 global_human_view_registry()
182 .read()
183 .unwrap_or_else(|poisoned| poisoned.into_inner())
184 .clone()
185}
186
187#[must_use]
189pub fn render_human(envelope: &Envelope) -> String {
190 render_human_with_view(envelope, None)
191}
192
193#[must_use]
195pub fn render_human_with_registry(envelope: &Envelope, registry: &HumanViewRegistry) -> String {
196 let system = envelope
197 .metadata
198 .as_ref()
199 .map(|metadata| metadata.system.as_str())
200 .unwrap_or_default();
201 render_human_with_registry_for_schema(envelope, registry, system)
202}
203
204#[must_use]
206pub fn render_human_with_registry_for_schema(
207 envelope: &Envelope,
208 registry: &HumanViewRegistry,
209 schema_id: &str,
210) -> String {
211 if let Some(error) = &envelope.error {
212 return format!("Error: {}\n", error.message);
213 }
214 if let Some(data) = &envelope.data
215 && let Some(custom) = registry.custom(schema_id)
216 {
217 return custom.render(data);
218 }
219 render_human_with_view(envelope, registry.columns(schema_id))
220}
221
222#[must_use]
224pub fn render_human_with_view(envelope: &Envelope, columns: Option<&[TableColumn]>) -> String {
225 if let Some(error) = &envelope.error {
226 return format!("Error: {}\n", error.message);
227 }
228 let Some(data) = &envelope.data else {
229 return "(no data)\n".to_owned();
230 };
231 if let Some(columns) = columns {
232 return match data {
233 Value::Array(items) => render_array_with_columns(items, columns),
234 Value::Object(map) => render_object_with_columns(map, columns),
235 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {
236 format!("{}\n", format_value(data))
237 }
238 };
239 }
240 match data {
241 Value::Array(items) => render_array(items),
242 Value::Object(map) => {
243 if map.is_empty() {
244 "(no data)\n".to_owned()
245 } else {
246 let mut keys = map.keys().collect::<Vec<_>>();
247 keys.sort();
248 let mut out = String::new();
249 for key in keys {
250 out.push_str(&format!("{key}: {}\n", format_value(&map[key])));
251 }
252 out
253 }
254 }
255 other => format!("{}\n", format_plain_value(other)),
256 }
257}
258
259fn render_array_with_columns(items: &[Value], columns: &[TableColumn]) -> String {
260 if items.is_empty() {
261 return "(no results)\n".to_owned();
262 }
263 if !items.iter().all(Value::is_object) {
264 return render_array_lines(items);
265 }
266 let mut widths = columns
267 .iter()
268 .map(|column| column.header.len())
269 .collect::<Vec<_>>();
270 let rows = items
271 .iter()
272 .map(|item| {
273 columns
274 .iter()
275 .enumerate()
276 .map(|(index, column)| {
277 let value = item
278 .as_object()
279 .and_then(|map| map.get(&column.field))
280 .map_or_else(String::new, format_value);
281 widths[index] = widths[index].max(value.len()).min(40);
282 value
283 })
284 .collect::<Vec<_>>()
285 })
286 .collect::<Vec<_>>();
287 render_table(
288 &columns
289 .iter()
290 .map(|column| column.header.clone())
291 .collect::<Vec<_>>(),
292 &widths,
293 &rows,
294 )
295}
296
297fn render_object_with_columns(
298 map: &serde_json::Map<String, Value>,
299 columns: &[TableColumn],
300) -> String {
301 if map.is_empty() {
302 return "(no data)\n".to_owned();
303 }
304 let mut out = String::new();
305 for column in columns {
306 let value = map
307 .get(&column.field)
308 .map_or_else(String::new, format_value);
309 out.push_str(&format!("{}: {value}\n", column.header));
310 }
311 out
312}
313
314fn render_array(items: &[Value]) -> String {
315 if items.is_empty() {
316 return "(no results)\n".to_owned();
317 }
318 let Some(first) = items.first() else {
319 return "(no results)\n".to_owned();
320 };
321 let Value::Object(first_map) = first else {
322 return render_array_lines(items);
323 };
324 if !items.iter().all(Value::is_object) {
325 return render_array_lines(items);
326 }
327 let mut cols = first_map.keys().cloned().collect::<Vec<_>>();
328 cols.sort();
329 if cols.is_empty() {
330 return "(no results)\n".to_owned();
331 }
332 let mut widths = cols.iter().map(String::len).collect::<Vec<_>>();
333 let rows = items
334 .iter()
335 .map(|item| {
336 cols.iter()
337 .enumerate()
338 .map(|(index, col)| {
339 let value = item
340 .as_object()
341 .and_then(|map| map.get(col))
342 .map_or_else(String::new, format_value);
343 widths[index] = widths[index].max(value.len()).min(40);
344 value
345 })
346 .collect::<Vec<_>>()
347 })
348 .collect::<Vec<_>>();
349
350 render_table(&cols, &widths, &rows)
351}
352
353fn render_array_lines(items: &[Value]) -> String {
354 let mut out = String::new();
355 for item in items {
356 out.push_str(&format!("{}\n", format_plain_value(item)));
357 }
358 out
359}
360
361fn render_table(headers: &[String], widths: &[usize], rows: &[Vec<String>]) -> String {
362 let mut out = String::new();
363 for (index, header) in headers.iter().enumerate() {
364 if index > 0 {
365 out.push_str(" ");
366 }
367 out.push_str(&format!(
368 "{:<width$}",
369 header.to_uppercase(),
370 width = widths[index]
371 ));
372 }
373 out.push('\n');
374 for (index, width) in widths.iter().enumerate() {
375 if index > 0 {
376 out.push_str(" ");
377 }
378 out.push_str(&"-".repeat(*width));
379 }
380 out.push('\n');
381 for row in rows {
382 for (index, value) in row.iter().enumerate() {
383 if index > 0 {
384 out.push_str(" ");
385 }
386 out.push_str(&format!(
387 "{:<width$}",
388 truncate(value, widths[index]),
389 width = widths[index]
390 ));
391 }
392 out.push('\n');
393 }
394 out.push_str(&format!("\n({} rows)\n", rows.len()));
395 out
396}
397
398fn format_value(value: &Value) -> String {
399 match value {
400 Value::Null => String::new(),
401 Value::Bool(true) => "yes".to_owned(),
402 Value::Bool(false) => "no".to_owned(),
403 Value::Number(number) => format_number(number),
404 Value::String(value) => value.clone(),
405 Value::Array(items) => items
406 .iter()
407 .map(format_value)
408 .collect::<Vec<_>>()
409 .join(", "),
410 Value::Object(_) => serde_json::to_string(value).unwrap_or_else(|_| "{}".to_owned()),
411 }
412}
413
414fn format_plain_value(value: &Value) -> String {
415 match value {
416 Value::Null => "<nil>".to_owned(),
417 Value::Bool(value) => value.to_string(),
418 Value::Number(number) => format_number(number),
419 Value::String(value) => value.clone(),
420 Value::Array(items) => {
421 let values = items
422 .iter()
423 .map(format_plain_value)
424 .collect::<Vec<_>>()
425 .join(" ");
426 format!("[{values}]")
427 }
428 Value::Object(object) => {
429 let mut pairs = object
430 .iter()
431 .map(|(key, value)| (key.clone(), value.clone()))
432 .collect::<Vec<_>>();
433 pairs.sort_by(|left, right| left.0.cmp(&right.0));
434 let object = pairs
435 .into_iter()
436 .collect::<serde_json::Map<String, Value>>();
437 serde_json::to_string(&Value::Object(object)).unwrap_or_else(|_| "{}".to_owned())
438 }
439 }
440}
441
442fn truncate(value: &str, width: usize) -> String {
443 if value.len() <= width {
444 return value.to_owned();
445 }
446 if width <= 3 {
447 return value.chars().take(width).collect();
448 }
449 let mut out = value.chars().take(width - 3).collect::<String>();
450 out.push_str("...");
451 out
452}
453
454fn format_number(number: &serde_json::Number) -> String {
455 number.to_string()
456}