paramdef/types/container/
matrix.rs

1//! Matrix container for table-based data entry.
2//!
3//! Matrix represents a table where rows are questions/items and columns are
4//! possible values. Each row produces a single value selected from columns.
5//! Inspired by `SurveyJS` matrix question type.
6
7use std::any::Any;
8use std::fmt;
9use std::sync::Arc;
10
11use crate::core::{Flags, FxHashSet, Key, Metadata, SmartStr};
12use crate::types::kind::NodeKind;
13use crate::types::traits::{Container, Node};
14
15/// A row in a Matrix container.
16///
17/// Each row represents an item (e.g., a question) that needs a value
18/// selected from the matrix columns.
19#[derive(Debug, Clone)]
20pub struct MatrixRow {
21    /// Unique key for this row.
22    pub key: Key,
23    /// Display label for this row.
24    pub label: SmartStr,
25    /// Optional description or help text.
26    pub description: Option<SmartStr>,
27}
28
29impl MatrixRow {
30    /// Creates a new row with key and label.
31    #[must_use]
32    pub fn new(key: impl Into<Key>, label: impl Into<SmartStr>) -> Self {
33        Self {
34            key: key.into(),
35            label: label.into(),
36            description: None,
37        }
38    }
39
40    /// Creates a new row with description.
41    #[must_use]
42    pub fn with_description(
43        key: impl Into<Key>,
44        label: impl Into<SmartStr>,
45        description: impl Into<SmartStr>,
46    ) -> Self {
47        Self {
48            key: key.into(),
49            label: label.into(),
50            description: Some(description.into()),
51        }
52    }
53}
54
55/// A column in a Matrix container.
56///
57/// Each column represents a possible value that can be selected for any row.
58#[derive(Debug, Clone)]
59pub struct MatrixColumn {
60    /// Value stored when this column is selected.
61    pub value: SmartStr,
62    /// Display label for this column.
63    pub label: SmartStr,
64    /// Optional weight or score for this column (useful for scoring).
65    pub weight: Option<i32>,
66    /// If true, selecting this column excludes all other columns in the row.
67    ///
68    /// Useful for "Not Applicable", "N/A", or "Don't Know" options.
69    pub exclusive: bool,
70}
71
72impl MatrixColumn {
73    /// Creates a new column with value and label.
74    #[must_use]
75    pub fn new(value: impl Into<SmartStr>, label: impl Into<SmartStr>) -> Self {
76        Self {
77            value: value.into(),
78            label: label.into(),
79            weight: None,
80            exclusive: false,
81        }
82    }
83
84    /// Creates a new column with a weight for scoring.
85    #[must_use]
86    pub fn with_weight(
87        value: impl Into<SmartStr>,
88        label: impl Into<SmartStr>,
89        weight: i32,
90    ) -> Self {
91        Self {
92            value: value.into(),
93            label: label.into(),
94            weight: Some(weight),
95            exclusive: false,
96        }
97    }
98
99    /// Creates an exclusive column that deselects other columns when selected.
100    ///
101    /// Useful for "Not Applicable", "N/A", or "Don't Know" options.
102    #[must_use]
103    pub fn exclusive(value: impl Into<SmartStr>, label: impl Into<SmartStr>) -> Self {
104        Self {
105            value: value.into(),
106            label: label.into(),
107            weight: None,
108            exclusive: true,
109        }
110    }
111
112    /// Creates an exclusive column with a weight.
113    #[must_use]
114    pub fn exclusive_with_weight(
115        value: impl Into<SmartStr>,
116        label: impl Into<SmartStr>,
117        weight: i32,
118    ) -> Self {
119        Self {
120            value: value.into(),
121            label: label.into(),
122            weight: Some(weight),
123            exclusive: true,
124        }
125    }
126
127    /// Returns true if this column is exclusive.
128    #[must_use]
129    pub fn is_exclusive(&self) -> bool {
130        self.exclusive
131    }
132
133    /// Creates columns from simple string labels.
134    ///
135    /// The value and label will be the same for each column.
136    #[must_use]
137    pub fn from_labels<S: Into<SmartStr> + Clone>(labels: &[S]) -> Vec<Self> {
138        labels
139            .iter()
140            .cloned()
141            .map(|label| {
142                let s = label.into();
143                Self {
144                    value: s.clone(),
145                    label: s,
146                    weight: None,
147                    exclusive: false,
148                }
149            })
150            .collect()
151    }
152}
153
154/// Selection mode for matrix cells.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
156pub enum MatrixCellType {
157    /// Radio buttons - single selection per row (default).
158    #[default]
159    Radio,
160    /// Checkboxes - multiple selections per row.
161    Checkbox,
162    /// Dropdown - single selection via dropdown menu.
163    Dropdown,
164    /// Text input - free text entry per cell.
165    Text,
166    /// Rating - star/number rating per row.
167    Rating,
168}
169
170impl MatrixCellType {
171    /// Returns the name of this cell type.
172    #[must_use]
173    pub fn name(&self) -> &'static str {
174        match self {
175            Self::Radio => "radio",
176            Self::Checkbox => "checkbox",
177            Self::Dropdown => "dropdown",
178            Self::Text => "text",
179            Self::Rating => "rating",
180        }
181    }
182
183    /// Returns true if this cell type allows multiple selections.
184    #[must_use]
185    pub fn is_multi_select(&self) -> bool {
186        matches!(self, Self::Checkbox)
187    }
188}
189
190/// A container for table-based data entry.
191///
192/// Matrix is a specialized container that displays data in a table format
193/// where rows represent items/questions and columns represent possible values.
194///
195/// # Value Format
196///
197/// For single-select (Radio/Dropdown):
198/// ```json
199/// {
200///   "row1_key": "column_value",
201///   "row2_key": "column_value"
202/// }
203/// ```
204///
205/// For multi-select (Checkbox):
206/// ```json
207/// {
208///   "row1_key": ["value1", "value2"],
209///   "row2_key": ["value1"]
210/// }
211/// ```
212///
213/// # Example
214///
215/// ```ignore
216/// use paramdef::container::Matrix;
217///
218/// // Satisfaction survey matrix
219/// let satisfaction = Matrix::builder("satisfaction")
220///     .label("Rate your satisfaction")
221///     .rows([
222///         ("price", "Price"),
223///         ("quality", "Quality"),
224///         ("support", "Customer Support"),
225///     ])
226///     .columns([
227///         ("1", "Very Poor"),
228///         ("2", "Poor"),
229///         ("3", "Fair"),
230///         ("4", "Good"),
231///         ("5", "Excellent"),
232///     ])
233///     .required()
234///     .build()
235///     .unwrap();
236///
237/// // Value: { "price": "4", "quality": "5", "support": "3" }
238/// ```
239#[derive(Clone)]
240pub struct Matrix {
241    metadata: Metadata,
242    flags: Flags,
243    rows: Vec<MatrixRow>,
244    columns: Vec<MatrixColumn>,
245    cell_type: MatrixCellType,
246    /// If true, all rows must have a value.
247    all_rows_required: bool,
248    /// Show row numbers.
249    show_row_numbers: bool,
250    /// Alternate row styling.
251    alternate_rows: bool,
252}
253
254impl fmt::Debug for Matrix {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        f.debug_struct("Matrix")
257            .field("metadata", &self.metadata)
258            .field("flags", &self.flags)
259            .field("row_count", &self.rows.len())
260            .field("column_count", &self.columns.len())
261            .field("cell_type", &self.cell_type)
262            .finish_non_exhaustive()
263    }
264}
265
266impl Matrix {
267    /// Creates a new builder for a Matrix.
268    #[must_use]
269    pub fn builder(key: impl Into<Key>) -> MatrixBuilder {
270        MatrixBuilder::new(key)
271    }
272
273    /// Returns the flags for this matrix.
274    #[must_use]
275    pub fn flags(&self) -> Flags {
276        self.flags
277    }
278
279    /// Returns all rows.
280    #[must_use]
281    pub fn rows(&self) -> &[MatrixRow] {
282        &self.rows
283    }
284
285    /// Returns the number of rows.
286    #[must_use]
287    pub fn row_count(&self) -> usize {
288        self.rows.len()
289    }
290
291    /// Returns all columns.
292    #[must_use]
293    pub fn columns(&self) -> &[MatrixColumn] {
294        &self.columns
295    }
296
297    /// Returns the number of columns.
298    #[must_use]
299    pub fn column_count(&self) -> usize {
300        self.columns.len()
301    }
302
303    /// Returns the cell type.
304    #[must_use]
305    pub fn cell_type(&self) -> MatrixCellType {
306        self.cell_type
307    }
308
309    /// Returns true if all rows are required to have a value.
310    #[must_use]
311    pub fn all_rows_required(&self) -> bool {
312        self.all_rows_required
313    }
314
315    /// Returns true if row numbers should be shown.
316    #[must_use]
317    pub fn show_row_numbers(&self) -> bool {
318        self.show_row_numbers
319    }
320
321    /// Returns true if alternate row styling is enabled.
322    #[must_use]
323    pub fn alternate_rows(&self) -> bool {
324        self.alternate_rows
325    }
326
327    /// Gets a row by key.
328    #[must_use]
329    pub fn get_row(&self, key: &str) -> Option<&MatrixRow> {
330        self.rows.iter().find(|r| r.key == key)
331    }
332
333    /// Gets a column by value.
334    #[must_use]
335    pub fn get_column(&self, value: &str) -> Option<&MatrixColumn> {
336        self.columns.iter().find(|c| c.value == value)
337    }
338
339    /// Returns exclusive columns (columns that deselect others when selected).
340    pub fn exclusive_columns(&self) -> impl Iterator<Item = &MatrixColumn> {
341        self.columns.iter().filter(|c| c.exclusive)
342    }
343
344    /// Returns true if this matrix has any exclusive columns.
345    #[must_use]
346    pub fn has_exclusive_columns(&self) -> bool {
347        self.columns.iter().any(|c| c.exclusive)
348    }
349
350    /// Returns an iterator over row keys.
351    pub fn row_keys(&self) -> impl Iterator<Item = &Key> {
352        self.rows.iter().map(|r| &r.key)
353    }
354
355    /// Returns an iterator over column values.
356    pub fn column_values(&self) -> impl Iterator<Item = &SmartStr> {
357        self.columns.iter().map(|c| &c.value)
358    }
359}
360
361impl Node for Matrix {
362    fn metadata(&self) -> &Metadata {
363        &self.metadata
364    }
365
366    fn key(&self) -> &Key {
367        self.metadata.key()
368    }
369
370    fn kind(&self) -> NodeKind {
371        NodeKind::Container
372    }
373
374    fn as_any(&self) -> &dyn Any {
375        self
376    }
377
378    fn as_any_mut(&mut self) -> &mut dyn Any {
379        self
380    }
381}
382
383impl Container for Matrix {
384    fn children(&self) -> &[Arc<dyn Node>] {
385        // Matrix doesn't have child nodes in the traditional sense.
386        // Rows and columns are metadata, not nodes.
387        &[]
388    }
389}
390
391// =============================================================================
392// Builder
393// =============================================================================
394
395/// Builder for [`Matrix`].
396#[derive(Debug)]
397pub struct MatrixBuilder {
398    key: Key,
399    label: Option<SmartStr>,
400    description: Option<SmartStr>,
401    flags: Flags,
402    rows: Vec<MatrixRow>,
403    columns: Vec<MatrixColumn>,
404    cell_type: MatrixCellType,
405    all_rows_required: bool,
406    show_row_numbers: bool,
407    alternate_rows: bool,
408}
409
410impl MatrixBuilder {
411    /// Creates a new builder with the given key.
412    #[must_use]
413    pub fn new(key: impl Into<Key>) -> Self {
414        Self {
415            key: key.into(),
416            label: None,
417            description: None,
418            flags: Flags::empty(),
419            rows: Vec::new(),
420            columns: Vec::new(),
421            cell_type: MatrixCellType::default(),
422            all_rows_required: false,
423            show_row_numbers: false,
424            alternate_rows: true,
425        }
426    }
427
428    /// Sets the label for this matrix.
429    #[must_use]
430    pub fn label(mut self, label: impl Into<SmartStr>) -> Self {
431        self.label = Some(label.into());
432        self
433    }
434
435    /// Sets the description for this matrix.
436    #[must_use]
437    pub fn description(mut self, description: impl Into<SmartStr>) -> Self {
438        self.description = Some(description.into());
439        self
440    }
441
442    /// Sets the flags for this matrix.
443    #[must_use]
444    pub fn flags(mut self, flags: Flags) -> Self {
445        self.flags = flags;
446        self
447    }
448
449    /// Marks this matrix as required.
450    #[must_use]
451    pub fn required(mut self) -> Self {
452        self.flags |= Flags::REQUIRED;
453        self
454    }
455
456    /// Adds a single row.
457    #[must_use]
458    pub fn row(mut self, key: impl Into<Key>, label: impl Into<SmartStr>) -> Self {
459        self.rows.push(MatrixRow::new(key, label));
460        self
461    }
462
463    /// Adds a row with description.
464    #[must_use]
465    pub fn row_with_description(
466        mut self,
467        key: impl Into<Key>,
468        label: impl Into<SmartStr>,
469        description: impl Into<SmartStr>,
470    ) -> Self {
471        self.rows
472            .push(MatrixRow::with_description(key, label, description));
473        self
474    }
475
476    /// Adds multiple rows from (key, label) tuples.
477    #[must_use]
478    pub fn rows<K, L, I>(mut self, rows: I) -> Self
479    where
480        K: Into<Key>,
481        L: Into<SmartStr>,
482        I: IntoIterator<Item = (K, L)>,
483    {
484        for (key, label) in rows {
485            self.rows.push(MatrixRow::new(key, label));
486        }
487        self
488    }
489
490    /// Adds multiple rows from simple labels (key = label).
491    #[must_use]
492    pub fn rows_from_labels<S, I>(mut self, labels: I) -> Self
493    where
494        S: Into<SmartStr> + Clone,
495        I: IntoIterator<Item = S>,
496    {
497        for label in labels {
498            let s = label.into();
499            self.rows.push(MatrixRow {
500                key: Key::from(s.as_str()),
501                label: s,
502                description: None,
503            });
504        }
505        self
506    }
507
508    /// Adds a single column.
509    #[must_use]
510    pub fn column(mut self, value: impl Into<SmartStr>, label: impl Into<SmartStr>) -> Self {
511        self.columns.push(MatrixColumn::new(value, label));
512        self
513    }
514
515    /// Adds a column with weight for scoring.
516    #[must_use]
517    pub fn column_with_weight(
518        mut self,
519        value: impl Into<SmartStr>,
520        label: impl Into<SmartStr>,
521        weight: i32,
522    ) -> Self {
523        self.columns
524            .push(MatrixColumn::with_weight(value, label, weight));
525        self
526    }
527
528    /// Adds an exclusive column that deselects others when selected.
529    ///
530    /// Useful for "Not Applicable", "N/A", or "Don't Know" options.
531    #[must_use]
532    pub fn exclusive_column(
533        mut self,
534        value: impl Into<SmartStr>,
535        label: impl Into<SmartStr>,
536    ) -> Self {
537        self.columns.push(MatrixColumn::exclusive(value, label));
538        self
539    }
540
541    /// Adds multiple columns from (value, label) tuples.
542    #[must_use]
543    pub fn columns<V, L, I>(mut self, columns: I) -> Self
544    where
545        V: Into<SmartStr>,
546        L: Into<SmartStr>,
547        I: IntoIterator<Item = (V, L)>,
548    {
549        for (value, label) in columns {
550            self.columns.push(MatrixColumn::new(value, label));
551        }
552        self
553    }
554
555    /// Adds multiple columns from simple labels (value = label).
556    #[must_use]
557    pub fn columns_from_labels<S, I>(mut self, labels: I) -> Self
558    where
559        S: Into<SmartStr> + Clone,
560        I: IntoIterator<Item = S>,
561    {
562        for label in labels {
563            let s = label.into();
564            self.columns.push(MatrixColumn {
565                value: s.clone(),
566                label: s,
567                weight: None,
568                exclusive: false,
569            });
570        }
571        self
572    }
573
574    /// Sets the cell type for this matrix.
575    #[must_use]
576    pub fn cell_type(mut self, cell_type: MatrixCellType) -> Self {
577        self.cell_type = cell_type;
578        self
579    }
580
581    /// Sets the cell type to radio buttons (single select).
582    #[must_use]
583    pub fn radio(mut self) -> Self {
584        self.cell_type = MatrixCellType::Radio;
585        self
586    }
587
588    /// Sets the cell type to checkboxes (multi select).
589    #[must_use]
590    pub fn checkbox(mut self) -> Self {
591        self.cell_type = MatrixCellType::Checkbox;
592        self
593    }
594
595    /// Sets the cell type to dropdown.
596    #[must_use]
597    pub fn dropdown(mut self) -> Self {
598        self.cell_type = MatrixCellType::Dropdown;
599        self
600    }
601
602    /// Requires all rows to have a value.
603    #[must_use]
604    pub fn all_rows_required(mut self, required: bool) -> Self {
605        self.all_rows_required = required;
606        self
607    }
608
609    /// Shows row numbers.
610    #[must_use]
611    pub fn show_row_numbers(mut self, show: bool) -> Self {
612        self.show_row_numbers = show;
613        self
614    }
615
616    /// Enables alternate row styling.
617    #[must_use]
618    pub fn alternate_rows(mut self, alternate: bool) -> Self {
619        self.alternate_rows = alternate;
620        self
621    }
622
623    /// Builds the Matrix.
624    ///
625    /// # Errors
626    ///
627    /// Returns an error if:
628    /// - No rows were added
629    /// - No columns were added
630    /// - Duplicate row keys exist
631    /// - Duplicate column values exist
632    pub fn build(self) -> crate::core::Result<Matrix> {
633        if self.rows.is_empty() {
634            return Err(crate::core::Error::missing_required("rows"));
635        }
636
637        if self.columns.is_empty() {
638            return Err(crate::core::Error::missing_required("columns"));
639        }
640
641        // Check for duplicate row keys
642        let mut seen_row_keys = FxHashSet::default();
643        for row in &self.rows {
644            if !seen_row_keys.insert(&row.key) {
645                return Err(crate::core::Error::validation(
646                    "duplicate_key",
647                    format!("duplicate row key: {}", row.key),
648                ));
649            }
650        }
651
652        // Check for duplicate column values
653        let mut seen_column_values = FxHashSet::default();
654        for column in &self.columns {
655            if !seen_column_values.insert(&column.value) {
656                return Err(crate::core::Error::validation(
657                    "duplicate_value",
658                    format!("duplicate column value: {}", column.value),
659                ));
660            }
661        }
662
663        let mut metadata = Metadata::new(self.key);
664        if let Some(label) = self.label {
665            metadata = metadata.with_label(label);
666        }
667        if let Some(description) = self.description {
668            metadata = metadata.with_description(description);
669        }
670
671        Ok(Matrix {
672            metadata,
673            flags: self.flags,
674            rows: self.rows,
675            columns: self.columns,
676            cell_type: self.cell_type,
677            all_rows_required: self.all_rows_required,
678            show_row_numbers: self.show_row_numbers,
679            alternate_rows: self.alternate_rows,
680        })
681    }
682}
683
684// =============================================================================
685// Tests
686// =============================================================================
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691
692    #[test]
693    fn test_matrix_basic() {
694        let matrix = Matrix::builder("satisfaction")
695            .label("Rate your satisfaction")
696            .row("price", "Price")
697            .row("quality", "Quality")
698            .column("1", "Poor")
699            .column("2", "Fair")
700            .column("3", "Good")
701            .build()
702            .unwrap();
703
704        assert_eq!(matrix.key().as_str(), "satisfaction");
705        assert_eq!(matrix.metadata().label(), Some("Rate your satisfaction"));
706        assert_eq!(matrix.kind(), NodeKind::Container);
707        assert_eq!(matrix.row_count(), 2);
708        assert_eq!(matrix.column_count(), 3);
709        assert_eq!(matrix.cell_type(), MatrixCellType::Radio);
710    }
711
712    #[test]
713    fn test_matrix_with_tuples() {
714        let matrix = Matrix::builder("survey")
715            .rows([
716                ("price", "Price"),
717                ("quality", "Quality"),
718                ("speed", "Speed"),
719            ])
720            .columns([
721                ("1", "Very Poor"),
722                ("2", "Poor"),
723                ("3", "Fair"),
724                ("4", "Good"),
725                ("5", "Excellent"),
726            ])
727            .build()
728            .unwrap();
729
730        assert_eq!(matrix.row_count(), 3);
731        assert_eq!(matrix.column_count(), 5);
732    }
733
734    #[test]
735    fn test_matrix_from_labels() {
736        let matrix = Matrix::builder("features")
737            .rows_from_labels(["Feature A", "Feature B", "Feature C"])
738            .columns_from_labels(["Yes", "No", "Maybe"])
739            .build()
740            .unwrap();
741
742        assert_eq!(matrix.row_count(), 3);
743        assert_eq!(matrix.column_count(), 3);
744
745        // Keys should be same as labels
746        let row = matrix.get_row("Feature A");
747        assert!(row.is_some());
748        assert_eq!(row.unwrap().label, "Feature A");
749    }
750
751    #[test]
752    fn test_matrix_cell_types() {
753        let radio = Matrix::builder("m")
754            .row("r", "R")
755            .column("c", "C")
756            .radio()
757            .build()
758            .unwrap();
759        assert_eq!(radio.cell_type(), MatrixCellType::Radio);
760        assert!(!radio.cell_type().is_multi_select());
761
762        let checkbox = Matrix::builder("m")
763            .row("r", "R")
764            .column("c", "C")
765            .checkbox()
766            .build()
767            .unwrap();
768        assert_eq!(checkbox.cell_type(), MatrixCellType::Checkbox);
769        assert!(checkbox.cell_type().is_multi_select());
770
771        let dropdown = Matrix::builder("m")
772            .row("r", "R")
773            .column("c", "C")
774            .dropdown()
775            .build()
776            .unwrap();
777        assert_eq!(dropdown.cell_type(), MatrixCellType::Dropdown);
778    }
779
780    #[test]
781    fn test_matrix_column_weights() {
782        let matrix = Matrix::builder("rating")
783            .row("item", "Item")
784            .column_with_weight("1", "Poor", 1)
785            .column_with_weight("2", "Fair", 2)
786            .column_with_weight("3", "Good", 3)
787            .column_with_weight("4", "Excellent", 4)
788            .build()
789            .unwrap();
790
791        let col = matrix.get_column("3").unwrap();
792        assert_eq!(col.weight, Some(3));
793    }
794
795    #[test]
796    fn test_matrix_options() {
797        let matrix = Matrix::builder("m")
798            .row("r", "R")
799            .column("c", "C")
800            .all_rows_required(true)
801            .show_row_numbers(true)
802            .alternate_rows(false)
803            .required()
804            .build()
805            .unwrap();
806
807        assert!(matrix.all_rows_required());
808        assert!(matrix.show_row_numbers());
809        assert!(!matrix.alternate_rows());
810        assert!(matrix.flags().contains(Flags::REQUIRED));
811    }
812
813    #[test]
814    fn test_matrix_row_with_description() {
815        let matrix = Matrix::builder("m")
816            .row_with_description("price", "Price", "How satisfied are you with the price?")
817            .column("1", "Bad")
818            .build()
819            .unwrap();
820
821        let row = matrix.get_row("price").unwrap();
822        assert_eq!(
823            row.description.as_deref(),
824            Some("How satisfied are you with the price?")
825        );
826    }
827
828    #[test]
829    fn test_matrix_requires_rows() {
830        let result = Matrix::builder("m").column("c", "C").build();
831        assert!(result.is_err());
832    }
833
834    #[test]
835    fn test_matrix_requires_columns() {
836        let result = Matrix::builder("m").row("r", "R").build();
837        assert!(result.is_err());
838    }
839
840    #[test]
841    fn test_matrix_duplicate_row_keys() {
842        let result = Matrix::builder("m")
843            .row("same", "First")
844            .row("same", "Second")
845            .column("c", "C")
846            .build();
847        assert!(result.is_err());
848    }
849
850    #[test]
851    fn test_matrix_duplicate_column_values() {
852        let result = Matrix::builder("m")
853            .row("r", "R")
854            .column("same", "First")
855            .column("same", "Second")
856            .build();
857        assert!(result.is_err());
858    }
859
860    #[test]
861    fn test_matrix_iterators() {
862        let matrix = Matrix::builder("m")
863            .rows([("a", "A"), ("b", "B")])
864            .columns([("1", "One"), ("2", "Two")])
865            .build()
866            .unwrap();
867
868        let row_keys: Vec<&str> = matrix.row_keys().map(|k| k.as_str()).collect();
869        assert_eq!(row_keys, vec!["a", "b"]);
870
871        let col_values: Vec<&str> = matrix.column_values().map(|v| v.as_str()).collect();
872        assert_eq!(col_values, vec!["1", "2"]);
873    }
874
875    #[test]
876    fn test_matrix_children_empty() {
877        let matrix = Matrix::builder("m")
878            .row("r", "R")
879            .column("c", "C")
880            .build()
881            .unwrap();
882
883        // Matrix has no child nodes - rows/columns are metadata
884        assert!(matrix.children().is_empty());
885    }
886
887    #[test]
888    fn test_cell_type_names() {
889        assert_eq!(MatrixCellType::Radio.name(), "radio");
890        assert_eq!(MatrixCellType::Checkbox.name(), "checkbox");
891        assert_eq!(MatrixCellType::Dropdown.name(), "dropdown");
892        assert_eq!(MatrixCellType::Text.name(), "text");
893        assert_eq!(MatrixCellType::Rating.name(), "rating");
894    }
895
896    #[test]
897    fn test_matrix_exclusive_columns() {
898        let matrix = Matrix::builder("satisfaction")
899            .row("price", "Price")
900            .row("quality", "Quality")
901            .column("1", "Poor")
902            .column("2", "Fair")
903            .column("3", "Good")
904            .exclusive_column("na", "Not Applicable")
905            .build()
906            .unwrap();
907
908        assert!(matrix.has_exclusive_columns());
909        assert_eq!(matrix.exclusive_columns().count(), 1);
910
911        let na_col = matrix.get_column("na").unwrap();
912        assert!(na_col.is_exclusive());
913
914        let good_col = matrix.get_column("3").unwrap();
915        assert!(!good_col.is_exclusive());
916    }
917
918    #[test]
919    fn test_matrix_column_exclusive_constructors() {
920        let col = MatrixColumn::exclusive("na", "N/A");
921        assert!(col.is_exclusive());
922        assert!(col.weight.is_none());
923
924        let col_weighted = MatrixColumn::exclusive_with_weight("na", "N/A", 0);
925        assert!(col_weighted.is_exclusive());
926        assert_eq!(col_weighted.weight, Some(0));
927    }
928}