use std::fmt;
use serde::de::{self, Deserialize, Visitor, Unexpected};
use super::super::{Caption, DEFAULT_FONT, DEFAULT_HALIGN, DEFAULT_COLOR, DEFAULT_OUTLINE_COLOR};
const FIELDS: &'static [&'static str] = &[
"text", "align", "valign", "font", "color", "outline",
];
const REQUIRED_FIELDS_COUNT: usize = 2;
const EXPECTING_MSG: &'static str = "map or struct with image macro caption";
lazy_static! {
static ref EXPECTING_FIELD_COUNT_MSG: String = format!(
"at least {} and no more than {}", REQUIRED_FIELDS_COUNT, FIELDS.len());
}
impl<'de> Deserialize<'de> for Caption {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: de::Deserializer<'de>
{
deserializer.deserialize_map(CaptionVisitor)
}
}
struct CaptionVisitor;
impl<'de> Visitor<'de> for CaptionVisitor {
type Value = Caption;
fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "{}", EXPECTING_MSG)
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where V: de::MapAccess<'de>
{
if let Some(size) = map.size_hint() {
if size < REQUIRED_FIELDS_COUNT || size > FIELDS.len() {
return Err(de::Error::invalid_length(
size, &(&*EXPECTING_FIELD_COUNT_MSG as &str)));
}
}
let mut text = None;
let mut halign = None;
let mut valign = None;
let mut font = None;
let mut color = None;
let mut outline: Option<Option<_>> = None;
while let Some(key) = map.next_key::<String>()? {
let key = key.trim().to_lowercase();
match key.as_str() {
"text" => {
if text.is_some() {
return Err(de::Error::duplicate_field("text"));
}
let value: String = map.next_value()?;
if value.is_empty() {
return Err(de::Error::invalid_value(
Unexpected::Str(&value), &"non-empty string"));
}
text = Some(value);
}
"align" | "halign" => {
if halign.is_some() {
return Err(de::Error::duplicate_field("align"));
}
halign = Some(map.next_value()?);
}
"valign" => {
if valign.is_some() {
return Err(de::Error::duplicate_field("valign"));
}
valign = Some(map.next_value()?);
}
"font" => {
if font.is_some() {
return Err(de::Error::duplicate_field("font"));
}
font = Some(map.next_value()?);
}
"color" => {
if color.is_some() {
return Err(de::Error::duplicate_field("color"));
}
color = Some(map.next_value()?);
}
"outline" => {
if outline.is_some() {
return Err(de::Error::duplicate_field("outline"));
}
outline = Some(map.next_value()?);
}
key => return Err(de::Error::unknown_field(key, FIELDS)),
}
}
let text = text.ok_or_else(|| de::Error::missing_field("text"))?;
let halign = halign.unwrap_or(DEFAULT_HALIGN);
let valign = valign.ok_or_else(|| de::Error::missing_field("valign"))?;
let font = font.unwrap_or(DEFAULT_FONT).into();
let color = color.unwrap_or(DEFAULT_COLOR);
let outline = outline.unwrap_or_else(|| Some(DEFAULT_OUTLINE_COLOR));
Ok(Caption{text, halign, valign, font, color, outline})
}
}
#[cfg(test)]
mod tests {
mod generic {
use itertools::Itertools;
use serde_test::{assert_de_tokens, assert_de_tokens_error, Token as T};
use ::model::{Color, HAlign, VAlign};
use super::super::{Caption, EXPECTING_FIELD_COUNT_MSG, EXPECTING_MSG, FIELDS};
lazy_static! {
static ref EXPECTING_FIELD_MSG: String = format!("one of {}",
FIELDS.iter().format_with(", ", |x, f| f(&format_args!("`{}`", x))));
}
#[test]
fn must_be_map() {
assert_de_tokens_error::<Caption>(
&[T::Unit],
&format!("invalid type: unit value, expected {}", EXPECTING_MSG));
assert_de_tokens_error::<Caption>(
&[T::Bool(true)],
&format!("invalid type: boolean `true`, expected {}", EXPECTING_MSG));
assert_de_tokens_error::<Caption>(
&[T::I32(42)],
&format!("invalid type: integer `42`, expected {}", EXPECTING_MSG));
assert_de_tokens_error::<Caption>(
&[T::Char(0x42 as char)],
&format!(r#"invalid type: string "B", expected {}"#, EXPECTING_MSG));
assert_de_tokens_error::<Caption>(
&[T::Tuple { len: 1 }, T::Str("foo")],
&format!("invalid type: sequence, expected {}", EXPECTING_MSG));
assert_de_tokens_error::<Caption>(
&[T::Str("test")],
&format!(r#"invalid type: string "test", expected {}"#, EXPECTING_MSG));
assert_de_tokens_error::<Caption>(
&[T::String("test")],
&format!(r#"invalid type: string "test", expected {}"#, EXPECTING_MSG));
}
#[test]
fn must_have_required_fields() {
assert_de_tokens_error::<Caption>(
&[T::Map{len: Some(1)}],
&format!("invalid length 1, expected {}", *EXPECTING_FIELD_COUNT_MSG));
assert_de_tokens_error::<Caption>(
&[T::Map { len: None }, T::MapEnd],
"missing field `text`");
assert_de_tokens_error::<Caption>(&[
T::Map { len: None },
T::Str("something"), T::Str("or other"),
], &format!("unknown field `something`, expected {}", *EXPECTING_FIELD_MSG));
assert_de_tokens_error::<Caption>(&[
T::Map { len: None },
T::Str("text"), T::Str("very caption"),
T::MapEnd,
], "missing field `valign`");
assert_de_tokens(&Caption::text_at(VAlign::Top, "Test"), &[
T::Map { len: None },
T::Str("text"), T::Str("Test"),
T::Str("valign"), T::Enum{name: "VAlign"}, T::Str("top"), T::Unit,
T::MapEnd,
]);
assert_de_tokens_error::<Caption>(&[
T::Map { len: None },
T::Str("text"), T::Str(""),
], r#"invalid value: string "", expected non-empty string"#);
}
#[test]
fn can_have_optional_fields() {
assert_de_tokens(
&Caption{halign: HAlign::Center, ..Caption::text_at(VAlign::Top, "Test")},
&[
T::Map { len: None },
T::Str("text"), T::Str("Test"),
T::Str("valign"), T::Enum{name: "VAlign"}, T::Str("top"), T::Unit,
T::Str("halign"), T::Enum{name: "HAlign"}, T::Str("center"), T::Unit,
T::MapEnd,
]);
assert_de_tokens(
&Caption{font: "Comic Sans".into(), ..Caption::text_at(VAlign::Top, "Test")},
&[
T::Map { len: None },
T::Str("text"), T::Str("Test"),
T::Str("valign"), T::Enum{name: "VAlign"}, T::Str("top"), T::Unit,
T::Str("font"), T::BorrowedStr("Comic Sans"),
T::MapEnd,
]);
assert_de_tokens(
&Caption{color: Color(1, 2, 3), ..Caption::text_at(VAlign::Top, "Test")},
&[
T::Map { len: None },
T::Str("text"), T::Str("Test"),
T::Str("valign"), T::Enum{name: "VAlign"}, T::Str("top"), T::Unit,
T::Str("color"), T::Seq { len: Some(3) }, T::U8(1), T::U8(2), T::U8(3), T::SeqEnd,
T::MapEnd,
]);
assert_de_tokens_error::<Caption>(
&[T::Map{len: Some(9)}],
&format!("invalid length 9, expected {}", *EXPECTING_FIELD_COUNT_MSG));
}
#[test]
fn can_have_null_outline() {
assert_de_tokens(
&Caption{outline: None, ..Caption::text_at(VAlign::Top, "Test")},
&[
T::Map { len: None },
T::Str("text"), T::Str("Test"),
T::Str("valign"), T::Enum{name: "VAlign"}, T::Str("top"), T::Unit,
T::Str("outline"), T::None,
T::MapEnd,
]);
}
}
mod json {
use serde_json::from_value as from_json;
use spectral::prelude::*;
use ::model::{Color, Caption, DEFAULT_OUTLINE_COLOR};
#[test]
fn required_fields() {
assert_that!(from_json::<Caption>(json!({"text": "Test"})))
.is_err().matches(|e| format!("{}", e).contains("at least"));
assert_that!(from_json::<Caption>(json!({"halign": "left", "valign": "top"})))
.is_err().matches(|e| format!("{}", e).contains("text"));
assert_that!(from_json::<Caption>(json!({"text": "", "valign": "center"})))
.is_err().matches(|e| format!("{}", e).contains("non-empty string"));
}
#[test]
fn default_outline() {
let caption = json!({"text": "Test", "valign": "top"});
assert_that!(from_json::<Caption>(caption)).is_ok()
.map(|c| &c.outline).is_some().is_equal_to(&DEFAULT_OUTLINE_COLOR);
}
#[test]
fn default_outline_around_non_default_color() {
let caption = json!({"text": "Test", "valign": "top", "color": [0, 0, 255]});
assert_that!(from_json::<Caption>(caption)).is_ok()
.map(|c| &c.outline).is_some().is_equal_to(&DEFAULT_OUTLINE_COLOR);
}
#[test]
fn outline_custom_color() {
let caption = json!({"text": "Test", "valign": "top", "outline": "red"});
assert_that!(from_json::<Caption>(caption)).is_ok()
.map(|c| &c.outline).is_some().is_equal_to(&Color(0xff, 0x0, 0x0));
}
#[test]
fn outline_disabled_if_null() {
let caption = json!({"text": "Test", "valign": "top", "outline": null});
assert_that!(from_json::<Caption>(caption)).is_ok()
.map(|c| &c.outline).is_none();
}
}
}