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