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 #(#includes)*
46 #tokens
47 })
48 }
49 Err(e) => syntax_err!(item_struct.span(), "Error generating message bundle: {}", e)
50 .to_compile_error()
51 .into(),
52 }
53}
54
55fn get_project_dir() -> Option<OsString> {
56 env::var_os("CARGO_MANIFEST_DIR_OVERRIDE") .or_else(|| env::var_os("CARGO_MANIFEST_DIR"))
58}
59
60struct MessageBundleAttr {
61 builder: MessageBundleBuilder,
62 includes: Vec<String>,
63}
64
65struct FluentResource {
66 path: String,
67 language: String,
68 span: Span,
69}
70
71impl Parse for FluentResource {
72 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
73 let span = input.span();
74 let content;
75 syn::parenthesized!(content in input);
76 let path: String = content.parse::<LitStr>()?.value();
77 content.parse::<Token![,]>()?;
78 let language: String = content.parse::<LitStr>()?.value();
79 Ok(FluentResource {
80 path,
81 language,
82 span,
83 })
84 }
85}
86
87struct FunctionMapping {
88 fluent_id: LitStr,
89 fn_ident: Option<Ident>,
90}
91
92impl Parse for FunctionMapping {
93 fn parse(input: syn::parse::ParseStream) -> SyntaxResult<Self> {
94 let fluent_id = input.parse::<LitStr>()?;
95 let fn_ident = if input.peek(Token![=]) {
96 input.parse::<Token![=]>()?;
97 Some(input.parse::<Ident>()?)
98 } else {
99 None
100 };
101 Ok(FunctionMapping {
102 fluent_id,
103 fn_ident,
104 })
105 }
106}
107
108impl Parse for MessageBundleAttr {
109 fn parse(input: syn::parse::ParseStream) -> SyntaxResult<Self> {
110 let base_dir = get_project_dir()
111 .ok_or_else(|| syntax_err!(input.span(), "Unable to get project directory"))?;
112
113 let mut fluent_resources: Vec<FluentResource> = Vec::new();
114 let mut function_mappings: Vec<FunctionMapping> = Vec::new();
115 let mut lang_def: Option<LitStr> = None;
116 let mut formatter: Option<LitStr> = None;
117
118 while !input.is_empty() {
119 let ident: Ident = input.parse()?;
120 input.parse::<Token![=]>()?;
121
122 match ident.to_string().as_str() {
123 "resources" => {
124 let resource_list;
125 syn::bracketed!(resource_list in input);
126 let resources: Punctuated<FluentResource, Comma> =
127 resource_list.parse_terminated(FluentResource::parse, Token![,])?;
128 fluent_resources.extend(resources);
129 }
130 "default_language" => {
131 lang_def = Some(input.parse()?);
132 }
133 "functions" => {
134 let content;
135 syn::parenthesized!(content in input);
136 let fn_mappings: Punctuated<FunctionMapping, Comma> =
137 content.parse_terminated(FunctionMapping::parse, Token![,])?;
138 function_mappings.extend(fn_mappings);
139 }
140 "formatter" => {
141 formatter = Some(input.parse()?);
142 }
143 attr => return Err(syntax_err!(ident.span(), "Unexpected attribute {attr}")),
144 }
145
146 if !input.is_empty() {
147 input.parse::<Token![,]>()?;
148 }
149 }
150
151 if fluent_resources.is_empty() {
152 Err(syntax_err!(
153 input.span(),
154 "No Fluent resources defined. Missing or empty 'resources' attribute"
155 ))
156 } else if lang_def.is_none() {
157 Err(syntax_err!(
158 input.span(),
159 "No default/fallback language is set. Missing 'default_language' attribute"
160 ))
161 } else {
162 let mut builder = MessageBundleBuilder::default();
163 let mut includes = Vec::new();
164
165 builder
166 .set_resources_dir(base_dir)
167 .set_default_language(&lang_def.unwrap().value())
168 .map_err(|e| syntax_err!(input.span(), "Error parsing default language: {}", e))?;
169
170 if let Some(formatter_fn) = formatter {
171 builder
172 .set_message_formatter_fn(&formatter_fn.value())
173 .map_err(|e| {
174 syntax_err!(
175 formatter_fn.span(),
176 "Error parsing formatter definition: {}",
177 e
178 )
179 })?;
180 }
181
182 if !function_mappings.is_empty() {
183 builder.set_function_call_generator(BundleFunctionCallGenerator::new(
184 function_mappings,
185 ));
186 }
187
188 for resource in fluent_resources {
189 builder
190 .add_resource(&resource.language, &resource.path)
191 .map_err(|e| syntax_err!(resource.span, "Error processing resource: {}", e))?;
192 includes.push(resource.path);
193 }
194
195 Ok(MessageBundleAttr { builder, includes })
196 }
197 }
198}
199
200struct BundleFunctionCallGenerator {
201 fns: HashMap<String, TokenStream2>,
202 registry: FunctionRegistry,
203}
204
205impl BundleFunctionCallGenerator {
206 pub fn new(fn_mappings: Vec<FunctionMapping>) -> Self {
207 let fns = fn_mappings
208 .into_iter()
209 .map(|mapping| {
210 let ident = mapping
211 .fn_ident
212 .unwrap_or_else(|| format_ident!("{}", mapping.fluent_id.value()));
213 (
214 mapping.fluent_id.value(),
215 quote! {
216 #ident
217 },
218 )
219 })
220 .collect();
221
222 let registry = FunctionRegistry::default();
223
224 Self { fns, registry }
225 }
226}
227
228impl FunctionCallGenerator for BundleFunctionCallGenerator {
229 fn generate(
230 &self,
231 function_name: &str,
232 positional_args: &Ident,
233 named_args: &Ident,
234 ) -> Option<TokenStream2> {
235 if let Some(fn_ident) = self.fns.get(function_name) {
236 Some(quote! {
237 Self::#fn_ident(&#positional_args, &#named_args)
238 })
239 } else {
240 self.registry
241 .generate(function_name, positional_args, named_args)
242 }
243 }
244}