serde_valid 0.16.0-alpha

JSON Schema based validation tool using with serde.
Documentation
use fluent_0::{FluentArgs, FluentBundle, FluentResource};

use crate::validation::{
    ArrayErrors, Errors, ItemErrorsMap, ObjectErrors, PropertyErrorsMap, VecErrors,
};

pub trait IntoLocalization {
    type Target;

    fn into_localization(self, bundle: &FluentBundle<FluentResource>) -> Self::Target;
}

impl IntoLocalization for Errors<crate::validation::Error> {
    type Target = Errors<String>;

    fn into_localization(self, bundle: &FluentBundle<FluentResource>) -> Self::Target {
        match self {
            Errors::Array(array) => Errors::Array(array.into_localization(bundle)),
            Errors::Object(object) => Errors::Object(object.into_localization(bundle)),
            Errors::NewType(newtype) => Errors::NewType(newtype.into_localization(bundle)),
        }
    }
}

impl IntoLocalization for ArrayErrors<crate::validation::Error> {
    type Target = ArrayErrors<String>;

    fn into_localization(self, bundle: &FluentBundle<FluentResource>) -> Self::Target {
        ArrayErrors {
            errors: self.errors.into_localization(bundle),
            items: self.items.into_localization(bundle),
        }
    }
}

impl IntoLocalization for ObjectErrors<crate::validation::Error> {
    type Target = ObjectErrors<String>;

    fn into_localization(self, bundle: &FluentBundle<FluentResource>) -> Self::Target {
        ObjectErrors {
            errors: self.errors.into_localization(bundle),
            properties: self.properties.into_localization(bundle),
        }
    }
}

impl IntoLocalization for VecErrors<crate::validation::Error> {
    type Target = VecErrors<String>;

    fn into_localization(self, bundle: &FluentBundle<FluentResource>) -> Self::Target {
        self.into_iter()
            .map(|error| error.into_localization(bundle))
            .collect()
    }
}

impl IntoLocalization for ItemErrorsMap<crate::validation::Error> {
    type Target = ItemErrorsMap<String>;

    fn into_localization(self, bundle: &FluentBundle<FluentResource>) -> Self::Target {
        self.into_iter()
            .map(|(index, error)| (index, error.into_localization(bundle)))
            .collect()
    }
}

impl IntoLocalization for PropertyErrorsMap<crate::validation::Error> {
    type Target = PropertyErrorsMap<String>;

    fn into_localization(self, bundle: &FluentBundle<FluentResource>) -> Self::Target {
        self.into_iter()
            .map(|(property, error)| (property, error.into_localization(bundle)))
            .collect()
    }
}

impl IntoLocalization for crate::validation::Error {
    type Target = String;

    fn into_localization(self, bundle: &FluentBundle<FluentResource>) -> Self::Target {
        match self {
            Self::Minimum(message) => localize_or_default(&message, bundle),
            Self::Maximum(message) => localize_or_default(&message, bundle),
            Self::ExclusiveMinimum(message) => localize_or_default(&message, bundle),
            Self::ExclusiveMaximum(message) => localize_or_default(&message, bundle),
            Self::MultipleOf(message) => localize_or_default(&message, bundle),
            Self::MinLength(message) => localize_or_default(&message, bundle),
            Self::MaxLength(message) => localize_or_default(&message, bundle),
            Self::Pattern(message) => localize_or_default(&message, bundle),
            Self::MinItems(message) => localize_or_default(&message, bundle),
            Self::MaxItems(message) => localize_or_default(&message, bundle),
            Self::UniqueItems(message) => localize_or_default(&message, bundle),
            Self::MinProperties(message) => localize_or_default(&message, bundle),
            Self::MaxProperties(message) => localize_or_default(&message, bundle),
            Self::Enumerate(message) => localize_or_default(&message, bundle),
            Self::Custom(message) => message,
            Self::Items(message) => format!("{message}"),
            Self::Properties(message) => format!("{message}"),
            Self::Fluent(message) => {
                localize(Some(&message), bundle).unwrap_or_else(|| format!("{message}"))
            }
        }
    }
}

fn localize(
    message: Option<&crate::fluent::Message>,
    bundle: &FluentBundle<FluentResource>,
) -> Option<String> {
    if let Some(fluent_message) = message {
        if let Some(msg) = bundle.get_message(fluent_message.id) {
            if let Some(pattern) = msg.value() {
                let mut errors = vec![];
                let args = FluentArgs::from_iter(fluent_message.args.to_owned());
                let value = bundle
                    .format_pattern(pattern, Some(&args), &mut errors)
                    .to_string();

                if errors.is_empty() {
                    return Some(value);
                }
            }
        }
    }
    None
}

fn localize_or_default<E>(
    message: &crate::validation::Message<E>,
    bundle: &FluentBundle<FluentResource>,
) -> String {
    if let Some(value) = localize(message.fluent_message.as_ref(), bundle) {
        value
    } else {
        format!("{message}")
    }
}

#[cfg(test)]
mod test {
    use crate::{fluent::Message, validation::CustomMessage};

    use super::*;
    use fluent_0::{FluentResource, FluentValue};
    use serde_valid_literal::Number;
    use unic_langid::LanguageIdentifier;

    #[test]
    fn into_localization_without_args() {
        let ftl_string = "hello-world = Hello, world!".to_string();
        let res = FluentResource::try_new(ftl_string).expect("Failed to parse an FTL string.");

        let langid_en: LanguageIdentifier = "en-US".parse().expect("Parsing failed");
        let mut bundle = FluentBundle::new(vec![langid_en]);
        bundle.add_resource(res).unwrap();

        let error = crate::validation::Error::Fluent(Message {
            id: "hello-world",
            args: vec![],
        });

        assert_eq!(error.into_localization(&bundle), "Hello, world!");
    }

    #[test]
    fn into_localization_with_args() {
        let ftl_string = "intro = Welcome, { $name }.".to_string();
        let res = FluentResource::try_new(ftl_string).expect("Failed to parse an FTL string.");

        let langid_en: LanguageIdentifier = "en-US".parse().expect("Parsing failed");
        let mut bundle = FluentBundle::new(vec![langid_en]);
        bundle.add_resource(res).unwrap();

        let error = crate::validation::Error::Fluent(Message {
            id: "intro",
            args: vec![("name", FluentValue::from("John"))],
        });

        assert_eq!(
            error.into_localization(&bundle),
            "Welcome, \u{2068}John\u{2069}."
        );
    }

    #[test]
    fn into_localization_from_validation_error() {
        let ftl_string = "intro = Welcome, { $name }.".to_string();
        let res = FluentResource::try_new(ftl_string).expect("Failed to parse an FTL string.");

        let langid_en: LanguageIdentifier = "en-US".parse().expect("Parsing failed");
        let mut bundle = FluentBundle::new(vec![langid_en]);
        bundle.add_resource(res).unwrap();

        let error = crate::validation::Error::Maximum(
            CustomMessage {
                message_fn: crate::validation::ToDefaultMessage::to_default_message,
                fluent_message: Some(Message {
                    id: "intro",
                    args: vec![("name", FluentValue::from("John"))],
                }),
            }
            .into_message(crate::MaximumError {
                maximum: Number::I32(10),
            }),
        );

        assert_eq!(
            error.into_localization(&bundle),
            "Welcome, \u{2068}John\u{2069}."
        );
    }
}