Skip to content

Commit

Permalink
feat: replace fluent l10n format.
Browse files Browse the repository at this point in the history
  • Loading branch information
yassun7010 committed Jun 22, 2024
1 parent 3d6f0f5 commit 8fe4dcd
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 4 deletions.
54 changes: 54 additions & 0 deletions serde_valid/tests/deprecated_fluent_test.rs
Original file line number Diff line number Diff line change
@@ -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<String>) -> FluentBundle<FluentResource> {
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, 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(
["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()
);
}
}
4 changes: 2 additions & 2 deletions serde_valid/tests/fluent_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ mod tests {
fn fluent_error() {
#[derive(Debug, Deserialize, Validate)]
struct Test {
#[validate(minimum = 5, fluent("hello-world"))]
#[validate(minimum = 5, message_l10n = fluent("hello-world"))]
a: u32,
#[validate(maximum = 10, fluent("intro", name = "taro"))]
#[validate(maximum = 10, message_l10n = fluent("intro", name = "taro"))]
b: u32,
}

Expand Down
10 changes: 10 additions & 0 deletions serde_valid_derive/src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
}
57 changes: 55 additions & 2 deletions serde_valid_derive/src/attribute/common/message_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand All @@ -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)]),
},
}
}

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

#[cfg(feature = "fluent")]
fn get_fluent_message(
fn get_fluent_message_from_meta(
message_type: &MetaListCustomMessage,
path: &syn::Path,
fn_define: &CommaSeparatedNestedMetas,
Expand Down Expand Up @@ -210,6 +215,54 @@ fn get_fluent_message(
}
}

#[cfg(feature = "fluent")]
fn get_fluent_message_from_call_expr(
fn_define: &syn::ExprCall,
) -> Result<WithWarnings<MessageFormat>, 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 {
Expand Down
43 changes: 43 additions & 0 deletions serde_valid_derive/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>();

let filterd_candidates =
Expand Down Expand Up @@ -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.")
}
Expand Down

0 comments on commit 8fe4dcd

Please sign in to comment.