diff --git a/serde_valid/README.md b/serde_valid/README.md index 1340433..e71d215 100644 --- a/serde_valid/README.md +++ b/serde_valid/README.md @@ -176,6 +176,10 @@ assert_eq!( You can also use [fluent](https://projectfluent.org/) localization by using `fluent` feature. +Allow the following attributes: +- `#[validate(..., fluent("message-id", key1 = value1, ...))]` +- `#[validate(..., message_l10n = fluent("message-id", key1 = value1, ...))]` + ```rust use unic_langid::LanguageIdentifier; use serde_json::json; diff --git a/serde_valid/src/lib.rs b/serde_valid/src/lib.rs index 8ea62c3..992cf3b 100644 --- a/serde_valid/src/lib.rs +++ b/serde_valid/src/lib.rs @@ -176,6 +176,10 @@ //! //! You can also use [fluent](https://projectfluent.org/) localization by using `fluent` feature. //! +//! Allow the following attributes: +//! - `#[validate(..., fluent("message-id", key1 = value1, ...))]` +//! - `#[validate(..., message_l10n = fluent("message-id", key1 = value1, ...))]` +//! //! ```rust //! # #[cfg(feature = "fluent")] { //! # use fluent::{FluentBundle, FluentResource}; diff --git a/serde_valid/tests/message_l10n_fluent_test.rs b/serde_valid/tests/message_l10n_fluent_test.rs new file mode 100644 index 0000000..6225566 --- /dev/null +++ b/serde_valid/tests/message_l10n_fluent_test.rs @@ -0,0 +1,54 @@ +#[cfg(feature = "fluent")] +mod tests { + use fluent::{FluentBundle, FluentResource}; + use serde::Deserialize; + use serde_json::json; + use serde_valid::{fluent::Localize, Validate}; + use unic_langid::LanguageIdentifier; + + fn get_bundle(source: impl Into) -> FluentBundle { + let res = FluentResource::try_new(source.into()).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, message_l10n = fluent("hello-world"))] + a: u32, + #[validate(maximum = 10, message_l10n = fluent("intro", name = "taro"))] + b: u32, + } + + let test = Test { a: 1, b: 11 }; + let a = test.validate().unwrap_err().localize(&get_bundle( + ["hello-world = Hello, world!", "intro = Welcome, { $name }."].join("\n"), + )); + + 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.rs b/serde_valid_derive/src/attribute.rs index 6e02277..f9067bd 100644 --- a/serde_valid_derive/src/attribute.rs +++ b/serde_valid_derive/src/attribute.rs @@ -145,9 +145,19 @@ enum_str! { } } +#[cfg(not(feature = "fluent"))] +enum_str! { + pub enum MetaNameValueCustomMessage { + Message = "message", + MessageFn = "message_fn", + } +} + +#[cfg(feature = "fluent")] enum_str! { pub enum MetaNameValueCustomMessage { Message = "message", MessageFn = "message_fn", + MessageL10n = "message_l10n", } } diff --git a/serde_valid_derive/src/attribute/common/message_format.rs b/serde_valid_derive/src/attribute/common/message_format.rs index ee192d2..f59cb6f 100644 --- a/serde_valid_derive/src/attribute/common/message_format.rs +++ b/serde_valid_derive/src/attribute/common/message_format.rs @@ -84,7 +84,7 @@ fn extract_custom_message_format_from_meta_list( }), #[cfg(feature = "fluent")] message_type @ (MetaListCustomMessage::I18n | MetaListCustomMessage::Fluent) => { - get_fluent_message(message_type, path, &message_fn_define) + get_fluent_message_from_meta(message_type, path, &message_fn_define) } } } @@ -96,6 +96,11 @@ fn extract_custom_message_format_from_name_value( match custom_message_type { MetaNameValueCustomMessage::Message => get_message(&name_value.value), MetaNameValueCustomMessage::MessageFn => get_message_fn_from_meta_name_value(name_value), + #[cfg(feature = "fluent")] + MetaNameValueCustomMessage::MessageL10n => match &name_value.value { + syn::Expr::Call(call) => get_fluent_message_from_call_expr(call), + _ => Err(vec![crate::Error::l10n_need_fn_call(&name_value.value)]), + }, } } @@ -151,7 +156,7 @@ fn get_message(expr: &syn::Expr) -> Result, crate::E } #[cfg(feature = "fluent")] -fn get_fluent_message( +fn get_fluent_message_from_meta( message_type: &MetaListCustomMessage, path: &syn::Path, fn_define: &CommaSeparatedNestedMetas, @@ -210,6 +215,54 @@ fn get_fluent_message( } } +#[cfg(feature = "fluent")] +fn get_fluent_message_from_call_expr( + fn_define: &syn::ExprCall, +) -> Result, crate::Errors> { + use quote::ToTokens; + + if fn_define.func.to_token_stream().to_string() != "fluent" { + Err(vec![crate::Error::l10n_fn_name_not_allow(&fn_define.func)])? + }; + + let mut fn_args = fn_define.args.iter(); + let fluent_id = match fn_args.next() { + Some(syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(fluent_id), + .. + })) => fluent_id, + Some(expr) => Err(vec![crate::Error::fluent_id_must_be_str_lit(expr)])?, + None => Err(vec![crate::Error::fluent_id_not_found( + &fn_define.paren_token, + )])?, + }; + + let mut errors = vec![]; + let fluent_args = TokenStream::from_iter(fn_args.filter_map(|arg| { + if let syn::Expr::Assign(assign) = arg { + let key = &assign.left.to_token_stream().to_string(); + let value = &assign.right; + Some(quote!((#key, ::serde_valid::export::fluent::FluentValue::from(#value)))) + } else { + errors.push(crate::Error::fluent_allow_arg(arg)); + None + } + })); + + if errors.is_empty() { + Ok(WithWarnings::new(quote!( + ::serde_valid::validation::error::Format::Fluent( + ::serde_valid::fluent::Message{ + id: #fluent_id, + args: vec![#fluent_args] + } + ) + ))) + } else { + Err(errors) + } +} + #[cfg(feature = "fluent")] fn get_fluent_id(nested_meta: &NestedMeta) -> Option<&syn::LitStr> { match nested_meta { diff --git a/serde_valid_derive/src/error.rs b/serde_valid_derive/src/error.rs index 7f5fe7c..65cdcd0 100644 --- a/serde_valid_derive/src/error.rs +++ b/serde_valid_derive/src/error.rs @@ -348,6 +348,8 @@ impl Error { let candidates = &(MetaPathCustomMessage::iter().map(|x| x.name())) .chain(MetaListCustomMessage::iter().map(|x| x.name())) .chain(MetaNameValueCustomMessage::iter().map(|x| x.name())) + .unique() + .sorted() .collect::>(); let filterd_candidates = @@ -471,6 +473,47 @@ impl Error { ) } + #[cfg(feature = "fluent")] + pub fn l10n_need_fn_call(expr: &syn::Expr) -> Self { + Self::new( + expr.span(), + "#[validate(..., message_l10n = ???)] needs fn calling.".to_string(), + ) + } + + #[cfg(feature = "fluent")] + pub fn l10n_fn_name_not_allow(fn_name: &syn::Expr) -> Self { + Self::new( + fn_name.span(), + "#[validate(..., message_l10n = ???(...))] allows only \"fluent\".".to_string(), + ) + } + + #[cfg(feature = "fluent")] + pub fn fluent_id_must_be_str_lit(expr: &syn::Expr) -> Self { + Self::new( + expr.span(), + "#[validate(..., message_l10n = fluent(???, ...))] allow only string literal of the fluent id.", + ) + } + + #[cfg(feature = "fluent")] + pub fn fluent_id_not_found(paren: &syn::token::Paren) -> Self { + Self::new( + paren.span.span(), + "#[validate(..., message_l10n = fluent(???))] need the fluent id.", + ) + } + + #[cfg(feature = "fluent")] + pub fn fluent_allow_arg(expr: &syn::Expr) -> Self { + Self::new( + expr.span(), + "#[validate(..., message_l10n = fluent(..., ???))] allows only \"key=value\" of the fluent arg." + .to_string(), + ) + } + pub fn literal_only(span: impl Spanned) -> Self { Self::new(span.span(), "Allow literal only.") }