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::FloatArraySlice {
179 parent,
180 offset,
181 len,
182 }) => {
183 let start = *offset as usize;
184 let end = start + *len as usize;
185 let elems: Vec<String> = parent.data[start..end]
186 .iter()
187 .map(|v| {
188 if *v == v.trunc() && v.abs() < 1e15 {
189 format!("{}", *v as i64)
190 } else {
191 format!("{}", v)
192 }
193 })
194 .collect();
195 ContentNode::plain(format!("[{}]", elems.join(", ")))
196 }
197 Some(HeapValue::BoolArray(a)) => {
198 let elems: Vec<String> = a
199 .iter()
200 .map(|v| if *v != 0 { "true" } else { "false" }.to_string())
201 .collect();
202 ContentNode::plain(format!("[{}]", elems.join(", ")))
203 }
204 _ => ContentNode::plain(format!("{}", value)),
205 }
206}
207
208fn render_typed_object_as_content(
210 schema_id: u64,
211 slots: &[shape_value::slot::ValueSlot],
212 heap_mask: u64,
213) -> ContentNode {
214 let sid = schema_id as SchemaId;
215 if let Some(schema) = lookup_schema_by_id_public(sid) {
216 let mut pairs = Vec::with_capacity(schema.fields.len());
217 for (i, field_def) in schema.fields.iter().enumerate() {
218 if i < slots.len() {
219 let val = extract_slot_value(&slots[i], heap_mask, i, &field_def.field_type);
220 let value_node = render_as_content(&val);
221 pairs.push((field_def.name.clone(), value_node));
222 }
223 }
224 ContentNode::KeyValue(pairs)
225 } else {
226 ContentNode::plain(format!("TypedObject(schema={})", schema_id))
228 }
229}
230
231fn extract_slot_value(
233 slot: &shape_value::slot::ValueSlot,
234 heap_mask: u64,
235 index: usize,
236 field_type: &crate::type_schema::FieldType,
237) -> ValueWord {
238 use crate::type_schema::FieldType;
239 if heap_mask & (1u64 << index) != 0 {
240 slot.as_heap_nb()
241 } else {
242 match field_type {
243 FieldType::I64 => ValueWord::from_i64(slot.as_f64() as i64),
244 FieldType::Bool => ValueWord::from_bool(slot.as_bool()),
245 FieldType::Decimal => ValueWord::from_decimal(
246 rust_decimal::Decimal::from_f64_retain(slot.as_f64()).unwrap_or_default(),
247 ),
248 _ => ValueWord::from_f64(slot.as_f64()),
249 }
250 }
251}
252
253fn render_array_as_content(arr: &[ValueWord]) -> ContentNode {
258 if arr.is_empty() {
259 return ContentNode::plain("[]".to_string());
260 }
261
262 if let Some(HeapValue::TypedObject { .. }) = arr.first().and_then(|v| v.as_heap_ref()) {
264 return render_typed_array_as_table(arr);
265 }
266
267 let items: Vec<String> = arr.iter().map(|v| format!("{}", v)).collect();
269 ContentNode::plain(format!("[{}]", items.join(", ")))
270}
271
272fn render_typed_array_as_table(arr: &[ValueWord]) -> ContentNode {
278 if let Some((schema_id, _, _)) = arr.first().and_then(|v| v.as_typed_object()) {
280 let sid = schema_id as SchemaId;
281 if let Some(schema) = lookup_schema_by_id_public(sid) {
282 let headers: Vec<String> = schema.fields.iter().map(|f| f.name.clone()).collect();
283
284 let mut rows: Vec<Vec<ContentNode>> = Vec::with_capacity(arr.len());
285 for elem in arr {
286 if let Some((_eid, slots, heap_mask)) = elem.as_typed_object() {
287 let mut row_cells: Vec<ContentNode> = Vec::with_capacity(schema.fields.len());
288 for (i, field_def) in schema.fields.iter().enumerate() {
289 if i < slots.len() {
290 let val =
291 extract_slot_value(&slots[i], heap_mask, i, &field_def.field_type);
292 row_cells.push(render_as_content(&val));
293 } else {
294 row_cells.push(ContentNode::plain("".to_string()));
295 }
296 }
297 rows.push(row_cells);
298 } else {
299 let mut cells = vec![ContentNode::plain(format!("{}", elem))];
301 cells.resize(headers.len(), ContentNode::plain("".to_string()));
302 rows.push(cells);
303 }
304 }
305
306 return ContentNode::Table(ContentTable {
307 headers,
308 rows,
309 border: BorderStyle::default(),
310 max_rows: None,
311 column_types: None,
312 total_rows: None,
313 sortable: false,
314 });
315 }
316 }
317
318 let mut rows: Vec<Vec<ContentNode>> = Vec::with_capacity(arr.len());
320 for elem in arr {
321 rows.push(vec![ContentNode::plain(format!("{}", elem))]);
322 }
323
324 ContentNode::Table(ContentTable {
325 headers: vec!["value".to_string()],
326 rows,
327 border: BorderStyle::default(),
328 max_rows: None,
329 column_types: None,
330 total_rows: None,
331 sortable: false,
332 })
333}
334
335pub fn datatable_to_content_node(dt: &DataTable, max_rows: Option<usize>) -> ContentNode {
341 use arrow_array::Array;
342
343 let headers = dt.column_names();
344 let total = dt.row_count();
345 let limit = max_rows.unwrap_or(total).min(total);
346
347 let schema = dt.inner().schema();
349 let column_types: Vec<String> = schema
350 .fields()
351 .iter()
352 .map(|f| arrow_type_label(f.data_type()))
353 .collect();
354
355 let batch = dt.inner();
357 let mut rows = Vec::with_capacity(limit);
358 for row_idx in 0..limit {
359 let mut cells = Vec::with_capacity(headers.len());
360 for col_idx in 0..headers.len() {
361 let col = batch.column(col_idx);
362 let text = if col.is_null(row_idx) {
363 "null".to_string()
364 } else {
365 arrow_cell_display(col.as_ref(), row_idx)
366 };
367 cells.push(ContentNode::plain(text));
368 }
369 rows.push(cells);
370 }
371
372 ContentNode::Table(ContentTable {
373 headers,
374 rows,
375 border: BorderStyle::default(),
376 max_rows: None, column_types: Some(column_types),
378 total_rows: if total > limit { Some(total) } else { None },
379 sortable: true,
380 })
381}
382
383fn arrow_type_label(dt: &arrow_schema::DataType) -> String {
385 use arrow_schema::DataType;
386 match dt {
387 DataType::Float16 | DataType::Float32 | DataType::Float64 => "number".to_string(),
388 DataType::Int8 | DataType::Int16 | DataType::Int32 | DataType::Int64 => {
389 "number".to_string()
390 }
391 DataType::UInt8 | DataType::UInt16 | DataType::UInt32 | DataType::UInt64 => {
392 "number".to_string()
393 }
394 DataType::Boolean => "boolean".to_string(),
395 DataType::Utf8 | DataType::LargeUtf8 => "string".to_string(),
396 DataType::Date32 | DataType::Date64 => "date".to_string(),
397 DataType::Timestamp(_, _) => "date".to_string(),
398 DataType::Duration(_) => "duration".to_string(),
399 DataType::Decimal128(_, _) | DataType::Decimal256(_, _) => "number".to_string(),
400 _ => "string".to_string(),
401 }
402}
403
404fn arrow_cell_display(array: &dyn arrow_array::Array, index: usize) -> String {
406 use arrow_array::cast::AsArray;
407 use arrow_array::types::*;
408 use arrow_schema::DataType;
409
410 match array.data_type() {
411 DataType::Float64 => format!("{}", array.as_primitive::<Float64Type>().value(index)),
412 DataType::Float32 => format!("{}", array.as_primitive::<Float32Type>().value(index)),
413 DataType::Int64 => format!("{}", array.as_primitive::<Int64Type>().value(index)),
414 DataType::Int32 => format!("{}", array.as_primitive::<Int32Type>().value(index)),
415 DataType::Int16 => format!("{}", array.as_primitive::<Int16Type>().value(index)),
416 DataType::Int8 => format!("{}", array.as_primitive::<Int8Type>().value(index)),
417 DataType::UInt64 => format!("{}", array.as_primitive::<UInt64Type>().value(index)),
418 DataType::UInt32 => format!("{}", array.as_primitive::<UInt32Type>().value(index)),
419 DataType::UInt16 => format!("{}", array.as_primitive::<UInt16Type>().value(index)),
420 DataType::UInt8 => format!("{}", array.as_primitive::<UInt8Type>().value(index)),
421 DataType::Boolean => format!("{}", array.as_boolean().value(index)),
422 DataType::Utf8 => array.as_string::<i32>().value(index).to_string(),
423 DataType::LargeUtf8 => array.as_string::<i64>().value(index).to_string(),
424 DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, _) => {
425 let ts = array
426 .as_primitive::<TimestampMicrosecondType>()
427 .value(index);
428 match chrono::DateTime::from_timestamp_micros(ts) {
429 Some(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(),
430 None => ts.to_string(),
431 }
432 }
433 DataType::Timestamp(arrow_schema::TimeUnit::Millisecond, _) => {
434 let ts = array
435 .as_primitive::<TimestampMillisecondType>()
436 .value(index);
437 match chrono::DateTime::from_timestamp_millis(ts) {
438 Some(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(),
439 None => ts.to_string(),
440 }
441 }
442 _ => format!("{}", index),
443 }
444}
445
446fn render_hashmap_as_content(keys: &[ValueWord], values: &[ValueWord]) -> ContentNode {
448 let mut pairs = Vec::with_capacity(keys.len());
449 for (k, v) in keys.iter().zip(values.iter()) {
450 let key_str = if let Some(s) = k.as_str() {
451 s.to_string()
452 } else {
453 format!("{}", k)
454 };
455 let value_node = render_as_content(v);
456 pairs.push((key_str, value_node));
457 }
458 ContentNode::KeyValue(pairs)
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use shape_value::content::ContentNode;
465 use std::sync::Arc;
466
467 #[test]
468 fn test_render_string_as_plain_text() {
469 let val = ValueWord::from_string(Arc::new("hello".to_string()));
470 let node = render_as_content(&val);
471 assert_eq!(node, ContentNode::plain("hello"));
472 }
473
474 #[test]
475 fn test_render_integer_as_plain_text() {
476 let val = ValueWord::from_i64(42);
477 let node = render_as_content(&val);
478 assert_eq!(node, ContentNode::plain("42"));
479 }
480
481 #[test]
482 fn test_render_float_as_plain_text() {
483 let val = ValueWord::from_f64(3.14);
484 let node = render_as_content(&val);
485 let text = node.to_string();
486 assert!(text.contains("3.14"), "expected 3.14, got: {}", text);
487 }
488
489 #[test]
490 fn test_render_bool_true() {
491 let val = ValueWord::from_bool(true);
492 let node = render_as_content(&val);
493 assert_eq!(node, ContentNode::plain("true"));
494 }
495
496 #[test]
497 fn test_render_bool_false() {
498 let val = ValueWord::from_bool(false);
499 let node = render_as_content(&val);
500 assert_eq!(node, ContentNode::plain("false"));
501 }
502
503 #[test]
504 fn test_render_none() {
505 let val = ValueWord::none();
506 let node = render_as_content(&val);
507 assert_eq!(node, ContentNode::plain("none"));
508 }
509
510 #[test]
511 fn test_render_content_node_passthrough() {
512 let original = ContentNode::plain("already content");
513 let val = ValueWord::from_content(original.clone());
514 let node = render_as_content(&val);
515 assert_eq!(node, original);
516 }
517
518 #[test]
519 fn test_render_scalar_array() {
520 let arr = Arc::new(vec![
521 ValueWord::from_i64(1),
522 ValueWord::from_i64(2),
523 ValueWord::from_i64(3),
524 ]);
525 let val = ValueWord::from_array(arr);
526 let node = render_as_content(&val);
527 assert_eq!(node, ContentNode::plain("[1, 2, 3]"));
528 }
529
530 #[test]
531 fn test_render_empty_array() {
532 let arr = Arc::new(vec![]);
533 let val = ValueWord::from_array(arr);
534 let node = render_as_content(&val);
535 assert_eq!(node, ContentNode::plain("[]"));
536 }
537
538 #[test]
539 fn test_render_hashmap_as_key_value() {
540 let keys = vec![ValueWord::from_string(Arc::new("name".to_string()))];
541 let values = vec![ValueWord::from_string(Arc::new("Alice".to_string()))];
542 let val = ValueWord::from_hashmap_pairs(keys, values);
543 let node = render_as_content(&val);
544 match &node {
545 ContentNode::KeyValue(pairs) => {
546 assert_eq!(pairs.len(), 1);
547 assert_eq!(pairs[0].0, "name");
548 assert_eq!(pairs[0].1, ContentNode::plain("Alice"));
549 }
550 _ => panic!("expected KeyValue, got: {:?}", node),
551 }
552 }
553
554 #[test]
555 fn test_render_decimal_as_plain_text() {
556 use rust_decimal::Decimal;
557 let val = ValueWord::from_decimal(Decimal::new(1234, 2)); let node = render_as_content(&val);
559 assert_eq!(node, ContentNode::plain("12.34"));
560 }
561
562 #[test]
563 fn test_render_unit() {
564 let val = ValueWord::unit();
565 let node = render_as_content(&val);
566 assert_eq!(node, ContentNode::plain("()"));
567 }
568
569 #[test]
570 fn test_typed_object_renders_as_key_value() {
571 use crate::type_schema::typed_object_from_pairs;
572
573 let obj = typed_object_from_pairs(&[
574 (
575 "name",
576 ValueWord::from_string(Arc::new("Alice".to_string())),
577 ),
578 ("age", ValueWord::from_i64(30)),
579 ]);
580 let node = render_as_content(&obj);
581 match &node {
582 ContentNode::KeyValue(pairs) => {
583 assert_eq!(pairs.len(), 2);
584 let names: Vec<&str> = pairs.iter().map(|(k, _)| k.as_str()).collect();
586 assert!(
587 names.contains(&"name"),
588 "expected 'name' field, got: {:?}",
589 names
590 );
591 assert!(
592 names.contains(&"age"),
593 "expected 'age' field, got: {:?}",
594 names
595 );
596 }
597 _ => panic!("expected KeyValue for TypedObject, got: {:?}", node),
598 }
599 }
600
601 #[test]
602 fn test_typed_array_renders_as_table_with_headers() {
603 use crate::type_schema::typed_object_from_pairs;
604
605 let row1 = typed_object_from_pairs(&[
606 ("x", ValueWord::from_i64(1)),
607 ("y", ValueWord::from_i64(2)),
608 ]);
609 let row2 = typed_object_from_pairs(&[
610 ("x", ValueWord::from_i64(3)),
611 ("y", ValueWord::from_i64(4)),
612 ]);
613 let arr = Arc::new(vec![row1, row2]);
614 let val = ValueWord::from_array(arr);
615 let node = render_as_content(&val);
616 match &node {
617 ContentNode::Table(table) => {
618 assert_eq!(table.headers.len(), 2);
619 assert!(
620 table.headers.contains(&"x".to_string()),
621 "expected 'x' header"
622 );
623 assert!(
624 table.headers.contains(&"y".to_string()),
625 "expected 'y' header"
626 );
627 assert_eq!(table.rows.len(), 2);
628 assert_eq!(table.rows[0].len(), 2);
630 assert_eq!(table.rows[1].len(), 2);
631 }
632 _ => panic!("expected Table for Vec<TypedObject>, got: {:?}", node),
633 }
634 }
635
636 #[test]
637 fn test_adapter_capabilities() {
638 let terminal = capabilities_for_adapter(adapters::TERMINAL);
639 assert!(terminal.ansi);
640 assert!(terminal.color);
641 assert!(terminal.unicode);
642
643 let plain = capabilities_for_adapter(adapters::PLAIN);
644 assert!(!plain.ansi);
645 assert!(!plain.color);
646
647 let html = capabilities_for_adapter(adapters::HTML);
648 assert!(!html.ansi);
649 assert!(html.color);
650 assert!(html.interactive);
651
652 let json = capabilities_for_adapter(adapters::JSON);
653 assert!(!json.ansi);
654 assert!(!json.color);
655 assert!(json.unicode);
656 }
657
658 #[test]
659 fn test_render_as_content_for_falls_through() {
660 let val = ValueWord::from_i64(42);
661 let caps = capabilities_for_adapter(adapters::TERMINAL);
662 let node = render_as_content_for(&val, adapters::TERMINAL, &caps);
663 assert_eq!(node, ContentNode::plain("42"));
664 }
665
666 #[test]
667 fn test_datatable_to_content_node() {
668 use arrow_schema::{DataType, Field};
669 use shape_value::DataTableBuilder;
670
671 let mut builder = DataTableBuilder::with_fields(vec![
672 Field::new("name", DataType::Utf8, false),
673 Field::new("value", DataType::Float64, false),
674 ]);
675 builder.add_string_column(vec!["alpha", "beta", "gamma"]);
676 builder.add_f64_column(vec![1.0, 2.0, 3.0]);
677 let dt = builder.finish().expect("should build DataTable");
678
679 let node = datatable_to_content_node(&dt, None);
680 match &node {
681 ContentNode::Table(table) => {
682 assert_eq!(table.headers, vec!["name", "value"]);
683 assert_eq!(table.rows.len(), 3);
684 assert_eq!(table.rows[0][0], ContentNode::plain("alpha"));
685 assert_eq!(table.rows[0][1], ContentNode::plain("1"));
686 assert!(table.column_types.is_some());
687 let types = table.column_types.as_ref().unwrap();
688 assert_eq!(types[0], "string");
689 assert_eq!(types[1], "number");
690 assert!(table.sortable);
691 }
692 _ => panic!("expected Table, got: {:?}", node),
693 }
694 }
695
696 #[test]
697 fn test_datatable_to_content_node_with_max_rows() {
698 use arrow_schema::{DataType, Field};
699 use shape_value::DataTableBuilder;
700
701 let mut builder =
702 DataTableBuilder::with_fields(vec![Field::new("x", DataType::Int64, false)]);
703 builder.add_i64_column(vec![10, 20, 30, 40, 50]);
704 let dt = builder.finish().expect("should build DataTable");
705
706 let node = datatable_to_content_node(&dt, Some(2));
707 match &node {
708 ContentNode::Table(table) => {
709 assert_eq!(table.rows.len(), 2);
710 assert_eq!(table.total_rows, Some(5));
711 }
712 _ => panic!("expected Table, got: {:?}", node),
713 }
714 }
715
716 #[test]
717 fn test_datatable_renders_via_content_dispatch() {
718 use arrow_schema::{DataType, Field};
719 use shape_value::DataTableBuilder;
720
721 let mut builder =
722 DataTableBuilder::with_fields(vec![Field::new("col", DataType::Utf8, false)]);
723 builder.add_string_column(vec!["hello"]);
724 let dt = builder.finish().expect("should build DataTable");
725
726 let val = ValueWord::from_datatable(Arc::new(dt));
727 let node = render_as_content(&val);
728 match &node {
729 ContentNode::Table(table) => {
730 assert_eq!(table.headers, vec!["col"]);
731 assert_eq!(table.rows.len(), 1);
732 }
733 _ => panic!("expected Table for DataTable, got: {:?}", node),
734 }
735 }
736}