hmmcli 0.6.0

A small command-line note-taking app
Documentation
use super::{
    error::{self, Error},
    Result,
};
use chrono::prelude::*;
use csv::StringRecord;
use std::convert::{TryFrom, TryInto};
use std::io::Write;

pub struct Entry {
    datetime: DateTime<FixedOffset>,
    message: String,
}

impl Entry {
    pub fn new(datetime: DateTime<FixedOffset>, message: String) -> Self {
        Entry { datetime, message }
    }

    pub fn with_message(message: &str) -> Self {
        Self::new(Utc::now().into(), message.trim().to_owned())
    }

    pub fn datetime(&self) -> &DateTime<FixedOffset> {
        &self.datetime
    }

    pub fn message(&self) -> &str {
        &self.message
    }

    pub fn contains(&self, s: &str) -> bool {
        self.message.contains(s)
    }

    pub fn write(&self, mut w: impl Write) -> Result<()> {
        Ok(w.write_all(self.to_csv_row()?.as_bytes())?)
    }

    pub fn to_csv_row(&self) -> Result<String> {
        let mut buf = Vec::new();
        {
            let mut writer = csv::Writer::from_writer(&mut buf);
            writer.write_record(&[
                self.datetime.to_rfc3339(),
                serde_json::to_string(&self.message)?,
            ])?;
        }
        Ok(String::from_utf8(buf)?)
    }
}

impl TryFrom<quick_csv::Row> for Entry {
    type Error = Error;

    fn try_from(r: quick_csv::Row) -> Result<Self> {
        let mut cols = r.columns()?;

        let date = cols
            .next()
            .ok_or_else(|| error::from_str("malformed CSV"))?;
        let msg = cols
            .next()
            .ok_or_else(|| error::from_str("malformed CSV"))?;

        Ok(Entry {
            datetime: chrono::DateTime::parse_from_rfc3339(date)?,
            message: serde_json::from_str(msg)?,
        })
    }
}

impl TryFrom<&StringRecord> for Entry {
    type Error = Error;

    fn try_from(sr: &StringRecord) -> Result<Self> {
        let date = sr.get(0).ok_or_else(|| error::from_str("malformed CSV"))?;
        let msg = sr.get(1).ok_or_else(|| error::from_str("malformed CSV"))?;

        Ok(Entry {
            datetime: chrono::DateTime::parse_from_rfc3339(date)?,
            message: serde_json::from_str(msg)?,
        })
    }
}

impl TryFrom<&str> for Entry {
    type Error = Error;

    fn try_from(s: &str) -> Result<Self> {
        let mut csv = quick_csv::Csv::from_string(s);
        let next = csv
            .next()
            .ok_or_else(|| error::from_str("malformed CSV"))??;
        next.try_into()
    }
}

impl TryFrom<String> for Entry {
    type Error = Error;

    fn try_from(s: String) -> Result<Self> {
        s.as_str().try_into()
    }
}

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

    #[test_case("2012-01-01T00:00:00+00:00,\"\"\"hello world\"\"\""   => ("2012-01-01T00:00:00+00:00".to_owned(), "hello world".to_owned()) ; "basic entry")]
    #[test_case("2012-01-01T00:00:00+00:00,\"\"\"hello\\nworld\"\"\"" => ("2012-01-01T00:00:00+00:00".to_owned(), "hello\nworld".to_owned()) ; "entry with newline")]
    #[test_case("2012-01-01T01:00:00+01:00,\"\"\"hello world\"\"\""   => ("2012-01-01T01:00:00+01:00".to_owned(), "hello world".to_owned()) ; "entry with non-UTC timezone")]
    #[test_case("2012-01-01T00:00:00+00:00,\"\"\"\"\"\""              => ("2012-01-01T00:00:00+00:00".to_owned(), "".to_owned()) ; "empty entry")]
    fn test_from_str(s: &str) -> (String, String) {
        let entry: Entry = s.try_into().unwrap();
        (entry.datetime().to_rfc3339(), entry.message().to_owned())
    }

    #[test_case("not a csv" => "malformed CSV" ; "not a csv")]
    #[test_case("." => "malformed CSV" ; "single dot")]
    #[test_case("" => "malformed CSV" ; "empty string")]
    fn test_invalid_lines(s: &str) -> String {
        let result: Result<Entry> = s.try_into();
        assert!(result.is_err());
        result.err().unwrap().to_string()
    }
}