napchart 0.3.1

Rust interface for the https://napchart.com API alpha
Documentation
/*
 * --------------------
 * THIS FILE IS LICENSED UNDER MIT
 * THE FOLLOWING MESSAGE IS NOT A LICENSE
 *
 * <barrow@tilde.team> wrote this file.
 * by reading this text, you are reading "TRANS RIGHTS".
 * this file and the content within it is the gay agenda.
 * if we meet some day, and you think this stuff is worth it,
 * you can buy me a beer, tea, or something stronger.
 * -Ezra Barrow
 * --------------------
 */
#![warn(missing_docs)]
// #![feature(external_doc)]
// #![doc(include = "../README.md")]
//! # napchart-rs
//!
//! [![GitHub last commit](https://img.shields.io/github/last-commit/barrowsys/napchart-rs)](https://github.com/barrowsys/napchart-rs)
//! [![Crates.io](https://img.shields.io/crates/v/napchart)](https://crates.io/crates/napchart/)
//! [![Docs.rs](https://docs.rs/napchart/badge.svg)](https://docs.rs/napchart)
//!
//! A strongly-typed rust interface to the <https://napchart.com> API.
//!
//! The public napchart api is pretty barebones right now, but this will let you use it!
//!
//! ## Usage
//!
//! Add to your Cargo.toml:
//! ```text
//! [dependencies]
//! napchart = "0.3"
//! ```
//!
//! ## Examples
//!
//! ### Creating a new napchart from scratch
//! Example: <https://napchart.com/snapshot/O6kunUfuL>
//! ```
//! use napchart::prelude::*;
//!
//! let mut chart = Napchart::default()
//!     .shape(ChartShape::Circle)
//!     .lanes(1);
//! let first_lane = chart.get_lane_mut(0).unwrap();
//! first_lane.add_element(0, 60).unwrap()
//!     .text("Hour One");
//! first_lane.add_element(180, 240).unwrap()
//!     .text("Hour Four");
//! let second_lane = chart.add_lane();
//! second_lane.add_element(0, 120).unwrap()
//!     .color(ChartColor::Blue);
//! second_lane.add_element(120, 240).unwrap()
//!     .color(ChartColor::Green)
//!     .text("Cool green time");
//! ```
//!
//! ### Downloading a napchart
//! Example Chart: <https://napchart.com/3tbkt>
//! ```
//! use napchart::api::BlockingClient;
//!
//! let client = BlockingClient::default();
//! let rchart = client.get_chart("3tbkt").unwrap();
//! assert_eq!(rchart.chartid, String::from("3tbkt"));
//! assert_eq!(rchart.title, Some(String::from("State test chart")));
//! assert_eq!(rchart.chart.shape, napchart::ChartShape::Circle);
//! assert_eq!(rchart.chart.lanes_len(), 1);
//! ```
//!
//! ### Uploading a napchart as a snapshot
//! Example Output: <https://napchart.com/snapshot/TpCfggr4i>
//! ```no_run
//! use napchart::prelude::*;
//! use napchart::api::BlockingClient;
//!
//! let client = BlockingClient::default();
//! let mut chart = Napchart::default();
//! let lane = chart.add_lane();
//! lane.add_element(420, 1260)
//!     .unwrap()
//!     .text("Nighttime")
//!     .color(ChartColor::Gray);
//! let upload_builder = chart.upload()
//!     .title("readme doctest")
//!     .description("https://crates.io/crates/napchart");
//! let remote_chart = client.create_snapshot(upload_builder).unwrap();
//! assert!(!remote_chart.chartid.is_empty());
//! ```

use chrono::prelude::*;
use colorsys::Rgb;
use noneifempty::NoneIfEmpty;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::iter::repeat;
use std::str::FromStr;
use std::string::ToString;

pub mod api;

mod raw;

mod error;
pub use error::ErrorKind;
use error::Result;

/// Contains aliases to the useful imports.
/// ```
/// use napchart::prelude::*;
/// let mut chart: Napchart = Napchart::default()
///     .shape(ChartShape::Wide);
/// let lane: &mut ChartLane = chart.add_lane();
/// let elem: &mut ChartElement = lane.add_element(0, 60).unwrap()
///     .color(ChartColor::Green);
/// ```
pub mod prelude {
    pub use super::ChartColor;
    pub use super::ChartElement;
    pub use super::ChartLane;
    pub use super::ChartShape;
    pub use super::ElementData;
    pub use super::Napchart;
    pub use super::RemoteNapchart;
}

#[allow(missing_docs)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
/// The shape of a napchart
pub enum ChartShape {
    Circle,
    Wide,
    Line,
}
impl Default for ChartShape {
    fn default() -> Self {
        Self::Circle
    }
}
impl FromStr for ChartShape {
    type Err = ErrorKind;
    fn from_str(s: &str) -> Result<Self> {
        Ok(match s {
            "circle" => Self::Circle,
            "wide" => Self::Wide,
            "line" => Self::Line,
            _ => return Err(ErrorKind::InvalidChartShape(s.to_string())),
        })
    }
}
impl ToString for ChartShape {
    fn to_string(&self) -> String {
        match self {
            ChartShape::Circle => String::from("circle"),
            ChartShape::Wide => String::from("wide"),
            ChartShape::Line => String::from("line"),
        }
    }
}

// #[allow(missing_docs)]
// #[derive(Clone, Debug, PartialEq, Eq, Hash)]
// /// The colors available for chart elements
// pub enum BuiltinColors {
//     Red,
//     Blue,
//     Brown,
//     Green,
//     Gray,
//     Yellow,
//     Purple,
//     Pink,
// }

#[allow(missing_docs)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
/// The colors available for chart elements
pub enum ChartColor {
    Red,
    Blue,
    Brown,
    Green,
    Gray,
    Yellow,
    Purple,
    Pink,
    #[serde(rename = "custom_0")]
    Custom0,
    #[serde(rename = "custom_1")]
    Custom1,
    #[serde(rename = "custom_2")]
    Custom2,
    #[serde(rename = "custom_3")]
    Custom3,
}
impl ChartColor {
    /// True if a color is custom, false if it's one of the builtin colors
    /// ```
    /// # use napchart::*;
    /// assert!(!ChartColor::Blue.is_custom());
    /// assert!(ChartColor::Custom2.is_custom());
    /// ```
    pub fn is_custom(&self) -> bool {
        match self {
            ChartColor::Red
            | ChartColor::Blue
            | ChartColor::Brown
            | ChartColor::Green
            | ChartColor::Gray
            | ChartColor::Yellow
            | ChartColor::Purple
            | ChartColor::Pink => false,
            ChartColor::Custom0
            | ChartColor::Custom1
            | ChartColor::Custom2
            | ChartColor::Custom3 => true,
        }
    }
    /// True if a color is builtin, false if it's one of the custom colors
    /// ```
    /// # use napchart::*;
    /// assert!(ChartColor::Blue.is_builtin());
    /// assert!(!ChartColor::Custom2.is_builtin());
    /// ```
    pub fn is_builtin(&self) -> bool {
        match self {
            ChartColor::Red
            | ChartColor::Blue
            | ChartColor::Brown
            | ChartColor::Green
            | ChartColor::Gray
            | ChartColor::Yellow
            | ChartColor::Purple
            | ChartColor::Pink => true,
            ChartColor::Custom0
            | ChartColor::Custom1
            | ChartColor::Custom2
            | ChartColor::Custom3 => false,
        }
    }
    fn custom_index(&self) -> Option<usize> {
        match self {
            ChartColor::Custom0 => Some(0),
            ChartColor::Custom1 => Some(1),
            ChartColor::Custom2 => Some(2),
            ChartColor::Custom3 => Some(3),
            _ => None,
        }
    }
    fn from_index(i: usize) -> Self {
        assert!(i <= 3);
        match i {
            0 => ChartColor::Custom0,
            1 => ChartColor::Custom1,
            2 => ChartColor::Custom2,
            3 => ChartColor::Custom3,
            _ => unreachable!(),
        }
    }
}
impl Default for ChartColor {
    fn default() -> Self {
        Self::Red
    }
}
impl ToString for ChartColor {
    fn to_string(&self) -> String {
        match self {
            ChartColor::Red => String::from("red"),
            ChartColor::Blue => String::from("blue"),
            ChartColor::Brown => String::from("brown"),
            ChartColor::Green => String::from("green"),
            ChartColor::Gray => String::from("gray"),
            ChartColor::Yellow => String::from("yellow"),
            ChartColor::Purple => String::from("purple"),
            ChartColor::Pink => String::from("pink"),
            ChartColor::Custom0 => String::from("custom_0"),
            ChartColor::Custom1 => String::from("custom_1"),
            ChartColor::Custom2 => String::from("custom_2"),
            ChartColor::Custom3 => String::from("custom_3"),
        }
    }
}
impl FromStr for ChartColor {
    type Err = ErrorKind;
    fn from_str(s: &str) -> Result<Self> {
        Ok(match s {
            "red" => ChartColor::Red,
            "blue" => ChartColor::Blue,
            "brown" => ChartColor::Brown,
            "green" => ChartColor::Green,
            "gray" => ChartColor::Gray,
            "yellow" => ChartColor::Yellow,
            "purple" => ChartColor::Purple,
            "pink" => ChartColor::Pink,
            "custom_0" => ChartColor::Custom0,
            "custom_1" => ChartColor::Custom1,
            "custom_2" => ChartColor::Custom2,
            "custom_3" => ChartColor::Custom3,
            _ => return Err(ErrorKind::InvalidChartColor(s.to_string())),
        })
    }
}

#[derive(Clone, Debug, PartialEq)]
/// A napchart, as seen on <https://napchart.com/>
pub struct Napchart {
    /// The default shape of the napchart on napchart.com
    pub shape: ChartShape,
    lanes: Vec<ChartLane>,
    /// String tags associated with element colors.
    /// These are displayed in the inner area of a napchart,
    /// along with the accumulated amount of time each color takes up.
    color_tags: HashMap<ChartColor, String>,
    /// RGB values for the four custom colors.
    /// If a custom color is None, it is INVALID/Undefined Behavior to set a chart element to it.
    custom_colors: [Option<Rgb>; 4],
}
impl Default for Napchart {
    fn default() -> Self {
        Self {
            shape: Default::default(),
            lanes: Default::default(),
            color_tags: Default::default(),
            custom_colors: [None, None, None, None],
        }
    }
}
impl Napchart {
    /// Append a new blank lane to the chart and return a mutable reference to it.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// let mut lane = chart.add_lane();
    /// assert!(lane.is_empty());
    /// assert_eq!(chart.lanes_len(), 1);
    /// ```
    pub fn add_lane(&mut self) -> &mut ChartLane {
        self.lanes.push(ChartLane::default());
        self.lanes.last_mut().unwrap()
    }
    /// Get a reference to the given lane, or None if out of bounds.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// chart.add_lane();
    /// assert!(chart.get_lane(0).is_some());
    /// assert!(chart.get_lane(1).is_none());
    /// ```
    pub fn get_lane(&self, i: usize) -> Option<&ChartLane> {
        self.lanes.get(i)
    }
    /// Get a mutable reference to the given lane, or None if out of bounds.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// chart.add_lane();
    /// assert!(chart.get_lane_mut(0).is_some());
    /// assert!(chart.get_lane_mut(1).is_none());
    /// ```
    pub fn get_lane_mut(&mut self, i: usize) -> Option<&mut ChartLane> {
        self.lanes.get_mut(i)
    }
    /// Remove the given lane from the chart and return it, or None if out of bounds.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// chart.add_lane();
    /// let lane = chart.remove_lane(0);
    /// assert!(lane.is_some());
    /// assert_eq!(chart.lanes_len(), 0);
    /// ```
    pub fn remove_lane(&mut self, i: usize) -> Option<ChartLane> {
        if i < self.lanes.len() {
            Some(self.lanes.remove(i))
        } else {
            None
        }
    }
    /// Get the total number of lanes in the chart.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// assert_eq!(chart.lanes_len(), 0);
    /// chart.add_lane();
    /// assert_eq!(chart.lanes_len(), 1);
    /// chart.add_lane();
    /// assert_eq!(chart.lanes_len(), 2);
    /// chart.remove_lane(1);
    /// chart.remove_lane(0);
    /// assert_eq!(chart.lanes_len(), 0);
    /// ```
    pub fn lanes_len(&self) -> usize {
        self.lanes.len()
    }
    /// Create an UploadBuilder with a reference to this chart.
    /// ```
    /// # use napchart::*;
    /// # use napchart::api::mock::BlockingClient;
    /// let client = BlockingClient::default();
    /// let chart = Napchart::default();
    /// let upload: napchart::api::UploadBuilder = chart.upload().title("My Cool Chart");
    /// assert!(client.create_snapshot(upload).is_ok());
    /// ```
    pub fn upload(&self) -> api::UploadBuilder {
        api::UploadBuilder::new(self)
    }
}
/// Getters and setters for color tags and custom colors
impl Napchart {
    /// Get the text tag for a color.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// // Napcharts start out with no color tags
    /// assert!(chart.get_color_tag(ChartColor::Blue).is_none());
    ///
    /// chart.set_color_tag(ChartColor::Blue, "Core Sleep").unwrap();
    ///
    /// assert_eq!(chart.get_color_tag(ChartColor::Blue), Some("Core Sleep"));
    /// ```
    pub fn get_color_tag(&self, color: ChartColor) -> Option<&str> {
        self.color_tags.get(&color).map(|s| s.as_str())
    }
    /// Get an iterator over ChartColors and their tags.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    ///
    /// chart.set_color_tag(ChartColor::Blue, "Nap");
    /// chart.set_color_tag(ChartColor::Gray, "Core");
    /// let mut iter = chart.color_tags_iter();
    /// assert!(iter.next().is_some());
    /// assert!(iter.next().is_some());
    /// assert!(iter.next().is_none());
    /// ```
    pub fn color_tags_iter(&self) -> impl Iterator<Item = (&ChartColor, &String)> + '_ {
        self.color_tags.iter()
    }
    /// Set the text tag for a color, returning the value that was replaced.
    /// Returns ErrorKind::CustomColorUnset if you attempt to set the tag on an undefined custom
    /// color.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    ///
    /// let original: Option<String> = chart.set_color_tag(ChartColor::Blue, "Core Sleep").unwrap();
    /// assert!(original.is_none()); // Replaced nothing
    /// assert_eq!(chart.get_color_tag(ChartColor::Blue), Some("Core Sleep"));
    ///
    /// let second: Option<String> = chart.set_color_tag(ChartColor::Blue, "Nap").unwrap();
    /// assert_eq!(second, Some(String::from("Core Sleep")));
    /// assert_eq!(chart.get_color_tag(ChartColor::Blue), Some("Nap"));
    /// ```
    pub fn set_color_tag<T: ToString>(
        &mut self,
        color: ChartColor,
        tag: T,
    ) -> Result<Option<String>> {
        if let Some(index) = color.custom_index() {
            if self.custom_colors[index].is_none() {
                return Err(ErrorKind::CustomColorUnset(index));
            }
        }
        Ok(self.color_tags.insert(color, tag.to_string()))
    }
    /// Set the text tag for a color.
    /// This function does not check custom colors are valid!!
    /// It is invalid/undefined behavior to upload a napchart that uses a custom color without
    /// defining its colorvalue,, "uses" meaning "has a color tag" and/or "is set on an element".
    /// ```
    /// # use napchart::*;
    /// use colorsys::Rgb;
    /// let mut chart = Napchart::default();
    ///
    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from_hex_str("DEDBEF").unwrap());
    /// let res = chart.set_color_tag(ChartColor::Custom0, "Dead Beef");
    /// assert!(res.is_ok());
    ///
    /// chart.remove_custom_color(ChartColor::Custom0);
    /// let res = chart.set_color_tag(ChartColor::Custom0, "Dead Beef");
    /// assert!(res.is_err());
    /// assert!(matches!(res.unwrap_err(), napchart::ErrorKind::CustomColorUnset(0)));
    /// ```
    pub fn set_color_tag_unchecked<T: ToString>(
        &mut self,
        color: ChartColor,
        tag: T,
    ) -> Option<String> {
        self.color_tags.insert(color, tag.to_string())
    }
    /// Removing the text tag for a color, returning the previous value.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    ///
    /// let original: Option<String> = chart.set_color_tag(ChartColor::Blue, "Core Sleep").unwrap();
    /// assert!(original.is_none()); // Replaced nothing
    /// assert_eq!(chart.get_color_tag(ChartColor::Blue), Some("Core Sleep"));
    ///
    /// let second: Option<String> = chart.set_color_tag(ChartColor::Blue, "Nap").unwrap();
    /// assert_eq!(second, Some(String::from("Core Sleep")));
    /// assert_eq!(chart.get_color_tag(ChartColor::Blue), Some("Nap"));
    /// ```
    pub fn remove_color_tag(&mut self, color: ChartColor) -> Option<String> {
        self.color_tags.remove(&color)
    }
    /// Gets the rgb value of a custom color.
    /// May only be called with CustomX ChartColors (asserts ChartColor::is_custom).
    /// ```
    /// # use napchart::*;
    /// use colorsys::Rgb;
    /// let mut chart = Napchart::default();
    ///
    /// assert!(chart.get_custom_color(ChartColor::Custom0).is_none());
    ///
    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from_hex_str("DEDBEF").unwrap());
    /// assert_eq!(chart.get_custom_color(ChartColor::Custom0), Some(&Rgb::from((0xDE, 0xDB, 0xEF))));
    /// ```
    pub fn get_custom_color(&self, id: ChartColor) -> Option<&Rgb> {
        assert!(id.is_custom());
        let i = id.custom_index().unwrap();
        self.custom_colors[i].as_ref()
    }
    /// Get an iterator over custom color (as usize indexes) and their RGB values.
    /// ```
    /// # use napchart::*;
    /// use colorsys::Rgb;
    /// let mut chart = Napchart::default();
    ///
    /// chart.set_custom_color(ChartColor::Custom2, Rgb::from((0xDE, 0xDB, 0xEF)));
    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from((0xB0, 0x0B, 0xE5)));
    /// let mut iter = chart.custom_colors_iter_index();
    /// assert_eq!(iter.next(), Some((0, &Rgb::from((0xB0, 0x0B, 0xE5)))));
    /// assert_eq!(iter.next(), Some((2, &Rgb::from((0xDE, 0xDB, 0xEF)))));
    /// assert!(iter.next().is_none());
    /// ```
    pub fn custom_colors_iter_index(&self) -> impl Iterator<Item = (usize, &Rgb)> + '_ {
        self.custom_colors
            .iter()
            .enumerate()
            .filter_map(|(u, c)| c.as_ref().map(|c| (u, c)))
    }
    /// Get an iterator over custom color (as ChartColors) and their RGB values.
    /// ```
    /// # use napchart::*;
    /// use colorsys::Rgb;
    /// let mut chart = Napchart::default();
    ///
    /// chart.set_custom_color(ChartColor::Custom2, Rgb::from((0xDE, 0xDB, 0xEF)));
    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from((0xB0, 0x0B, 0xE5)));
    /// let mut iter = chart.custom_colors_iter_color();
    /// assert_eq!(iter.next(), Some((ChartColor::Custom0, &Rgb::from((0xB0, 0x0B, 0xE5)))));
    /// assert_eq!(iter.next(), Some((ChartColor::Custom2, &Rgb::from((0xDE, 0xDB, 0xEF)))));
    /// assert!(iter.next().is_none());
    /// ```
    pub fn custom_colors_iter_color(&self) -> impl Iterator<Item = (ChartColor, &Rgb)> + '_ {
        self.custom_colors
            .iter()
            .enumerate()
            .filter_map(|(u, c)| c.as_ref().map(|c| (u, c)))
            .map(|(u, c)| (ChartColor::from_index(u), c))
    }
    /// Sets the rgb value of a custom color, returning the previous value.
    /// May only be called with CustomX ChartColors (asserts ChartColor::is_custom).
    /// ```
    /// # use napchart::*;
    /// use colorsys::Rgb;
    /// let mut chart = Napchart::default();
    ///
    /// assert!(chart.get_custom_color(ChartColor::Custom0).is_none());
    ///
    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from_hex_str("DEDBEF").unwrap());
    /// assert_eq!(chart.get_custom_color(ChartColor::Custom0), Some(&Rgb::from((0xDE, 0xDB, 0xEF))));
    /// ```
    pub fn set_custom_color(&mut self, id: ChartColor, color: Rgb) -> Option<Rgb> {
        assert!(id.is_custom());
        let i = id.custom_index().unwrap();
        self.custom_colors[i].replace(color)
    }
    /// Unsets the rgb value of a custom color, returning the previous value.
    /// May only be called with CustomX ChartColors (asserts ChartColor::is_custom).
    /// Also removes the color_tag associated with the custom color.
    /// (See [_unchecked](#method.remove_custom_color_unchecked))
    /// ```
    /// # use napchart::*;
    /// use colorsys::Rgb;
    /// let mut chart = Napchart::default();
    ///
    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from_hex_str("DEDBEF").unwrap());
    /// chart.set_color_tag(ChartColor::Custom0, "Dead Beef").unwrap();
    ///
    /// chart.remove_custom_color(ChartColor::Custom0);
    ///
    /// assert!(chart.get_custom_color(ChartColor::Custom0).is_none());
    /// assert!(chart.get_color_tag(ChartColor::Custom0).is_none());
    /// ```
    pub fn remove_custom_color(&mut self, id: ChartColor) -> Option<Rgb> {
        assert!(id.is_custom());
        self.remove_color_tag(id.clone());
        self.remove_custom_color_unchecked(id)
    }
    /// Unsets the rgb value of a custom color, returning the previous value.
    /// May only be called with CustomX ChartColors (asserts ChartColor::is_custom).
    /// Does not remove a color_tag associated with the custom color.
    /// It is invalid/undefined behavior to upload a napchart that uses a custom color without
    /// defining its colorvalue,, "uses" meaning "has a color tag" and/or "is set on an element".
    /// ```
    /// # use napchart::*;
    /// use colorsys::Rgb;
    /// let mut chart = Napchart::default();
    ///
    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from_hex_str("DEDBEF").unwrap());
    /// chart.set_color_tag(ChartColor::Custom0, "Dead Beef").unwrap();
    ///
    /// chart.remove_custom_color_unchecked(ChartColor::Custom0);
    ///
    /// assert!(chart.get_custom_color(ChartColor::Custom0).is_none());
    /// assert!(chart.get_color_tag(ChartColor::Custom0).is_some()); // UB if uploaded!
    /// ```
    pub fn remove_custom_color_unchecked(&mut self, id: ChartColor) -> Option<Rgb> {
        assert!(id.is_custom());
        let i = id.custom_index().unwrap();
        self.custom_colors[i].take()
    }
}
/// Builder functions to create new napcharts.
///
/// ```
/// # use napchart::*;
/// let chart = Napchart::default()
///                 .lanes(3)
///                 .shape(ChartShape::Wide);
/// assert_eq!(chart.lanes_len(), 3);
/// assert_eq!(chart.shape, ChartShape::Wide);
/// ```
impl Napchart {
    /// Return Napchart with shape set.
    /// ```
    /// # use napchart::*;
    /// let chart = Napchart::default();
    /// assert_eq!(chart.shape, ChartShape::Circle);
    ///
    /// let wide_chart = Napchart::default().shape(ChartShape::Wide);
    /// assert_eq!(wide_chart.shape, ChartShape::Wide);
    /// ```
    pub fn shape(self, shape: ChartShape) -> Self {
        Self { shape, ..self }
    }
    /// Return Napchart with a given number of empty lanes.
    /// ```
    /// # use napchart::*;
    /// let chart = Napchart::default();
    /// assert_eq!(chart.lanes_len(), 0);
    ///
    /// let chart2 = Napchart::default().lanes(3);
    /// assert_eq!(chart2.lanes_len(), 3);
    /// ```
    pub fn lanes(self, count: usize) -> Self {
        Self {
            lanes: repeat(ChartLane::default()).take(count).collect(),
            ..self
        }
    }
}

/// A napchart downloaded from <https://napchart.com>.
/// Includes extra metadata around the internal Napchart, such as the chart's ID, title, author, update time, etc.
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoteNapchart {
    /// The chart's ID code. Chartids are unique.
    /// Should be in one of the following formats:
    /// * 5 chars (`napchart.com/xxxxx`) (deprecated)
    /// * 6 chars (`napchart.com/xxxxxx`) (deprecated)
    /// * 9 chars snapshot (`napchart.com/snapshot/xxxxxxxxx`)
    /// * 9 chars user chart (`napchart.com/:user/xxxxxxxxx`)
    /// * 9 chars user chart with title (`napchart.com/:user/Some-title-here-xxxxxxxxx`)
    pub chartid: String,
    /// The title of the napchart, or None if unset
    pub title: Option<String>,
    /// The description of the napchart, or None if unset
    pub description: Option<String>,
    /// The user that saved this napchart, or None if anonymous
    pub username: Option<String>,
    /// The time that this chart was last saved
    pub last_updated: DateTime<Utc>,
    /// True if this napchart was saved as a snapshot
    pub is_snapshot: bool,
    /// True if this napchart is private
    pub is_private: bool,
    #[serde(skip)]
    /// The public link to this napchart.
    /// (Note: We should be able to generate this from the other metadata)
    public_link: Option<String>,
    #[serde(skip)]
    /// The chart itself
    pub chart: Napchart,
}
impl RemoteNapchart {
    /// True if both RemoteNapcharts are the same, ignoring chartid, last_updated, and public_link.
    /// Used by the api tests to make sure BlockingClient and AsyncClient are doing the same thing.
    pub fn semantic_eq(&self, other: &Self) -> bool {
        self.title == other.title
            && self.description == other.description
            && self.username == other.username
            && self.is_snapshot == other.is_snapshot
            && self.chart == other.chart
    }
}

/// A single lane of a napchart
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ChartLane {
    /// Whether the lane is locked on napchart.com.
    /// Has no effect in rust.
    pub locked: bool,
    elements: Vec<ChartElement>,
}
impl ChartLane {
    /// Clear all elements from the lane.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// let mut lane = chart.add_lane();
    /// lane.add_element(0, 60).unwrap();
    /// lane.add_element(60, 120).unwrap();
    /// lane.add_element(120, 180).unwrap();
    /// assert!(!lane.is_empty());
    /// lane.clear();
    /// assert!(lane.is_empty());
    /// ```
    pub fn clear(&mut self) {
        self.elements.clear();
    }
    /// True if the lane has no elements.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// let mut lane = chart.add_lane();
    /// assert!(lane.is_empty());
    /// lane.add_element(0, 60).unwrap();
    /// assert!(!lane.is_empty());
    /// ```
    pub fn is_empty(&self) -> bool {
        self.elements.is_empty()
    }
    /// The number of elements in the lane.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// let mut lane = chart.add_lane();
    /// assert_eq!(lane.elems_len(), 0);
    /// lane.add_element(0, 60).unwrap();
    /// lane.add_element(60, 120).unwrap();
    /// lane.add_element(120, 180).unwrap();
    /// assert_eq!(lane.elems_len(), 3);
    /// ```
    pub fn elems_len(&self) -> usize {
        self.elements.len()
    }
    /// Add a new element to the lane.
    /// Start and end must both be between [0 and 1440).
    /// Error if the new element would overlap with the existing elements.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// let mut lane = chart.add_lane();
    /// assert!(lane.add_element(0, 30).is_ok());
    /// assert!(lane.add_element(15, 45).is_err());
    /// assert!(lane.add_element(30, 60).is_ok());
    /// assert_eq!(lane.elems_len(), 2);
    /// ```
    pub fn add_element(&mut self, start: u16, end: u16) -> Result<&mut ChartElement> {
        assert!(start <= 1440);
        assert!(end <= 1440);
        // Turns self.elements into a vec of (start, end, index),
        // splitting midnight-crossers in two.
        let mut elems: Vec<(u16, u16, usize)> = Vec::new();
        for (i, e) in self.elements.iter().enumerate() {
            if e.start < e.end {
                elems.push((e.start, e.end, i));
            } else {
                elems.push((e.start, 1440, i));
                elems.push((0, e.end, i));
            }
        }
        for e in elems.into_iter() {
            // If the new element starts or ends within any of the current elements
            if (start >= e.0 && start < e.1) || (end > e.0 && end <= e.1) {
                // Error out
                let e = &self.elements[e.2];
                return Err(ErrorKind::ElementOverlap((start, end), (e.start, e.end)));
            }
        }
        // Otherwise, add the element...
        self.elements.push(ChartElement {
            start,
            end,
            ..Default::default()
        });
        // ...and return it
        Ok(self.elements.last_mut().unwrap())
    }
    /// Get an iterator over the elements in the lane.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// let mut lane = chart.add_lane();
    /// lane.add_element(0, 60).unwrap();
    /// lane.add_element(60, 120).unwrap();
    /// lane.add_element(120, 180).unwrap();
    /// let mut iter = lane.elems_iter();
    /// assert_eq!(iter.next().unwrap().get_position(), (0, 60));
    /// assert_eq!(iter.next().unwrap().get_position(), (60, 120));
    /// assert_eq!(iter.next().unwrap().get_position(), (120, 180));
    /// ```
    pub fn elems_iter(&self) -> std::slice::Iter<ChartElement> {
        self.elements.iter()
    }
}

/// A single element in a napchart.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChartElement {
    start: u16,
    end: u16,
    #[serde(flatten)]
    /// Additional metadata for the element.
    pub data: ElementData,
}
impl ChartElement {
    /// Returns the position of the element as (start, end),
    /// where start and end are minutes past midnight.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// let mut lane = chart.add_lane();
    /// let elem = lane.add_element(0, 60).unwrap();
    /// assert_eq!(elem.get_position(), (0, 60));
    /// ```
    pub fn get_position(&self) -> (u16, u16) {
        (self.start, self.end)
    }
    // unsafe fn set_position(&mut self, start: u16, end: u16) {
    //     self.start = start;
    //     self.end = end;
    // }
    /// &mut builder function to set the text of an element.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// let mut lane = chart.add_lane();
    /// let elem = lane.add_element(0, 60).unwrap().text("Hour One");
    /// assert_eq!(elem.data.text, String::from("Hour One"));
    /// ```
    pub fn text<T: ToString>(&mut self, text: T) -> &mut Self {
        self.data.text = text.to_string();
        self
    }
    /// &mut builder function to set the color of an element.
    /// ```
    /// # use napchart::*;
    /// let mut chart = Napchart::default();
    /// let mut lane = chart.add_lane();
    /// let elem = lane.add_element(0, 60).unwrap().color(ChartColor::Blue);
    /// assert_eq!(elem.data.color, ChartColor::Blue);
    /// ```
    pub fn color(&mut self, color: ChartColor) -> &mut Self {
        self.data.color = color;
        self
    }
}

/// Additional metadata for an element.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ElementData {
    /// The text description attached to the element
    pub text: String,
    /// The element's color
    pub color: ChartColor,
}

impl TryFrom<Napchart> for raw::ChartSchema {
    type Error = ErrorKind;
    fn try_from(chart: Napchart) -> Result<raw::ChartSchema> {
        let cc = chart.custom_colors;
        Ok(raw::ChartSchema {
            lanes: chart.lanes.len(),
            shape: chart.shape,
            lanes_config: chart
                .lanes
                .iter()
                .map(|l| raw::LaneConfig { locked: l.locked })
                .enumerate()
                .collect(),
            elements: chart
                .lanes
                .into_iter()
                .enumerate()
                .map(|(i, l)| (repeat(i), l.elements.into_iter()))
                .flat_map(|(i, l)| i.zip(l))
                .map(|(lane, element)| raw::LanedChartElement { lane, element })
                .collect(),
            color_tags: chart
                .color_tags
                .into_iter()
                .map(|(color, tag)| (color.custom_index(), color, tag))
                .map(|(rgb, color, tag)| (rgb.and_then(|i| cc[i].as_ref()), color, tag))
                .map(|(rgb, color, tag)| (rgb.map(Rgb::to_css_string), color, tag))
                .map(|(rgb, color, tag)| raw::ColorTag { tag, color, rgb })
                .collect(),
        })
    }
}

impl TryFrom<raw::ChartCreationReturn> for RemoteNapchart {
    type Error = ErrorKind;
    fn try_from(raw: raw::ChartCreationReturn) -> Result<RemoteNapchart> {
        use raw::ColorTag;
        let cd = raw.chart_document.chart_data;
        let lc = cd.lanes_config;
        let meta = raw.chart_document.metadata;
        let meta = RemoteNapchart {
            public_link: raw.public_link.none_if_empty(),
            username: meta.username.filter(|u| u != "anonymous"),
            ..meta
        };
        let chart = Napchart {
            shape: cd.shape,
            custom_colors: {
                let r = [None, None, None, None];
                cd.color_tags
                    .iter()
                    .map(|ColorTag { color, rgb, .. }| (color.custom_index(), rgb.as_deref()))
                    .filter_map(|(color, rgb)| Option::zip(color, rgb))
                    .try_fold::<[_; 4], _, Result<_>>(r, |mut r, (color, rgb)| {
                        r[color] = Some(Rgb::from_hex_str(rgb)?);
                        Ok(r)
                    })?
            },
            color_tags: {
                cd.color_tags
                    .into_iter()
                    .map(|tag| (tag.color, tag.tag))
                    .collect()
            },
            lanes: {
                let mut vec: Vec<_> = (0..cd.lanes)
                    .map(|i| lc.get(&i).map(|c| c.locked).unwrap_or(false))
                    .map(|locked| (locked, ChartLane::default()))
                    .map(|(locked, lane)| ChartLane { locked, ..lane })
                    .collect();
                for e in cd.elements.into_iter() {
                    let lane = vec
                        .get_mut(e.lane)
                        .ok_or(ErrorKind::InvalidLane(e.lane, cd.lanes))?;
                    lane.elements.push(e.element);
                }
                vec
            },
        };
        Ok(RemoteNapchart { chart, ..meta })
    }
}