napchart/lib.rs
1/*
2 * --------------------
3 * THIS FILE IS LICENSED UNDER MIT
4 * THE FOLLOWING MESSAGE IS NOT A LICENSE
5 *
6 * <barrow@tilde.team> wrote this file.
7 * by reading this text, you are reading "TRANS RIGHTS".
8 * this file and the content within it is the gay agenda.
9 * if we meet some day, and you think this stuff is worth it,
10 * you can buy me a beer, tea, or something stronger.
11 * -Ezra Barrow
12 * --------------------
13 */
14#![warn(missing_docs)]
15// #![feature(external_doc)]
16// #![doc(include = "../README.md")]
17//! # napchart-rs
18//!
19//! [](https://github.com/barrowsys/napchart-rs)
20//! [](https://crates.io/crates/napchart/)
21//! [](https://docs.rs/napchart)
22//!
23//! A strongly-typed rust interface to the <https://napchart.com> API.
24//!
25//! The public napchart api is pretty barebones right now, but this will let you use it!
26//!
27//! ## Usage
28//!
29//! Add to your Cargo.toml:
30//! ```text
31//! [dependencies]
32//! napchart = "0.3"
33//! ```
34//!
35//! ## Examples
36//!
37//! ### Creating a new napchart from scratch
38//! Example: <https://napchart.com/snapshot/O6kunUfuL>
39//! ```
40//! use napchart::prelude::*;
41//!
42//! let mut chart = Napchart::default()
43//!     .shape(ChartShape::Circle)
44//!     .lanes(1);
45//! let first_lane = chart.get_lane_mut(0).unwrap();
46//! first_lane.add_element(0, 60).unwrap()
47//!     .text("Hour One");
48//! first_lane.add_element(180, 240).unwrap()
49//!     .text("Hour Four");
50//! let second_lane = chart.add_lane();
51//! second_lane.add_element(0, 120).unwrap()
52//!     .color(ChartColor::Blue);
53//! second_lane.add_element(120, 240).unwrap()
54//!     .color(ChartColor::Green)
55//!     .text("Cool green time");
56//! ```
57//!
58//! ### Downloading a napchart
59//! Example Chart: <https://napchart.com/3tbkt>
60//! ```
61//! use napchart::api::BlockingClient;
62//!
63//! let client = BlockingClient::default();
64//! let rchart = client.get_chart("3tbkt").unwrap();
65//! assert_eq!(rchart.chartid, String::from("3tbkt"));
66//! assert_eq!(rchart.title, Some(String::from("State test chart")));
67//! assert_eq!(rchart.chart.shape, napchart::ChartShape::Circle);
68//! assert_eq!(rchart.chart.lanes_len(), 1);
69//! ```
70//!
71//! ### Uploading a napchart as a snapshot
72//! Example Output: <https://napchart.com/snapshot/TpCfggr4i>
73//! ```no_run
74//! use napchart::prelude::*;
75//! use napchart::api::BlockingClient;
76//!
77//! let client = BlockingClient::default();
78//! let mut chart = Napchart::default();
79//! let lane = chart.add_lane();
80//! lane.add_element(420, 1260)
81//!     .unwrap()
82//!     .text("Nighttime")
83//!     .color(ChartColor::Gray);
84//! let upload_builder = chart.upload()
85//!     .title("readme doctest")
86//!     .description("https://crates.io/crates/napchart");
87//! let remote_chart = client.create_snapshot(upload_builder).unwrap();
88//! assert!(!remote_chart.chartid.is_empty());
89//! ```
90
91use chrono::prelude::*;
92use colorsys::Rgb;
93use noneifempty::NoneIfEmpty;
94use serde::{Deserialize, Serialize};
95use std::collections::HashMap;
96use std::convert::TryFrom;
97use std::iter::repeat;
98use std::str::FromStr;
99use std::string::ToString;
100
101pub mod api;
102
103mod raw;
104
105mod error;
106pub use error::ErrorKind;
107use error::Result;
108
109/// Contains aliases to the useful imports.
110/// ```
111/// use napchart::prelude::*;
112/// let mut chart: Napchart = Napchart::default()
113///     .shape(ChartShape::Wide);
114/// let lane: &mut ChartLane = chart.add_lane();
115/// let elem: &mut ChartElement = lane.add_element(0, 60).unwrap()
116///     .color(ChartColor::Green);
117/// ```
118pub mod prelude {
119    pub use super::ChartColor;
120    pub use super::ChartElement;
121    pub use super::ChartLane;
122    pub use super::ChartShape;
123    pub use super::ElementData;
124    pub use super::Napchart;
125    pub use super::RemoteNapchart;
126}
127
128#[allow(missing_docs)]
129#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
130#[serde(rename_all = "lowercase")]
131/// The shape of a napchart
132pub enum ChartShape {
133    Circle,
134    Wide,
135    Line,
136}
137impl Default for ChartShape {
138    fn default() -> Self {
139        Self::Circle
140    }
141}
142impl FromStr for ChartShape {
143    type Err = ErrorKind;
144    fn from_str(s: &str) -> Result<Self> {
145        Ok(match s {
146            "circle" => Self::Circle,
147            "wide" => Self::Wide,
148            "line" => Self::Line,
149            _ => return Err(ErrorKind::InvalidChartShape(s.to_string())),
150        })
151    }
152}
153impl ToString for ChartShape {
154    fn to_string(&self) -> String {
155        match self {
156            ChartShape::Circle => String::from("circle"),
157            ChartShape::Wide => String::from("wide"),
158            ChartShape::Line => String::from("line"),
159        }
160    }
161}
162
163// #[allow(missing_docs)]
164// #[derive(Clone, Debug, PartialEq, Eq, Hash)]
165// /// The colors available for chart elements
166// pub enum BuiltinColors {
167//     Red,
168//     Blue,
169//     Brown,
170//     Green,
171//     Gray,
172//     Yellow,
173//     Purple,
174//     Pink,
175// }
176
177#[allow(missing_docs)]
178#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
179#[serde(rename_all = "lowercase")]
180/// The colors available for chart elements
181pub enum ChartColor {
182    Red,
183    Blue,
184    Brown,
185    Green,
186    Gray,
187    Yellow,
188    Purple,
189    Pink,
190    #[serde(rename = "custom_0")]
191    Custom0,
192    #[serde(rename = "custom_1")]
193    Custom1,
194    #[serde(rename = "custom_2")]
195    Custom2,
196    #[serde(rename = "custom_3")]
197    Custom3,
198}
199impl ChartColor {
200    /// True if a color is custom, false if it's one of the builtin colors
201    /// ```
202    /// # use napchart::*;
203    /// assert!(!ChartColor::Blue.is_custom());
204    /// assert!(ChartColor::Custom2.is_custom());
205    /// ```
206    pub fn is_custom(&self) -> bool {
207        match self {
208            ChartColor::Red
209            | ChartColor::Blue
210            | ChartColor::Brown
211            | ChartColor::Green
212            | ChartColor::Gray
213            | ChartColor::Yellow
214            | ChartColor::Purple
215            | ChartColor::Pink => false,
216            ChartColor::Custom0
217            | ChartColor::Custom1
218            | ChartColor::Custom2
219            | ChartColor::Custom3 => true,
220        }
221    }
222    /// True if a color is builtin, false if it's one of the custom colors
223    /// ```
224    /// # use napchart::*;
225    /// assert!(ChartColor::Blue.is_builtin());
226    /// assert!(!ChartColor::Custom2.is_builtin());
227    /// ```
228    pub fn is_builtin(&self) -> bool {
229        match self {
230            ChartColor::Red
231            | ChartColor::Blue
232            | ChartColor::Brown
233            | ChartColor::Green
234            | ChartColor::Gray
235            | ChartColor::Yellow
236            | ChartColor::Purple
237            | ChartColor::Pink => true,
238            ChartColor::Custom0
239            | ChartColor::Custom1
240            | ChartColor::Custom2
241            | ChartColor::Custom3 => false,
242        }
243    }
244    fn custom_index(&self) -> Option<usize> {
245        match self {
246            ChartColor::Custom0 => Some(0),
247            ChartColor::Custom1 => Some(1),
248            ChartColor::Custom2 => Some(2),
249            ChartColor::Custom3 => Some(3),
250            _ => None,
251        }
252    }
253    fn from_index(i: usize) -> Self {
254        assert!(i <= 3);
255        match i {
256            0 => ChartColor::Custom0,
257            1 => ChartColor::Custom1,
258            2 => ChartColor::Custom2,
259            3 => ChartColor::Custom3,
260            _ => unreachable!(),
261        }
262    }
263}
264impl Default for ChartColor {
265    fn default() -> Self {
266        Self::Red
267    }
268}
269impl ToString for ChartColor {
270    fn to_string(&self) -> String {
271        match self {
272            ChartColor::Red => String::from("red"),
273            ChartColor::Blue => String::from("blue"),
274            ChartColor::Brown => String::from("brown"),
275            ChartColor::Green => String::from("green"),
276            ChartColor::Gray => String::from("gray"),
277            ChartColor::Yellow => String::from("yellow"),
278            ChartColor::Purple => String::from("purple"),
279            ChartColor::Pink => String::from("pink"),
280            ChartColor::Custom0 => String::from("custom_0"),
281            ChartColor::Custom1 => String::from("custom_1"),
282            ChartColor::Custom2 => String::from("custom_2"),
283            ChartColor::Custom3 => String::from("custom_3"),
284        }
285    }
286}
287impl FromStr for ChartColor {
288    type Err = ErrorKind;
289    fn from_str(s: &str) -> Result<Self> {
290        Ok(match s {
291            "red" => ChartColor::Red,
292            "blue" => ChartColor::Blue,
293            "brown" => ChartColor::Brown,
294            "green" => ChartColor::Green,
295            "gray" => ChartColor::Gray,
296            "yellow" => ChartColor::Yellow,
297            "purple" => ChartColor::Purple,
298            "pink" => ChartColor::Pink,
299            "custom_0" => ChartColor::Custom0,
300            "custom_1" => ChartColor::Custom1,
301            "custom_2" => ChartColor::Custom2,
302            "custom_3" => ChartColor::Custom3,
303            _ => return Err(ErrorKind::InvalidChartColor(s.to_string())),
304        })
305    }
306}
307
308#[derive(Clone, Debug, PartialEq)]
309/// A napchart, as seen on <https://napchart.com/>
310pub struct Napchart {
311    /// The default shape of the napchart on napchart.com
312    pub shape: ChartShape,
313    lanes: Vec<ChartLane>,
314    /// String tags associated with element colors.
315    /// These are displayed in the inner area of a napchart,
316    /// along with the accumulated amount of time each color takes up.
317    color_tags: HashMap<ChartColor, String>,
318    /// RGB values for the four custom colors.
319    /// If a custom color is None, it is INVALID/Undefined Behavior to set a chart element to it.
320    custom_colors: [Option<Rgb>; 4],
321}
322impl Default for Napchart {
323    fn default() -> Self {
324        Self {
325            shape: Default::default(),
326            lanes: Default::default(),
327            color_tags: Default::default(),
328            custom_colors: [None, None, None, None],
329        }
330    }
331}
332impl Napchart {
333    /// Append a new blank lane to the chart and return a mutable reference to it.
334    /// ```
335    /// # use napchart::*;
336    /// let mut chart = Napchart::default();
337    /// let mut lane = chart.add_lane();
338    /// assert!(lane.is_empty());
339    /// assert_eq!(chart.lanes_len(), 1);
340    /// ```
341    pub fn add_lane(&mut self) -> &mut ChartLane {
342        self.lanes.push(ChartLane::default());
343        self.lanes.last_mut().unwrap()
344    }
345    /// Get a reference to the given lane, or None if out of bounds.
346    /// ```
347    /// # use napchart::*;
348    /// let mut chart = Napchart::default();
349    /// chart.add_lane();
350    /// assert!(chart.get_lane(0).is_some());
351    /// assert!(chart.get_lane(1).is_none());
352    /// ```
353    pub fn get_lane(&self, i: usize) -> Option<&ChartLane> {
354        self.lanes.get(i)
355    }
356    /// Get a mutable reference to the given lane, or None if out of bounds.
357    /// ```
358    /// # use napchart::*;
359    /// let mut chart = Napchart::default();
360    /// chart.add_lane();
361    /// assert!(chart.get_lane_mut(0).is_some());
362    /// assert!(chart.get_lane_mut(1).is_none());
363    /// ```
364    pub fn get_lane_mut(&mut self, i: usize) -> Option<&mut ChartLane> {
365        self.lanes.get_mut(i)
366    }
367    /// Remove the given lane from the chart and return it, or None if out of bounds.
368    /// ```
369    /// # use napchart::*;
370    /// let mut chart = Napchart::default();
371    /// chart.add_lane();
372    /// let lane = chart.remove_lane(0);
373    /// assert!(lane.is_some());
374    /// assert_eq!(chart.lanes_len(), 0);
375    /// ```
376    pub fn remove_lane(&mut self, i: usize) -> Option<ChartLane> {
377        if i < self.lanes.len() {
378            Some(self.lanes.remove(i))
379        } else {
380            None
381        }
382    }
383    /// Get the total number of lanes in the chart.
384    /// ```
385    /// # use napchart::*;
386    /// let mut chart = Napchart::default();
387    /// assert_eq!(chart.lanes_len(), 0);
388    /// chart.add_lane();
389    /// assert_eq!(chart.lanes_len(), 1);
390    /// chart.add_lane();
391    /// assert_eq!(chart.lanes_len(), 2);
392    /// chart.remove_lane(1);
393    /// chart.remove_lane(0);
394    /// assert_eq!(chart.lanes_len(), 0);
395    /// ```
396    pub fn lanes_len(&self) -> usize {
397        self.lanes.len()
398    }
399    /// Create an UploadBuilder with a reference to this chart.
400    /// ```
401    /// # use napchart::*;
402    /// # use napchart::api::mock::BlockingClient;
403    /// let client = BlockingClient::default();
404    /// let chart = Napchart::default();
405    /// let upload: napchart::api::UploadBuilder = chart.upload().title("My Cool Chart");
406    /// assert!(client.create_snapshot(upload).is_ok());
407    /// ```
408    pub fn upload(&self) -> api::UploadBuilder {
409        api::UploadBuilder::new(self)
410    }
411}
412/// Getters and setters for color tags and custom colors
413impl Napchart {
414    /// Get the text tag for a color.
415    /// ```
416    /// # use napchart::*;
417    /// let mut chart = Napchart::default();
418    /// // Napcharts start out with no color tags
419    /// assert!(chart.get_color_tag(ChartColor::Blue).is_none());
420    ///
421    /// chart.set_color_tag(ChartColor::Blue, "Core Sleep").unwrap();
422    ///
423    /// assert_eq!(chart.get_color_tag(ChartColor::Blue), Some("Core Sleep"));
424    /// ```
425    pub fn get_color_tag(&self, color: ChartColor) -> Option<&str> {
426        self.color_tags.get(&color).map(|s| s.as_str())
427    }
428    /// Get an iterator over ChartColors and their tags.
429    /// ```
430    /// # use napchart::*;
431    /// let mut chart = Napchart::default();
432    ///
433    /// chart.set_color_tag(ChartColor::Blue, "Nap");
434    /// chart.set_color_tag(ChartColor::Gray, "Core");
435    /// let mut iter = chart.color_tags_iter();
436    /// assert!(iter.next().is_some());
437    /// assert!(iter.next().is_some());
438    /// assert!(iter.next().is_none());
439    /// ```
440    pub fn color_tags_iter(&self) -> impl Iterator<Item = (&ChartColor, &String)> + '_ {
441        self.color_tags.iter()
442    }
443    /// Set the text tag for a color, returning the value that was replaced.
444    /// Returns ErrorKind::CustomColorUnset if you attempt to set the tag on an undefined custom
445    /// color.
446    /// ```
447    /// # use napchart::*;
448    /// let mut chart = Napchart::default();
449    ///
450    /// let original: Option<String> = chart.set_color_tag(ChartColor::Blue, "Core Sleep").unwrap();
451    /// assert!(original.is_none()); // Replaced nothing
452    /// assert_eq!(chart.get_color_tag(ChartColor::Blue), Some("Core Sleep"));
453    ///
454    /// let second: Option<String> = chart.set_color_tag(ChartColor::Blue, "Nap").unwrap();
455    /// assert_eq!(second, Some(String::from("Core Sleep")));
456    /// assert_eq!(chart.get_color_tag(ChartColor::Blue), Some("Nap"));
457    /// ```
458    pub fn set_color_tag<T: ToString>(
459        &mut self,
460        color: ChartColor,
461        tag: T,
462    ) -> Result<Option<String>> {
463        if let Some(index) = color.custom_index() {
464            if self.custom_colors[index].is_none() {
465                return Err(ErrorKind::CustomColorUnset(index));
466            }
467        }
468        Ok(self.color_tags.insert(color, tag.to_string()))
469    }
470    /// Set the text tag for a color.
471    /// This function does not check custom colors are valid!!
472    /// It is invalid/undefined behavior to upload a napchart that uses a custom color without
473    /// defining its colorvalue,, "uses" meaning "has a color tag" and/or "is set on an element".
474    /// ```
475    /// # use napchart::*;
476    /// use colorsys::Rgb;
477    /// let mut chart = Napchart::default();
478    ///
479    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from_hex_str("DEDBEF").unwrap());
480    /// let res = chart.set_color_tag(ChartColor::Custom0, "Dead Beef");
481    /// assert!(res.is_ok());
482    ///
483    /// chart.remove_custom_color(ChartColor::Custom0);
484    /// let res = chart.set_color_tag(ChartColor::Custom0, "Dead Beef");
485    /// assert!(res.is_err());
486    /// assert!(matches!(res.unwrap_err(), napchart::ErrorKind::CustomColorUnset(0)));
487    /// ```
488    pub fn set_color_tag_unchecked<T: ToString>(
489        &mut self,
490        color: ChartColor,
491        tag: T,
492    ) -> Option<String> {
493        self.color_tags.insert(color, tag.to_string())
494    }
495    /// Removing the text tag for a color, returning the previous value.
496    /// ```
497    /// # use napchart::*;
498    /// let mut chart = Napchart::default();
499    ///
500    /// let original: Option<String> = chart.set_color_tag(ChartColor::Blue, "Core Sleep").unwrap();
501    /// assert!(original.is_none()); // Replaced nothing
502    /// assert_eq!(chart.get_color_tag(ChartColor::Blue), Some("Core Sleep"));
503    ///
504    /// let second: Option<String> = chart.set_color_tag(ChartColor::Blue, "Nap").unwrap();
505    /// assert_eq!(second, Some(String::from("Core Sleep")));
506    /// assert_eq!(chart.get_color_tag(ChartColor::Blue), Some("Nap"));
507    /// ```
508    pub fn remove_color_tag(&mut self, color: ChartColor) -> Option<String> {
509        self.color_tags.remove(&color)
510    }
511    /// Gets the rgb value of a custom color.
512    /// May only be called with CustomX ChartColors (asserts ChartColor::is_custom).
513    /// ```
514    /// # use napchart::*;
515    /// use colorsys::Rgb;
516    /// let mut chart = Napchart::default();
517    ///
518    /// assert!(chart.get_custom_color(ChartColor::Custom0).is_none());
519    ///
520    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from_hex_str("DEDBEF").unwrap());
521    /// assert_eq!(chart.get_custom_color(ChartColor::Custom0), Some(&Rgb::from((0xDE, 0xDB, 0xEF))));
522    /// ```
523    pub fn get_custom_color(&self, id: ChartColor) -> Option<&Rgb> {
524        assert!(id.is_custom());
525        let i = id.custom_index().unwrap();
526        self.custom_colors[i].as_ref()
527    }
528    /// Get an iterator over custom color (as usize indexes) and their RGB values.
529    /// ```
530    /// # use napchart::*;
531    /// use colorsys::Rgb;
532    /// let mut chart = Napchart::default();
533    ///
534    /// chart.set_custom_color(ChartColor::Custom2, Rgb::from((0xDE, 0xDB, 0xEF)));
535    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from((0xB0, 0x0B, 0xE5)));
536    /// let mut iter = chart.custom_colors_iter_index();
537    /// assert_eq!(iter.next(), Some((0, &Rgb::from((0xB0, 0x0B, 0xE5)))));
538    /// assert_eq!(iter.next(), Some((2, &Rgb::from((0xDE, 0xDB, 0xEF)))));
539    /// assert!(iter.next().is_none());
540    /// ```
541    pub fn custom_colors_iter_index(&self) -> impl Iterator<Item = (usize, &Rgb)> + '_ {
542        self.custom_colors
543            .iter()
544            .enumerate()
545            .filter_map(|(u, c)| c.as_ref().map(|c| (u, c)))
546    }
547    /// Get an iterator over custom color (as ChartColors) and their RGB values.
548    /// ```
549    /// # use napchart::*;
550    /// use colorsys::Rgb;
551    /// let mut chart = Napchart::default();
552    ///
553    /// chart.set_custom_color(ChartColor::Custom2, Rgb::from((0xDE, 0xDB, 0xEF)));
554    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from((0xB0, 0x0B, 0xE5)));
555    /// let mut iter = chart.custom_colors_iter_color();
556    /// assert_eq!(iter.next(), Some((ChartColor::Custom0, &Rgb::from((0xB0, 0x0B, 0xE5)))));
557    /// assert_eq!(iter.next(), Some((ChartColor::Custom2, &Rgb::from((0xDE, 0xDB, 0xEF)))));
558    /// assert!(iter.next().is_none());
559    /// ```
560    pub fn custom_colors_iter_color(&self) -> impl Iterator<Item = (ChartColor, &Rgb)> + '_ {
561        self.custom_colors
562            .iter()
563            .enumerate()
564            .filter_map(|(u, c)| c.as_ref().map(|c| (u, c)))
565            .map(|(u, c)| (ChartColor::from_index(u), c))
566    }
567    /// Sets the rgb value of a custom color, returning the previous value.
568    /// May only be called with CustomX ChartColors (asserts ChartColor::is_custom).
569    /// ```
570    /// # use napchart::*;
571    /// use colorsys::Rgb;
572    /// let mut chart = Napchart::default();
573    ///
574    /// assert!(chart.get_custom_color(ChartColor::Custom0).is_none());
575    ///
576    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from_hex_str("DEDBEF").unwrap());
577    /// assert_eq!(chart.get_custom_color(ChartColor::Custom0), Some(&Rgb::from((0xDE, 0xDB, 0xEF))));
578    /// ```
579    pub fn set_custom_color(&mut self, id: ChartColor, color: Rgb) -> Option<Rgb> {
580        assert!(id.is_custom());
581        let i = id.custom_index().unwrap();
582        self.custom_colors[i].replace(color)
583    }
584    /// Unsets the rgb value of a custom color, returning the previous value.
585    /// May only be called with CustomX ChartColors (asserts ChartColor::is_custom).
586    /// Also removes the color_tag associated with the custom color.
587    /// (See [_unchecked](#method.remove_custom_color_unchecked))
588    /// ```
589    /// # use napchart::*;
590    /// use colorsys::Rgb;
591    /// let mut chart = Napchart::default();
592    ///
593    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from_hex_str("DEDBEF").unwrap());
594    /// chart.set_color_tag(ChartColor::Custom0, "Dead Beef").unwrap();
595    ///
596    /// chart.remove_custom_color(ChartColor::Custom0);
597    ///
598    /// assert!(chart.get_custom_color(ChartColor::Custom0).is_none());
599    /// assert!(chart.get_color_tag(ChartColor::Custom0).is_none());
600    /// ```
601    pub fn remove_custom_color(&mut self, id: ChartColor) -> Option<Rgb> {
602        assert!(id.is_custom());
603        self.remove_color_tag(id.clone());
604        self.remove_custom_color_unchecked(id)
605    }
606    /// Unsets the rgb value of a custom color, returning the previous value.
607    /// May only be called with CustomX ChartColors (asserts ChartColor::is_custom).
608    /// Does not remove a color_tag associated with the custom color.
609    /// It is invalid/undefined behavior to upload a napchart that uses a custom color without
610    /// defining its colorvalue,, "uses" meaning "has a color tag" and/or "is set on an element".
611    /// ```
612    /// # use napchart::*;
613    /// use colorsys::Rgb;
614    /// let mut chart = Napchart::default();
615    ///
616    /// chart.set_custom_color(ChartColor::Custom0, Rgb::from_hex_str("DEDBEF").unwrap());
617    /// chart.set_color_tag(ChartColor::Custom0, "Dead Beef").unwrap();
618    ///
619    /// chart.remove_custom_color_unchecked(ChartColor::Custom0);
620    ///
621    /// assert!(chart.get_custom_color(ChartColor::Custom0).is_none());
622    /// assert!(chart.get_color_tag(ChartColor::Custom0).is_some()); // UB if uploaded!
623    /// ```
624    pub fn remove_custom_color_unchecked(&mut self, id: ChartColor) -> Option<Rgb> {
625        assert!(id.is_custom());
626        let i = id.custom_index().unwrap();
627        self.custom_colors[i].take()
628    }
629}
630/// Builder functions to create new napcharts.
631///
632/// ```
633/// # use napchart::*;
634/// let chart = Napchart::default()
635///                 .lanes(3)
636///                 .shape(ChartShape::Wide);
637/// assert_eq!(chart.lanes_len(), 3);
638/// assert_eq!(chart.shape, ChartShape::Wide);
639/// ```
640impl Napchart {
641    /// Return Napchart with shape set.
642    /// ```
643    /// # use napchart::*;
644    /// let chart = Napchart::default();
645    /// assert_eq!(chart.shape, ChartShape::Circle);
646    ///
647    /// let wide_chart = Napchart::default().shape(ChartShape::Wide);
648    /// assert_eq!(wide_chart.shape, ChartShape::Wide);
649    /// ```
650    pub fn shape(self, shape: ChartShape) -> Self {
651        Self { shape, ..self }
652    }
653    /// Return Napchart with a given number of empty lanes.
654    /// ```
655    /// # use napchart::*;
656    /// let chart = Napchart::default();
657    /// assert_eq!(chart.lanes_len(), 0);
658    ///
659    /// let chart2 = Napchart::default().lanes(3);
660    /// assert_eq!(chart2.lanes_len(), 3);
661    /// ```
662    pub fn lanes(self, count: usize) -> Self {
663        Self {
664            lanes: repeat(ChartLane::default()).take(count).collect(),
665            ..self
666        }
667    }
668}
669
670/// A napchart downloaded from <https://napchart.com>.
671/// Includes extra metadata around the internal Napchart, such as the chart's ID, title, author, update time, etc.
672#[derive(Debug, PartialEq, Serialize, Deserialize)]
673#[serde(rename_all = "camelCase")]
674pub struct RemoteNapchart {
675    /// The chart's ID code. Chartids are unique.
676    /// Should be in one of the following formats:
677    /// * 5 chars (`napchart.com/xxxxx`) (deprecated)
678    /// * 6 chars (`napchart.com/xxxxxx`) (deprecated)
679    /// * 9 chars snapshot (`napchart.com/snapshot/xxxxxxxxx`)
680    /// * 9 chars user chart (`napchart.com/:user/xxxxxxxxx`)
681    /// * 9 chars user chart with title (`napchart.com/:user/Some-title-here-xxxxxxxxx`)
682    pub chartid: String,
683    /// The title of the napchart, or None if unset
684    pub title: Option<String>,
685    /// The description of the napchart, or None if unset
686    pub description: Option<String>,
687    /// The user that saved this napchart, or None if anonymous
688    pub username: Option<String>,
689    /// The time that this chart was last saved
690    pub last_updated: DateTime<Utc>,
691    /// True if this napchart was saved as a snapshot
692    pub is_snapshot: bool,
693    /// True if this napchart is private
694    pub is_private: bool,
695    #[serde(skip)]
696    /// The public link to this napchart.
697    /// (Note: We should be able to generate this from the other metadata)
698    public_link: Option<String>,
699    #[serde(skip)]
700    /// The chart itself
701    pub chart: Napchart,
702}
703impl RemoteNapchart {
704    /// True if both RemoteNapcharts are the same, ignoring chartid, last_updated, and public_link.
705    /// Used by the api tests to make sure BlockingClient and AsyncClient are doing the same thing.
706    pub fn semantic_eq(&self, other: &Self) -> bool {
707        self.title == other.title
708            && self.description == other.description
709            && self.username == other.username
710            && self.is_snapshot == other.is_snapshot
711            && self.chart == other.chart
712    }
713}
714
715/// A single lane of a napchart
716#[derive(Clone, Debug, Default, PartialEq, Eq)]
717pub struct ChartLane {
718    /// Whether the lane is locked on napchart.com.
719    /// Has no effect in rust.
720    pub locked: bool,
721    elements: Vec<ChartElement>,
722}
723impl ChartLane {
724    /// Clear all elements from the lane.
725    /// ```
726    /// # use napchart::*;
727    /// let mut chart = Napchart::default();
728    /// let mut lane = chart.add_lane();
729    /// lane.add_element(0, 60).unwrap();
730    /// lane.add_element(60, 120).unwrap();
731    /// lane.add_element(120, 180).unwrap();
732    /// assert!(!lane.is_empty());
733    /// lane.clear();
734    /// assert!(lane.is_empty());
735    /// ```
736    pub fn clear(&mut self) {
737        self.elements.clear();
738    }
739    /// True if the lane has no elements.
740    /// ```
741    /// # use napchart::*;
742    /// let mut chart = Napchart::default();
743    /// let mut lane = chart.add_lane();
744    /// assert!(lane.is_empty());
745    /// lane.add_element(0, 60).unwrap();
746    /// assert!(!lane.is_empty());
747    /// ```
748    pub fn is_empty(&self) -> bool {
749        self.elements.is_empty()
750    }
751    /// The number of elements in the lane.
752    /// ```
753    /// # use napchart::*;
754    /// let mut chart = Napchart::default();
755    /// let mut lane = chart.add_lane();
756    /// assert_eq!(lane.elems_len(), 0);
757    /// lane.add_element(0, 60).unwrap();
758    /// lane.add_element(60, 120).unwrap();
759    /// lane.add_element(120, 180).unwrap();
760    /// assert_eq!(lane.elems_len(), 3);
761    /// ```
762    pub fn elems_len(&self) -> usize {
763        self.elements.len()
764    }
765    /// Add a new element to the lane.
766    /// Start and end must both be between [0 and 1440).
767    /// Error if the new element would overlap with the existing elements.
768    /// ```
769    /// # use napchart::*;
770    /// let mut chart = Napchart::default();
771    /// let mut lane = chart.add_lane();
772    /// assert!(lane.add_element(0, 30).is_ok());
773    /// assert!(lane.add_element(15, 45).is_err());
774    /// assert!(lane.add_element(30, 60).is_ok());
775    /// assert_eq!(lane.elems_len(), 2);
776    /// ```
777    pub fn add_element(&mut self, start: u16, end: u16) -> Result<&mut ChartElement> {
778        assert!(start <= 1440);
779        assert!(end <= 1440);
780        // Turns self.elements into a vec of (start, end, index),
781        // splitting midnight-crossers in two.
782        let mut elems: Vec<(u16, u16, usize)> = Vec::new();
783        for (i, e) in self.elements.iter().enumerate() {
784            if e.start < e.end {
785                elems.push((e.start, e.end, i));
786            } else {
787                elems.push((e.start, 1440, i));
788                elems.push((0, e.end, i));
789            }
790        }
791        for e in elems.into_iter() {
792            // If the new element starts or ends within any of the current elements
793            if (start >= e.0 && start < e.1) || (end > e.0 && end <= e.1) {
794                // Error out
795                let e = &self.elements[e.2];
796                return Err(ErrorKind::ElementOverlap((start, end), (e.start, e.end)));
797            }
798        }
799        // Otherwise, add the element...
800        self.elements.push(ChartElement {
801            start,
802            end,
803            ..Default::default()
804        });
805        // ...and return it
806        Ok(self.elements.last_mut().unwrap())
807    }
808    /// Get an iterator over the elements in the lane.
809    /// ```
810    /// # use napchart::*;
811    /// let mut chart = Napchart::default();
812    /// let mut lane = chart.add_lane();
813    /// lane.add_element(0, 60).unwrap();
814    /// lane.add_element(60, 120).unwrap();
815    /// lane.add_element(120, 180).unwrap();
816    /// let mut iter = lane.elems_iter();
817    /// assert_eq!(iter.next().unwrap().get_position(), (0, 60));
818    /// assert_eq!(iter.next().unwrap().get_position(), (60, 120));
819    /// assert_eq!(iter.next().unwrap().get_position(), (120, 180));
820    /// ```
821    pub fn elems_iter(&self) -> std::slice::Iter<ChartElement> {
822        self.elements.iter()
823    }
824}
825
826/// A single element in a napchart.
827#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
828pub struct ChartElement {
829    start: u16,
830    end: u16,
831    #[serde(flatten)]
832    /// Additional metadata for the element.
833    pub data: ElementData,
834}
835impl ChartElement {
836    /// Returns the position of the element as (start, end),
837    /// where start and end are minutes past midnight.
838    /// ```
839    /// # use napchart::*;
840    /// let mut chart = Napchart::default();
841    /// let mut lane = chart.add_lane();
842    /// let elem = lane.add_element(0, 60).unwrap();
843    /// assert_eq!(elem.get_position(), (0, 60));
844    /// ```
845    pub fn get_position(&self) -> (u16, u16) {
846        (self.start, self.end)
847    }
848    // unsafe fn set_position(&mut self, start: u16, end: u16) {
849    //     self.start = start;
850    //     self.end = end;
851    // }
852    /// &mut builder function to set the text of an element.
853    /// ```
854    /// # use napchart::*;
855    /// let mut chart = Napchart::default();
856    /// let mut lane = chart.add_lane();
857    /// let elem = lane.add_element(0, 60).unwrap().text("Hour One");
858    /// assert_eq!(elem.data.text, String::from("Hour One"));
859    /// ```
860    pub fn text<T: ToString>(&mut self, text: T) -> &mut Self {
861        self.data.text = text.to_string();
862        self
863    }
864    /// &mut builder function to set the color of an element.
865    /// ```
866    /// # use napchart::*;
867    /// let mut chart = Napchart::default();
868    /// let mut lane = chart.add_lane();
869    /// let elem = lane.add_element(0, 60).unwrap().color(ChartColor::Blue);
870    /// assert_eq!(elem.data.color, ChartColor::Blue);
871    /// ```
872    pub fn color(&mut self, color: ChartColor) -> &mut Self {
873        self.data.color = color;
874        self
875    }
876}
877
878/// Additional metadata for an element.
879#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
880pub struct ElementData {
881    /// The text description attached to the element
882    pub text: String,
883    /// The element's color
884    pub color: ChartColor,
885}
886
887impl TryFrom<Napchart> for raw::ChartSchema {
888    type Error = ErrorKind;
889    fn try_from(chart: Napchart) -> Result<raw::ChartSchema> {
890        let cc = chart.custom_colors;
891        Ok(raw::ChartSchema {
892            lanes: chart.lanes.len(),
893            shape: chart.shape,
894            lanes_config: chart
895                .lanes
896                .iter()
897                .map(|l| raw::LaneConfig { locked: l.locked })
898                .enumerate()
899                .collect(),
900            elements: chart
901                .lanes
902                .into_iter()
903                .enumerate()
904                .map(|(i, l)| (repeat(i), l.elements.into_iter()))
905                .flat_map(|(i, l)| i.zip(l))
906                .map(|(lane, element)| raw::LanedChartElement { lane, element })
907                .collect(),
908            color_tags: chart
909                .color_tags
910                .into_iter()
911                .map(|(color, tag)| (color.custom_index(), color, tag))
912                .map(|(rgb, color, tag)| (rgb.and_then(|i| cc[i].as_ref()), color, tag))
913                .map(|(rgb, color, tag)| (rgb.map(Rgb::to_css_string), color, tag))
914                .map(|(rgb, color, tag)| raw::ColorTag { tag, color, rgb })
915                .collect(),
916        })
917    }
918}
919
920impl TryFrom<raw::ChartCreationReturn> for RemoteNapchart {
921    type Error = ErrorKind;
922    fn try_from(raw: raw::ChartCreationReturn) -> Result<RemoteNapchart> {
923        use raw::ColorTag;
924        let cd = raw.chart_document.chart_data;
925        let lc = cd.lanes_config;
926        let meta = raw.chart_document.metadata;
927        let meta = RemoteNapchart {
928            public_link: raw.public_link.none_if_empty(),
929            username: meta.username.filter(|u| u != "anonymous"),
930            ..meta
931        };
932        let chart = Napchart {
933            shape: cd.shape,
934            custom_colors: {
935                let r = [None, None, None, None];
936                cd.color_tags
937                    .iter()
938                    .map(|ColorTag { color, rgb, .. }| (color.custom_index(), rgb.as_deref()))
939                    .filter_map(|(color, rgb)| Option::zip(color, rgb))
940                    .try_fold::<[_; 4], _, Result<_>>(r, |mut r, (color, rgb)| {
941                        r[color] = Some(Rgb::from_hex_str(rgb)?);
942                        Ok(r)
943                    })?
944            },
945            color_tags: {
946                cd.color_tags
947                    .into_iter()
948                    .map(|tag| (tag.color, tag.tag))
949                    .collect()
950            },
951            lanes: {
952                let mut vec: Vec<_> = (0..cd.lanes)
953                    .map(|i| lc.get(&i).map(|c| c.locked).unwrap_or(false))
954                    .map(|locked| (locked, ChartLane::default()))
955                    .map(|(locked, lane)| ChartLane { locked, ..lane })
956                    .collect();
957                for e in cd.elements.into_iter() {
958                    let lane = vec
959                        .get_mut(e.lane)
960                        .ok_or(ErrorKind::InvalidLane(e.lane, cd.lanes))?;
961                    lane.elements.push(e.element);
962                }
963                vec
964            },
965        };
966        Ok(RemoteNapchart { chart, ..meta })
967    }
968}