spikes 0.2.0

Drop-in feedback collection for static HTML mockups
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Reviewer {
    pub id: String,
    pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoundingBox {
    pub x: f64,
    pub y: f64,
    pub width: f64,
    pub height: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Viewport {
    pub width: u32,
    pub height: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum SpikeType {
    Page,
    Element,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Rating {
    Love,
    Like,
    Meh,
    No,
}

impl std::fmt::Display for Rating {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Rating::Love => write!(f, "love"),
            Rating::Like => write!(f, "like"),
            Rating::Meh => write!(f, "meh"),
            Rating::No => write!(f, "no"),
        }
    }
}

impl std::str::FromStr for Rating {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "love" => Ok(Rating::Love),
            "like" => Ok(Rating::Like),
            "meh" => Ok(Rating::Meh),
            "no" => Ok(Rating::No),
            _ => Err(format!("Invalid rating: {}", s)),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Spike {
    pub id: String,
    #[serde(rename = "type")]
    pub spike_type: SpikeType,
    pub project_key: String,
    pub page: String,
    pub url: String,
    pub reviewer: Reviewer,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub selector: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub element_text: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bounding_box: Option<BoundingBox>,
    pub rating: Option<Rating>,
    pub comments: String,
    pub timestamp: String,
    pub viewport: Viewport,
}

impl Spike {
    pub fn rating_str(&self) -> &str {
        match &self.rating {
            Some(r) => match r {
                Rating::Love => "love",
                Rating::Like => "like",
                Rating::Meh => "meh",
                Rating::No => "no",
            },
            None => "-",
        }
    }

    pub fn type_str(&self) -> &str {
        match self.spike_type {
            SpikeType::Page => "page",
            SpikeType::Element => "element",
        }
    }
}