Skip to main content

shape_runtime/
content_dispatch.rs

1//! Content trait dispatch — render any ValueWord value as a ContentNode.
2//!
3//! The `render_as_content` function implements the Content trait dispatch logic:
4//! 1. If the value IS already a ContentNode → return as-is
5//! 2. Match by NanTag/HeapValue type → built-in Content impls for primitives
6//! 3. Fallback → Display as ContentNode::plain(display_string)
7//!
8//! Built-in Content implementations:
9//! - string → ContentNode::plain(self)
10//! - number/int/decimal → ContentNode::plain(formatted)
11//! - bool → ContentNode::plain("true"/"false")
12//! - Vec<TypedObject> → ContentNode::Table with columns from schema fields
13//! - Vec<scalar> → ContentNode::plain("[1, 2, 3]")
14//! - HashMap<K,V> → ContentNode::KeyValue
15//! - TypedObject → ContentNode::KeyValue with field names from schema
16//!
17//! ## ContentFor<Adapter>
18//!
19//! The `render_as_content_for` function adds adapter-aware dispatch:
20//! 1. ContentFor<CurrentAdapter> → adapter-specific rendering
21//! 2. Content → generic content rendering
22//! 3. Display fallback → plain text
23//!
24//! Adapter types: Terminal, Html, Markdown, Json, Plain
25
26use 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
33/// Well-known adapter names for ContentFor<Adapter> dispatch.
34pub 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
42/// Optional user-defined Content impl resolver.
43///
44/// When set, `render_as_content` calls this function before falling through to
45/// built-in dispatch. If the resolver returns `Some(node)`, that node is used.
46/// The resolver is typically set by the VM executor to check for user-defined
47/// `impl Content for MyType { fn render(self) -> ContentNode }` blocks.
48pub 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
53/// Register a user-defined Content trait resolver.
54///
55/// Called by the VM during initialization to enable user-implementable Content trait.
56pub fn set_user_content_resolver(resolver: Box<UserContentResolver>) {
57    let _ = USER_CONTENT_RESOLVER.set(resolver);
58}
59
60/// Render a ValueWord value as a ContentNode using Content dispatch.
61///
62/// Dispatch order:
63/// 1. If value IS a ContentNode → return as-is
64/// 2. If a user-defined Content impl exists → call user's render()
65/// 3. If value type has a built-in Content impl → produce structured output
66/// 4. Else → Display fallback → ContentNode::plain(display_string)
67pub fn render_as_content(value: &ValueWord) -> ContentNode {
68    // Fast path: already a content node
69    if let Some(node) = value.as_content() {
70        return node.clone();
71    }
72
73    // Check for user-defined Content impl
74    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
91/// Render a ValueWord value as a ContentNode with adapter-specific dispatch.
92///
93/// Dispatch order:
94/// 1. ContentFor<adapter> → adapter-specific rendering (future: user-defined impls)
95/// 2. Content → generic content rendering via `render_as_content`
96/// 3. Display fallback → plain text
97///
98/// The `caps` parameter provides renderer capabilities so the Content impl
99/// can adapt output (e.g., use ANSI codes only when `caps.ansi` is true).
100/// Optional adapter-specific Content resolver.
101pub 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
107/// Register a user-defined ContentFor<Adapter> resolver.
108pub 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    // Check for user-defined ContentFor<Adapter> impl
118    if let Some(resolver) = USER_CONTENT_FOR_RESOLVER.get() {
119        if let Some(node) = resolver(value, adapter, caps) {
120            return node;
121        }
122    }
123    // Fall through to generic Content dispatch
124    render_as_content(value)
125}
126
127/// Create a RendererCapabilities descriptor for a given adapter name.
128pub 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
144/// Dispatch Content rendering for heap-allocated values.
145fn 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        // Typed arrays: render as plain text with bracket notation
161        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
189/// Render a single TypedObject as ContentNode::KeyValue using schema field names.
190fn 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        // Schema not found — fall back to Display
208        ContentNode::plain(format!("TypedObject(schema={})", schema_id))
209    }
210}
211
212/// Extract a ValueWord value from a ValueSlot using the schema field type.
213fn 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
234/// Render an array as a ContentNode.
235///
236/// For arrays of typed objects, renders as a table with columns from the schema.
237/// For scalar arrays, renders as "[1, 2, 3]".
238fn render_array_as_content(arr: &[ValueWord]) -> ContentNode {
239    if arr.is_empty() {
240        return ContentNode::plain("[]".to_string());
241    }
242
243    // Check first element to determine rendering strategy
244    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    // Scalar array → "[1, 2, 3]"
249    let items: Vec<String> = arr.iter().map(|v| format!("{}", v)).collect();
250    ContentNode::plain(format!("[{}]", items.join(", ")))
251}
252
253/// Render an array of typed objects as a ContentNode::Table.
254///
255/// Extracts headers from the first element's schema and renders each row's
256/// field values as Content-dispatched cells. Falls back to single-column
257/// display if the schema cannot be resolved.
258fn render_typed_array_as_table(arr: &[ValueWord]) -> ContentNode {
259    // Get schema from first element
260    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                    // Non-TypedObject element in the array — single cell fallback
281                    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    // Fallback: single-column display
300    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
316/// Convert a DataTable (Arrow RecordBatch wrapper) to a ContentNode::Table.
317///
318/// Extracts column names as headers, determines column types from the Arrow schema,
319/// and converts each cell to a plain text ContentNode. Optionally limits the number
320/// of rows displayed.
321pub 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    // Determine column types from Arrow schema
329    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    // Build rows
337    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, // already truncated above
358        column_types: Some(column_types),
359        total_rows: if total > limit { Some(total) } else { None },
360        sortable: true,
361    })
362}
363
364/// Map Arrow DataType to a human-readable type label.
365fn 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
385/// Display a single Arrow cell value as a string.
386fn 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
427/// Render a HashMap as ContentNode::KeyValue pairs.
428fn 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)); // 12.34
539        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                // Field names come from schema
566                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                // Each row should have 2 cells
610                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}