1use crate::content_renderer::RendererCapabilities;
27use crate::type_schema::{SchemaId, lookup_schema_by_id_public};
28use shape_value::content::{BorderStyle, ContentNode, ContentTable};
29use shape_value::heap_value::HeapValue;
30use shape_value::value_word::NanTag;
31use shape_value::{DataTable, ValueWord};
32
33pub mod adapters {
35 pub const TERMINAL: &str = "Terminal";
36 pub const HTML: &str = "Html";
37 pub const MARKDOWN: &str = "Markdown";
38 pub const JSON: &str = "Json";
39 pub const PLAIN: &str = "Plain";
40}
41
42pub type UserContentResolver = dyn Fn(&ValueWord) -> Option<ContentNode> + Send + Sync;
49
50static USER_CONTENT_RESOLVER: std::sync::OnceLock<Box<UserContentResolver>> =
51 std::sync::OnceLock::new();
52
53pub fn set_user_content_resolver(resolver: Box<UserContentResolver>) {
57 let _ = USER_CONTENT_RESOLVER.set(resolver);
58}
59
60pub fn render_as_content(value: &ValueWord) -> ContentNode {
68 if let Some(node) = value.as_content() {
70 return node.clone();
71 }
72
73 if let Some(resolver) = USER_CONTENT_RESOLVER.get() {
75 if let Some(node) = resolver(value) {
76 return node;
77 }
78 }
79
80 match value.tag() {
81 NanTag::I48 => ContentNode::plain(format!("{}", value)),
82 NanTag::F64 => ContentNode::plain(format!("{}", value)),
83 NanTag::Bool => ContentNode::plain(format!("{}", value)),
84 NanTag::None => ContentNode::plain("none".to_string()),
85 NanTag::Unit => ContentNode::plain("()".to_string()),
86 NanTag::Heap => render_heap_as_content(value),
87 _ => ContentNode::plain(format!("{}", value)),
88 }
89}
90
91pub type UserContentForResolver =
102 dyn Fn(&ValueWord, &str, &RendererCapabilities) -> Option<ContentNode> + Send + Sync;
103
104static USER_CONTENT_FOR_RESOLVER: std::sync::OnceLock<Box<UserContentForResolver>> =
105 std::sync::OnceLock::new();
106
107pub fn set_user_content_for_resolver(resolver: Box<UserContentForResolver>) {
109 let _ = USER_CONTENT_FOR_RESOLVER.set(resolver);
110}
111
112pub fn render_as_content_for(
113 value: &ValueWord,
114 adapter: &str,
115 caps: &RendererCapabilities,
116) -> ContentNode {
117 if let Some(resolver) = USER_CONTENT_FOR_RESOLVER.get() {
119 if let Some(node) = resolver(value, adapter, caps) {
120 return node;
121 }
122 }
123 render_as_content(value)
125}
126
127pub fn capabilities_for_adapter(adapter: &str) -> RendererCapabilities {
129 match adapter {
130 adapters::TERMINAL => RendererCapabilities::terminal(),
131 adapters::HTML => RendererCapabilities::html(),
132 adapters::MARKDOWN => RendererCapabilities::markdown(),
133 adapters::PLAIN => RendererCapabilities::plain(),
134 adapters::JSON => RendererCapabilities {
135 ansi: false,
136 unicode: true,
137 color: false,
138 interactive: false,
139 },
140 _ => RendererCapabilities::plain(),
141 }
142}
143
144fn render_heap_as_content(value: &ValueWord) -> ContentNode {
146 match value.as_heap_ref() {
147 Some(HeapValue::String(s)) => ContentNode::plain(s.as_ref().clone()),
148 Some(HeapValue::Decimal(d)) => ContentNode::plain(d.to_string()),
149 Some(HeapValue::BigInt(i)) => ContentNode::plain(i.to_string()),
150 Some(HeapValue::Array(arr)) => render_array_as_content(arr),
151 Some(HeapValue::HashMap(d)) => render_hashmap_as_content(&d.keys, &d.values),
152 Some(HeapValue::TypedObject {
153 schema_id,
154 slots,
155 heap_mask,
156 }) => render_typed_object_as_content(*schema_id, slots, *heap_mask),
157 Some(HeapValue::DataTable(dt)) => datatable_to_content_node(dt, None),
158 Some(HeapValue::TypedTable { table, .. }) => datatable_to_content_node(table, None),
159 Some(HeapValue::IndexedTable { table, .. }) => datatable_to_content_node(table, None),
160 Some(HeapValue::IntArray(a)) => {
162 let elems: Vec<String> = a.iter().map(|v| v.to_string()).collect();
163 ContentNode::plain(format!("[{}]", elems.join(", ")))
164 }
165 Some(HeapValue::FloatArray(a)) => {
166 let elems: Vec<String> = a
167 .iter()
168 .map(|v| {
169 if *v == v.trunc() && v.abs() < 1e15 {
170 format!("{}", *v as i64)
171 } else {
172 format!("{}", v)
173 }
174 })
175 .collect();
176 ContentNode::plain(format!("[{}]", elems.join(", ")))
177 }
178 Some(HeapValue::BoolArray(a)) => {
179 let elems: Vec<String> = a
180 .iter()
181 .map(|v| if *v != 0 { "true" } else { "false" }.to_string())
182 .collect();
183 ContentNode::plain(format!("[{}]", elems.join(", ")))
184 }
185 _ => ContentNode::plain(format!("{}", value)),
186 }
187}
188
189fn render_typed_object_as_content(
191 schema_id: u64,
192 slots: &[shape_value::slot::ValueSlot],
193 heap_mask: u64,
194) -> ContentNode {
195 let sid = schema_id as SchemaId;
196 if let Some(schema) = lookup_schema_by_id_public(sid) {
197 let mut pairs = Vec::with_capacity(schema.fields.len());
198 for (i, field_def) in schema.fields.iter().enumerate() {
199 if i < slots.len() {
200 let val = extract_slot_value(&slots[i], heap_mask, i, &field_def.field_type);
201 let value_node = render_as_content(&val);
202 pairs.push((field_def.name.clone(), value_node));
203 }
204 }
205 ContentNode::KeyValue(pairs)
206 } else {
207 ContentNode::plain(format!("TypedObject(schema={})", schema_id))
209 }
210}
211
212fn extract_slot_value(
214 slot: &shape_value::slot::ValueSlot,
215 heap_mask: u64,
216 index: usize,
217 field_type: &crate::type_schema::FieldType,
218) -> ValueWord {
219 use crate::type_schema::FieldType;
220 if heap_mask & (1u64 << index) != 0 {
221 slot.as_heap_nb()
222 } else {
223 match field_type {
224 FieldType::I64 => ValueWord::from_i64(slot.as_f64() as i64),
225 FieldType::Bool => ValueWord::from_bool(slot.as_bool()),
226 FieldType::Decimal => ValueWord::from_decimal(
227 rust_decimal::Decimal::from_f64_retain(slot.as_f64()).unwrap_or_default(),
228 ),
229 _ => ValueWord::from_f64(slot.as_f64()),
230 }
231 }
232}
233
234fn render_array_as_content(arr: &[ValueWord]) -> ContentNode {
239 if arr.is_empty() {
240 return ContentNode::plain("[]".to_string());
241 }
242
243 if let Some(HeapValue::TypedObject { .. }) = arr.first().and_then(|v| v.as_heap_ref()) {
245 return render_typed_array_as_table(arr);
246 }
247
248 let items: Vec<String> = arr.iter().map(|v| format!("{}", v)).collect();
250 ContentNode::plain(format!("[{}]", items.join(", ")))
251}
252
253fn render_typed_array_as_table(arr: &[ValueWord]) -> ContentNode {
259 if let Some((schema_id, _, _)) = arr.first().and_then(|v| v.as_typed_object()) {
261 let sid = schema_id as SchemaId;
262 if let Some(schema) = lookup_schema_by_id_public(sid) {
263 let headers: Vec<String> = schema.fields.iter().map(|f| f.name.clone()).collect();
264
265 let mut rows: Vec<Vec<ContentNode>> = Vec::with_capacity(arr.len());
266 for elem in arr {
267 if let Some((_eid, slots, heap_mask)) = elem.as_typed_object() {
268 let mut row_cells: Vec<ContentNode> = Vec::with_capacity(schema.fields.len());
269 for (i, field_def) in schema.fields.iter().enumerate() {
270 if i < slots.len() {
271 let val =
272 extract_slot_value(&slots[i], heap_mask, i, &field_def.field_type);
273 row_cells.push(render_as_content(&val));
274 } else {
275 row_cells.push(ContentNode::plain("".to_string()));
276 }
277 }
278 rows.push(row_cells);
279 } else {
280 let mut cells = vec![ContentNode::plain(format!("{}", elem))];
282 cells.resize(headers.len(), ContentNode::plain("".to_string()));
283 rows.push(cells);
284 }
285 }
286
287 return ContentNode::Table(ContentTable {
288 headers,
289 rows,
290 border: BorderStyle::default(),
291 max_rows: None,
292 column_types: None,
293 total_rows: None,
294 sortable: false,
295 });
296 }
297 }
298
299 let mut rows: Vec<Vec<ContentNode>> = Vec::with_capacity(arr.len());
301 for elem in arr {
302 rows.push(vec![ContentNode::plain(format!("{}", elem))]);
303 }
304
305 ContentNode::Table(ContentTable {
306 headers: vec!["value".to_string()],
307 rows,
308 border: BorderStyle::default(),
309 max_rows: None,
310 column_types: None,
311 total_rows: None,
312 sortable: false,
313 })
314}
315
316pub fn datatable_to_content_node(dt: &DataTable, max_rows: Option<usize>) -> ContentNode {
322 use arrow_array::Array;
323
324 let headers = dt.column_names();
325 let total = dt.row_count();
326 let limit = max_rows.unwrap_or(total).min(total);
327
328 let schema = dt.inner().schema();
330 let column_types: Vec<String> = schema
331 .fields()
332 .iter()
333 .map(|f| arrow_type_label(f.data_type()))
334 .collect();
335
336 let batch = dt.inner();
338 let mut rows = Vec::with_capacity(limit);
339 for row_idx in 0..limit {
340 let mut cells = Vec::with_capacity(headers.len());
341 for col_idx in 0..headers.len() {
342 let col = batch.column(col_idx);
343 let text = if col.is_null(row_idx) {
344 "null".to_string()
345 } else {
346 arrow_cell_display(col.as_ref(), row_idx)
347 };
348 cells.push(ContentNode::plain(text));
349 }
350 rows.push(cells);
351 }
352
353 ContentNode::Table(ContentTable {
354 headers,
355 rows,
356 border: BorderStyle::default(),
357 max_rows: None, column_types: Some(column_types),
359 total_rows: if total > limit { Some(total) } else { None },
360 sortable: true,
361 })
362}
363
364fn arrow_type_label(dt: &arrow_schema::DataType) -> String {
366 use arrow_schema::DataType;
367 match dt {
368 DataType::Float16 | DataType::Float32 | DataType::Float64 => "number".to_string(),
369 DataType::Int8 | DataType::Int16 | DataType::Int32 | DataType::Int64 => {
370 "number".to_string()
371 }
372 DataType::UInt8 | DataType::UInt16 | DataType::UInt32 | DataType::UInt64 => {
373 "number".to_string()
374 }
375 DataType::Boolean => "boolean".to_string(),
376 DataType::Utf8 | DataType::LargeUtf8 => "string".to_string(),
377 DataType::Date32 | DataType::Date64 => "date".to_string(),
378 DataType::Timestamp(_, _) => "date".to_string(),
379 DataType::Duration(_) => "duration".to_string(),
380 DataType::Decimal128(_, _) | DataType::Decimal256(_, _) => "number".to_string(),
381 _ => "string".to_string(),
382 }
383}
384
385fn arrow_cell_display(array: &dyn arrow_array::Array, index: usize) -> String {
387 use arrow_array::cast::AsArray;
388 use arrow_array::types::*;
389 use arrow_schema::DataType;
390
391 match array.data_type() {
392 DataType::Float64 => format!("{}", array.as_primitive::<Float64Type>().value(index)),
393 DataType::Float32 => format!("{}", array.as_primitive::<Float32Type>().value(index)),
394 DataType::Int64 => format!("{}", array.as_primitive::<Int64Type>().value(index)),
395 DataType::Int32 => format!("{}", array.as_primitive::<Int32Type>().value(index)),
396 DataType::Int16 => format!("{}", array.as_primitive::<Int16Type>().value(index)),
397 DataType::Int8 => format!("{}", array.as_primitive::<Int8Type>().value(index)),
398 DataType::UInt64 => format!("{}", array.as_primitive::<UInt64Type>().value(index)),
399 DataType::UInt32 => format!("{}", array.as_primitive::<UInt32Type>().value(index)),
400 DataType::UInt16 => format!("{}", array.as_primitive::<UInt16Type>().value(index)),
401 DataType::UInt8 => format!("{}", array.as_primitive::<UInt8Type>().value(index)),
402 DataType::Boolean => format!("{}", array.as_boolean().value(index)),
403 DataType::Utf8 => array.as_string::<i32>().value(index).to_string(),
404 DataType::LargeUtf8 => array.as_string::<i64>().value(index).to_string(),
405 DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, _) => {
406 let ts = array
407 .as_primitive::<TimestampMicrosecondType>()
408 .value(index);
409 match chrono::DateTime::from_timestamp_micros(ts) {
410 Some(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(),
411 None => ts.to_string(),
412 }
413 }
414 DataType::Timestamp(arrow_schema::TimeUnit::Millisecond, _) => {
415 let ts = array
416 .as_primitive::<TimestampMillisecondType>()
417 .value(index);
418 match chrono::DateTime::from_timestamp_millis(ts) {
419 Some(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(),
420 None => ts.to_string(),
421 }
422 }
423 _ => format!("{}", index),
424 }
425}
426
427fn render_hashmap_as_content(keys: &[ValueWord], values: &[ValueWord]) -> ContentNode {
429 let mut pairs = Vec::with_capacity(keys.len());
430 for (k, v) in keys.iter().zip(values.iter()) {
431 let key_str = if let Some(s) = k.as_str() {
432 s.to_string()
433 } else {
434 format!("{}", k)
435 };
436 let value_node = render_as_content(v);
437 pairs.push((key_str, value_node));
438 }
439 ContentNode::KeyValue(pairs)
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use shape_value::content::ContentNode;
446 use std::sync::Arc;
447
448 #[test]
449 fn test_render_string_as_plain_text() {
450 let val = ValueWord::from_string(Arc::new("hello".to_string()));
451 let node = render_as_content(&val);
452 assert_eq!(node, ContentNode::plain("hello"));
453 }
454
455 #[test]
456 fn test_render_integer_as_plain_text() {
457 let val = ValueWord::from_i64(42);
458 let node = render_as_content(&val);
459 assert_eq!(node, ContentNode::plain("42"));
460 }
461
462 #[test]
463 fn test_render_float_as_plain_text() {
464 let val = ValueWord::from_f64(3.14);
465 let node = render_as_content(&val);
466 let text = node.to_string();
467 assert!(text.contains("3.14"), "expected 3.14, got: {}", text);
468 }
469
470 #[test]
471 fn test_render_bool_true() {
472 let val = ValueWord::from_bool(true);
473 let node = render_as_content(&val);
474 assert_eq!(node, ContentNode::plain("true"));
475 }
476
477 #[test]
478 fn test_render_bool_false() {
479 let val = ValueWord::from_bool(false);
480 let node = render_as_content(&val);
481 assert_eq!(node, ContentNode::plain("false"));
482 }
483
484 #[test]
485 fn test_render_none() {
486 let val = ValueWord::none();
487 let node = render_as_content(&val);
488 assert_eq!(node, ContentNode::plain("none"));
489 }
490
491 #[test]
492 fn test_render_content_node_passthrough() {
493 let original = ContentNode::plain("already content");
494 let val = ValueWord::from_content(original.clone());
495 let node = render_as_content(&val);
496 assert_eq!(node, original);
497 }
498
499 #[test]
500 fn test_render_scalar_array() {
501 let arr = Arc::new(vec![
502 ValueWord::from_i64(1),
503 ValueWord::from_i64(2),
504 ValueWord::from_i64(3),
505 ]);
506 let val = ValueWord::from_array(arr);
507 let node = render_as_content(&val);
508 assert_eq!(node, ContentNode::plain("[1, 2, 3]"));
509 }
510
511 #[test]
512 fn test_render_empty_array() {
513 let arr = Arc::new(vec![]);
514 let val = ValueWord::from_array(arr);
515 let node = render_as_content(&val);
516 assert_eq!(node, ContentNode::plain("[]"));
517 }
518
519 #[test]
520 fn test_render_hashmap_as_key_value() {
521 let keys = vec![ValueWord::from_string(Arc::new("name".to_string()))];
522 let values = vec![ValueWord::from_string(Arc::new("Alice".to_string()))];
523 let val = ValueWord::from_hashmap_pairs(keys, values);
524 let node = render_as_content(&val);
525 match &node {
526 ContentNode::KeyValue(pairs) => {
527 assert_eq!(pairs.len(), 1);
528 assert_eq!(pairs[0].0, "name");
529 assert_eq!(pairs[0].1, ContentNode::plain("Alice"));
530 }
531 _ => panic!("expected KeyValue, got: {:?}", node),
532 }
533 }
534
535 #[test]
536 fn test_render_decimal_as_plain_text() {
537 use rust_decimal::Decimal;
538 let val = ValueWord::from_decimal(Decimal::new(1234, 2)); let node = render_as_content(&val);
540 assert_eq!(node, ContentNode::plain("12.34"));
541 }
542
543 #[test]
544 fn test_render_unit() {
545 let val = ValueWord::unit();
546 let node = render_as_content(&val);
547 assert_eq!(node, ContentNode::plain("()"));
548 }
549
550 #[test]
551 fn test_typed_object_renders_as_key_value() {
552 use crate::type_schema::typed_object_from_pairs;
553
554 let obj = typed_object_from_pairs(&[
555 (
556 "name",
557 ValueWord::from_string(Arc::new("Alice".to_string())),
558 ),
559 ("age", ValueWord::from_i64(30)),
560 ]);
561 let node = render_as_content(&obj);
562 match &node {
563 ContentNode::KeyValue(pairs) => {
564 assert_eq!(pairs.len(), 2);
565 let names: Vec<&str> = pairs.iter().map(|(k, _)| k.as_str()).collect();
567 assert!(
568 names.contains(&"name"),
569 "expected 'name' field, got: {:?}",
570 names
571 );
572 assert!(
573 names.contains(&"age"),
574 "expected 'age' field, got: {:?}",
575 names
576 );
577 }
578 _ => panic!("expected KeyValue for TypedObject, got: {:?}", node),
579 }
580 }
581
582 #[test]
583 fn test_typed_array_renders_as_table_with_headers() {
584 use crate::type_schema::typed_object_from_pairs;
585
586 let row1 = typed_object_from_pairs(&[
587 ("x", ValueWord::from_i64(1)),
588 ("y", ValueWord::from_i64(2)),
589 ]);
590 let row2 = typed_object_from_pairs(&[
591 ("x", ValueWord::from_i64(3)),
592 ("y", ValueWord::from_i64(4)),
593 ]);
594 let arr = Arc::new(vec![row1, row2]);
595 let val = ValueWord::from_array(arr);
596 let node = render_as_content(&val);
597 match &node {
598 ContentNode::Table(table) => {
599 assert_eq!(table.headers.len(), 2);
600 assert!(
601 table.headers.contains(&"x".to_string()),
602 "expected 'x' header"
603 );
604 assert!(
605 table.headers.contains(&"y".to_string()),
606 "expected 'y' header"
607 );
608 assert_eq!(table.rows.len(), 2);
609 assert_eq!(table.rows[0].len(), 2);
611 assert_eq!(table.rows[1].len(), 2);
612 }
613 _ => panic!("expected Table for Vec<TypedObject>, got: {:?}", node),
614 }
615 }
616
617 #[test]
618 fn test_adapter_capabilities() {
619 let terminal = capabilities_for_adapter(adapters::TERMINAL);
620 assert!(terminal.ansi);
621 assert!(terminal.color);
622 assert!(terminal.unicode);
623
624 let plain = capabilities_for_adapter(adapters::PLAIN);
625 assert!(!plain.ansi);
626 assert!(!plain.color);
627
628 let html = capabilities_for_adapter(adapters::HTML);
629 assert!(!html.ansi);
630 assert!(html.color);
631 assert!(html.interactive);
632
633 let json = capabilities_for_adapter(adapters::JSON);
634 assert!(!json.ansi);
635 assert!(!json.color);
636 assert!(json.unicode);
637 }
638
639 #[test]
640 fn test_render_as_content_for_falls_through() {
641 let val = ValueWord::from_i64(42);
642 let caps = capabilities_for_adapter(adapters::TERMINAL);
643 let node = render_as_content_for(&val, adapters::TERMINAL, &caps);
644 assert_eq!(node, ContentNode::plain("42"));
645 }
646
647 #[test]
648 fn test_datatable_to_content_node() {
649 use arrow_schema::{DataType, Field};
650 use shape_value::DataTableBuilder;
651
652 let mut builder = DataTableBuilder::with_fields(vec![
653 Field::new("name", DataType::Utf8, false),
654 Field::new("value", DataType::Float64, false),
655 ]);
656 builder.add_string_column(vec!["alpha", "beta", "gamma"]);
657 builder.add_f64_column(vec![1.0, 2.0, 3.0]);
658 let dt = builder.finish().expect("should build DataTable");
659
660 let node = datatable_to_content_node(&dt, None);
661 match &node {
662 ContentNode::Table(table) => {
663 assert_eq!(table.headers, vec!["name", "value"]);
664 assert_eq!(table.rows.len(), 3);
665 assert_eq!(table.rows[0][0], ContentNode::plain("alpha"));
666 assert_eq!(table.rows[0][1], ContentNode::plain("1"));
667 assert!(table.column_types.is_some());
668 let types = table.column_types.as_ref().unwrap();
669 assert_eq!(types[0], "string");
670 assert_eq!(types[1], "number");
671 assert!(table.sortable);
672 }
673 _ => panic!("expected Table, got: {:?}", node),
674 }
675 }
676
677 #[test]
678 fn test_datatable_to_content_node_with_max_rows() {
679 use arrow_schema::{DataType, Field};
680 use shape_value::DataTableBuilder;
681
682 let mut builder =
683 DataTableBuilder::with_fields(vec![Field::new("x", DataType::Int64, false)]);
684 builder.add_i64_column(vec![10, 20, 30, 40, 50]);
685 let dt = builder.finish().expect("should build DataTable");
686
687 let node = datatable_to_content_node(&dt, Some(2));
688 match &node {
689 ContentNode::Table(table) => {
690 assert_eq!(table.rows.len(), 2);
691 assert_eq!(table.total_rows, Some(5));
692 }
693 _ => panic!("expected Table, got: {:?}", node),
694 }
695 }
696
697 #[test]
698 fn test_datatable_renders_via_content_dispatch() {
699 use arrow_schema::{DataType, Field};
700 use shape_value::DataTableBuilder;
701
702 let mut builder =
703 DataTableBuilder::with_fields(vec![Field::new("col", DataType::Utf8, false)]);
704 builder.add_string_column(vec!["hello"]);
705 let dt = builder.finish().expect("should build DataTable");
706
707 let val = ValueWord::from_datatable(Arc::new(dt));
708 let node = render_as_content(&val);
709 match &node {
710 ContentNode::Table(table) => {
711 assert_eq!(table.headers, vec!["col"]);
712 assert_eq!(table.rows.len(), 1);
713 }
714 _ => panic!("expected Table for DataTable, got: {:?}", node),
715 }
716 }
717}