diff --git a/serde_valid/Cargo.toml b/serde_valid/Cargo.toml index 923e6c3..9396cd5 100644 --- a/serde_valid/Cargo.toml +++ b/serde_valid/Cargo.toml @@ -30,7 +30,8 @@ thiserror = "^1.0" unicode-segmentation = "^1.7" [dev-dependencies] -unic-langid = "0.9.1" +intl-memoizer = "0.5" +unic-langid = "0.9" [features] default = ["i128", "json"] diff --git a/serde_valid/src/features/fluent.rs b/serde_valid/src/features/fluent.rs index 2434835..ecb94a4 100644 --- a/serde_valid/src/features/fluent.rs +++ b/serde_valid/src/features/fluent.rs @@ -4,6 +4,7 @@ mod message; mod try_localize; pub use error::LocalizedError; +pub use fluent_0::FluentValue; pub use localize::Localize; pub use message::Message; pub use try_localize::TryLocalize; diff --git a/serde_valid/src/features/fluent/error.rs b/serde_valid/src/features/fluent/error.rs index 1d48386..6fe382e 100644 --- a/serde_valid/src/features/fluent/error.rs +++ b/serde_valid/src/features/fluent/error.rs @@ -7,3 +7,13 @@ pub enum LocalizedError { Items(ArrayErrors), Properties(ObjectErrors), } + +impl std::fmt::Display for LocalizedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LocalizedError::String(string) => write!(f, "{}", string), + LocalizedError::Items(items) => write!(f, "{}", items), + LocalizedError::Properties(properties) => write!(f, "{}", properties), + } + } +} diff --git a/serde_valid/src/features/fluent/localize.rs b/serde_valid/src/features/fluent/localize.rs index 5f1ed63..922321b 100644 --- a/serde_valid/src/features/fluent/localize.rs +++ b/serde_valid/src/features/fluent/localize.rs @@ -1,4 +1,4 @@ -use fluent_0::{FluentBundle, FluentResource}; +use fluent_0::{bundle::FluentBundle, FluentResource}; use crate::validation::error::{ ArrayErrors, Errors, FormatDefault, ItemErrorsMap, ObjectErrors, PropertyErrorsMap, VecErrors, @@ -9,13 +9,18 @@ use super::{LocalizedError, TryLocalize}; pub trait Localize { type Target; - fn localize(&self, bundle: &FluentBundle) -> Self::Target; + fn localize(&self, bundle: &FluentBundle) -> Self::Target + where + M: fluent_0::memoizer::MemoizerKind; } impl Localize for Errors { type Target = Errors; - fn localize(&self, bundle: &FluentBundle) -> Self::Target { + fn localize(&self, bundle: &FluentBundle) -> Self::Target + where + M: fluent_0::memoizer::MemoizerKind, + { match self { Errors::Array(array) => Errors::Array(array.localize(bundle)), Errors::Object(object) => Errors::Object(object.localize(bundle)), @@ -27,7 +32,10 @@ impl Localize for Errors { impl Localize for ArrayErrors { type Target = ArrayErrors; - fn localize(&self, bundle: &FluentBundle) -> Self::Target { + fn localize(&self, bundle: &FluentBundle) -> Self::Target + where + M: fluent_0::memoizer::MemoizerKind, + { ArrayErrors { errors: self.errors.localize(bundle), items: self.items.localize(bundle), @@ -38,7 +46,10 @@ impl Localize for ArrayErrors { impl Localize for ObjectErrors { type Target = ObjectErrors; - fn localize(&self, bundle: &FluentBundle) -> Self::Target { + fn localize(&self, bundle: &FluentBundle) -> Self::Target + where + M: fluent_0::memoizer::MemoizerKind, + { ObjectErrors { errors: self.errors.localize(bundle), properties: self.properties.localize(bundle), @@ -49,7 +60,10 @@ impl Localize for ObjectErrors { impl Localize for VecErrors { type Target = VecErrors; - fn localize(&self, bundle: &FluentBundle) -> Self::Target { + fn localize(&self, bundle: &FluentBundle) -> Self::Target + where + M: fluent_0::memoizer::MemoizerKind, + { self.iter().map(|error| error.localize(bundle)).collect() } } @@ -57,7 +71,10 @@ impl Localize for VecErrors { impl Localize for ItemErrorsMap { type Target = ItemErrorsMap; - fn localize(&self, bundle: &FluentBundle) -> Self::Target { + fn localize(&self, bundle: &FluentBundle) -> Self::Target + where + M: fluent_0::memoizer::MemoizerKind, + { self.iter() .map(|(index, error)| (*index, error.localize(bundle))) .collect() @@ -67,7 +84,10 @@ impl Localize for ItemErrorsMap { impl Localize for PropertyErrorsMap { type Target = PropertyErrorsMap; - fn localize(&self, bundle: &FluentBundle) -> Self::Target { + fn localize(&self, bundle: &FluentBundle) -> Self::Target + where + M: fluent_0::memoizer::MemoizerKind, + { self.iter() .map(|(property, error)| (property.to_string(), error.localize(bundle))) .collect() @@ -77,7 +97,10 @@ impl Localize for PropertyErrorsMap { impl Localize for crate::validation::Error { type Target = LocalizedError; - fn localize(&self, bundle: &FluentBundle) -> Self::Target { + fn localize(&self, bundle: &FluentBundle) -> Self::Target + where + M: fluent_0::memoizer::MemoizerKind, + { match self { Self::Minimum(message) => message.localize(bundle), Self::Maximum(message) => message.localize(bundle), @@ -109,7 +132,10 @@ where { type Target = LocalizedError; - fn localize(&self, bundle: &FluentBundle) -> Self::Target { + fn localize(&self, bundle: &FluentBundle) -> Self::Target + where + M: fluent_0::memoizer::MemoizerKind, + { self.try_localize(bundle) .unwrap_or_else(|_| LocalizedError::String(self.format_default())) } @@ -118,7 +144,10 @@ where impl Localize for crate::features::fluent::Message { type Target = Option; - fn localize(&self, bundle: &FluentBundle) -> Self::Target { + fn localize(&self, bundle: &FluentBundle) -> Self::Target + where + M: fluent_0::memoizer::MemoizerKind, + { self.try_localize(bundle) .unwrap_or_else(|e: Vec| { Some(LocalizedError::String(format!("FluentErrors: {:?}", e))) @@ -135,22 +164,28 @@ mod test { use serde_json::json; use unic_langid::LanguageIdentifier; - #[test] - fn localize_without_args() -> crate::tests::Result<()> { - let ftl_string = "hello-world = Hello, world!".to_string(); + fn get_bundle() -> FluentBundle { + 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(&bundle))?, + serde_json::to_value(error.localize(&get_bundle()))?, json!("Hello, world!") ); diff --git a/serde_valid/src/features/fluent/try_localize.rs b/serde_valid/src/features/fluent/try_localize.rs index 7d7715a..2e103dd 100644 --- a/serde_valid/src/features/fluent/try_localize.rs +++ b/serde_valid/src/features/fluent/try_localize.rs @@ -1,4 +1,4 @@ -use fluent_0::{FluentArgs, FluentBundle, FluentError, FluentResource}; +use fluent_0::{bundle::FluentBundle, FluentArgs, FluentError, FluentResource}; use crate::validation::error::{ ArrayErrors, Errors, FormatDefault, ItemErrorsMap, ObjectErrors, PropertyErrorsMap, VecErrors, @@ -9,19 +9,24 @@ use super::LocalizedError; pub trait TryLocalize { type Target; - fn try_localize( + fn try_localize( &self, - bundle: &FluentBundle, - ) -> Result>; + bundle: &FluentBundle, + ) -> Result> + where + M: fluent_0::memoizer::MemoizerKind; } impl TryLocalize for Errors { type Target = Errors; - fn try_localize( + fn try_localize( &self, - bundle: &FluentBundle, - ) -> Result> { + bundle: &FluentBundle, + ) -> Result> + where + M: fluent_0::memoizer::MemoizerKind, + { match self { Errors::Array(array) => Ok(Errors::Array(array.try_localize(bundle)?)), Errors::Object(object) => Ok(Errors::Object(object.try_localize(bundle)?)), @@ -33,10 +38,13 @@ impl TryLocalize for Errors { impl TryLocalize for ArrayErrors { type Target = ArrayErrors; - fn try_localize( + fn try_localize( &self, - bundle: &FluentBundle, - ) -> Result> { + bundle: &FluentBundle, + ) -> Result> + where + M: fluent_0::memoizer::MemoizerKind, + { match ( self.errors.try_localize(bundle), self.items.try_localize(bundle), @@ -52,10 +60,13 @@ impl TryLocalize for ArrayErrors { impl TryLocalize for ObjectErrors { type Target = ObjectErrors; - fn try_localize( + fn try_localize( &self, - bundle: &FluentBundle, - ) -> Result> { + bundle: &FluentBundle, + ) -> Result> + where + M: fluent_0::memoizer::MemoizerKind, + { match ( self.errors.try_localize(bundle), self.properties.try_localize(bundle), @@ -71,10 +82,13 @@ impl TryLocalize for ObjectErrors { impl TryLocalize for VecErrors { type Target = VecErrors; - fn try_localize( + fn try_localize( &self, - bundle: &FluentBundle, - ) -> Result> { + bundle: &FluentBundle, + ) -> Result> + where + M: fluent_0::memoizer::MemoizerKind, + { self.iter() .map(|error| error.try_localize(bundle)) .collect() @@ -84,10 +98,13 @@ impl TryLocalize for VecErrors { impl TryLocalize for ItemErrorsMap { type Target = ItemErrorsMap; - fn try_localize( + fn try_localize( &self, - bundle: &FluentBundle, - ) -> Result> { + bundle: &FluentBundle, + ) -> Result> + where + M: fluent_0::memoizer::MemoizerKind, + { let mut errors = vec![]; let target = self .iter() @@ -113,10 +130,13 @@ impl TryLocalize for ItemErrorsMap { impl TryLocalize for PropertyErrorsMap { type Target = PropertyErrorsMap; - fn try_localize( + fn try_localize( &self, - bundle: &FluentBundle, - ) -> Result> { + bundle: &FluentBundle, + ) -> Result> + where + M: fluent_0::memoizer::MemoizerKind, + { let mut errors = vec![]; let target = self .iter() @@ -142,10 +162,13 @@ impl TryLocalize for PropertyErrorsMap { impl TryLocalize for crate::validation::Error { type Target = LocalizedError; - fn try_localize( + fn try_localize( &self, - bundle: &FluentBundle, - ) -> Result> { + bundle: &FluentBundle, + ) -> Result> + where + M: fluent_0::memoizer::MemoizerKind, + { match self { Self::Minimum(message) => message.try_localize(bundle), Self::Maximum(message) => message.try_localize(bundle), @@ -179,10 +202,13 @@ where { type Target = LocalizedError; - fn try_localize( + fn try_localize( &self, - bundle: &FluentBundle, - ) -> Result> { + bundle: &FluentBundle, + ) -> Result> + where + M: fluent_0::memoizer::MemoizerKind, + { if let Some(message) = self.fluent_message() { if let Some(localized) = message.try_localize(bundle)? { return Ok(localized); @@ -195,10 +221,13 @@ where impl TryLocalize for crate::features::fluent::Message { type Target = Option; - fn try_localize( + fn try_localize( &self, - bundle: &FluentBundle, - ) -> Result> { + bundle: &FluentBundle, + ) -> Result> + where + M: fluent_0::memoizer::MemoizerKind, + { if let Some(msg) = bundle.get_message(self.id) { if let Some(pattern) = msg.value() { let mut errors = vec![]; diff --git a/serde_valid/src/validation/error/array_erros.rs b/serde_valid/src/validation/error/array_erros.rs index fc642d6..5f34ada 100644 --- a/serde_valid/src/validation/error/array_erros.rs +++ b/serde_valid/src/validation/error/array_erros.rs @@ -48,7 +48,10 @@ where } } -impl std::fmt::Display for ArrayErrors { +impl std::fmt::Display for ArrayErrors +where + E: serde::Serialize, +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match serde_json::to_string(&self) { Ok(json_string) => { diff --git a/serde_valid/src/validation/error/errors.rs b/serde_valid/src/validation/error/errors.rs index 55de875..b9fff52 100644 --- a/serde_valid/src/validation/error/errors.rs +++ b/serde_valid/src/validation/error/errors.rs @@ -75,7 +75,10 @@ where } } -impl std::fmt::Display for Errors { +impl std::fmt::Display for Errors +where + E: serde::Serialize + std::fmt::Display, +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Array(errors) => std::fmt::Display::fmt(errors, f), diff --git a/serde_valid/tests/fluent_test.rs b/serde_valid/tests/fluent_test.rs new file mode 100644 index 0000000..67d71e6 --- /dev/null +++ b/serde_valid/tests/fluent_test.rs @@ -0,0 +1,54 @@ +#[cfg(feature = "fluent")] +mod tests { + use fluent_0::{FluentBundle, FluentResource}; + use serde::Deserialize; + use serde_json::json; + use serde_valid::{fluent::Localize, Validate}; + use unic_langid::LanguageIdentifier; + + fn get_bundle() -> FluentBundle { + 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 fluent_error() { + #[derive(Debug, Deserialize, Validate)] + struct Test { + #[validate(minimum = 5, fluent("hello-world"))] + a: u32, + #[validate(maximum = 10, fluent("intro", name = "taro"))] + b: u32, + } + + let test = Test { a: 1, b: 11 }; + let a = test.validate().unwrap_err().localize(&get_bundle()); + assert_eq!( + a.to_string(), + json!({ + "errors": [], + "properties": { + "a": { + "errors": [ + "Hello, world!" + ] + }, + "b": { + "errors": [ + "Welcome, \u{2068}taro\u{2069}." + ] + } + } + }) + .to_string() + ); + } +} diff --git a/serde_valid_derive/src/attribute/common/message_format.rs b/serde_valid_derive/src/attribute/common/message_format.rs index 2cf98f1..c318b58 100644 --- a/serde_valid_derive/src/attribute/common/message_format.rs +++ b/serde_valid_derive/src/attribute/common/message_format.rs @@ -121,26 +121,64 @@ fn get_fluent_message( path: &syn::Path, fn_define: &CommaSeparatedNestedMetas, ) -> Result { + use quote::ToTokens; + + use crate::types::CommaSeparatedTokenStreams; + match fn_define.len() { 0 => Err(vec![crate::Error::fluent_need_item(message_type, path)]), - 1 => match &fn_define[0] { - NestedMeta::Lit(syn::Lit::Str(id)) => Ok(quote!( + 1 => { + let id = get_fluent_id(&fn_define[0]) + .ok_or_else(|| vec![crate::Error::fluent_allow_key(message_type, &fn_define[0])])?; + + Ok(quote!( ::serde_valid::validation::error::Format::Fluent( ::serde_valid::fluent::Message{ - id: #id, - args: vec![] - } + id: #id, + args: vec![] + } ) - )), - _ => Err(vec![crate::Error::fluent_allow_key( - message_type, - &fn_define[0], - )]), - }, - _ => Err(fn_define - .iter() - .skip(1) - .map(|args| crate::Error::fluent_allow_args(message_type, args)) - .collect()), + )) + } + _ => { + let mut errors = vec![]; + let id = get_fluent_id(&fn_define[0]) + .ok_or_else(|| vec![crate::Error::fluent_allow_key(message_type, &fn_define[0])])?; + + let args = fn_define + .iter() + .skip(1) + .filter_map(|arg| { + if let NestedMeta::Meta(syn::Meta::NameValue(name_value)) = arg { + let key = &name_value.path.to_token_stream().to_string(); + let value = &name_value.value; + Some(quote!((#key, ::serde_valid::fluent::FluentValue::from(#value)))) + } else { + errors.push(crate::Error::fluent_allow_args(message_type, arg)); + None + } + }) + .collect::(); + if errors.is_empty() { + Ok(quote!( + ::serde_valid::validation::error::Format::Fluent( + ::serde_valid::fluent::Message{ + id: #id, + args: vec![#args] + } + ) + )) + } else { + Err(errors) + } + } + } +} + +#[cfg(feature = "fluent")] +fn get_fluent_id(nested_meta: &NestedMeta) -> Option<&syn::LitStr> { + match nested_meta { + NestedMeta::Lit(syn::Lit::Str(id)) => Some(id), + _ => None, } }