Skip to main content

fluent_static_macros/
lib.rs

1use std::{collections::HashMap, env, ffi::OsString};
2
3use fluent_static_codegen::{
4    function::{FunctionCallGenerator, FunctionRegistry},
5    MessageBundleBuilder,
6};
7use proc_macro::TokenStream;
8use proc_macro2::{Span, TokenStream as TokenStream2};
9use quote::{format_ident, quote};
10use syn::{
11    parse::Parse, parse_macro_input, punctuated::Punctuated, spanned::Spanned, token::Comma, Ident,
12    ItemStruct, LitStr, Result as SyntaxResult, Token,
13};
14
15macro_rules! syntax_err {
16    ($input:expr, $message:expr $(, $args:expr)*) => {
17        ::syn::Error::new($input, format!($message $(, $args)*))
18    }
19}
20
21#[proc_macro_attribute]
22pub fn message_bundle(args: TokenStream, input: TokenStream) -> TokenStream {
23    let item_struct = parse_macro_input!(input as ItemStruct);
24    let name = item_struct.ident.to_string();
25    let MessageBundleAttr {
26        mut builder,
27        includes,
28    } = parse_macro_input!(args as MessageBundleAttr);
29    builder.set_bundle_name(&name);
30    match builder.build() {
31        Ok(result) => {
32            let tokens = result.tokens();
33            let includes: Vec<TokenStream2> = includes
34                .iter()
35                .map(|path| {
36                    quote! {
37                        #[cfg(trybuild)]
38                        const _: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR_OVERRIDE"), "/", #path));
39                        #[cfg(not(trybuild))]
40                        const _: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", #path));
41                    }
42                })
43                .collect();
44            TokenStream::from(quote! {
45                #[doc(hidden)]
46                mod __resource_tracking {
47                    #![allow(unexpected_cfgs)]
48                    #(#includes)*
49                }
50                #tokens
51            })
52        }
53        Err(e) => syntax_err!(item_struct.span(), "Error generating message bundle: {}", e)
54            .to_compile_error()
55            .into(),
56    }
57}
58
59fn get_project_dir() -> Option<OsString> {
60    env::var_os("CARGO_MANIFEST_DIR_OVERRIDE") // used for tests
61        .or_else(|| env::var_os("CARGO_MANIFEST_DIR"))
62}
63
64struct MessageBundleAttr {
65    builder: MessageBundleBuilder,
66    includes: Vec<String>,
67}
68
69struct FluentResource {
70    path: String,
71    language: String,
72    span: Span,
73}
74
75impl Parse for FluentResource {
76    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
77        let span = input.span();
78        let content;
79        syn::parenthesized!(content in input);
80        let path: String = content.parse::<LitStr>()?.value();
81        content.parse::<Token![,]>()?;
82        let language: String = content.parse::<LitStr>()?.value();
83        Ok(FluentResource {
84            path,
85            language,
86            span,
87        })
88    }
89}
90
91struct FunctionMapping {
92    fluent_id: LitStr,
93    fn_ident: Option<Ident>,
94}
95
96impl Parse for FunctionMapping {
97    fn parse(input: syn::parse::ParseStream) -> SyntaxResult<Self> {
98        let fluent_id = input.parse::<LitStr>()?;
99        let fn_ident = if input.peek(Token![=]) {
100            input.parse::<Token![=]>()?;
101            Some(input.parse::<Ident>()?)
102        } else {
103            None
104        };
105        Ok(FunctionMapping {
106            fluent_id,
107            fn_ident,
108        })
109    }
110}
111
112impl Parse for MessageBundleAttr {
113    fn parse(input: syn::parse::ParseStream) -> SyntaxResult<Self> {
114        let base_dir = get_project_dir()
115            .ok_or_else(|| syntax_err!(input.span(), "Unable to get project directory"))?;
116
117        let mut fluent_resources: Vec<FluentResource> = Vec::new();
118        let mut function_mappings: Vec<FunctionMapping> = Vec::new();
119        let mut lang_def: Option<LitStr> = None;
120        let mut formatter: Option<LitStr> = None;
121
122        while !input.is_empty() {
123            let ident: Ident = input.parse()?;
124            input.parse::<Token![=]>()?;
125
126            match ident.to_string().as_str() {
127                "resources" => {
128                    let resource_list;
129                    syn::bracketed!(resource_list in input);
130                    let resources: Punctuated<FluentResource, Comma> =
131                        resource_list.parse_terminated(FluentResource::parse, Token![,])?;
132                    fluent_resources.extend(resources);
133                }
134                "default_language" => {
135                    lang_def = Some(input.parse()?);
136                }
137                "functions" => {
138                    let content;
139                    syn::parenthesized!(content in input);
140                    let fn_mappings: Punctuated<FunctionMapping, Comma> =
141                        content.parse_terminated(FunctionMapping::parse, Token![,])?;
142                    function_mappings.extend(fn_mappings);
143                }
144                "formatter" => {
145                    formatter = Some(input.parse()?);
146                }
147                attr => return Err(syntax_err!(ident.span(), "Unexpected attribute {attr}")),
148            }
149
150            if !input.is_empty() {
151                input.parse::<Token![,]>()?;
152            }
153        }
154
155        if fluent_resources.is_empty() {
156            Err(syntax_err!(
157                input.span(),
158                "No Fluent resources defined. Missing or empty 'resources' attribute"
159            ))
160        } else if lang_def.is_none() {
161            Err(syntax_err!(
162                input.span(),
163                "No default/fallback language is set. Missing 'default_language' attribute"
164            ))
165        } else {
166            let mut builder = MessageBundleBuilder::default();
167            let mut includes = Vec::new();
168
169            builder
170                .set_resources_dir(base_dir)
171                .set_default_language(&lang_def.unwrap().value())
172                .map_err(|e| syntax_err!(input.span(), "Error parsing default language: {}", e))?;
173
174            if let Some(formatter_fn) = formatter {
175                builder
176                    .set_message_formatter_fn(&formatter_fn.value())
177                    .map_err(|e| {
178                        syntax_err!(
179                            formatter_fn.span(),
180                            "Error parsing formatter definition: {}",
181                            e
182                        )
183                    })?;
184            }
185
186            if !function_mappings.is_empty() {
187                builder.set_function_call_generator(BundleFunctionCallGenerator::new(
188                    function_mappings,
189                ));
190            }
191
192            for resource in fluent_resources {
193                builder
194                    .add_resource(&resource.language, &resource.path)
195                    .map_err(|e| syntax_err!(resource.span, "Error processing resource: {}", e))?;
196                includes.push(resource.path);
197            }
198
199            Ok(MessageBundleAttr { builder, includes })
200        }
201    }
202}
203
204struct BundleFunctionCallGenerator {
205    fns: HashMap<String, TokenStream2>,
206    registry: FunctionRegistry,
207}
208
209impl BundleFunctionCallGenerator {
210    pub fn new(fn_mappings: Vec<FunctionMapping>) -> Self {
211        let fns = fn_mappings
212            .into_iter()
213            .map(|mapping| {
214                let ident = mapping
215                    .fn_ident
216                    .unwrap_or_else(|| format_ident!("{}", mapping.fluent_id.value()));
217                (
218                    mapping.fluent_id.value(),
219                    quote! {
220                        #ident
221                    },
222                )
223            })
224            .collect();
225
226        let registry = FunctionRegistry::default();
227
228        Self { fns, registry }
229    }
230}
231
232impl FunctionCallGenerator for BundleFunctionCallGenerator {
233    fn generate(
234        &self,
235        function_name: &str,
236        positional_args: &Ident,
237        named_args: &Ident,
238    ) -> Option<TokenStream2> {
239        if let Some(fn_ident) = self.fns.get(function_name) {
240            Some(quote! {
241                Self::#fn_ident(&#positional_args, &#named_args)
242            })
243        } else {
244            self.registry
245                .generate(function_name, positional_args, named_args)
246        }
247    }
248}