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