1use 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#[derive(Debug, Clone)]
20pub struct MatrixRow {
21 pub key: Key,
23 pub label: SmartStr,
25 pub description: Option<SmartStr>,
27}
28
29impl MatrixRow {
30 #[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 #[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#[derive(Debug, Clone)]
59pub struct MatrixColumn {
60 pub value: SmartStr,
62 pub label: SmartStr,
64 pub weight: Option<i32>,
66 pub exclusive: bool,
70}
71
72impl MatrixColumn {
73 #[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 #[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 #[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 #[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 #[must_use]
129 pub fn is_exclusive(&self) -> bool {
130 self.exclusive
131 }
132
133 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
156pub enum MatrixCellType {
157 #[default]
159 Radio,
160 Checkbox,
162 Dropdown,
164 Text,
166 Rating,
168}
169
170impl MatrixCellType {
171 #[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 #[must_use]
185 pub fn is_multi_select(&self) -> bool {
186 matches!(self, Self::Checkbox)
187 }
188}
189
190#[derive(Clone)]
240pub struct Matrix {
241 metadata: Metadata,
242 flags: Flags,
243 rows: Vec<MatrixRow>,
244 columns: Vec<MatrixColumn>,
245 cell_type: MatrixCellType,
246 all_rows_required: bool,
248 show_row_numbers: bool,
250 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 #[must_use]
269 pub fn builder(key: impl Into<Key>) -> MatrixBuilder {
270 MatrixBuilder::new(key)
271 }
272
273 #[must_use]
275 pub fn flags(&self) -> Flags {
276 self.flags
277 }
278
279 #[must_use]
281 pub fn rows(&self) -> &[MatrixRow] {
282 &self.rows
283 }
284
285 #[must_use]
287 pub fn row_count(&self) -> usize {
288 self.rows.len()
289 }
290
291 #[must_use]
293 pub fn columns(&self) -> &[MatrixColumn] {
294 &self.columns
295 }
296
297 #[must_use]
299 pub fn column_count(&self) -> usize {
300 self.columns.len()
301 }
302
303 #[must_use]
305 pub fn cell_type(&self) -> MatrixCellType {
306 self.cell_type
307 }
308
309 #[must_use]
311 pub fn all_rows_required(&self) -> bool {
312 self.all_rows_required
313 }
314
315 #[must_use]
317 pub fn show_row_numbers(&self) -> bool {
318 self.show_row_numbers
319 }
320
321 #[must_use]
323 pub fn alternate_rows(&self) -> bool {
324 self.alternate_rows
325 }
326
327 #[must_use]
329 pub fn get_row(&self, key: &str) -> Option<&MatrixRow> {
330 self.rows.iter().find(|r| r.key == key)
331 }
332
333 #[must_use]
335 pub fn get_column(&self, value: &str) -> Option<&MatrixColumn> {
336 self.columns.iter().find(|c| c.value == value)
337 }
338
339 pub fn exclusive_columns(&self) -> impl Iterator<Item = &MatrixColumn> {
341 self.columns.iter().filter(|c| c.exclusive)
342 }
343
344 #[must_use]
346 pub fn has_exclusive_columns(&self) -> bool {
347 self.columns.iter().any(|c| c.exclusive)
348 }
349
350 pub fn row_keys(&self) -> impl Iterator<Item = &Key> {
352 self.rows.iter().map(|r| &r.key)
353 }
354
355 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 &[]
388 }
389}
390
391#[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 #[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 #[must_use]
430 pub fn label(mut self, label: impl Into<SmartStr>) -> Self {
431 self.label = Some(label.into());
432 self
433 }
434
435 #[must_use]
437 pub fn description(mut self, description: impl Into<SmartStr>) -> Self {
438 self.description = Some(description.into());
439 self
440 }
441
442 #[must_use]
444 pub fn flags(mut self, flags: Flags) -> Self {
445 self.flags = flags;
446 self
447 }
448
449 #[must_use]
451 pub fn required(mut self) -> Self {
452 self.flags |= Flags::REQUIRED;
453 self
454 }
455
456 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
576 pub fn cell_type(mut self, cell_type: MatrixCellType) -> Self {
577 self.cell_type = cell_type;
578 self
579 }
580
581 #[must_use]
583 pub fn radio(mut self) -> Self {
584 self.cell_type = MatrixCellType::Radio;
585 self
586 }
587
588 #[must_use]
590 pub fn checkbox(mut self) -> Self {
591 self.cell_type = MatrixCellType::Checkbox;
592 self
593 }
594
595 #[must_use]
597 pub fn dropdown(mut self) -> Self {
598 self.cell_type = MatrixCellType::Dropdown;
599 self
600 }
601
602 #[must_use]
604 pub fn all_rows_required(mut self, required: bool) -> Self {
605 self.all_rows_required = required;
606 self
607 }
608
609 #[must_use]
611 pub fn show_row_numbers(mut self, show: bool) -> Self {
612 self.show_row_numbers = show;
613 self
614 }
615
616 #[must_use]
618 pub fn alternate_rows(mut self, alternate: bool) -> Self {
619 self.alternate_rows = alternate;
620 self
621 }
622
623 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 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 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#[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 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 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}