Move macro-specific code into `macro_impl` module
Dependencies
- [2]
7U2DXFMPRefactor `fluent_embed::Localize` to support overriding locales - [3]
AAERM7PBAdd selector tests for the `fr` locale - [4]
OWXLFLRMMerge `cli_macros` shim into `fluent_embed` - [5]
UOMQT7LTAdd support for cardinal CLDR plural selectors - [6]
5TEX4MNUSplit `fluent_embed` into `group` and `parse` modules - [7]
4XADHKM6Convert fluent variable references to snake case - [8]
XEEXWJLGAdd simple end-to-end test for selectors - [9]
BQ6N55O7Refactor how `Group` stores messages - [10]
V5S5K33AAdd basic error handling for invalid paths in proc_macro attribute - [11]
RLX6XPNZReturn an error when user provides an exact path - [12]
KZLFC7OWRename `fluent_embed_runtime` to `fluent_embed` - [13]
NO3PDO7PRefactor `fluent_embed` to support structs - [14]
6ABVDTXZImprove `fluent_embed_derive` test suite - [15]
O77KA6C4Create `fluent_embed` crate - [16]
2XQ6ZB4WStore multiple locales in a single `Group` - [17]
5FIVUZYFUnify `fluent_embed` macro API as `localize()` - [18]
ROSR4HD5Parse captured glob as locale - [19]
F5LG7WENEmit compilation errors from Fluent source code - [20]
QSK7JRBAAdd simple `attribute_path` function - [21]
BANMRGROSwitch `wax` to temporary fork - [22]
QFPQZR4KRefactor `fluent_embed` - [23]
2SITVDYWHandle common errors in Fluent code - [24]
HHJDRLLNCreate `fluent_embed_runtime` crate - [25]
XGNME3WRMove `Group::derive_enum` to new `crate::parse_macro` module - [26]
D652S2N3Rename `parse` module to `parse_fluent`
Change contents
- replacement in fluent_embed_derive/tests/selectors.rs at line 6
use rstest::{fixture, rstest};use rstest::rstest; - edit in fluent_embed_derive/tests/selectors.rs at line 42
let expected_message = data.expected_message(locale); - file addition: macro_impl[4.41]
- file addition: mod.rs[0.43]
use crate::fluent;use icu_locid::locale;use miette::Diagnostic;use quote::quote;use thiserror::Error;mod attribute;pub mod derive;pub mod error;#[derive(Diagnostic, Debug, Error)]#[diagnostic(transparent)]#[error(transparent)]pub enum MacroError {Attribute(#[from] attribute::Error),#[error("invalid Fluent source code")]Fluent(#[from] fluent::Error),}pub fn localize(attribute: &syn::LitStr,derive_input: &syn::DeriveInput,) -> Result<proc_macro2::TokenStream, MacroError> {let locales = attribute::locales(attribute)?;// TODO: user-controlled canonical localelet group = fluent::Group::new(locale!("en-US"), locales)?;let canonical_locale = group.canonical_locale().id.clone().to_string();let locales = match &derive_input.data {syn::Data::Struct(_struct_data) => derive::locales_for_ident(&group, &derive_input.ident),syn::Data::Enum(enum_data) => derive::locales_for_enum(&group, &enum_data.variants),syn::Data::Union(_) => todo!(),};let message_body = match &derive_input.data {syn::Data::Struct(struct_data) => {derive::message_for_struct(group, &derive_input.ident, &struct_data.fields)}syn::Data::Enum(enum_data) => derive::messages_for_enum(group, &enum_data.variants),syn::Data::Union(_) => todo!(),}?;let ident = &derive_input.ident;Ok(quote! {impl ::fluent_embed::Localize for #ident {const CANONICAL_LOCALE: ::fluent_embed::icu_locid::LanguageIdentifier =::fluent_embed::icu_locid::langid!(#canonical_locale);fn message_for_locale(&self, locale: &::fluent_embed::icu_locid::LanguageIdentifier) -> String {#message_body}fn localize(&self) -> String {let available_locales = #locales;let selected_locale = ::fluent_embed::locale_select::match_locales(&available_locales, &Self::CANONICAL_LOCALE);self.message_for_locale(&selected_locale)}}})} - file move: error.rs → error.rs
- replacement in fluent_embed_derive/src/macro_impl/error.rs at line 1
use super::{AttributeError, MacroError};use super::{attribute, MacroError}; - replacement in fluent_embed_derive/src/macro_impl/error.rs at line 3
use proc_macro_error::{abort, emit_call_site_error, emit_error};use quote::{quote, ToTokens};use proc_macro2::TokenStream;use proc_macro_error::{emit_call_site_error, emit_error};use syn::spanned::Spanned; - replacement in fluent_embed_derive/src/macro_impl/error.rs at line 7
fn attribute(error: AttributeError, derive_attribute: &syn::LitStr) {fn attribute(error: attribute::Error, attribute_stream: TokenStream) { - replacement in fluent_embed_derive/src/macro_impl/error.rs at line 9
AttributeError::Build(build_error) => {for location in build_error.locations() {// Create a token stream from the attribute's string literallet [proc_macro2::TokenTree::Literal(ref string_literal)] = derive_attribute.to_token_stream().into_iter().collect::<Vec<_>>()[..]else {abort!(derive_attribute, "unexpected macro attribute");};let (span_start, span_length) = location.span();let error_source = string_literal// Offset by 1 to skip the starting `"` double-quote character.subspan(span_start + 1..=span_start + span_length)// Fall back to the whole attribute if `subspan()` returns `None`// This will always happend on stable as subspan is nightly-only:// https://docs.rs/proc-macro2/latest/proc_macro2/struct.Literal.html#method.subspan.unwrap_or(derive_attribute.span());attribute::Error::Build(build_error) => {for located_error in build_error.locations() {let error_source = attribute_stream.span(); - replacement in fluent_embed_derive/src/macro_impl/error.rs at line 14
note = location.to_string();note = located_error.to_string(); - replacement in fluent_embed_derive/src/macro_impl/error.rs at line 18
AttributeError::Walk(walk_error) => {attribute::Error::Walk(walk_error) => { - replacement in fluent_embed_derive/src/macro_impl/error.rs at line 33
emit_error! { derive_attribute, "error at depth {} while walking path", walk_error.depth();emit_error! { attribute_stream, "error at depth {} while walking path", walk_error.depth(); - replacement in fluent_embed_derive/src/macro_impl/error.rs at line 37
AttributeError::NoMatches {attribute::Error::NoMatches { - replacement in fluent_embed_derive/src/macro_impl/error.rs at line 45
emit_error! { derive_attribute, "cannot match against an exact path";emit_error! { attribute_stream, "cannot match against an exact path"; - replacement in fluent_embed_derive/src/macro_impl/error.rs at line 55
fn fluent(error: super::fluent::Error) {fn fluent(error: crate::fluent::Error) { - replacement in fluent_embed_derive/src/macro_impl/error.rs at line 74
pub fn handle(error: MacroError,derive_attribute: &syn::LitStr,ident: &syn::Ident,) -> proc_macro2::TokenStream {pub fn emit(error: MacroError, attribute_stream: TokenStream) { - replacement in fluent_embed_derive/src/macro_impl/error.rs at line 76
MacroError::Attribute(error) => attribute(error, derive_attribute),MacroError::Attribute(error) => attribute(error, attribute_stream), - edit in fluent_embed_derive/src/macro_impl/error.rs at line 78
};// Generate a minimal `localize()` implementation so the error is self-containedquote! {impl #ident {fn localize(&self) -> String {unimplemented!("Encountered error in derive macro")}} - file move: derive.rs → derive.rs
- replacement in fluent_embed_derive/src/macro_impl/derive.rs at line 16
pub struct DeriveContext {pub struct Context { - replacement in fluent_embed_derive/src/macro_impl/derive.rs at line 24
reference_kind: &DeriveContext,reference_kind: &Context, - replacement in fluent_embed_derive/src/macro_impl/derive.rs at line 87
syn::Fields::Named(named_fields) => DeriveContext {syn::Fields::Named(named_fields) => Context { - replacement in fluent_embed_derive/src/macro_impl/derive.rs at line 151
syn::Fields::Named(named_fields) => DeriveContext {syn::Fields::Named(named_fields) => Context { - file addition: attribute.rs[0.43]
use std::collections::HashMap;use std::path::PathBuf;use icu_locid::Locale;use miette::Diagnostic;use thiserror::Error;use wax::walk::Entry;#[derive(Diagnostic, Debug, Error)]#[error(transparent)]pub enum Error {Build(#[from] wax::BuildError),Walk(#[from] wax::walk::WalkError),#[error("Directory could not be matched by glob in macro attribute")]NoMatches {path: PathBuf,complete_match: String,},}pub fn locales(attribute: &syn::LitStr) -> Result<HashMap<Locale, PathBuf>, Error> {// Read the fluent file at the given pathlet manifest_root = std::env::var("CARGO_MANIFEST_DIR").unwrap();let attribute_glob = attribute.value();let mut locales = HashMap::new();let glob = wax::Glob::new(&attribute_glob)?;for potential_entry in glob.walk(&manifest_root) {// TODO: this assumes that the locale is the first capturelet entry = potential_entry?;let captured_locale = if let Some(captured) = entry.matched().get(1) {captured} else {return Err(Error::NoMatches {path: entry.path().to_path_buf(),complete_match: entry.matched().complete().to_string(),});};// Captured directories may have a suffix of `/`let stripped_locale = captured_locale.strip_suffix('/').unwrap_or(captured_locale);// TODO: return an error instead of paniclet locale = Locale::try_from_bytes(stripped_locale.as_bytes()).unwrap();// Insert this locale (and make sure it's unique!)let previous_value = locales.insert(locale, entry.into_path());assert!(previous_value.is_none());}Ok(locales)} - edit in fluent_embed_derive/src/lib.rs at line 1[4.4253]→[4.4253:4284](∅→∅),[4.4284]→[4.0:24](∅→∅),[4.24]→[4.4284:4318](∅→∅),[4.4284]→[4.4284:4318](∅→∅),[4.4318]→[4.6612:6636](∅→∅)
use std::collections::HashMap;use std::path::PathBuf;use icu_locid::{locale, Locale};use miette::Diagnostic; - edit in fluent_embed_derive/src/lib.rs at line 5
use thiserror::Error;use wax::walk::Entry; - edit in fluent_embed_derive/src/lib.rs at line 6
mod derive;mod error; - replacement in fluent_embed_derive/src/lib.rs at line 7[4.63]→[4.6718:6777](∅→∅),[4.6718]→[4.6718:6777](∅→∅),[4.6777]→[4.157:179](∅→∅),[4.179]→[4.107:143](∅→∅),[4.107]→[4.107:143](∅→∅),[4.143]→[4.23:63](∅→∅),[4.63]→[4.6778:6852](∅→∅),[4.197]→[4.6778:6852](∅→∅),[4.6852]→[4.216:294](∅→∅),[4.216]→[4.216:294](∅→∅),[4.294]→[4.6853:6941](∅→∅),[4.6941]→[4.180:198](∅→∅),[4.198]→[4.6963:7002](∅→∅),[4.6963]→[4.6963:7002](∅→∅),[4.7002]→[4.0:43](∅→∅),[4.43]→[4.199:234](∅→∅),[4.234]→[4.294:296](∅→∅),[4.7079]→[4.294:296](∅→∅),[4.294]→[4.294:296](∅→∅),[4.296]→[4.1386:1387](∅→∅),[4.912]→[4.1386:1387](∅→∅),[4.1625]→[4.1386:1387](∅→∅),[4.3486]→[4.1386:1387](∅→∅),[4.1386]→[4.1386:1387](∅→∅),[4.1387]→[4.235:262](∅→∅),[4.262]→[4.95:180](∅→∅),[4.95]→[4.95:180](∅→∅),[4.87]→[4.4453:4569](∅→∅),[4.180]→[4.4453:4569](∅→∅),[4.384]→[4.4453:4569](∅→∅),[4.4453]→[4.4453:4569](∅→∅),[4.4569]→[4.181:225](∅→∅),[4.225]→[4.4616:4617](∅→∅),[4.4616]→[4.4616:4617](∅→∅),[4.4617]→[4.226:264](∅→∅),[4.264]→[4.4657:4658](∅→∅),[4.7116]→[4.4657:4658](∅→∅),[4.4657]→[4.4657:4658](∅→∅),[4.4658]→[4.88:137](∅→∅),[4.137]→[4.4715:4837](∅→∅),[4.4715]→[4.4715:4837](∅→∅),[4.4837]→[4.138:176](∅→∅),[4.176]→[4.7117:7118](∅→∅),[4.7118]→[4.385:702](∅→∅),[4.176]→[4.385:702](∅→∅),[4.702]→[4.4946:5096](∅→∅),[4.4946]→[4.4946:5096](∅→∅),[4.5096]→[4.44:94](∅→∅),[4.94]→[4.5096:5179](∅→∅),[4.5096]→[4.5096:5179](∅→∅),[4.5179]→[4.265:439](∅→∅),[4.439]→[4.5421:5428](∅→∅),[4.7240]→[4.5421:5428](∅→∅),[4.5421]→[4.5421:5428](∅→∅),[4.5428]→[4.440:456](∅→∅),[4.225]→[4.5472:5475](∅→∅),[4.456]→[4.5472:5475](∅→∅),[4.7296]→[4.5472:5475](∅→∅),[4.5472]→[4.5472:5475](∅→∅),[4.5475]→[4.263:281](∅→∅),[4.281]→[4.474:540](∅→∅),[4.474]→[4.474:540](∅→∅),[4.540]→[4.282:388](∅→∅),[4.388]→[4.664:774](∅→∅),[4.664]→[4.664:774](∅→∅),[4.774]→[4.1308:1384](∅→∅),[4.1384]→[2.0:285](∅→∅)
#[derive(Diagnostic, Debug, Error)]#[error(transparent)]enum AttributeError {Build(#[from] wax::BuildError),Walk(#[from] wax::walk::WalkError),#[error("Directory could not be matched by glob in macro attribute")]NoMatches {path: PathBuf,complete_match: String,},}#[derive(Diagnostic, Debug, Error)]#[diagnostic(transparent)]#[error(transparent)]enum MacroError {Attribute(#[from] AttributeError),#[error("invalid Fluent source code")]Fluent(#[from] fluent::Error),}fn locales_from_attribute(attribute: &syn::LitStr,) -> Result<HashMap<Locale, PathBuf>, AttributeError> {// Read the fluent file at the given pathlet manifest_root = std::env::var("CARGO_MANIFEST_DIR").unwrap();let attribute_glob = attribute.value();let mut locales = HashMap::new();let glob = wax::Glob::new(&attribute_glob)?;for potential_entry in glob.walk(&manifest_root) {// TODO: this assumes that the locale is the first capturelet entry = potential_entry?;let captured_locale = if let Some(captured) = entry.matched().get(1) {captured} else {return Err(AttributeError::NoMatches {path: entry.path().to_path_buf(),complete_match: entry.matched().complete().to_string(),});};// Captured directories may have a suffix of `/`let stripped_locale = captured_locale.strip_suffix('/').unwrap_or(captured_locale);// TODO: return an error instead of paniclet locale = Locale::try_from_bytes(stripped_locale.as_bytes()).unwrap();// Insert this locale (and make sure it's unique!)let previous_value = locales.insert(locale, entry.into_path());assert!(previous_value.is_none());}Ok(locales)}fn localize_impl(attribute: &syn::LitStr,derive_input: &syn::DeriveInput,) -> Result<proc_macro2::TokenStream, MacroError> {let locales = locales_from_attribute(attribute)?;// TODO: user-controlled canonical localelet group = fluent::Group::new(locale!("en-US"), locales)?;let canonical_locale = group.canonical_locale().id.clone().to_string();let locales = match &derive_input.data {syn::Data::Struct(_struct_data) => derive::locales_for_ident(&group, &derive_input.ident),syn::Data::Enum(enum_data) => derive::locales_for_enum(&group, &enum_data.variants),syn::Data::Union(_) => todo!(),};mod macro_impl; - edit in fluent_embed_derive/src/lib.rs at line 9[4.5597]→[2.286:336](∅→∅),[2.336]→[4.7399:7443](∅→∅),[4.5639]→[4.7399:7443](∅→∅),[4.7443]→[2.337:425](∅→∅),[2.425]→[4.7531:7541](∅→∅),[4.855]→[4.7531:7541](∅→∅),[4.7531]→[4.7531:7541](∅→∅),[4.7541]→[2.426:519](∅→∅),[2.519]→[4.5830:5870](∅→∅),[4.940]→[4.5830:5870](∅→∅),[4.5830]→[4.5830:5870](∅→∅),[4.5870]→[4.7542:7550](∅→∅),[4.7550]→[4.5877:5915](∅→∅),[4.5877]→[4.5877:5915](∅→∅),[4.5915]→[4.1385:1386](∅→∅),[4.1386]→[4.384:400](∅→∅),[4.5915]→[4.384:400](∅→∅),[4.400]→[4.62:268](∅→∅),[4.268]→[2.520:675](∅→∅),[4.268]→[4.6021:6064](∅→∅),[2.675]→[4.6021:6064](∅→∅),[4.1617]→[4.6021:6064](∅→∅),[4.6021]→[4.6021:6064](∅→∅),[4.6064]→[2.676:914](∅→∅),[2.914]→[4.6086:6110](∅→∅),[4.6086]→[4.6086:6110](∅→∅),[4.6110]→[4.401:408](∅→∅),[4.408]→[4.6116:6118](∅→∅),[4.6116]→[4.6116:6118](∅→∅),[4.6118]→[4.389:390](∅→∅)
let message_body = match &derive_input.data {syn::Data::Struct(struct_data) => {derive::message_for_struct(group, &derive_input.ident, &struct_data.fields)}syn::Data::Enum(enum_data) => derive::messages_for_enum(group, &enum_data.variants),syn::Data::Union(_) => todo!(),}?;let ident = &derive_input.ident;Ok(quote! {impl ::fluent_embed::Localize for #ident {const CANONICAL_LOCALE: ::fluent_embed::icu_locid::LanguageIdentifier =::fluent_embed::icu_locid::langid!(#canonical_locale);fn message_for_locale(&self, locale: &::fluent_embed::icu_locid::LanguageIdentifier) -> String {#message_body}fn localize(&self) -> String {let available_locales = #locales;let selected_locale = ::fluent_embed::locale_select::match_locales(&available_locales, &Self::CANONICAL_LOCALE);self.message_for_locale(&selected_locale)}}})} - edit in fluent_embed_derive/src/lib.rs at line 13
let attribute_stream = proc_macro2::TokenStream::from(attribute.clone()); - replacement in fluent_embed_derive/src/lib.rs at line 17
let implementation = localize_impl(&derive_attribute, &derive_input).unwrap_or_else(|error| error::handle(error, &derive_attribute, &derive_input.ident));match macro_impl::localize(&derive_attribute, &derive_input) {Ok(implementation) => {// No errors found, emit the generated `fluent_embed::Localize` implementationquote! {#original_item - replacement in fluent_embed_derive/src/lib.rs at line 23
quote! {#original_item#implementation}}Err(error) => {// Emit the relevant error messagemacro_impl::error::emit(error, attribute_stream); - replacement in fluent_embed_derive/src/lib.rs at line 30
#implementation// Generate a minimal `localize()` implementation so the error is self-containedlet ident = derive_input.ident;quote! {impl #ident {fn localize(&self) -> String {unimplemented!("Encountered error in derive macro")}}}} - replacement in fluent_embed_derive/src/fluent/mod.rs at line 1
use crate::derive::DeriveContext;use crate::macro_impl::derive; - replacement in fluent_embed_derive/src/fluent/mod.rs at line 66
derive_context: &'context DeriveContext,derive_context: &'context derive::Context, - replacement in fluent_embed_derive/src/fluent/mod.rs at line 179
derive_context: &DeriveContext,derive_context: &derive::Context, - replacement in fluent_embed_derive/src/fluent/group.rs at line 2
use crate::derive::DeriveContext;use crate::macro_impl::derive; - replacement in fluent_embed_derive/src/fluent/group.rs at line 80
derive_context: &DeriveContext,derive_context: &derive::Context, - replacement in fluent_embed_derive/src/fluent/group.rs at line 91
derive_context: &DeriveContext,derive_context: &derive::Context, - replacement in fluent_embed_derive/src/fluent/ast.rs at line 2
use crate::derive::ReferenceKind;use crate::macro_impl::derive::ReferenceKind;