Split `fluent_embed` into `group` and `parse` modules

finchie
Feb 14, 2024, 7:48 AM
5TEX4MNUC4LDDRMNEOVCFNUUEZAGUXMKO3OIEQFXWRQKXSHY2NRQC

Dependencies

  • [2] 4XADHKM6 Convert fluent variable references to snake case
  • [3] K4XW4OBW Create derive macro helper function for simple enums
  • [4] NFHPBRB5 Support named fields in enum variants
  • [5] MIHGKLME Fix generated `format!` macro call
  • [6] EMXNTYAM Require `Locale` to be passed as a parameter to `localize()`
  • [7] BMUMO42I Add support for inline string and number literals
  • [8] UOMQT7LT Add support for cardinal CLDR plural selectors
  • [9] O77KA6C4 Create `fluent_embed` crate
  • [10] 3C3CHSY5 Implement `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 supported
    let 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 implementation
    let 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-matching
    let 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 supported
    let 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!()
    }
    }
    }
    [3.74]
    [3.1386]
    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 implementation
    let 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-matching
    let 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),*
    }
    }
    }
    }
    }
    }