Split `fluent_embed` into `group` and `parse` modules
Dependencies
- [2]
4XADHKM6Convert fluent variable references to snake case - [3]
K4XW4OBWCreate derive macro helper function for simple enums - [4]
NFHPBRB5Support named fields in enum variants - [5]
MIHGKLMEFix generated `format!` macro call - [6]
EMXNTYAMRequire `Locale` to be passed as a parameter to `localize()` - [7]
BMUMO42IAdd support for inline string and number literals - [8]
UOMQT7LTAdd support for cardinal CLDR plural selectors - [9]
O77KA6C4Create `fluent_embed` crate - [10]
3C3CHSY5Implement `to_syn` for groups containing simple text messages
Change contents
- file addition: parse.rs[3.41]
use fluent_syntax::ast::{Expression, InlineExpression, Pattern, PatternElement, Variant, VariantKey,};use heck::{ToPascalCase, ToSnakeCase};use quote::{format_ident, quote};use syn::parse_quote;pub(crate) fn match_fluent_pattern<'resource>(pattern: &'resource Pattern<&'resource str>,) -> syn::Expr {let mut format_body = String::new();let mut format_arguments = Vec::new();for element in &pattern.elements {match element {PatternElement::TextElement { value } => format_body.push_str(value),PatternElement::Placeable { expression } => {let expression = match_placeable_expression(&expression);format_body.push_str("{}");format_arguments.push(quote!(#expression));}}}let format_body_literal = proc_macro2::Literal::string(format_body.to_string().as_str());parse_quote!(format!(#format_body_literal, #(#format_arguments),*))}fn match_placeable_expression<'resource>(expression: &'resource Expression<&'resource str>,) -> syn::Expr {match expression {Expression::Select { selector, variants } => {let match_target = match_inline_expression(selector);let match_arms: Vec<syn::Arm> = variants.iter().map(match_variant).collect();parse_quote! {match plural_rules.category_for(*#match_target) {#(#match_arms),*}}}Expression::Inline(expression) => match_inline_expression(expression),}}fn match_inline_expression<'resource>(expression: &'resource InlineExpression<&'resource str>,) -> syn::Expr {match expression {InlineExpression::StringLiteral { value } => {let string_literal = proc_macro2::Literal::string(value);parse_quote!(#string_literal)}InlineExpression::NumberLiteral { value } => {// FIXME: i128 is "good enough" for now but an incorrect representation!// e.g. decimals not supportedlet parsed_value = i128::from_str_radix(value, 10).unwrap();let number_literal = proc_macro2::Literal::i128_unsuffixed(parsed_value);parse_quote!(#number_literal)}InlineExpression::VariableReference { id } => {let ident = format_ident!("{}", id.name.to_snake_case());parse_quote!(#ident)}_ => {dbg!(expression);todo!()}}}fn match_variant<'resource>(variant: &Variant<&'resource str>) -> syn::Arm {let base_pattern = match_pattern(&variant.key);let body = match_fluent_pattern(&variant.value);// Default patterns match anything else// TODO: this can potentially generate unreachable patterns,// should be replaced with a more sophisticated implementationlet pattern = if variant.default {parse_quote!(#base_pattern | _)} else {base_pattern};parse_quote!(#pattern => #body)}fn match_pattern<'resource>(pattern: &VariantKey<&'resource str>) -> syn::Pat {match pattern {VariantKey::Identifier { name } => {let ident = format_ident!("{}", name.to_pascal_case());parse_quote!(::icu_plurals::PluralCategory::#ident)}VariantKey::NumberLiteral { value } => todo!(),}} - replacement in fluent_embed/src/lib.rs at line 1[3.74]→[3.0:26](∅→∅),[3.26]→[3.0:110](∅→∅),[3.110]→[3.111:114](∅→∅),[3.111]→[3.111:114](∅→∅),[3.114]→[2.0:52](∅→∅),[3.43]→[3.139:173](∅→∅),[2.52]→[3.139:173](∅→∅),[3.139]→[3.139:173](∅→∅),[3.132]→[3.68:90](∅→∅),[3.173]→[3.68:90](∅→∅),[3.68]→[3.68:90](∅→∅),[3.90]→[3.127:145](∅→∅),[3.127]→[3.127:145](∅→∅),[3.145]→[3.91:124](∅→∅),[3.124]→[3.174:315](∅→∅),[3.174]→[3.174:315](∅→∅),[3.315]→[3.125:225](∅→∅),[3.225]→[3.133:228](∅→∅),[3.228]→[3.174:247](∅→∅),[3.247]→[3.554:570](∅→∅),[3.294]→[3.554:570](∅→∅),[3.554]→[3.554:570](∅→∅),[3.570]→[3.44:174](∅→∅),[3.174]→[3.315:991](∅→∅),[3.570]→[3.315:991](∅→∅),[3.315]→[3.315:991](∅→∅),[3.991]→[3.571:577](∅→∅),[3.577]→[3.175:456](∅→∅),[3.456]→[3.577:578](∅→∅),[3.577]→[3.577:578](∅→∅),[3.578]→[3.457:749](∅→∅),[3.749]→[3.0:323](∅→∅),[3.323]→[2.53:215](∅→∅),[2.215]→[3.749:750](∅→∅),[3.428]→[3.749:750](∅→∅),[3.749]→[3.749:750](∅→∅),[3.750]→[3.429:643](∅→∅),[3.643]→[3.790:889](∅→∅),[3.790]→[3.790:889](∅→∅),[3.889]→[3.0:90](∅→∅),[3.90]→[3.936:1089](∅→∅),[3.936]→[3.936:1089](∅→∅),[3.796]→[3.991:999](∅→∅),[3.1089]→[3.991:999](∅→∅),[3.991]→[3.991:999](∅→∅),[3.999]→[3.295:296](∅→∅),[3.296]→[3.248:343](∅→∅),[3.343]→[3.62:146](∅→∅),[3.384]→[3.62:146](∅→∅),[3.146]→[3.507:571](∅→∅),[3.507]→[3.507:571](∅→∅),[3.571]→[3.147:229](∅→∅),[3.229]→[3.660:792](∅→∅),[3.660]→[3.660:792](∅→∅),[3.792]→[3.230:334](∅→∅),[3.334]→[3.902:932](∅→∅),[3.902]→[3.902:932](∅→∅),[3.932]→[3.797:798](∅→∅),[3.999]→[3.797:798](∅→∅),[3.798]→[3.933:1028](∅→∅),[3.1028]→[3.335:407](∅→∅),[3.407]→[3.1095:1235](∅→∅),[3.1095]→[3.1095:1235](∅→∅),[3.1235]→[3.344:583](∅→∅),[3.583]→[3.91:212](∅→∅),[3.212]→[3.974:975](∅→∅),[3.974]→[3.974:975](∅→∅),[3.1125]→[3.1125:1149](∅→∅),[3.1149]→[3.1323:1511](∅→∅),[3.1323]→[3.1323:1511](∅→∅),[3.1511]→[3.899:916](∅→∅),[3.899]→[3.899:916](∅→∅),[3.916]→[3.1512:2106](∅→∅),[3.2106]→[3.1150:1206](∅→∅),[3.1206]→[2.216:286](∅→∅),[2.286]→[3.1260:1386](∅→∅),[3.1260]→[3.1260:1386](∅→∅)
use fluent_syntax::ast::{Entry, Expression, InlineExpression, Message, Pattern, PatternElement, Resource, Variant,VariantKey,};use heck::{ToKebabCase, ToPascalCase, ToSnakeCase};use quote::{format_ident, quote};use syn::parse_quote;#[derive(Debug)]pub enum GroupEntry<'resource> {Message(&'resource Message<&'resource str>),}#[derive(Debug)]pub struct Group<'resource> {children: Vec<GroupEntry<'resource>>,}impl<'resource> GroupEntry<'resource> {fn to_syn(&self) -> syn::Expr {match self {Self::Message(message) => message.value.as_ref().map_or_else(|| parse_quote!(()), match_fluent_pattern),}}fn id(&self) -> &'resource str {match self {Self::Message(message) => message.id.name,}}}impl<'resource> TryFrom<&'resource Entry<&'resource str>> for GroupEntry<'resource> {type Error = ();fn try_from(value: &'resource Entry<&'resource str>) -> Result<Self, Self::Error> {match value {Entry::Message(message) => Ok(Self::Message(message)),_ => Err(()),}}}impl<'resource> Group<'resource> {pub fn from_resource(resource: &'resource Resource<&'resource str>) -> Self {let mut children = Vec::new();for entry in &resource.body {let matched_entry = GroupEntry::try_from(entry).unwrap();children.push(matched_entry);}Self { children }}pub fn derive_enum(&self,ident: syn::Ident,enum_data: syn::DataEnum,) -> proc_macro2::TokenStream {let mut idents = Vec::with_capacity(enum_data.variants.len());let mut messages = Vec::with_capacity(enum_data.variants.len());for variant in enum_data.variants {let kebab_case_ident = variant.ident.to_string().to_kebab_case();let message = self.children.iter().find(|child| child.id() == kebab_case_ident).unwrap();let variant_ident = variant.ident;idents.push(match variant.fields {syn::Fields::Named(fields) => {// Get the name of each field for pattern-matchinglet field_idents = fields.named.iter().map(|field| field.ident.as_ref().unwrap()).map(|ident| format_ident!("{}", ident.to_string().to_snake_case()));quote!(#variant_ident { #(#field_idents),* })}syn::Fields::Unnamed(_) => todo!(),syn::Fields::Unit => quote!(#variant_ident),});messages.push(message.to_syn());}quote! {impl #ident {fn localize(&self, plural_rules: &::icu_plurals::PluralRules) -> String {match self {#(Self::#idents => #messages),*}}}}}}fn match_fluent_pattern<'resource>(pattern: &'resource Pattern<&'resource str>) -> syn::Expr {let mut format_body = String::new();let mut format_arguments = Vec::new();for element in &pattern.elements {match element {PatternElement::TextElement { value } => format_body.push_str(value),PatternElement::Placeable { expression } => {let expression = match_placeable_expression(&expression);format_body.push_str("{}");format_arguments.push(quote!(#expression));}}}let format_body_literal = proc_macro2::Literal::string(format_body.to_string().as_str());parse_quote!(format!(#format_body_literal, #(#format_arguments),*))}fn match_placeable_expression<'resource>(expression: &'resource Expression<&'resource str>,) -> syn::Expr {match expression {Expression::Select { selector, variants } => {let match_target = match_inline_expression(selector);let match_arms: Vec<syn::Arm> = variants.iter().map(match_variant).collect();parse_quote! {match plural_rules.category_for(*#match_target) {#(#match_arms),*}}}Expression::Inline(expression) => match_inline_expression(expression),}}fn match_inline_expression<'resource>(expression: &'resource InlineExpression<&'resource str>,) -> syn::Expr {match expression {InlineExpression::StringLiteral { value } => {let string_literal = proc_macro2::Literal::string(value);parse_quote!(#string_literal)}InlineExpression::NumberLiteral { value } => {// FIXME: i128 is "good enough" for now but an incorrect representation!// e.g. decimals not supportedlet parsed_value = i128::from_str_radix(value, 10).unwrap();let number_literal = proc_macro2::Literal::i128_unsuffixed(parsed_value);parse_quote!(#number_literal)}InlineExpression::VariableReference { id } => {let ident = format_ident!("{}", id.name.to_snake_case());parse_quote!(#ident)}_ => {dbg!(expression);todo!()}}}mod group;mod parse; - replacement in fluent_embed/src/lib.rs at line 4[3.1387]→[3.1387:2051](∅→∅),[3.2051]→[2.287:355](∅→∅),[2.355]→[3.213:277](∅→∅),[3.2124]→[3.213:277](∅→∅),[3.277]→[3.2173:2239](∅→∅),[3.2173]→[3.2173:2239](∅→∅),[3.2239]→[3.1040:1048](∅→∅),[3.1040]→[3.1040:1048](∅→∅)
fn match_variant<'resource>(variant: &Variant<&'resource str>) -> syn::Arm {let base_pattern = match_pattern(&variant.key);let body = match_fluent_pattern(&variant.value);// Default patterns match anything else// TODO: this can potentially generate unreachable patterns,// should be replaced with a more sophisticated implementationlet pattern = if variant.default {parse_quote!(#base_pattern | _)} else {base_pattern};parse_quote!(#pattern => #body)}fn match_pattern<'resource>(pattern: &VariantKey<&'resource str>) -> syn::Pat {match pattern {VariantKey::Identifier { name } => {let ident = format_ident!("{}", name.to_pascal_case());parse_quote!(::icu_plurals::PluralCategory::#ident)}VariantKey::NumberLiteral { value } => todo!(),}}[3.1387]pub use group::Group; - file addition: group.rs[3.41]
use fluent_syntax::ast::{Entry, Message, Resource};use heck::{ToKebabCase, ToSnakeCase};use quote::{format_ident, quote};use syn::parse_quote;#[derive(Debug)]pub enum GroupEntry<'resource> {Message(&'resource Message<&'resource str>),}#[derive(Debug)]pub struct Group<'resource> {children: Vec<GroupEntry<'resource>>,}impl<'resource> GroupEntry<'resource> {fn to_syn(&self) -> syn::Expr {match self {Self::Message(message) => message.value.as_ref().map_or_else(|| parse_quote!(()), crate::parse::match_fluent_pattern),}}fn id(&self) -> &'resource str {match self {Self::Message(message) => message.id.name,}}}impl<'resource> TryFrom<&'resource Entry<&'resource str>> for GroupEntry<'resource> {type Error = ();fn try_from(value: &'resource Entry<&'resource str>) -> Result<Self, Self::Error> {match value {Entry::Message(message) => Ok(Self::Message(message)),_ => Err(()),}}}impl<'resource> Group<'resource> {pub fn from_resource(resource: &'resource Resource<&'resource str>) -> Self {let mut children = Vec::new();for entry in &resource.body {let matched_entry = GroupEntry::try_from(entry).unwrap();children.push(matched_entry);}Self { children }}pub fn derive_enum(&self,ident: syn::Ident,enum_data: syn::DataEnum,) -> proc_macro2::TokenStream {let mut idents = Vec::with_capacity(enum_data.variants.len());let mut messages = Vec::with_capacity(enum_data.variants.len());for variant in enum_data.variants {let kebab_case_ident = variant.ident.to_string().to_kebab_case();let message = self.children.iter().find(|child| child.id() == kebab_case_ident).unwrap();let variant_ident = variant.ident;idents.push(match variant.fields {syn::Fields::Named(fields) => {// Get the name of each field for pattern-matchinglet field_idents = fields.named.iter().map(|field| field.ident.as_ref().unwrap()).map(|ident| format_ident!("{}", ident.to_string().to_snake_case()));quote!(#variant_ident { #(#field_idents),* })}syn::Fields::Unnamed(_) => todo!(),syn::Fields::Unit => quote!(#variant_ident),});messages.push(message.to_syn());}quote! {impl #ident {fn localize(&self, plural_rules: &::icu_plurals::PluralRules) -> String {match self {#(Self::#idents => #messages),*}}}}}}