Skip to main content

rd_helpdb/
demo.rs

1//! Typed access to an installed package's `Meta/demo.rds` index.
2
3use rd_rds::{RObject, matrix::CharacterMatrix};
4
5use crate::Error;
6
7/// One row of `Meta/demo.rds`.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct DemoEntry {
10    pub name: String,
11    pub title: String,
12}
13
14/// A validated, owned `Meta/demo.rds` matrix.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct DemoIndex {
17    entries: Vec<DemoEntry>,
18}
19
20impl DemoIndex {
21    /// Validates and copies a decoded `Meta/demo.rds` object.
22    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    /// Iterates over entries in their on-disk row order.
42    pub fn entries(&self) -> impl ExactSizeIterator<Item = &DemoEntry> {
43        self.entries.iter()
44    }
45
46    /// Returns the number of demo entries.
47    pub fn len(&self) -> usize {
48        self.entries.len()
49    }
50
51    /// Returns whether the index contains no demo entries.
52    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}