//! Chunk labels.
//!
//! # Example
//!
//! ```rust
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use obnam::label::*;
//!
//! let label = Label::try_from("client-chunk")?;
//! assert_eq!(label.to_string(), "client-chunk");
//! # Ok(())
//! # }
//! ```

use std::{fmt, str::FromStr};

use serde::{Deserialize, Serialize};

// Characters allows in chunk labels.
const ALLOWED: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.";

/// A label for a chunk.
///
/// Labels are short URL-safe strings chosen by the backup client. They
/// may only contain letters (a-z) in upper or lower case, digits, dot,
/// and dash. This means they do not need to be encoded for URLs.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Label {
    label: String,
}

impl Label {
    /// Label as a string slice.
    pub fn as_str(&self) -> &str {
        &self.label
    }
}

impl TryFrom<&str> for Label {
    type Error = LabelError;
    fn try_from(value: &str) -> Result<Self, Self::Error> {
        let mut label = String::new();
        for c in value.chars() {
            if ALLOWED.contains(c) {
                label.push(c);
            } else {
                return Err(LabelError::NotAllowed(c));
            }
        }
        Ok(Self { label })
    }
}

impl TryFrom<&String> for Label {
    type Error = LabelError;
    fn try_from(value: &String) -> Result<Self, Self::Error> {
        Self::try_from(value.as_str())
    }
}

impl TryFrom<String> for Label {
    type Error = LabelError;
    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::try_from(value.as_str())
    }
}

impl FromStr for Label {
    type Err = LabelError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match Label::try_from(s) {
            Ok(label) => Ok(label),
            Err(err) => Err(err),
        }
    }
}

impl fmt::Display for Label {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.label)
    }
}

/// Errors from chunk identifiers.
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum LabelError {
    /// Can't create label with character that isn't allowed.
    #[error("chunk label contains character that is not allowed: {0}")]
    NotAllowed(char),
}

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

    type AnyError<T> = Result<T, Box<dyn std::error::Error>>;

    #[test]
    fn label_is_equal_to_itself() -> AnyError<()> {
        let a = Label::try_from("abcdef")?;
        let b = Label::try_from("abcdef")?;
        assert_eq!(a, b);
        Ok(())
    }

    #[test]
    fn safe_label() -> AnyError<()> {
        let label = Label::try_from(ALLOWED)?;
        assert_eq!(label.to_string(), ALLOWED);
        Ok(())
    }

    #[test]
    fn slash() {
        assert_eq!(Label::try_from("/"), Err(LabelError::NotAllowed('/')));
    }

    #[test]
    fn colon() {
        assert_eq!(Label::try_from(":"), Err(LabelError::NotAllowed(':')));
    }

    #[test]
    fn question_mark() {
        assert_eq!(Label::try_from("?"), Err(LabelError::NotAllowed('?')));
    }

    #[test]
    fn equals() {
        assert_eq!(Label::try_from("="), Err(LabelError::NotAllowed('=')));
    }
}
