use serde::de::{Deserialize, Deserializer};
use serde::ser::{Serialize, SerializeStruct, Serializer};
use serde::{de, ser};
use crate::{Color, Identifier, Name, Plist};
#[derive(Debug, Clone, PartialEq)]
pub struct Guideline {
pub line: Line,
pub name: Option<Name>,
pub color: Option<Color>,
identifier: Option<Identifier>,
lib: Option<Plist>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Line {
Vertical(f64),
Horizontal(f64),
Angle {
x: f64,
y: f64,
degrees: f64,
},
}
impl Guideline {
pub fn new(
line: Line,
name: Option<Name>,
color: Option<Color>,
identifier: Option<Identifier>,
lib: Option<Plist>,
) -> Self {
let mut this = Self { line, name, color, identifier: None, lib: None };
if let Some(id) = identifier {
this.replace_identifier(id);
}
if let Some(lib) = lib {
this.replace_lib(lib);
}
this
}
pub fn lib(&self) -> Option<&Plist> {
self.lib.as_ref()
}
pub fn lib_mut(&mut self) -> Option<&mut Plist> {
self.lib.as_mut()
}
pub fn replace_lib(&mut self, lib: Plist) -> Option<Plist> {
if self.identifier.is_none() {
self.identifier.replace(Identifier::from_uuidv4());
}
self.lib.replace(lib)
}
pub fn take_lib(&mut self) -> Option<Plist> {
self.lib.take()
}
pub fn identifier(&self) -> Option<&Identifier> {
self.identifier.as_ref()
}
pub fn replace_identifier(&mut self, id: Identifier) -> Option<Identifier> {
self.identifier.replace(id)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawGuideline {
x: Option<f64>,
y: Option<f64>,
angle: Option<f64>,
name: Option<Name>,
color: Option<Color>,
identifier: Option<Identifier>,
}
impl Serialize for Guideline {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let (x, y, angle) = match self.line {
Line::Vertical(x) => (Some(x), None, None),
Line::Horizontal(y) => (None, Some(y), None),
Line::Angle { x, y, degrees } => {
if !(0.0..=360.0).contains(°rees) {
return Err(ser::Error::custom("angle must be between 0 and 360 degrees."));
}
(Some(x), Some(y), Some(degrees))
}
};
let mut guideline = serializer.serialize_struct("RawGuideline", 6)?;
guideline.serialize_field("x", &x)?;
guideline.serialize_field("y", &y)?;
guideline.serialize_field("angle", &angle)?;
guideline.serialize_field("name", &self.name)?;
guideline.serialize_field("color", &self.color)?;
guideline.serialize_field("identifier", &self.identifier)?;
guideline.end()
}
}
impl<'de> Deserialize<'de> for Guideline {
fn deserialize<D>(deserializer: D) -> Result<Guideline, D::Error>
where
D: Deserializer<'de>,
{
let guideline = RawGuideline::deserialize(deserializer)?;
let x = guideline.x;
let y = guideline.y;
let angle = guideline.angle;
let line = match (x, y, angle) {
(Some(x), None, None) => Line::Vertical(x),
(None, Some(y), None) => Line::Horizontal(y),
(Some(x), Some(y), Some(degrees)) => {
if !(0.0..=360.0).contains(°rees) {
return Err(de::Error::custom("angle must be between 0 and 360 degrees."));
}
Line::Angle { x, y, degrees }
}
(None, None, _) => {
return Err(de::Error::custom("x or y must be present in a guideline."))
}
(None, Some(_), Some(_)) | (Some(_), None, Some(_)) => {
return Err(de::Error::custom(
"angle must only be specified when both x and y are specified.",
))
}
(Some(_), Some(_), None) => {
return Err(de::Error::custom(
"angle must be specified when both x and y are specified.",
))
}
};
Ok(Guideline::new(line, guideline.name, guideline.color, guideline.identifier, None))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_test::{assert_tokens, Token};
#[test]
fn guideline_parsing() {
let g1 = Guideline::new(
Line::Angle { x: 10.0, y: 20.0, degrees: 360.0 },
Some(Name::new_raw("hello")),
Some(Color::new(0.0, 0.5, 0.0, 0.5).unwrap()),
Some(Identifier::new_raw("abcABC123")),
None,
);
assert_tokens(
&g1,
&[
Token::Struct { name: "RawGuideline", len: 6 },
Token::Str("x"),
Token::Some,
Token::F64(10.0),
Token::Str("y"),
Token::Some,
Token::F64(20.0),
Token::Str("angle"),
Token::Some,
Token::F64(360.0),
Token::Str("name"),
Token::Some,
Token::Str("hello"),
Token::Str("color"),
Token::Some,
Token::Str("0,0.5,0,0.5"),
Token::Str("identifier"),
Token::Some,
Token::Str("abcABC123"),
Token::StructEnd,
],
);
}
}