1use rd_rds::{RObject, matrix::CharacterMatrix};
4
5use crate::Error;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct DemoEntry {
10 pub name: String,
11 pub title: String,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct DemoIndex {
17 entries: Vec<DemoEntry>,
18}
19
20impl DemoIndex {
21 pub fn from_object(root: &RObject) -> Result<Self, Error> {
23 let matrix = CharacterMatrix::try_from(root)
24 .map_err(|error| malformed(format!("invalid character matrix: {error}")))?;
25 if matrix.ncol() != 2 {
26 return Err(malformed(format!(
27 "expected exactly 2 columns, got {}",
28 matrix.ncol()
29 )));
30 }
31
32 let mut entries = Vec::with_capacity(matrix.nrow());
33 for row in 0..matrix.nrow() {
34 let name = required_cell(&matrix, row, 0, "Name")?;
35 let title = required_cell(&matrix, row, 1, "Title")?;
36 entries.push(DemoEntry { name, title });
37 }
38 Ok(Self { entries })
39 }
40
41 pub fn entries(&self) -> impl ExactSizeIterator<Item = &DemoEntry> {
43 self.entries.iter()
44 }
45
46 pub fn len(&self) -> usize {
48 self.entries.len()
49 }
50
51 pub fn is_empty(&self) -> bool {
53 self.entries.is_empty()
54 }
55}
56
57impl TryFrom<&RObject> for DemoIndex {
58 type Error = Error;
59
60 fn try_from(value: &RObject) -> Result<Self, Self::Error> {
61 Self::from_object(value)
62 }
63}
64
65fn required_cell(
66 matrix: &CharacterMatrix,
67 row: usize,
68 column: usize,
69 label: &str,
70) -> Result<String, Error> {
71 match matrix.get(row, column) {
72 Some(Some(value)) => Ok(value.to_owned()),
73 Some(None) => Err(malformed(format!(
74 "unexpected NA at row {row}, column {column} ({label})"
75 ))),
76 None => Err(malformed(format!(
77 "missing cell at row {row}, column {column} ({label})"
78 ))),
79 }
80}
81
82fn malformed(message: impl Into<String>) -> Error {
83 Error::MalformedIndex(format!("invalid Meta/demo.rds: {}", message.into()))
84}
85
86#[cfg(test)]
87mod tests {
88 use std::path::PathBuf;
89
90 use crate::read_rds_file;
91
92 use super::*;
93
94 fn fixture(name: &str) -> RObject {
95 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
96 .join("tests/fixtures/data")
97 .join(name);
98 read_rds_file(&path).unwrap_or_else(|error| panic!("read {}: {error}", path.display()))
99 }
100
101 #[test]
102 fn parses_two_columns_by_position_without_dimnames() {
103 let index =
104 DemoIndex::from_object(&fixture("demo_valid_v3.rds")).expect("valid demo fixture");
105 assert_eq!(
106 index.entries().collect::<Vec<_>>(),
107 vec![
108 &DemoEntry {
109 name: "first".into(),
110 title: "First demo".into(),
111 },
112 &DemoEntry {
113 name: "second".into(),
114 title: "".into(),
115 },
116 ]
117 );
118 }
119
120 #[test]
121 fn accepts_zero_row_matrix() {
122 let index =
123 DemoIndex::from_object(&fixture("demo_empty_v3.rds")).expect("empty demo fixture");
124 assert_eq!(index.len(), 0);
125 assert!(index.is_empty());
126 assert_eq!(index.entries().len(), 0);
127 }
128
129 #[test]
130 fn rejects_wrong_column_count() {
131 let error = DemoIndex::from_object(&fixture("demo_three_columns_v3.rds"))
132 .expect_err("three columns must fail");
133 assert!(error.to_string().contains("expected exactly 2 columns"));
134 }
135}