serde_valid 2.0.1

JSON Schema based validation tool using serde.
Documentation
use fluent::{bundle::FluentBundle, FluentResource};

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

use super::{LocalizedError, TryLocalize};

pub trait Localize {
    type Target;

    fn localize<M>(&self, bundle: &FluentBundle<FluentResource, M>) -> Self::Target
    where
        M: fluent::memoizer::MemoizerKind;
}

impl Localize for Errors<crate::validation::Error> {
    type Target = Errors<LocalizedError>;

    fn localize<M>(&self, bundle: &FluentBundle<FluentResource, M>) -> Self::Target
    where
        M: fluent::memoizer::MemoizerKind,
    {
        match self {
            Errors::Array(array) => Errors::Array(array.localize(bundle)),
            Errors::Object(object) => Errors::Object(object.localize(bundle)),
            Errors::NewType(newtype) => Errors::NewType(newtype.localize(bundle)),
        }
    }
}

impl Localize for ArrayErrors<crate::validation::Error> {
    type Target = ArrayErrors<LocalizedError>;

    fn localize<M>(&self, bundle: &FluentBundle<FluentResource, M>) -> Self::Target
    where
        M: fluent::memoizer::MemoizerKind,
    {
        ArrayErrors {
            errors: self.errors.localize(bundle),
            items: self.items.localize(bundle),
        }
    }
}

impl Localize for ObjectErrors<crate::validation::Error> {
    type Target = ObjectErrors<LocalizedError>;

    fn localize<M>(&self, bundle: &FluentBundle<FluentResource, M>) -> Self::Target
    where
        M: fluent::memoizer::MemoizerKind,
    {
        ObjectErrors {
            errors: self.errors.localize(bundle),
            properties: self.properties.localize(bundle),
        }
    }
}

impl Localize for VecErrors<crate::validation::Error> {
    type Target = VecErrors<LocalizedError>;

    fn localize<M>(&self, bundle: &FluentBundle<FluentResource, M>) -> Self::Target
    where
        M: fluent::memoizer::MemoizerKind,
    {
        self.iter().map(|error| error.localize(bundle)).collect()
    }
}

impl Localize for ItemErrorsMap<crate::validation::Error> {
    type Target = ItemErrorsMap<LocalizedError>;

    fn localize<M>(&self, bundle: &FluentBundle<FluentResource, M>) -> Self::Target
    where
        M: fluent::memoizer::MemoizerKind,
    {
        self.iter()
            .map(|(index, error)| (*index, error.localize(bundle)))
            .collect()
    }
}

impl Localize for PropertyErrorsMap<crate::validation::Error> {
    type Target = PropertyErrorsMap<LocalizedError>;

    fn localize<M>(&self, bundle: &FluentBundle<FluentResource, M>) -> Self::Target
    where
        M: fluent::memoizer::MemoizerKind,
    {
        self.iter()
            .map(|(property, error)| ((*property).clone(), error.localize(bundle)))
            .collect()
    }
}

impl Localize for crate::validation::Error {
    type Target = LocalizedError;

    fn localize<M>(&self, bundle: &FluentBundle<FluentResource, M>) -> Self::Target
    where
        M: fluent::memoizer::MemoizerKind,
    {
        match self {
            Self::Minimum(message) => message.localize(bundle),
            Self::Maximum(message) => message.localize(bundle),
            Self::ExclusiveMinimum(message) => message.localize(bundle),
            Self::ExclusiveMaximum(message) => message.localize(bundle),
            Self::MultipleOf(message) => message.localize(bundle),
            Self::MinLength(message) => message.localize(bundle),
            Self::MaxLength(message) => message.localize(bundle),
            Self::Pattern(message) => message.localize(bundle),
            Self::MinItems(message) => message.localize(bundle),
            Self::MaxItems(message) => message.localize(bundle),
            Self::UniqueItems(message) => message.localize(bundle),
            Self::MinProperties(message) => message.localize(bundle),
            Self::MaxProperties(message) => message.localize(bundle),
            Self::Enumerate(message) => message.localize(bundle),
            Self::Custom(message) => LocalizedError::String(message.to_string()),
            Self::Items(message) => LocalizedError::Items(message.localize(bundle)),
            Self::Properties(message) => LocalizedError::Properties(message.localize(bundle)),
            Self::Fluent(message) => message.localize(bundle).unwrap_or_else(|| {
                LocalizedError::String(format!("Fluent id not found: \"{}\"", message.id))
            }),
        }
    }
}

impl<E> Localize for crate::validation::error::Message<E>
where
    E: FormatDefault,
{
    type Target = LocalizedError;

    fn localize<M>(&self, bundle: &FluentBundle<FluentResource, M>) -> Self::Target
    where
        M: fluent::memoizer::MemoizerKind,
    {
        self.try_localize(bundle)
            .unwrap_or_else(|_| LocalizedError::String(self.format_default()))
    }
}

impl Localize for crate::features::fluent::Message {
    type Target = Option<LocalizedError>;

    fn localize<M>(&self, bundle: &FluentBundle<FluentResource, M>) -> Self::Target
    where
        M: fluent::memoizer::MemoizerKind,
    {
        self.try_localize(bundle)
            .unwrap_or_else(|e: Vec<fluent::FluentError>| {
                Some(LocalizedError::String(format!("FluentErrors: {:?}", e)))
            })
    }
}

#[cfg(test)]
mod test {
    use crate::fluent::Message;

    use super::*;
    use fluent::{FluentBundle, FluentResource, FluentValue};
    use serde_json::json;
    use unic_langid::LanguageIdentifier;

    fn get_bundle() -> FluentBundle<FluentResource> {
        let ftl_string = ["hello-world = Hello, world!", "intro = Welcome, { $name }."]
            .join("\n")
            .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();

        bundle
    }

    #[test]
    fn localize_without_args() -> crate::tests::Result<()> {
        let error = crate::validation::Error::Fluent(Message {
            id: "hello-world",
            args: vec![],
        });

        assert_eq!(
            serde_json::to_value(error.localize(&get_bundle()))?,
            json!("Hello, world!")
        );

        Ok(())
    }

    #[test]
    fn localize_fluetn_id_not_found() -> crate::tests::Result<()> {
        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",
            args: vec![],
        });

        assert_eq!(
            serde_json::to_value(error.localize(&bundle))?,
            json!("Fluent id not found: \"hello\"")
        );

        Ok(())
    }

    #[test]
    fn try_localize_with_args() -> crate::tests::Result<()> {
        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!(
            serde_json::to_value(error.localize(&bundle))?,
            json!("Welcome, \u{2068}John\u{2069}.")
        );

        Ok(())
    }

    #[test]
    fn try_localize_from_validation_error() -> crate::tests::Result<()> {
        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(
            crate::validation::error::Format::Fluent(Message {
                id: "intro",
                args: vec![("name", FluentValue::from("John"))],
            })
            .into_message(crate::MaximumError {
                maximum: serde_valid_literal::Number::I32(10),
            }),
        );

        assert_eq!(
            serde_json::to_value(error.localize(&bundle))?,
            json!("Welcome, \u{2068}John\u{2069}.")
        );

        Ok(())
    }
}