use std::hash::Hash;
use conflate::MergeFrom;
use indexmap::IndexMap;
use super::{Color, HAlign, VAlign};
#[non_exhaustive]
#[derive(Clone, Debug, conflate::Merge)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(rename = "Meme", deny_unknown_fields))]
pub struct PartialMeme {
#[merge(strategy = always_overwrite)]
#[cfg_attr(feature = "schemars", schemars(flatten))]
pub base: PartialMemeBase,
#[merge(strategy = append_or_recurse_indexmap)]
#[cfg_attr(feature = "schemars", schemars(default))]
pub text: IndexMap<String, PartialText>,
}
#[non_exhaustive]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(
feature = "schemars",
schemars(rename = "MemeBase", rename_all = "kebab-case")
)]
pub enum PartialMemeBase {
Canvas(PartialCanvas),
Image(String),
Extends(String),
}
#[cfg(feature = "serde")]
impl PartialMemeBase {
fn as_key(&self) -> PartialMemeKey {
match self {
PartialMemeBase::Canvas(_) => PartialMemeKey::Canvas,
PartialMemeBase::Image(_) => PartialMemeKey::Image,
PartialMemeBase::Extends(_) => PartialMemeKey::Extends,
}
}
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize, strum::VariantNames, strum::IntoStaticStr, strum::Display)]
#[serde(field_identifier, rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
enum PartialMemeKey {
#[serde(rename = "$schema")]
#[strum(to_string = "$schema")]
Schema,
Canvas,
Image,
Extends,
Text,
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for PartialMeme {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct PartialMemeVisitor;
impl<'de> serde::de::Visitor<'de> for PartialMemeVisitor {
type Value = PartialMeme;
fn expecting(&self, _: &mut std::fmt::Formatter) -> std::fmt::Result {
unimplemented!()
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut base: Option<PartialMemeBase> = None;
let mut text = None;
while let Some(key) = map.next_key()? {
match key {
PartialMemeKey::Schema => continue,
PartialMemeKey::Canvas => {
if let Some(base) = base {
return Err(serde::de::Error::custom(format!(
"conflicting field: {}",
base.as_key()
)));
}
base = Some(PartialMemeBase::Canvas(map.next_value()?));
}
PartialMemeKey::Image => {
if let Some(base) = base {
return Err(serde::de::Error::custom(format!(
"conflicting field: {}",
base.as_key()
)));
}
base = Some(PartialMemeBase::Image(map.next_value()?));
}
PartialMemeKey::Extends => {
if let Some(base) = base {
return Err(serde::de::Error::custom(format!(
"conflicting field: {}",
base.as_key()
)));
}
base = Some(PartialMemeBase::Extends(map.next_value()?));
}
PartialMemeKey::Text => {
if text.is_some() {
return Err(serde::de::Error::duplicate_field(key.into()));
}
text = Some(map.next_value()?);
}
}
}
Ok(PartialMeme {
base: base.ok_or_else(|| {
serde::de::Error::custom(format!(
"missing field, expected one of `{}`, `{}`, `{}`",
PartialMemeKey::Canvas,
PartialMemeKey::Image,
PartialMemeKey::Extends,
))
})?,
text: text.unwrap_or_default(),
})
}
}
deserializer.deserialize_struct(
"Meme",
<PartialMemeKey as strum::VariantNames>::VARIANTS,
PartialMemeVisitor,
)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, conflate::Merge)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename = "Canvas", deny_unknown_fields))]
#[merge(strategy = conflate::option::overwrite_none)]
pub struct PartialCanvas {
#[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
pub color: Option<Color>,
pub size: Option<(u32, u32)>,
}
#[non_exhaustive]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(rename = "Text", untagged))]
pub enum PartialText {
Text(String),
TextBox(PartialTextBox),
}
impl Default for PartialText {
fn default() -> Self {
Self::TextBox(Default::default())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for PartialText {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
serde_untagged::UntaggedEnumVisitor::new()
.string(|s| Ok(PartialText::Text(s.to_owned())))
.map(|map| map.deserialize().map(PartialText::TextBox))
.deserialize(deserializer)
}
}
impl conflate::Merge for PartialText {
fn merge(&mut self, parent: Self) {
*self = match (std::mem::take(self), parent) {
(Self::Text(text), Self::TextBox(text_box)) => Self::TextBox(PartialTextBox {
text: Some(text),
..text_box
}),
(Self::TextBox(text_box), Self::TextBox(parent_text_box)) => {
Self::TextBox(text_box.merge_from(parent_text_box))
}
(Self::TextBox(mut text_box), Self::Text(parent_text)) if text_box.text.is_none() => {
text_box.text = Some(parent_text);
Self::TextBox(text_box)
}
(slf, _) => slf,
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, conflate::Merge)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(rename = "TextBox", deny_unknown_fields, rename_all = "kebab-case")
)]
#[merge(strategy = conflate::option::overwrite_none)]
pub struct PartialTextBox {
pub text: Option<String>,
pub position: Option<(f32, f32)>,
pub size: Option<(f32, f32)>,
pub rotate: Option<f32>,
pub font: Option<String>,
pub caps: Option<bool>,
#[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
pub color: Option<Color>,
#[merge(strategy = conflate::option::recurse)]
pub outline: Option<PartialTextOutline>,
pub font_size: Option<f32>,
pub line_height: Option<f32>,
pub halign: Option<HAlign>,
pub valign: Option<VAlign>,
}
#[non_exhaustive]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(rename = "TextOutline", untagged))]
pub enum PartialTextOutline {
Enabled(bool),
Parameters(PartialTextOutlineParameters),
}
impl Default for PartialTextOutline {
fn default() -> Self {
Self::Parameters(Default::default())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for PartialTextOutline {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
serde_untagged::UntaggedEnumVisitor::new()
.bool(|v| Ok(PartialTextOutline::Enabled(v)))
.map(|v| v.deserialize().map(PartialTextOutline::Parameters))
.deserialize(deserializer)
}
}
impl conflate::Merge for PartialTextOutline {
fn merge(&mut self, parent: Self) {
*self = match (std::mem::take(self), parent) {
(Self::Enabled(enabled), Self::Parameters(parent_params)) => {
Self::Parameters(PartialTextOutlineParameters {
enabled,
..parent_params
})
}
(Self::Parameters(params), Self::Parameters(parent_params)) => {
Self::Parameters(params.merge_from(parent_params))
}
(slf, _) => slf,
};
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, conflate::Merge)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[merge(strategy = conflate::option::overwrite_none)]
#[cfg_attr(
feature = "serde",
serde(
rename = "TextOutlineParameters",
deny_unknown_fields,
rename_all = "kebab-case"
)
)]
pub struct PartialTextOutlineParameters {
#[merge(skip)]
#[cfg_attr(feature = "serde", serde(default = "true_"))]
pub enabled: bool,
#[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
pub color: Option<Color>,
pub width: Option<f32>,
}
#[cfg(feature = "serde")]
const fn true_() -> bool {
true
}
fn always_overwrite<T>(left: &mut T, right: T) {
*left = right;
}
fn append_or_recurse_indexmap<K: Eq + Hash, V: conflate::Merge>(
left: &mut IndexMap<K, V>,
right: IndexMap<K, V>,
) {
use indexmap::map::Entry;
for (k, v) in right {
match left.entry(k) {
Entry::Occupied(mut existing) => existing.get_mut().merge(v),
Entry::Vacant(empty) => {
empty.insert(v);
}
}
}
}