When a struct or enum variant has a single unnamed field, their implementation of Localize
now forwards to the field's implementation.
R2BAN2V6VS4OBNG6MK5BOUYQCJMGLD37IIE5RX4HXZLQ3C5AQRHAC
ARB66QTX6V7ZDXQUZQRDQGCUS2LOIB6SKMCL4SK7TVWK3FPBSI3QC
PGBXJWIHSVTRD7CGDSCPC4YHI65EBKMQFEX62RZWL4EZB63622XAC
XEEXWJLGVIPIGURSDU4ETZMGAIFTFDPECM4QWFOSRHU7GMGVOUVQC
CESJ4CTO26X4GBZBPXRXLOJT3JQJOGFN5EJSNAAZELNQRZF7QSYAC
VQBJBFEXRTJDBH27SVWRBCBFC7OOFOZ3DSMX7PE5BIZQLGHPVDYAC
YZ6PVVQCLWYRFM33CH6BDB7U6BSP5PM5LH3FMKRXV3BH5KCRFM4AC
3NMKD6I57ONAGHEN4PZIAV2KPYESVR4JL3DTWSHXKCMVJBEQ4GIQC
JWZT34UC7OTMMUZKGYFF6NDGIFNOA6TYXAZ6K66ELM3ZW7ZM7I5AC
QJC4IQITOQP65AFLA5CMH2EXHB6B3SOLW2XBV72U5ZQU2KOR2EIAC
RUCC2HKZZTUHN3G6IWS4NK3VYGXAI6PORJH2YZKPRAYSDWH63ESQC
LU6IFZFGPIKF3CBWZWITWVBSCYWF7Q4UXJDXVRWZ4XV7PKE5RSTQC
NEBSVXIASWSJO2CVU3VWRONIWJJDLL3VDS6WNKQBWQBRUUI7RSYAC
OWXLFLRMQDTXWN5QQQLJNAATWFWXIN2S4UQA2LC2A6AWX4UWM6LQC
7JPOCQEISAIOD7LV4JYBE6NNUWUKKNE73MEPQYTIZ7PP44ZAD2RAC
XGNME3WRU3MJDTFHUFJYARLVXWBZIH5ODBOIIFTXHNCBTZQH2R7QC
QFPQZR4K4UZ7R2GQZJG4NYBGVQJVL2ANIKGGTOHAMIRIBQHPSQGAC
F5LG7WENUUDRSCTDMA4M6BAC5RWTGQO45C4ZEBZDX6FHCTTHBVGQC
QSK7JRBA55ZRY322WXGNRROJL7NTFBR6MJPOOA5B2XD2JAVM4MWQC
7U2DXFMPZO4P53AMWYCVXG3EPB7UIAPEY4PDDINX4TTABHD5NGMQC
NO3PDO7PY7J3WPADNCS5VD6HKFY63E23I3SDR4DHXNVQJTG27RAAC
//! End-to-end test for unnamed field support in the `l10n_embed_derive` macro
mod common;
use icu_locale::{Locale, locale};
use l10n_embed::Localize;
use l10n_embed_derive::localize;
const DEFAULT_LOCALE: Locale = locale!("en-US");
const EXPECTED_LOCALES: [Locale; 4] = [
locale!("zh-CN"),
locale!("en-US"),
locale!("ja-JP"),
locale!("fr"),
];
pub struct LocalizableType;
impl Localize for LocalizableType {
fn available_locales(&self) -> Vec<Locale> {
EXPECTED_LOCALES.to_vec()
}
fn localize_for(&self, locale: &Locale) -> String {
format!("Localized with locale: {locale}")
}
}
#[localize("tests/locale/**/basic.ftl")]
pub struct MessageStruct(LocalizableType);
#[localize("tests/locale/**/basic.ftl")]
pub enum MessageEnum {
Greeting { name: String },
Forward(LocalizableType),
}
mod forward {
use super::common::compare_message;
use super::{DEFAULT_LOCALE, EXPECTED_LOCALES, LocalizableType, MessageEnum, MessageStruct};
use l10n_embed::Localize;
use pretty_assertions::assert_eq;
#[test]
fn message() {
let expected = "Localized with locale: en-US";
compare_message(MessageStruct(LocalizableType), expected, DEFAULT_LOCALE);
compare_message(
MessageEnum::Forward(LocalizableType),
expected,
DEFAULT_LOCALE,
);
}
#[test]
fn available_locales() {
let expected_locales = EXPECTED_LOCALES.to_vec();
assert_eq!(
expected_locales,
MessageStruct(LocalizableType).available_locales()
);
assert_eq!(
expected_locales,
MessageEnum::Forward(LocalizableType).available_locales()
);
}
}
/// Ensure other enum variants are not affected
mod preserves_enum_variants {
use super::common::compare_message;
use super::{DEFAULT_LOCALE, LocalizableType, MessageEnum};
use l10n_embed::Localize;
use pretty_assertions::{assert_eq, assert_ne};
#[test]
fn message() {
compare_message(
MessageEnum::Greeting {
name: String::from("Ferris"),
},
"Hello, Ferris!",
DEFAULT_LOCALE,
);
}
#[test]
fn available_locales() {
let greeting = MessageEnum::Greeting {
name: String::from("Ferris"),
};
assert_ne!(
greeting.available_locales(),
MessageEnum::Forward(LocalizableType).available_locales()
);
assert_eq!(vec![DEFAULT_LOCALE], greeting.available_locales());
}
}
Union,
#[error("Unnamed fields are not supported")]
UnnamedFields,
}
#[derive(Debug, Error)]
#[error("Unsupported Rust code")]
pub struct UnsupportedError {
span: syn::Ident,
reason: UnsupportedReason,
Union { span: syn::Ident },
#[error("Only 1 unnamed field is supported")]
UnnamedFields {
span: syn::Ident,
field_count: usize,
},
let named_fields: Vec<&syn::Type> = match &derive_input.data {
syn::Data::Struct(struct_data) => match &struct_data.fields {
syn::Fields::Named(named_fields) => {
named_fields.named.iter().map(|field| &field.ty).collect()
}
syn::Fields::Unit => Vec::new(),
syn::Fields::Unnamed(_unnamed_fields) => {
return Err(MacroError::Unsupported(UnsupportedError {
span: derive_input.ident.clone(),
reason: UnsupportedReason::UnnamedFields,
}));
}
},
let field_types: Vec<&syn::Type> = match &derive_input.data {
syn::Data::Struct(struct_data) => {
types_for_fields(&struct_data.fields, derive_input.ident.clone())?
}
.map(|variant| match &variant.fields {
syn::Fields::Named(named_fields) => {
Ok(named_fields.named.iter().map(|field| &field.ty).collect())
}
syn::Fields::Unit => Ok(Vec::new()),
syn::Fields::Unnamed(_unnamed_fields) => {
Err(MacroError::Unsupported(UnsupportedError {
span: variant.ident.clone(),
reason: UnsupportedReason::UnnamedFields,
}))
}
})
.collect::<Result<Vec<Vec<&syn::Type>>, _>>()?
.map(|variant| types_for_fields(&variant.fields, variant.ident.clone()))
.collect::<Result<Vec<Vec<&syn::Type>>, UnsupportedError>>()?
let additional_bounds = named_fields
.into_iter()
.map(|field| -> syn::WherePredicate {
// Attribute this bound to the original source code
let span = field.span();
parse_quote_spanned!(span=> #field: ::l10n_embed::Localize)
});
let additional_bounds = field_types.into_iter().map(|field| -> syn::WherePredicate {
// Attribute this bound to the original source code
let span = field.span();
parse_quote_spanned!(span=> #field: ::l10n_embed::Localize)
});
fn types_for_fields(
fields: &syn::Fields,
span: syn::Ident,
) -> Result<Vec<&syn::Type>, UnsupportedError> {
match fields {
syn::Fields::Named(named_fields) => {
Ok(named_fields.named.iter().map(|field| &field.ty).collect())
}
syn::Fields::Unit => Ok(Vec::new()),
syn::Fields::Unnamed(unnamed_fields) => {
let unnamed_field_types: Vec<&syn::Type> = unnamed_fields
.unnamed
.iter()
.map(|field| &field.ty)
.collect();
match unnamed_field_types.len() {
1 => Ok(unnamed_field_types),
_ => Err(UnsupportedError::UnnamedFields {
span,
field_count: unnamed_field_types.len(),
}),
}
}
}
}
UnsupportedReason::UnnamedFields => {
emit_error! { error.span, "only named fields are supported";
help = "Each field needs a name so it can be referenced by Fluent code";
note = "There must be at least one named field (unit structs are unsupported!)";
UnsupportedError::UnnamedFields { span, field_count } => {
emit_error! { span, "only 1 unnamed field is supported, got {} fields", field_count;
help = "When there are multiple fields, each field needs a name so it can be referenced by Fluent code";
note = "Using a single unnamed field forwards to that field's implementation of `Localize`";
fn expr_for_unnamed_fields(
unnamed_fields: &syn::FieldsUnnamed,
ident: &syn::Ident,
context: &Context,
) -> Result<TokenStream, UnsupportedError> {
let field_count = unnamed_fields.unnamed.iter().count();
if field_count != 1 {
return Err(UnsupportedError::UnnamedFields {
span: ident.clone(),
field_count,
});
}
let field_reference = match context.reference_kind {
ReferenceKind::EnumField => quote!(unnamed_field),
ReferenceKind::StructField => quote!(self.0),
};
Ok(quote!(#field_reference.localize_for(locale)))
}
pub fn locales_for_ident(group: &fluent::Group, ident: &syn::Ident) -> TokenStream {
let id = ident.to_string().to_kebab_case();
pub fn locales_for_ident(
group: &fluent::Group,
fields: &syn::Fields,
reference_kind: ReferenceKind,
ident: &syn::Ident,
) -> TokenStream {
match fields {
// Provide available locales based on what Fluent source files are available
syn::Fields::Named(_) | syn::Fields::Unit => {
let id = ident.to_string().to_kebab_case();
let locale_literals = group
.locales_for_message(&id)
.map(|locale| locale.id.to_string())
.map(|locale_string| syn::LitStr::new(&locale_string, proc_macro2::Span::call_site()));
let locale_literals = group
.locales_for_message(&id)
.map(|locale| locale.id.to_string())
.map(|locale_string| {
syn::LitStr::new(&locale_string, proc_macro2::Span::call_site())
});
quote!(
vec![self.canonical_locale(), #(::l10n_embed::macro_prelude::icu_locale::locale!(#locale_literals)),*]
)
}
// Forward to the unnamed field's implementation
syn::Fields::Unnamed(_unnamed_fields) => {
let unnamed_field_ident = match reference_kind {
ReferenceKind::EnumField => quote!(unnamed_field),
ReferenceKind::StructField => quote!(self.0),
};
// There is only one message for this struct, so just list every supported locale
quote!(
vec![self.canonical_locale(), #(::l10n_embed::macro_prelude::icu_locale::locale!(#locale_literals)),*]
)
quote!(#unnamed_field_ident.available_locales())
}
}
let references = match fields {
syn::Fields::Named(named_fields) => Context {
// Reference using `self.{reference_name}`
reference_kind: ReferenceKind::StructField,
// Create a list of unique field names that can be referenced
valid_references: unique_named_fields(named_fields),
},
syn::Fields::Unit => Context {
let context = match fields {
syn::Fields::Named(named_fields) => {
Context {
// Reference using `self.{reference_name}`
reference_kind: ReferenceKind::StructField,
// Create a list of unique field names that can be referenced
valid_references: unique_named_fields(named_fields),
}
}
syn::Fields::Unit | syn::Fields::Unnamed(_) => Context {
syn::Fields::Unnamed(_unnamed_fields) => {
return Err(MacroError::Unsupported(UnsupportedError {
span: ident.clone(),
reason: UnsupportedReason::UnnamedFields,
}));
}
Ok(expr_for_message(&mut group, ident, &references)?)
Ok(match fields {
syn::Fields::Named(_) | syn::Fields::Unit => expr_for_message(&mut group, ident, &context)?,
syn::Fields::Unnamed(unnamed_fields) => {
expr_for_unnamed_fields(unnamed_fields, ident, &context)?
}
})
// Simplify match code by always ignoring enum fields (even if they don't exist)
// We are matching the variant name, not any data, so each arm will have something like:
// Self::VariantName { .. }
// Even if `Self::VariantName` doesn't contain any data
let locales_for_variant = locales_for_ident(group, variant_ident);
match_arms.push(quote!(Self::#variant_ident { .. } => #locales_for_variant));
let destructuring_pattern = match &enum_variant.fields {
// Simplify match code by always ignoring enum fields (even if they don't exist)
// We are matching the variant name, not any data, so each arm will have something like:
// Self::VariantName { .. }
// Even if `Self::VariantName` doesn't contain any data
syn::Fields::Named(_) | syn::Fields::Unit => quote!(Self::#variant_ident { .. }),
// Bind the single unnamed field so we can use its implementation of `Localize::available_locales()`
syn::Fields::Unnamed(_unnamed_fields) => quote!(Self::#variant_ident(unnamed_field)),
};
let locales_for_variant = locales_for_ident(
group,
&enum_variant.fields,
ReferenceKind::EnumField,
variant_ident,
);
match_arms.push(quote!(#destructuring_pattern => #locales_for_variant));
syn::Fields::Unnamed(_unnamed_fields) => {
return Err(MacroError::Unsupported(UnsupportedError {
span: enum_variant.ident.clone(),
reason: UnsupportedReason::UnnamedFields,
}));
syn::Fields::Unnamed(unnamed_fields) => {
let field_count = unnamed_fields.unnamed.iter().count();
if field_count != 1 {
return Err(MacroError::Unsupported(UnsupportedError::UnnamedFields {
span: enum_variant.ident.clone(),
field_count,
}));
}
quote!(#variant_pascal_case(unnamed_field))
syn::Fields::Unnamed(_unnamed_fields) => {
return Err(MacroError::Unsupported(UnsupportedError {
span: enum_variant.ident.clone(),
reason: UnsupportedReason::UnnamedFields,
}));
};
let arm_body = match &enum_variant.fields {
syn::Fields::Named(_) | syn::Fields::Unit => {
expr_for_message(&mut group, &enum_variant.ident, &context)?
}
syn::Fields::Unnamed(unnamed_fields) => {
expr_for_unnamed_fields(unnamed_fields, &enum_variant.ident, &context)?