serializer/llm/types.rs
1//! Core data types for LLM and Human format serialization
2//!
3//! This module defines the value types for the DX **LLM format** (text, token-efficient).
4//! The LLM format is optimized for minimal token usage in LLM context windows,
5//! achieving 73%+ token savings compared to JSON.
6//!
7//! # Format Comparison
8//!
9//! DX provides two serialization formats:
10//!
11//! | Format | Type | Use Case | Performance |
12//! |--------|------|----------|-------------|
13//! | **Machine** | [`DxValue`](crate::DxValue) | Binary, zero-copy, runtime | RKYV format |
14//! | **LLM** | [`DxLlmValue`] | Text, token-efficient, LLM context | 73%+ token savings |
15//!
16//! # When to Use This Module
17//!
18//! Use [`DxLlmValue`] and the LLM format when:
19//! - You're preparing data for LLM context windows
20//! - Token efficiency is critical (API costs, context limits)
21//! - You need human-readable output
22//! - You're converting to/from JSON, YAML, or TOML
23//!
24//! Use [`DxValue`](crate::DxValue) when:
25//! - You need binary serialization with RKYV
26//! - You're working with binary data or network protocols
27//! - You want zero-copy deserialization
28//!
29//! # Document Structure
30//!
31//! The LLM format uses a document-based structure:
32//!
33//! - [`DxDocument`]: Top-level container with context, refs, and sections
34//! - [`DxSection`]: A data section with schema and rows (tabular data)
35//! - [`DxLlmValue`]: Individual values within sections
36//!
37//! # Thread Safety
38//!
39//! All types in this module implement `Send + Sync` and can be safely shared
40//! between threads:
41//!
42//! - [`DxDocument`]: The main document type, safe for concurrent read access
43//! - [`DxSection`]: Section data, safe for concurrent read access
44//! - [`DxLlmValue`]: Value enum, all variants are thread-safe
45//!
46//! For mutation, clone the document or use appropriate synchronization primitives
47//! like `Arc<Mutex<DxDocument>>` or `Arc<RwLock<DxDocument>>`.
48
49use indexmap::IndexMap;
50use std::fmt;
51
52/// Top-level document container for the DX LLM format.
53///
54/// A `DxDocument` represents a complete DX document in the LLM format,
55/// containing context configuration, reference definitions, and data sections.
56///
57/// # Structure
58///
59/// ```text
60/// #c context_key=value // Context section
61/// #: ref_key=ref_value // Reference definitions
62/// #d(id,name)[ // Data section 'd' with schema
63/// 1,Alice
64/// 2,Bob
65/// ]
66/// ```
67///
68/// # Examples
69///
70/// ## Creating a Document
71///
72/// ```rust
73/// use serializer::llm::{DxDocument, DxSection, DxLlmValue};
74///
75/// let mut doc = DxDocument::new();
76///
77/// // Add context
78/// doc.context.insert("version".to_string(), DxLlmValue::Str("1.0".to_string()));
79///
80/// // Add a reference
81/// doc.refs.insert("company".to_string(), "Acme Corp".to_string());
82///
83/// // Add a data section
84/// let mut section = DxSection::new(vec!["id".to_string(), "name".to_string()]);
85/// section.add_row(vec![DxLlmValue::Num(1.0), DxLlmValue::Str("Alice".to_string())]).unwrap();
86/// doc.sections.insert('d', section);
87/// ```
88///
89/// # Thread Safety
90///
91/// `DxDocument` implements `Send + Sync` and can be safely shared between threads
92/// for concurrent read access. For mutation, use appropriate synchronization.
93///
94/// # See Also
95///
96/// - [`DxSection`] - Data section with schema and rows
97/// - [`DxLlmValue`] - Value type for section data
98#[derive(Debug, Clone, PartialEq)]
99pub struct DxDocument {
100 /// Context/config section (`#c`).
101 ///
102 /// Contains configuration key-value pairs that apply to the entire document.
103 pub context: IndexMap<String, DxLlmValue>,
104 /// Reference definitions (`#:`).
105 ///
106 /// Defines reusable string values that can be referenced with `^key` syntax.
107 /// This enables deduplication of repeated values for token efficiency.
108 pub refs: IndexMap<String, String>,
109 /// Data sections (`#<letter>`).
110 ///
111 /// Each section is identified by a single character (e.g., 'd' for `#d`).
112 /// Sections contain tabular data with a schema and rows.
113 pub sections: IndexMap<char, DxSection>,
114 /// Section names mapping (char ID -> full name like "dependencies")
115 ///
116 /// Maps single-character section IDs to their full names for human-readable output.
117 pub section_names: IndexMap<char, String>,
118 /// Entry order tracking
119 ///
120 /// Tracks the order of all entries (context keys and section IDs) as they appear
121 /// in the original document. Each entry is either a context key or a section ID.
122 pub entry_order: Vec<EntryRef>,
123}
124
125/// Reference to an entry in the document (context or section)
126#[derive(Debug, Clone, PartialEq)]
127pub enum EntryRef {
128 /// Reference to a context entry by key
129 Context(String),
130 /// Reference to a section by its char ID
131 Section(char),
132}
133
134impl DxDocument {
135 /// Create a new empty document
136 #[must_use]
137 pub fn new() -> Self {
138 Self {
139 context: IndexMap::new(),
140 refs: IndexMap::new(),
141 sections: IndexMap::new(),
142 section_names: IndexMap::new(),
143 entry_order: Vec::new(),
144 }
145 }
146
147 /// Check if the document is empty
148 #[must_use]
149 pub fn is_empty(&self) -> bool {
150 self.context.is_empty() && self.refs.is_empty() && self.sections.is_empty()
151 }
152
153 /// Get a value by canonical LLM path.
154 ///
155 /// This supports both root keys (`project`) and object paths
156 /// (`project.name`) where the root value is an inline object such as
157 /// `project(name=dx-devtools version=0.1.0)`.
158 #[must_use]
159 pub fn get_path(&self, path: &str) -> Option<&DxLlmValue> {
160 if let Some(value) = self.context.get(path) {
161 return Some(value);
162 }
163
164 let (root, nested_path) = path.split_once('.')?;
165 let mut current = self.context.get(root)?;
166 for segment in nested_path.split('.') {
167 match current {
168 DxLlmValue::Obj(fields) => {
169 current = fields.get(segment)?;
170 }
171 _ => return None,
172 }
173 }
174 Some(current)
175 }
176
177 /// Find a wrapped dataframe/table section by its full section name.
178 #[must_use]
179 pub fn section_by_name(&self, name: &str) -> Option<&DxSection> {
180 self.section_names
181 .iter()
182 .find_map(|(section_id, section_name)| {
183 (section_name == name)
184 .then(|| self.sections.get(section_id))
185 .flatten()
186 })
187 }
188}
189
190impl Default for DxDocument {
191 fn default() -> Self {
192 Self::new()
193 }
194}
195
196/// A data section with schema-defined columns and rows.
197///
198/// `DxSection` represents tabular data in the DX LLM format. Each section
199/// has a schema (column names) and rows of [`DxLlmValue`] data.
200///
201/// # LLM Format Syntax
202///
203/// ```text
204/// #d(id,name,active)[ // Section 'd' with schema: id, name, active
205/// 1,Alice,+ // Row 1
206/// 2,Bob,- // Row 2
207/// ]
208/// ```
209///
210/// # Examples
211///
212/// ## Creating a Section
213///
214/// ```rust
215/// use serializer::llm::{DxSection, DxLlmValue};
216///
217/// let mut section = DxSection::new(vec![
218/// "id".to_string(),
219/// "name".to_string(),
220/// "active".to_string(),
221/// ]);
222///
223/// // Add rows (must match schema length)
224/// section.add_row(vec![
225/// DxLlmValue::Num(1.0),
226/// DxLlmValue::Str("Alice".to_string()),
227/// DxLlmValue::Bool(true),
228/// ]).unwrap();
229///
230/// assert_eq!(section.row_count(), 1);
231/// assert_eq!(section.column_count(), 3);
232/// ```
233///
234/// ## Error Handling
235///
236/// ```rust
237/// use serializer::llm::{DxSection, DxLlmValue};
238///
239/// let mut section = DxSection::new(vec!["a".to_string(), "b".to_string()]);
240///
241/// // Wrong number of columns returns an error
242/// let result = section.add_row(vec![DxLlmValue::Num(1.0)]);
243/// assert!(result.is_err());
244/// ```
245///
246/// # Thread Safety
247///
248/// `DxSection` implements `Send + Sync` and can be safely shared between threads.
249///
250/// # See Also
251///
252/// - [`DxDocument`] - Parent container for sections
253/// - [`DxLlmValue`] - Value type for row data
254#[derive(Debug, Clone, PartialEq)]
255pub struct DxSection {
256 /// Column names from the schema.
257 ///
258 /// Defines the structure of each row. All rows must have exactly
259 /// `schema.len()` values.
260 pub schema: Vec<String>,
261 /// Row data.
262 ///
263 /// Each row is a vector of [`DxLlmValue`] with length matching the schema.
264 pub rows: Vec<Vec<DxLlmValue>>,
265}
266
267impl DxSection {
268 /// Create a new section with the given schema
269 #[must_use]
270 pub fn new(schema: Vec<String>) -> Self {
271 Self {
272 schema,
273 rows: Vec::new(),
274 }
275 }
276
277 /// Add a row to the section
278 ///
279 /// # Errors
280 ///
281 /// Returns an error if the row length doesn't match the schema length.
282 pub fn add_row(&mut self, row: Vec<DxLlmValue>) -> Result<(), String> {
283 if row.len() != self.schema.len() {
284 return Err(format!(
285 "Row length {} doesn't match schema length {}",
286 row.len(),
287 self.schema.len()
288 ));
289 }
290 self.rows.push(row);
291 Ok(())
292 }
293
294 /// Get the number of rows
295 #[must_use]
296 pub fn row_count(&self) -> usize {
297 self.rows.len()
298 }
299
300 /// Get the zero-based column index by schema name.
301 #[must_use]
302 pub fn column_index(&self, name: &str) -> Option<usize> {
303 self.schema.iter().position(|column| column == name)
304 }
305
306 /// Return all values from a named column.
307 #[must_use]
308 pub fn column_values(&self, name: &str) -> Option<Vec<&DxLlmValue>> {
309 let column_index = self.column_index(name)?;
310 Some(
311 self.rows
312 .iter()
313 .filter_map(|row| row.get(column_index))
314 .collect(),
315 )
316 }
317
318 /// Find a value by matching one column and returning another.
319 #[must_use]
320 pub fn value_by_key(
321 &self,
322 key_column: &str,
323 key: &str,
324 value_column: &str,
325 ) -> Option<&DxLlmValue> {
326 let key_index = self.column_index(key_column)?;
327 let value_index = self.column_index(value_column)?;
328 self.rows.iter().find_map(|row| {
329 let row_key = row.get(key_index)?.to_string();
330 (row_key == key).then(|| row.get(value_index)).flatten()
331 })
332 }
333
334 /// Get the number of columns
335 #[must_use]
336 pub fn column_count(&self) -> usize {
337 self.schema.len()
338 }
339}
340
341/// Value type for the DX **LLM format** (text, token-efficient).
342///
343/// `DxLlmValue` represents all possible values in the DX LLM format, optimized
344/// for minimal token usage in LLM context windows. This format achieves 73%+
345/// token savings compared to JSON.
346///
347/// # Relationship to DxValue
348///
349/// DX provides two value types for different use cases:
350///
351/// - **[`DxValue`](crate::DxValue)**: For the binary machine format. Use when you need
352/// maximum performance, zero-copy parsing, or runtime data structures.
353///
354/// - **`DxLlmValue`** (this type): For the text LLM format. Use when preparing
355/// data for LLM context windows or when token efficiency matters.
356///
357/// ## Key Differences
358///
359/// | Aspect | `DxValue` | `DxLlmValue` |
360/// |--------|-----------|--------------|
361/// | Format | Binary | Text (Dx Serializer) |
362/// | Numbers | Separate `Int`/`Float` | Single `Num` |
363/// | Structured data | `Object`, `Table` | `Obj` variant or `DxSection` |
364/// | References | `Ref(usize)` | `Ref(String)` |
365/// | Optimization | Speed | Token count |
366///
367/// The type differences reflect the different use cases:
368/// - LLMs don't distinguish integer vs float, so `Num` is sufficient
369/// - LLM format uses string-based references for readability
370/// - Inline objects use `Obj` variant, tabular data uses `DxSection`
371///
372/// # When to Use DxLlmValue
373///
374/// Choose `DxLlmValue` when:
375/// - **Token efficiency matters**: 73%+ savings vs JSON reduces API costs
376/// - **LLM context limits**: Fit more data in limited context windows
377/// - **Human readability**: Text format is easier to inspect and debug
378/// - **Format conversion**: Converting to/from JSON, YAML, TOML
379///
380/// # Examples
381///
382/// ## Creating Values
383///
384/// ```rust
385/// use serializer::llm::DxLlmValue;
386/// use std::collections::HashMap;
387///
388/// // Primitive values
389/// let null = DxLlmValue::Null;
390/// let boolean = DxLlmValue::Bool(true);
391/// let number = DxLlmValue::Num(42.0);
392/// let string = DxLlmValue::Str("hello".to_string());
393///
394/// // Object value
395/// let mut fields = HashMap::new();
396/// fields.insert("host".to_string(), DxLlmValue::Str("localhost".to_string()));
397/// fields.insert("port".to_string(), DxLlmValue::Num(8080.0));
398/// let obj = DxLlmValue::Obj(fields);
399/// ```
400///
401/// ## Using From Traits
402///
403/// ```rust
404/// use serializer::llm::DxLlmValue;
405///
406/// // Convenient conversions
407/// let from_str: DxLlmValue = "hello".into();
408/// let from_string: DxLlmValue = String::from("world").into();
409/// let from_int: DxLlmValue = 42i64.into();
410/// let from_float: DxLlmValue = 3.14f64.into();
411/// let from_bool: DxLlmValue = true.into();
412/// ```
413///
414/// ## Working with Arrays
415///
416/// ```rust
417/// use serializer::llm::DxLlmValue;
418///
419/// let arr = DxLlmValue::Arr(vec![
420/// DxLlmValue::Num(1.0),
421/// DxLlmValue::Num(2.0),
422/// DxLlmValue::Num(3.0),
423/// ]);
424///
425/// if let DxLlmValue::Arr(values) = &arr {
426/// assert_eq!(values.len(), 3);
427/// }
428/// ```
429///
430/// ## Type Inspection
431///
432/// ```rust
433/// use serializer::llm::DxLlmValue;
434///
435/// let value = DxLlmValue::Num(42.0);
436/// assert_eq!(value.type_name(), "number");
437/// assert_eq!(value.as_num(), Some(42.0));
438/// assert!(value.as_str().is_none());
439/// ```
440///
441/// ## Display Formatting
442///
443/// ```rust
444/// use serializer::llm::DxLlmValue;
445///
446/// // Values format nicely for display
447/// assert_eq!(format!("{}", DxLlmValue::Num(42.0)), "42");
448/// assert_eq!(format!("{}", DxLlmValue::Bool(true)), "true");
449/// assert_eq!(format!("{}", DxLlmValue::Null), "null");
450/// assert_eq!(format!("{}", DxLlmValue::Ref("A".to_string())), "^A");
451/// ```
452///
453/// # Thread Safety
454///
455/// `DxLlmValue` implements `Send + Sync` and can be safely shared between threads.
456/// This is verified at compile time via static assertions.
457///
458/// # See Also
459///
460/// - [`DxValue`](crate::DxValue) - Value type for the binary machine format
461/// - [`DxDocument`] - Top-level document container for LLM format
462/// - [`DxSection`] - Section with schema and rows for tabular data
463#[derive(Debug, Clone, PartialEq)]
464pub enum DxLlmValue {
465 /// String value.
466 ///
467 /// In LLM format, strings are represented without quotes when possible,
468 /// contributing to token efficiency.
469 Str(String),
470 /// Numeric value (integer or float).
471 ///
472 /// Unlike [`DxValue`](crate::DxValue) which has separate `Int` and `Float`
473 /// variants, `DxLlmValue` uses a single `Num` variant because LLMs don't
474 /// distinguish between integer and floating-point numbers.
475 ///
476 /// Integers are stored as `f64` but display without decimal points when
477 /// the fractional part is zero (e.g., `42` not `42.0`).
478 Num(f64),
479 /// Boolean value.
480 ///
481 /// In LLM format: `true` or `false`
482 /// In Human format: `true` or `false`
483 Bool(bool),
484 /// Null value.
485 ///
486 /// In LLM format: `null`
487 /// In Human format: `null`
488 Null,
489 /// Array value.
490 ///
491 /// In LLM format: `*a,b,c` (inline array syntax)
492 Arr(Vec<DxLlmValue>),
493 /// Object value with key-value pairs.
494 ///
495 /// In LLM format: `name[key=value,key2=value2]` (inline object syntax)
496 ///
497 /// Objects are stored as an IndexMap for efficient key lookup while
498 /// maintaining insertion order and type safety. This is preferred over encoding objects
499 /// as strings, which would lose type information.
500 Obj(IndexMap<String, DxLlmValue>),
501 /// Reference pointer to a defined ref.
502 ///
503 /// In LLM format: `^key` references a value defined in the `#:` refs section.
504 /// This enables deduplication of repeated values, further reducing token count.
505 ///
506 /// Unlike [`DxValue::Ref`](crate::DxValue::Ref) which uses numeric indices,
507 /// `DxLlmValue::Ref` uses string keys for human readability.
508 Ref(String),
509}
510
511impl DxLlmValue {
512 /// Check if this value is null
513 #[must_use]
514 pub fn is_null(&self) -> bool {
515 matches!(self, DxLlmValue::Null)
516 }
517
518 /// Get the type name for error messages
519 #[must_use]
520 pub fn type_name(&self) -> &'static str {
521 match self {
522 DxLlmValue::Str(_) => "string",
523 DxLlmValue::Num(_) => "number",
524 DxLlmValue::Bool(_) => "bool",
525 DxLlmValue::Null => "null",
526 DxLlmValue::Arr(_) => "array",
527 DxLlmValue::Obj(_) => "object",
528 DxLlmValue::Ref(_) => "ref",
529 }
530 }
531
532 /// Try to get as string
533 #[must_use]
534 pub fn as_str(&self) -> Option<&str> {
535 match self {
536 DxLlmValue::Str(s) => Some(s),
537 _ => None,
538 }
539 }
540
541 /// Try to get as number
542 #[must_use]
543 pub fn as_num(&self) -> Option<f64> {
544 match self {
545 DxLlmValue::Num(n) => Some(*n),
546 _ => None,
547 }
548 }
549
550 /// Try to get as bool
551 #[must_use]
552 pub fn as_bool(&self) -> Option<bool> {
553 match self {
554 DxLlmValue::Bool(b) => Some(*b),
555 _ => None,
556 }
557 }
558
559 /// Try to get as array
560 #[must_use]
561 pub fn as_arr(&self) -> Option<&Vec<DxLlmValue>> {
562 match self {
563 DxLlmValue::Arr(arr) => Some(arr),
564 _ => None,
565 }
566 }
567
568 /// Try to get as object
569 #[must_use]
570 pub fn as_obj(&self) -> Option<&IndexMap<String, DxLlmValue>> {
571 match self {
572 DxLlmValue::Obj(obj) => Some(obj),
573 _ => None,
574 }
575 }
576
577 /// Try to get as reference key
578 #[must_use]
579 pub fn as_ref(&self) -> Option<&str> {
580 match self {
581 DxLlmValue::Ref(key) => Some(key),
582 _ => None,
583 }
584 }
585}
586
587impl fmt::Display for DxLlmValue {
588 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
589 match self {
590 DxLlmValue::Str(s) => write!(f, "{}", s),
591 DxLlmValue::Num(n) => {
592 if n.fract() == 0.0 {
593 write!(f, "{}", *n as i64)
594 } else {
595 write!(f, "{}", n)
596 }
597 }
598 DxLlmValue::Bool(b) => write!(f, "{}", if *b { "true" } else { "false" }),
599 DxLlmValue::Null => write!(f, "null"),
600 DxLlmValue::Arr(arr) => {
601 write!(f, "[")?;
602 for (i, v) in arr.iter().enumerate() {
603 if i > 0 {
604 write!(f, ", ")?;
605 }
606 write!(f, "{}", v)?;
607 }
608 write!(f, "]")
609 }
610 DxLlmValue::Obj(obj) => {
611 write!(f, "[")?;
612 for (i, (k, v)) in obj.iter().enumerate() {
613 if i > 0 {
614 write!(f, ",")?;
615 }
616 write!(f, "{}={}", k, v)?;
617 }
618 write!(f, "]")
619 }
620 DxLlmValue::Ref(key) => write!(f, "^{}", key),
621 }
622 }
623}
624
625impl From<&str> for DxLlmValue {
626 fn from(s: &str) -> Self {
627 DxLlmValue::Str(s.to_string())
628 }
629}
630
631impl From<String> for DxLlmValue {
632 fn from(s: String) -> Self {
633 DxLlmValue::Str(s)
634 }
635}
636
637impl From<f64> for DxLlmValue {
638 fn from(n: f64) -> Self {
639 DxLlmValue::Num(n)
640 }
641}
642
643impl From<i64> for DxLlmValue {
644 fn from(n: i64) -> Self {
645 DxLlmValue::Num(n as f64)
646 }
647}
648
649impl From<bool> for DxLlmValue {
650 fn from(b: bool) -> Self {
651 DxLlmValue::Bool(b)
652 }
653}
654
655// =============================================================================
656// Thread Safety Compile-Time Assertions
657// =============================================================================
658
659// These static assertions verify at compile time that our types are thread-safe.
660// If any of these types stop implementing Send or Sync, compilation will fail.
661
662/// Compile-time assertion that a type implements Send
663const fn _assert_send<T: Send>() {}
664
665/// Compile-time assertion that a type implements Sync
666const fn _assert_sync<T: Sync>() {}
667
668// Verify DxDocument is Send + Sync
669const _: () = _assert_send::<DxDocument>();
670const _: () = _assert_sync::<DxDocument>();
671
672// Verify DxSection is Send + Sync
673const _: () = _assert_send::<DxSection>();
674const _: () = _assert_sync::<DxSection>();
675
676// Verify DxLlmValue is Send + Sync
677const _: () = _assert_send::<DxLlmValue>();
678const _: () = _assert_sync::<DxLlmValue>();
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683
684 #[test]
685 fn test_dx_document_new() {
686 let doc = DxDocument::new();
687 assert!(doc.is_empty());
688 assert!(doc.context.is_empty());
689 assert!(doc.refs.is_empty());
690 assert!(doc.sections.is_empty());
691 }
692
693 #[test]
694 fn test_dx_section_add_row() {
695 let mut section = DxSection::new(vec!["id".to_string(), "name".to_string()]);
696
697 // Valid row
698 let result = section.add_row(vec![
699 DxLlmValue::Num(1.0),
700 DxLlmValue::Str("Test".to_string()),
701 ]);
702 assert!(result.is_ok());
703 assert_eq!(section.row_count(), 1);
704
705 // Invalid row (wrong length)
706 let result = section.add_row(vec![DxLlmValue::Num(2.0)]);
707 assert!(result.is_err());
708 }
709
710 #[test]
711 fn test_dx_document_canonical_path_and_named_section_access() {
712 let mut doc = DxDocument::new();
713 let mut project = IndexMap::new();
714 project.insert(
715 "name".to_string(),
716 DxLlmValue::Str("dx-devtools".to_string()),
717 );
718 project.insert("kind".to_string(), DxLlmValue::Str("www-app".to_string()));
719 doc.context
720 .insert("project".to_string(), DxLlmValue::Obj(project));
721
722 let mut tools = DxSection::new(vec![
723 "name".to_string(),
724 "command".to_string(),
725 "enabled".to_string(),
726 "output".to_string(),
727 ]);
728 tools
729 .add_row(vec![
730 DxLlmValue::Str("style".to_string()),
731 DxLlmValue::Str("dx style build".to_string()),
732 DxLlmValue::Bool(true),
733 DxLlmValue::Str("styles/app.generated.css".to_string()),
734 ])
735 .unwrap();
736 doc.sections.insert('t', tools);
737 doc.section_names.insert('t', "tools".to_string());
738
739 assert_eq!(
740 doc.get_path("project.name").unwrap().as_str(),
741 Some("dx-devtools")
742 );
743 let tools = doc.section_by_name("tools").unwrap();
744 assert_eq!(tools.column_index("command"), Some(1));
745 assert_eq!(
746 tools
747 .value_by_key("name", "style", "output")
748 .unwrap()
749 .as_str(),
750 Some("styles/app.generated.css")
751 );
752 }
753
754 #[test]
755 fn test_dx_llm_value_type_name() {
756 assert_eq!(DxLlmValue::Str("test".to_string()).type_name(), "string");
757 assert_eq!(DxLlmValue::Num(42.0).type_name(), "number");
758 assert_eq!(DxLlmValue::Bool(true).type_name(), "bool");
759 assert_eq!(DxLlmValue::Null.type_name(), "null");
760 assert_eq!(DxLlmValue::Arr(vec![]).type_name(), "array");
761 assert_eq!(DxLlmValue::Obj(IndexMap::new()).type_name(), "object");
762 assert_eq!(DxLlmValue::Ref("key".to_string()).type_name(), "ref");
763 }
764
765 #[test]
766 #[allow(clippy::approx_constant)] // Using 3.14 intentionally for test data
767 fn test_dx_llm_value_display() {
768 assert_eq!(format!("{}", DxLlmValue::Str("hello".to_string())), "hello");
769 assert_eq!(format!("{}", DxLlmValue::Num(42.0)), "42");
770 assert_eq!(format!("{}", DxLlmValue::Num(3.14)), "3.14");
771 assert_eq!(format!("{}", DxLlmValue::Bool(true)), "true");
772 assert_eq!(format!("{}", DxLlmValue::Bool(false)), "false");
773 assert_eq!(format!("{}", DxLlmValue::Null), "null");
774 assert_eq!(format!("{}", DxLlmValue::Ref("A".to_string())), "^A");
775 }
776
777 #[test]
778 fn test_dx_llm_value_obj() {
779 let mut fields = IndexMap::new();
780 fields.insert("host".to_string(), DxLlmValue::Str("localhost".to_string()));
781 fields.insert("port".to_string(), DxLlmValue::Num(8080.0));
782 let obj = DxLlmValue::Obj(fields);
783
784 assert_eq!(obj.type_name(), "object");
785 assert!(obj.as_obj().is_some());
786 let obj_map = obj.as_obj().unwrap();
787 assert_eq!(obj_map.len(), 2);
788 assert_eq!(obj_map.get("host").unwrap().as_str(), Some("localhost"));
789 assert_eq!(obj_map.get("port").unwrap().as_num(), Some(8080.0));
790 }
791
792 // Note: Send+Sync assertions are now compile-time checks at module level,
793 // not runtime tests. See the const assertions above the test module.
794}