1use std::{
2 collections::{BTreeMap, BTreeSet},
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 #[must_use]
136 pub fn has_view(&self, schema_id: &str) -> bool {
137 self.by_schema_id.contains_key(schema_id)
138 || self.custom_by_schema_id.contains_key(schema_id)
139 }
140}
141
142static GLOBAL_HUMAN_VIEW_REGISTRY: OnceLock<RwLock<HumanViewRegistry>> = OnceLock::new();
143
144fn global_human_view_registry() -> &'static RwLock<HumanViewRegistry> {
145 GLOBAL_HUMAN_VIEW_REGISTRY.get_or_init(|| RwLock::new(HumanViewRegistry::new()))
146}
147
148pub fn register_global_human_view(view: HumanViewDef) {
150 let mut registry = global_human_view_registry()
151 .write()
152 .unwrap_or_else(|poisoned| poisoned.into_inner());
153 registry.register(view);
154}
155
156pub fn register_global_human_view_func(
158 schema_id: impl Into<String>,
159 render: impl Fn(&Value) -> String + Send + Sync + 'static,
160) {
161 let mut registry = global_human_view_registry()
162 .write()
163 .unwrap_or_else(|poisoned| poisoned.into_inner());
164 registry.register_func(schema_id, render);
165}
166
167#[must_use]
169pub fn lookup_global_human_view_columns(schema_id: &str) -> Option<Vec<TableColumn>> {
170 global_human_view_registry()
171 .read()
172 .unwrap_or_else(|poisoned| poisoned.into_inner())
173 .columns(schema_id)
174 .map(<[TableColumn]>::to_vec)
175}
176
177#[must_use]
179pub fn lookup_global_human_view_func(schema_id: &str) -> Option<HumanViewRenderer> {
180 global_human_view_registry()
181 .read()
182 .unwrap_or_else(|poisoned| poisoned.into_inner())
183 .custom(schema_id)
184 .cloned()
185}
186
187#[must_use]
189pub fn global_human_view_registry_snapshot() -> HumanViewRegistry {
190 global_human_view_registry()
191 .read()
192 .unwrap_or_else(|poisoned| poisoned.into_inner())
193 .clone()
194}
195
196#[must_use]
198pub fn render_human(envelope: &Envelope) -> String {
199 render_human_with_view(envelope, None)
200}
201
202#[must_use]
204pub fn render_human_with_registry(envelope: &Envelope, registry: &HumanViewRegistry) -> String {
205 let system = envelope
206 .metadata
207 .as_ref()
208 .map(|metadata| metadata.system.as_str())
209 .unwrap_or_default();
210 render_human_with_registry_for_schema(envelope, registry, system)
211}
212
213#[must_use]
219pub fn render_human_with_registry_for_schema(
220 envelope: &Envelope,
221 registry: &HumanViewRegistry,
222 schema_id: &str,
223) -> String {
224 render_human_with_registry_selected(envelope, registry, schema_id, "")
225}
226
227#[must_use]
234pub fn render_human_with_registry_selected(
235 envelope: &Envelope,
236 registry: &HumanViewRegistry,
237 schema_id: &str,
238 fields: &str,
239) -> String {
240 if let Some(error) = &envelope.error {
241 return format!("Error: {}\n", error.message);
242 }
243 if let Some(data) = &envelope.data
244 && let Some(custom) = registry.custom(schema_id)
245 {
246 return custom.render(data);
247 }
248 match registry.columns(schema_id) {
249 Some(columns) => {
250 let selected = select_columns(columns, fields);
251 render_human_with_view(envelope, Some(&selected))
252 }
253 None => render_human_with_view(envelope, None),
254 }
255}
256
257fn select_columns(columns: &[TableColumn], fields: &str) -> Vec<TableColumn> {
261 let fields = fields.trim();
262 if fields.is_empty() || fields == "all" || fields == "*" {
263 return columns.to_vec();
264 }
265 let allowed: BTreeSet<&str> = fields
266 .split(',')
267 .map(str::trim)
268 .filter(|part| !part.is_empty())
269 .collect();
270 columns
271 .iter()
272 .filter(|column| allowed.contains(column.field.as_str()))
273 .cloned()
274 .collect()
275}
276
277#[must_use]
279pub fn render_human_with_view(envelope: &Envelope, columns: Option<&[TableColumn]>) -> String {
280 if let Some(error) = &envelope.error {
281 return format!("Error: {}\n", error.message);
282 }
283 let Some(data) = &envelope.data else {
284 return "(no data)\n".to_owned();
285 };
286 if let Some(columns) = columns {
287 return match data {
288 Value::Array(items) => render_array_with_columns(items, columns),
289 Value::Object(map) => render_object_with_columns(map, columns),
290 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {
291 format!("{}\n", format_value(data))
292 }
293 };
294 }
295 match data {
296 Value::Array(items) => render_array(items),
297 Value::Object(map) => {
298 if map.is_empty() {
299 "(no data)\n".to_owned()
300 } else {
301 let mut keys = map.keys().collect::<Vec<_>>();
302 keys.sort();
303 let mut out = String::new();
304 for key in keys {
305 out.push_str(&format!("{key}: {}\n", format_value(&map[key])));
306 }
307 out
308 }
309 }
310 other => format!("{}\n", format_plain_value(other)),
311 }
312}
313
314fn render_array_with_columns(items: &[Value], columns: &[TableColumn]) -> String {
315 if items.is_empty() {
316 return "(no results)\n".to_owned();
317 }
318 if !items.iter().all(Value::is_object) {
319 return render_array_lines(items);
320 }
321 let mut widths = columns
322 .iter()
323 .map(|column| column.header.len())
324 .collect::<Vec<_>>();
325 let rows = items
326 .iter()
327 .map(|item| {
328 columns
329 .iter()
330 .enumerate()
331 .map(|(index, column)| {
332 let value = item
333 .as_object()
334 .and_then(|map| map.get(&column.field))
335 .map_or_else(String::new, format_value);
336 widths[index] = widths[index].max(value.len()).min(40);
337 value
338 })
339 .collect::<Vec<_>>()
340 })
341 .collect::<Vec<_>>();
342 render_table(
343 &columns
344 .iter()
345 .map(|column| column.header.clone())
346 .collect::<Vec<_>>(),
347 &widths,
348 &rows,
349 )
350}
351
352fn render_object_with_columns(
353 map: &serde_json::Map<String, Value>,
354 columns: &[TableColumn],
355) -> String {
356 if map.is_empty() {
357 return "(no data)\n".to_owned();
358 }
359 let mut out = String::new();
360 for column in columns {
361 let value = map
362 .get(&column.field)
363 .map_or_else(String::new, format_value);
364 out.push_str(&format!("{}: {value}\n", column.header));
365 }
366 out
367}
368
369fn render_array(items: &[Value]) -> String {
370 if items.is_empty() {
371 return "(no results)\n".to_owned();
372 }
373 let Some(first) = items.first() else {
374 return "(no results)\n".to_owned();
375 };
376 let Value::Object(first_map) = first else {
377 return render_array_lines(items);
378 };
379 if !items.iter().all(Value::is_object) {
380 return render_array_lines(items);
381 }
382 let mut cols = first_map.keys().cloned().collect::<Vec<_>>();
383 cols.sort();
384 if cols.is_empty() {
385 return "(no results)\n".to_owned();
386 }
387 let mut widths = cols.iter().map(String::len).collect::<Vec<_>>();
388 let rows = items
389 .iter()
390 .map(|item| {
391 cols.iter()
392 .enumerate()
393 .map(|(index, col)| {
394 let value = item
395 .as_object()
396 .and_then(|map| map.get(col))
397 .map_or_else(String::new, format_value);
398 widths[index] = widths[index].max(value.len()).min(40);
399 value
400 })
401 .collect::<Vec<_>>()
402 })
403 .collect::<Vec<_>>();
404
405 render_table(&cols, &widths, &rows)
406}
407
408fn render_array_lines(items: &[Value]) -> String {
409 let mut out = String::new();
410 for item in items {
411 out.push_str(&format!("{}\n", format_plain_value(item)));
412 }
413 out
414}
415
416fn render_table(headers: &[String], widths: &[usize], rows: &[Vec<String>]) -> String {
417 let mut out = String::new();
418 for (index, header) in headers.iter().enumerate() {
419 if index > 0 {
420 out.push_str(" ");
421 }
422 out.push_str(&format!(
423 "{:<width$}",
424 header.to_uppercase(),
425 width = widths[index]
426 ));
427 }
428 out.push('\n');
429 for (index, width) in widths.iter().enumerate() {
430 if index > 0 {
431 out.push_str(" ");
432 }
433 out.push_str(&"-".repeat(*width));
434 }
435 out.push('\n');
436 for row in rows {
437 for (index, value) in row.iter().enumerate() {
438 if index > 0 {
439 out.push_str(" ");
440 }
441 out.push_str(&format!(
442 "{:<width$}",
443 truncate(value, widths[index]),
444 width = widths[index]
445 ));
446 }
447 out.push('\n');
448 }
449 out.push_str(&format!("\n({} rows)\n", rows.len()));
450 out
451}
452
453fn format_value(value: &Value) -> String {
454 match value {
455 Value::Null => String::new(),
456 Value::Bool(true) => "yes".to_owned(),
457 Value::Bool(false) => "no".to_owned(),
458 Value::Number(number) => format_number(number),
459 Value::String(value) => value.clone(),
460 Value::Array(items) => items
461 .iter()
462 .map(format_value)
463 .collect::<Vec<_>>()
464 .join(", "),
465 Value::Object(_) => serde_json::to_string(value).unwrap_or_else(|_| "{}".to_owned()),
466 }
467}
468
469fn format_plain_value(value: &Value) -> String {
470 match value {
471 Value::Null => "<nil>".to_owned(),
472 Value::Bool(value) => value.to_string(),
473 Value::Number(number) => format_number(number),
474 Value::String(value) => value.clone(),
475 Value::Array(items) => {
476 let values = items
477 .iter()
478 .map(format_plain_value)
479 .collect::<Vec<_>>()
480 .join(" ");
481 format!("[{values}]")
482 }
483 Value::Object(object) => {
484 let mut pairs = object
485 .iter()
486 .map(|(key, value)| (key.clone(), value.clone()))
487 .collect::<Vec<_>>();
488 pairs.sort_by(|left, right| left.0.cmp(&right.0));
489 let object = pairs
490 .into_iter()
491 .collect::<serde_json::Map<String, Value>>();
492 serde_json::to_string(&Value::Object(object)).unwrap_or_else(|_| "{}".to_owned())
493 }
494 }
495}
496
497fn truncate(value: &str, width: usize) -> String {
498 if value.len() <= width {
499 return value.to_owned();
500 }
501 if width <= 3 {
502 return value.chars().take(width).collect();
503 }
504 let mut out = value.chars().take(width - 3).collect::<String>();
505 out.push_str("...");
506 out
507}
508
509fn format_number(number: &serde_json::Number) -> String {
510 number.to_string()
511}