rs-bag 0.1.2

Read, write and modify ROS 2 rosbag2 sqlite3 (.db3) storage files
use std::path::PathBuf;

pub type Result<T, E = Error> = std::result::Result<T, E>;

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    #[error("database error: {0}")]
    Database(#[from] turso::Error),

    #[error("io error: {0}")]
    Io(#[from] std::io::Error),

    #[error("yaml error: {0}")]
    Yaml(#[from] serde_yaml::Error),

    #[error("CDR (de)serialization error: {0}")]
    Cdr(#[from] cdr::Error),

    #[error("bag directory {} does not exist", .0.display())]
    BagDirNotFound(PathBuf),

    #[error("bag directory {} contains no .db3 file", .0.display())]
    NoDb3File(PathBuf),

    #[error("db3 file {} does not exist", .0.display())]
    Db3FileNotFound(PathBuf),

    #[error("db3 file {} already exists", .0.display())]
    Db3FileAlreadyExists(PathBuf),

    #[error("bag directory {} already contains a metadata.yaml", .0.display())]
    BagHasMetadata(PathBuf),

    #[error("bag directory {} already contains .db3 file(s)", .0.display())]
    BagHasDb3Files(PathBuf),

    #[error("path {0:?} is not valid UTF-8")]
    NonUtf8Path(PathBuf),

    #[error("bag directory {0:?} has no usable directory name")]
    NoDirectoryName(PathBuf),

    #[error("unknown topic name: {0}")]
    TopicNotFound(String),

    #[error("unknown topic id: {0}")]
    TopicIdNotFound(i64),

    #[error("topic '{0}' already exists")]
    TopicAlreadyExists(String),

    #[error("inconsistent topic ids {ids:?} for '{topic}' across db3 files")]
    TopicIdMismatch { topic: String, ids: Vec<i64> },

    #[error("unsupported serialization format: '{0}' (expected 'cdr')")]
    UnsupportedSerializationFormat(String),

    #[error("expected {expected} for {field}")]
    RowConversion {
        field: &'static str,
        expected: &'static str,
    },
}

impl Error {
    pub(crate) fn row_conversion(field: &'static str, expected: &'static str) -> Self {
        Self::RowConversion { field, expected }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn from_conversions() {
        assert!(matches!(
            Error::from(turso::Error::QueryReturnedNoRows),
            Error::Database(_)
        ));
        assert!(matches!(
            Error::from(std::io::Error::new(std::io::ErrorKind::NotFound, "gone")),
            Error::Io(_)
        ));
    }

    #[test]
    fn display_messages() {
        assert_eq!(
            Error::RowConversion {
                field: "id",
                expected: "an integer"
            }
            .to_string(),
            "expected an integer for id"
        );
        assert_eq!(
            Error::NoDb3File(PathBuf::from("/tmp/bag")).to_string(),
            "bag directory /tmp/bag contains no .db3 file"
        );
        assert_eq!(
            Error::UnsupportedSerializationFormat("json".to_owned()).to_string(),
            "unsupported serialization format: 'json' (expected 'cdr')"
        );
        assert_eq!(
            Error::TopicAlreadyExists("/foo".to_owned()).to_string(),
            "topic '/foo' already exists"
        );
        assert_eq!(
            Error::TopicIdMismatch {
                topic: "/foo".to_owned(),
                ids: vec![2, 1],
            }
            .to_string(),
            "inconsistent topic ids [2, 1] for '/foo' across db3 files"
        );
    }
}