samling 0.13.1

App for managing apparel collections
Documentation
use postgres_types::{private::BytesMut, to_sql_checked, IsNull, ToSql, Type};
use samling_clorinde::JsonSql;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;

#[derive(
    Debug,
    Copy,
    Clone,
    Default,
    Serialize,
    Deserialize,
    Hash,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    strum::EnumIter,
    JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum Language {
    #[default]
    En,
    Sv,
    De,
}

#[derive(
    Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, JsonSchema, Default,
)]
pub struct I18nString {
    #[serde(default)]
    pub en: String,
    #[serde(default)]
    pub sv: String,
    #[serde(default)]
    pub de: String,
}

impl From<String> for I18nString {
    fn from(en: String) -> Self {
        Self::new_en(en)
    }
}

impl I18nString {
    pub fn new_en(en: String) -> Self {
        Self {
            en,
            ..Self::default()
        }
    }
    pub fn new(language: Language, value: String) -> Self {
        match language {
            Language::En => Self {
                en: value,
                ..Self::default()
            },
            Language::Sv => Self {
                sv: value,
                ..Self::default()
            },
            Language::De => Self {
                de: value,
                ..Self::default()
            },
        }
    }

    pub fn get<L: Into<Language>>(&self, language: L) -> &str {
        match language.into() {
            Language::En => &self.en,
            Language::Sv => &self.sv,
            Language::De => &self.de,
        }
    }

    pub fn get_or_default<L: Into<Language>>(&self, language: L) -> &str {
        let val = self.get(language);
        if val.is_empty() {
            &self.en
        } else {
            val
        }
    }

    pub fn set<L: Into<Language>>(&mut self, language: L, value: String) {
        match language.into() {
            Language::En => self.en = value,
            Language::Sv => self.sv = value,
            Language::De => self.de = value,
        }
    }

    pub fn merge(values: Vec<Self>) -> Self {
        let mut out = I18nString::default();
        for value in values {
            for language in Language::iter() {
                let found = value.get(language);
                if out.get(language) == "" && !found.is_empty() {
                    out.set(language, found.to_owned());
                }
            }
        }
        out
    }
}

impl ToSql for I18nString {
    fn to_sql(
        &self,
        ty: &Type,
        w: &mut BytesMut,
    ) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
        <serde_json::Value as ToSql>::to_sql(&serde_json::to_value(self)?, ty, w)
    }

    fn accepts(ty: &Type) -> bool {
        <serde_json::Value as ToSql>::accepts(ty)
    }

    to_sql_checked!();
}

impl JsonSql for I18nString {}

#[cfg(test)]
mod tests {
    use crate::Language;

    use super::I18nString;

    #[test]
    fn i18nstring_merge_works() {
        let values = vec![
            I18nString::new(Language::En, "Hello".to_string()),
            I18nString::new(Language::Sv, "Hej".to_string()),
            I18nString::new(Language::De, "Hallo".to_string()),
        ];
        assert_eq!(
            I18nString::merge(values),
            I18nString {
                en: "Hello".to_string(),
                sv: "Hej".to_string(),
                de: "Hallo".to_string(),
            }
        );
    }

    #[test]
    fn i18nstring_merge_takes_first_nonempty_string() {
        let values = vec![
            I18nString::new(Language::En, "Hello".to_string()),
            I18nString::new(Language::Sv, "".to_string()),
            I18nString::new(Language::Sv, "Tjena".to_string()),
        ];
        assert_eq!(
            I18nString::merge(values),
            I18nString {
                en: "Hello".to_string(),
                sv: "Tjena".to_string(),
                ..Default::default()
            }
        );
    }
}