Skip to content

Commit 8e1ca40

Browse files
authored
Merge pull request #77 from yassun7010/replace_fluent_l10n
Add messsage_l10n
2 parents 3d6f0f5 + d1741c5 commit 8e1ca40

File tree

6 files changed

+170
-2
lines changed

6 files changed

+170
-2
lines changed

serde_valid/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ assert_eq!(
176176

177177
You can also use [fluent](https://projectfluent.org/) localization by using `fluent` feature.
178178

179+
Allow the following attributes:
180+
- `#[validate(..., fluent("message-id", key1 = value1, ...))]`
181+
- `#[validate(..., message_l10n = fluent("message-id", key1 = value1, ...))]`
182+
179183
```rust
180184
use unic_langid::LanguageIdentifier;
181185
use serde_json::json;

serde_valid/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@
176176
//!
177177
//! You can also use [fluent](https://projectfluent.org/) localization by using `fluent` feature.
178178
//!
179+
//! Allow the following attributes:
180+
//! - `#[validate(..., fluent("message-id", key1 = value1, ...))]`
181+
//! - `#[validate(..., message_l10n = fluent("message-id", key1 = value1, ...))]`
182+
//!
179183
//! ```rust
180184
//! # #[cfg(feature = "fluent")] {
181185
//! # use fluent::{FluentBundle, FluentResource};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#[cfg(feature = "fluent")]
2+
mod tests {
3+
use fluent::{FluentBundle, FluentResource};
4+
use serde::Deserialize;
5+
use serde_json::json;
6+
use serde_valid::{fluent::Localize, Validate};
7+
use unic_langid::LanguageIdentifier;
8+
9+
fn get_bundle(source: impl Into<String>) -> FluentBundle<FluentResource> {
10+
let res = FluentResource::try_new(source.into()).expect("Failed to parse an FTL string.");
11+
12+
let langid_en: LanguageIdentifier = "en-US".parse().expect("Parsing failed");
13+
let mut bundle = FluentBundle::new(vec![langid_en]);
14+
bundle.add_resource(res).unwrap();
15+
16+
bundle
17+
}
18+
19+
#[test]
20+
fn fluent_error() {
21+
#[derive(Debug, Deserialize, Validate)]
22+
struct Test {
23+
#[validate(minimum = 5, message_l10n = fluent("hello-world"))]
24+
a: u32,
25+
#[validate(maximum = 10, message_l10n = fluent("intro", name = "taro"))]
26+
b: u32,
27+
}
28+
29+
let test = Test { a: 1, b: 11 };
30+
let a = test.validate().unwrap_err().localize(&get_bundle(
31+
["hello-world = Hello, world!", "intro = Welcome, { $name }."].join("\n"),
32+
));
33+
34+
assert_eq!(
35+
a.to_string(),
36+
json!({
37+
"errors": [],
38+
"properties": {
39+
"a": {
40+
"errors": [
41+
"Hello, world!"
42+
]
43+
},
44+
"b": {
45+
"errors": [
46+
"Welcome, \u{2068}taro\u{2069}."
47+
]
48+
}
49+
}
50+
})
51+
.to_string()
52+
);
53+
}
54+
}

serde_valid_derive/src/attribute.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,19 @@ enum_str! {
145145
}
146146
}
147147

148+
#[cfg(not(feature = "fluent"))]
149+
enum_str! {
150+
pub enum MetaNameValueCustomMessage {
151+
Message = "message",
152+
MessageFn = "message_fn",
153+
}
154+
}
155+
156+
#[cfg(feature = "fluent")]
148157
enum_str! {
149158
pub enum MetaNameValueCustomMessage {
150159
Message = "message",
151160
MessageFn = "message_fn",
161+
MessageL10n = "message_l10n",
152162
}
153163
}

serde_valid_derive/src/attribute/common/message_format.rs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ fn extract_custom_message_format_from_meta_list(
8484
}),
8585
#[cfg(feature = "fluent")]
8686
message_type @ (MetaListCustomMessage::I18n | MetaListCustomMessage::Fluent) => {
87-
get_fluent_message(message_type, path, &message_fn_define)
87+
get_fluent_message_from_meta(message_type, path, &message_fn_define)
8888
}
8989
}
9090
}
@@ -96,6 +96,11 @@ fn extract_custom_message_format_from_name_value(
9696
match custom_message_type {
9797
MetaNameValueCustomMessage::Message => get_message(&name_value.value),
9898
MetaNameValueCustomMessage::MessageFn => get_message_fn_from_meta_name_value(name_value),
99+
#[cfg(feature = "fluent")]
100+
MetaNameValueCustomMessage::MessageL10n => match &name_value.value {
101+
syn::Expr::Call(call) => get_fluent_message_from_call_expr(call),
102+
_ => Err(vec![crate::Error::l10n_need_fn_call(&name_value.value)]),
103+
},
99104
}
100105
}
101106

@@ -151,7 +156,7 @@ fn get_message(expr: &syn::Expr) -> Result<WithWarnings<MessageFormat>, crate::E
151156
}
152157

153158
#[cfg(feature = "fluent")]
154-
fn get_fluent_message(
159+
fn get_fluent_message_from_meta(
155160
message_type: &MetaListCustomMessage,
156161
path: &syn::Path,
157162
fn_define: &CommaSeparatedNestedMetas,
@@ -210,6 +215,54 @@ fn get_fluent_message(
210215
}
211216
}
212217

218+
#[cfg(feature = "fluent")]
219+
fn get_fluent_message_from_call_expr(
220+
fn_define: &syn::ExprCall,
221+
) -> Result<WithWarnings<MessageFormat>, crate::Errors> {
222+
use quote::ToTokens;
223+
224+
if fn_define.func.to_token_stream().to_string() != "fluent" {
225+
Err(vec![crate::Error::l10n_fn_name_not_allow(&fn_define.func)])?
226+
};
227+
228+
let mut fn_args = fn_define.args.iter();
229+
let fluent_id = match fn_args.next() {
230+
Some(syn::Expr::Lit(syn::ExprLit {
231+
lit: syn::Lit::Str(fluent_id),
232+
..
233+
})) => fluent_id,
234+
Some(expr) => Err(vec![crate::Error::fluent_id_must_be_str_lit(expr)])?,
235+
None => Err(vec![crate::Error::fluent_id_not_found(
236+
&fn_define.paren_token,
237+
)])?,
238+
};
239+
240+
let mut errors = vec![];
241+
let fluent_args = TokenStream::from_iter(fn_args.filter_map(|arg| {
242+
if let syn::Expr::Assign(assign) = arg {
243+
let key = &assign.left.to_token_stream().to_string();
244+
let value = &assign.right;
245+
Some(quote!((#key, ::serde_valid::export::fluent::FluentValue::from(#value))))
246+
} else {
247+
errors.push(crate::Error::fluent_allow_arg(arg));
248+
None
249+
}
250+
}));
251+
252+
if errors.is_empty() {
253+
Ok(WithWarnings::new(quote!(
254+
::serde_valid::validation::error::Format::Fluent(
255+
::serde_valid::fluent::Message{
256+
id: #fluent_id,
257+
args: vec![#fluent_args]
258+
}
259+
)
260+
)))
261+
} else {
262+
Err(errors)
263+
}
264+
}
265+
213266
#[cfg(feature = "fluent")]
214267
fn get_fluent_id(nested_meta: &NestedMeta) -> Option<&syn::LitStr> {
215268
match nested_meta {

serde_valid_derive/src/error.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ impl Error {
348348
let candidates = &(MetaPathCustomMessage::iter().map(|x| x.name()))
349349
.chain(MetaListCustomMessage::iter().map(|x| x.name()))
350350
.chain(MetaNameValueCustomMessage::iter().map(|x| x.name()))
351+
.unique()
352+
.sorted()
351353
.collect::<Vec<_>>();
352354

353355
let filterd_candidates =
@@ -471,6 +473,47 @@ impl Error {
471473
)
472474
}
473475

476+
#[cfg(feature = "fluent")]
477+
pub fn l10n_need_fn_call(expr: &syn::Expr) -> Self {
478+
Self::new(
479+
expr.span(),
480+
"#[validate(..., message_l10n = ???)] needs fn calling.".to_string(),
481+
)
482+
}
483+
484+
#[cfg(feature = "fluent")]
485+
pub fn l10n_fn_name_not_allow(fn_name: &syn::Expr) -> Self {
486+
Self::new(
487+
fn_name.span(),
488+
"#[validate(..., message_l10n = ???(...))] allows only \"fluent\".".to_string(),
489+
)
490+
}
491+
492+
#[cfg(feature = "fluent")]
493+
pub fn fluent_id_must_be_str_lit(expr: &syn::Expr) -> Self {
494+
Self::new(
495+
expr.span(),
496+
"#[validate(..., message_l10n = fluent(???, ...))] allow only string literal of the fluent id.",
497+
)
498+
}
499+
500+
#[cfg(feature = "fluent")]
501+
pub fn fluent_id_not_found(paren: &syn::token::Paren) -> Self {
502+
Self::new(
503+
paren.span.span(),
504+
"#[validate(..., message_l10n = fluent(???))] need the fluent id.",
505+
)
506+
}
507+
508+
#[cfg(feature = "fluent")]
509+
pub fn fluent_allow_arg(expr: &syn::Expr) -> Self {
510+
Self::new(
511+
expr.span(),
512+
"#[validate(..., message_l10n = fluent(..., ???))] allows only \"key=value\" of the fluent arg."
513+
.to_string(),
514+
)
515+
}
516+
474517
pub fn literal_only(span: impl Spanned) -> Self {
475518
Self::new(span.span(), "Allow literal only.")
476519
}

0 commit comments

Comments
 (0)