#![warn(missing_docs)]
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;
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")]
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, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
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 {
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,
}
}
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)]
pub struct Napchart {
pub shape: ChartShape,
lanes: Vec<ChartLane>,
color_tags: HashMap<ChartColor, String>,
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 {
pub fn add_lane(&mut self) -> &mut ChartLane {
self.lanes.push(ChartLane::default());
self.lanes.last_mut().unwrap()
}
pub fn get_lane(&self, i: usize) -> Option<&ChartLane> {
self.lanes.get(i)
}
pub fn get_lane_mut(&mut self, i: usize) -> Option<&mut ChartLane> {
self.lanes.get_mut(i)
}
pub fn remove_lane(&mut self, i: usize) -> Option<ChartLane> {
if i < self.lanes.len() {
Some(self.lanes.remove(i))
} else {
None
}
}
pub fn lanes_len(&self) -> usize {
self.lanes.len()
}
pub fn upload(&self) -> api::UploadBuilder {
api::UploadBuilder::new(self)
}
}
impl Napchart {
pub fn get_color_tag(&self, color: ChartColor) -> Option<&str> {
self.color_tags.get(&color).map(|s| s.as_str())
}
pub fn color_tags_iter(&self) -> impl Iterator<Item = (&ChartColor, &String)> + '_ {
self.color_tags.iter()
}
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()))
}
pub fn set_color_tag_unchecked<T: ToString>(
&mut self,
color: ChartColor,
tag: T,
) -> Option<String> {
self.color_tags.insert(color, tag.to_string())
}
pub fn remove_color_tag(&mut self, color: ChartColor) -> Option<String> {
self.color_tags.remove(&color)
}
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()
}
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)))
}
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))
}
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)
}
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)
}
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()
}
}
impl Napchart {
pub fn shape(self, shape: ChartShape) -> Self {
Self { shape, ..self }
}
pub fn lanes(self, count: usize) -> Self {
Self {
lanes: repeat(ChartLane::default()).take(count).collect(),
..self
}
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoteNapchart {
pub chartid: String,
pub title: Option<String>,
pub description: Option<String>,
pub username: Option<String>,
pub last_updated: DateTime<Utc>,
pub is_snapshot: bool,
pub is_private: bool,
#[serde(skip)]
public_link: Option<String>,
#[serde(skip)]
pub chart: Napchart,
}
impl RemoteNapchart {
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
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ChartLane {
pub locked: bool,
elements: Vec<ChartElement>,
}
impl ChartLane {
pub fn clear(&mut self) {
self.elements.clear();
}
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
pub fn elems_len(&self) -> usize {
self.elements.len()
}
pub fn add_element(&mut self, start: u16, end: u16) -> Result<&mut ChartElement> {
assert!(start <= 1440);
assert!(end <= 1440);
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 (start >= e.0 && start < e.1) || (end > e.0 && end <= e.1) {
let e = &self.elements[e.2];
return Err(ErrorKind::ElementOverlap((start, end), (e.start, e.end)));
}
}
self.elements.push(ChartElement {
start,
end,
..Default::default()
});
Ok(self.elements.last_mut().unwrap())
}
pub fn elems_iter(&self) -> std::slice::Iter<ChartElement> {
self.elements.iter()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChartElement {
start: u16,
end: u16,
#[serde(flatten)]
pub data: ElementData,
}
impl ChartElement {
pub fn get_position(&self) -> (u16, u16) {
(self.start, self.end)
}
pub fn text<T: ToString>(&mut self, text: T) -> &mut Self {
self.data.text = text.to_string();
self
}
pub fn color(&mut self, color: ChartColor) -> &mut Self {
self.data.color = color;
self
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ElementData {
pub text: String,
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 })
}
}