fluent_static_macros/
lib.rs1use 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") .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}