invoist 1.0.1

Official Rust client for the Invoist API: extract invoices, read a household's bills, verify webhooks.
Documentation
use std::fmt;
use std::str::FromStr;

use crate::Error;

/// A calendar month, sent to the API as `YYYY-MM`.
///
/// ```
/// use invoist::Month;
///
/// let october = Month::new(2026, 10).unwrap();
/// assert_eq!(october.to_string(), "2026-10");
/// assert_eq!("2026-10".parse::<Month>().unwrap(), october);
/// assert!(Month::new(2026, 13).is_err());
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Month {
    year: u16,
    month: u8,
}

impl Month {
    /// The month `month` (1 to 12) of `year` (0 to 9999).
    pub fn new(year: u16, month: u8) -> Result<Self, Error> {
        if year > 9999 || !(1..=12).contains(&month) {
            return Err(Error::invalid_argument(format!(
                "{year}-{month} is not a month: the year must be 0 to 9999 and the month 1 to 12"
            )));
        }
        Ok(Month { year, month })
    }

    /// The year.
    pub fn year(self) -> u16 {
        self.year
    }

    /// The month, 1 to 12.
    pub fn month(self) -> u8 {
        self.month
    }
}

impl fmt::Display for Month {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:04}-{:02}", self.year, self.month)
    }
}

impl FromStr for Month {
    type Err = Error;

    /// Parses `YYYY-MM`.
    fn from_str(s: &str) -> Result<Self, Error> {
        let invalid = || Error::invalid_argument(format!("{s:?} is not a month: use YYYY-MM"));
        let (year, month) = s.split_once('-').ok_or_else(invalid)?;
        let digits = |p: &str, n: usize| p.len() == n && p.bytes().all(|b| b.is_ascii_digit());
        if !digits(year, 4) || !digits(month, 2) {
            return Err(invalid());
        }
        let year = year.parse().map_err(|_| invalid())?;
        let month = month.parse().map_err(|_| invalid())?;
        Month::new(year, month)
    }
}

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

    fn try_from(s: &str) -> Result<Self, Error> {
        s.parse()
    }
}