The lib.rs file was a bit of a mess, so this should make the boundary cleaner in preparation for creating an actual proc macro
5TEX4MNUC4LDDRMNEOVCFNUUEZAGUXMKO3OIEQFXWRQKXSHY2NRQC 4XADHKM66YB7R2AWXJAYR3N7CG3EM323S5WVTJWUDILZS3TAMYRAC O77KA6C4UJGZXVGPEA7WCRQH6XYQJPWETSPDXI3VOKOSRQND7JEQC BMUMO42ICN3GQW77KUE2GTJPOA77SFDXJ4NNDO5NA2VJS267OXZAC UOMQT7LTURIIWHZT2ZHLCJG6XESYTN26EJC7IHRFR4PYJ355PNYAC K4XW4OBW5VWRCQZJNVV624E25SKRJPZ5WUXWVYHP6U7T7NPJFMFQC 3C3CHSY5FETUIE7W2VQ5V62GJNE3MW2WUM7GJDUFWSDB4WKNFR2AC NFHPBRB5AUJGWAN7UMUDUNFDGDOCKVUKC3AAPDTND7C7MJYISVVQC EMXNTYAMGWISSMAFQDBVGZ67OLM2SKZFH23J4ZWC5YSEAFAFL2JQC MIHGKLMEPO5O6KSXEBOLEPWGUUL3YBNKJC75G34J23EM55BDX2XQC 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!(),}}
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;
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!(),}}
pub use group::Group;
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),*}}}}}}