icu_provider_macros/
lib.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5// https://github.com/unicode-org/icu4x/blob/main/documents/process/boilerplate.md#library-annotations
6#![cfg_attr(
7    not(test),
8    deny(
9        clippy::indexing_slicing,
10        clippy::unwrap_used,
11        clippy::expect_used,
12        // Panics are OK in proc macros
13        // clippy::panic,
14        clippy::exhaustive_structs,
15        clippy::exhaustive_enums,
16        missing_debug_implementations,
17    )
18)]
19#![warn(missing_docs)]
20
21//! Proc macros for the ICU4X data provider.
22//!
23//! These macros are re-exported from `icu_provider`.
24
25extern crate proc_macro;
26use proc_macro::TokenStream;
27use proc_macro2::Span;
28use proc_macro2::TokenStream as TokenStream2;
29use quote::quote;
30use syn::parenthesized;
31use syn::parse::{self, Parse, ParseStream};
32use syn::parse_macro_input;
33use syn::punctuated::Punctuated;
34use syn::spanned::Spanned;
35use syn::DeriveInput;
36use syn::{Ident, LitStr, Path, Token};
37#[cfg(test)]
38mod tests;
39
40#[proc_macro_attribute]
41
42/// The `#[data_struct]` attribute should be applied to all types intended
43/// for use in a `DataStruct`.
44///
45/// It does the following things:
46///
47/// - `Apply #[derive(Yokeable, ZeroFrom)]`. The `ZeroFrom` derive can
48///    be customized with `#[zerofrom(clone)]` on non-ZeroFrom fields.
49///
50/// In addition, the attribute can be used to implement `DataMarker` and/or `KeyedDataMarker`
51/// by adding symbols with optional key strings:
52///
53/// ```
54/// # // We DO NOT want to pull in the `icu` crate as a dev-dependency,
55/// # // because that will rebuild the whole tree in proc macro mode
56/// # // when using cargo test --all-features --all-targets.
57/// # pub mod icu {
58/// #   pub mod locid_transform {
59/// #     pub mod fallback {
60/// #       pub use icu_provider::_internal::LocaleFallbackPriority;
61/// #     }
62/// #   }
63/// #   pub use icu_provider::_internal::locid;
64/// # }
65/// use icu::locid::extensions::unicode::key;
66/// use icu::locid_transform::fallback::*;
67/// use icu_provider::yoke;
68/// use icu_provider::zerofrom;
69/// use icu_provider::KeyedDataMarker;
70/// use std::borrow::Cow;
71///
72/// #[icu_provider::data_struct(
73///     FooV1Marker,
74///     BarV1Marker = "demo/bar@1",
75///     marker(
76///         BazV1Marker,
77///         "demo/baz@1",
78///         fallback_by = "region",
79///         extension_key = "ca"
80///     )
81/// )]
82/// pub struct FooV1<'data> {
83///     message: Cow<'data, str>,
84/// };
85///
86/// // Note: FooV1Marker implements `DataMarker` but not `KeyedDataMarker`.
87/// // The other two implement `KeyedDataMarker`.
88///
89/// assert_eq!(&*BarV1Marker::KEY.path(), "demo/bar@1");
90/// assert_eq!(
91///     BarV1Marker::KEY.metadata().fallback_priority,
92///     LocaleFallbackPriority::Language
93/// );
94/// assert_eq!(BarV1Marker::KEY.metadata().extension_key, None);
95///
96/// assert_eq!(&*BazV1Marker::KEY.path(), "demo/baz@1");
97/// assert_eq!(
98///     BazV1Marker::KEY.metadata().fallback_priority,
99///     LocaleFallbackPriority::Region
100/// );
101/// assert_eq!(BazV1Marker::KEY.metadata().extension_key, Some(key!("ca")));
102/// ```
103///
104/// If the `#[databake(path = ...)]` attribute is present on the data struct, this will also
105/// implement it on the markers.
106pub fn data_struct(attr: TokenStream, item: TokenStream) -> TokenStream {
107    TokenStream::from(data_struct_impl(
108        parse_macro_input!(attr as DataStructArgs),
109        parse_macro_input!(item as DeriveInput),
110    ))
111}
112
113pub(crate) struct DataStructArgs {
114    args: Punctuated<DataStructArg, Token![,]>,
115}
116
117impl Parse for DataStructArgs {
118    fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
119        let args = input.parse_terminated(DataStructArg::parse, Token![,])?;
120        Ok(Self { args })
121    }
122}
123struct DataStructArg {
124    marker_name: Path,
125    key_lit: Option<LitStr>,
126    fallback_by: Option<LitStr>,
127    extension_key: Option<LitStr>,
128    fallback_supplement: Option<LitStr>,
129    singleton: bool,
130}
131
132impl DataStructArg {
133    fn new(marker_name: Path) -> Self {
134        Self {
135            marker_name,
136            key_lit: None,
137            fallback_by: None,
138            extension_key: None,
139            fallback_supplement: None,
140            singleton: false,
141        }
142    }
143}
144
145impl Parse for DataStructArg {
146    fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
147        let path: Path = input.parse()?;
148
149        fn at_most_one_option<T>(
150            o: &mut Option<T>,
151            new: T,
152            name: &str,
153            span: Span,
154        ) -> parse::Result<()> {
155            if o.replace(new).is_some() {
156                Err(parse::Error::new(
157                    span,
158                    format!("marker() cannot contain multiple {name}s"),
159                ))
160            } else {
161                Ok(())
162            }
163        }
164
165        if path.is_ident("marker") {
166            let content;
167            let paren = parenthesized!(content in input);
168            let mut marker_name: Option<Path> = None;
169            let mut key_lit: Option<LitStr> = None;
170            let mut fallback_by: Option<LitStr> = None;
171            let mut extension_key: Option<LitStr> = None;
172            let mut fallback_supplement: Option<LitStr> = None;
173            let mut singleton = false;
174            let punct = content.parse_terminated(DataStructMarkerArg::parse, Token![,])?;
175
176            for entry in punct {
177                match entry {
178                    DataStructMarkerArg::Path(path) => {
179                        at_most_one_option(&mut marker_name, path, "marker", input.span())?;
180                    }
181                    DataStructMarkerArg::NameValue(name, value) => {
182                        if name == "fallback_by" {
183                            at_most_one_option(
184                                &mut fallback_by,
185                                value,
186                                "fallback_by",
187                                paren.span.join(),
188                            )?;
189                        } else if name == "extension_key" {
190                            at_most_one_option(
191                                &mut extension_key,
192                                value,
193                                "extension_key",
194                                paren.span.join(),
195                            )?;
196                        } else if name == "fallback_supplement" {
197                            at_most_one_option(
198                                &mut fallback_supplement,
199                                value,
200                                "fallback_supplement",
201                                paren.span.join(),
202                            )?;
203                        } else {
204                            return Err(parse::Error::new(
205                                name.span(),
206                                format!("unknown option {name} in marker()"),
207                            ));
208                        }
209                    }
210                    DataStructMarkerArg::Lit(lit) => {
211                        at_most_one_option(&mut key_lit, lit, "literal key", input.span())?;
212                    }
213                    DataStructMarkerArg::Singleton => {
214                        singleton = true;
215                    }
216                }
217            }
218            let marker_name = if let Some(marker_name) = marker_name {
219                marker_name
220            } else {
221                return Err(parse::Error::new(
222                    input.span(),
223                    "marker() must contain a marker!",
224                ));
225            };
226
227            Ok(Self {
228                marker_name,
229                key_lit,
230                fallback_by,
231                extension_key,
232                fallback_supplement,
233                singleton,
234            })
235        } else {
236            let mut this = DataStructArg::new(path);
237            let lookahead = input.lookahead1();
238            if lookahead.peek(Token![=]) {
239                let _t: Token![=] = input.parse()?;
240                let lit: LitStr = input.parse()?;
241                this.key_lit = Some(lit);
242                Ok(this)
243            } else {
244                Ok(this)
245            }
246        }
247    }
248}
249
250/// A single argument to `marker()` in `#[data_struct(..., marker(...), ...)]
251enum DataStructMarkerArg {
252    Path(Path),
253    NameValue(Ident, LitStr),
254    Lit(LitStr),
255    Singleton,
256}
257impl Parse for DataStructMarkerArg {
258    fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
259        let lookahead = input.lookahead1();
260        if lookahead.peek(LitStr) {
261            Ok(DataStructMarkerArg::Lit(input.parse()?))
262        } else {
263            let path: Path = input.parse()?;
264            let lookahead = input.lookahead1();
265            if lookahead.peek(Token![=]) {
266                let _tok: Token![=] = input.parse()?;
267                let ident = path.get_ident().ok_or_else(|| {
268                    parse::Error::new(path.span(), "Expected identifier before `=`, found path")
269                })?;
270                Ok(DataStructMarkerArg::NameValue(
271                    ident.clone(),
272                    input.parse()?,
273                ))
274            } else if path.is_ident("singleton") {
275                Ok(DataStructMarkerArg::Singleton)
276            } else {
277                Ok(DataStructMarkerArg::Path(path))
278            }
279        }
280    }
281}
282
283fn data_struct_impl(attr: DataStructArgs, input: DeriveInput) -> TokenStream2 {
284    if input.generics.type_params().count() > 0 {
285        return syn::Error::new(
286            input.generics.span(),
287            "#[data_struct] does not support type parameters",
288        )
289        .to_compile_error();
290    }
291    let lifetimes = input.generics.lifetimes().collect::<Vec<_>>();
292
293    let name = &input.ident;
294
295    let name_with_lt = if !lifetimes.is_empty() {
296        quote!(#name<'static>)
297    } else {
298        quote!(#name)
299    };
300
301    if lifetimes.len() > 1 {
302        return syn::Error::new(
303            input.generics.span(),
304            "#[data_struct] does not support more than one lifetime parameter",
305        )
306        .to_compile_error();
307    }
308
309    let bake_derive = input
310        .attrs
311        .iter()
312        .find(|a| a.path().is_ident("databake"))
313        .map(|a| {
314            quote! {
315                #[derive(databake::Bake)]
316                #a
317            }
318        })
319        .unwrap_or_else(|| quote! {});
320
321    let mut result = TokenStream2::new();
322
323    for single_attr in attr.args {
324        let DataStructArg {
325            marker_name,
326            key_lit,
327            fallback_by,
328            extension_key,
329            fallback_supplement,
330            singleton,
331        } = single_attr;
332
333        let docs = if let Some(ref key_lit) = key_lit {
334            let fallback_by_docs_str = match fallback_by {
335                Some(ref fallback_by) => fallback_by.value(),
336                None => "language (default)".to_string(),
337            };
338            let extension_key_docs_str = match extension_key {
339                Some(ref extension_key) => extension_key.value(),
340                None => "none (default)".to_string(),
341            };
342            format!("Marker type for [`{}`]: \"{}\"\n\n- Fallback priority: {}\n- Extension keyword: {}", name, key_lit.value(), fallback_by_docs_str, extension_key_docs_str)
343        } else {
344            format!("Marker type for [`{name}`]")
345        };
346
347        result.extend(quote!(
348            #[doc = #docs]
349            #bake_derive
350            pub struct #marker_name;
351            impl icu_provider::DataMarker for #marker_name {
352                type Yokeable = #name_with_lt;
353            }
354        ));
355
356        if let Some(key_lit) = key_lit {
357            let key_str = key_lit.value();
358            let fallback_by_expr = if let Some(fallback_by_lit) = fallback_by {
359                match fallback_by_lit.value().as_str() {
360                    "region" => {
361                        quote! {icu_provider::_internal::LocaleFallbackPriority::Region}
362                    }
363                    "collation" => {
364                        quote! {icu_provider::_internal::LocaleFallbackPriority::Collation}
365                    }
366                    "language" => {
367                        quote! {icu_provider::_internal::LocaleFallbackPriority::Language}
368                    }
369                    _ => panic!("Invalid value for fallback_by"),
370                }
371            } else {
372                quote! {icu_provider::_internal::LocaleFallbackPriority::const_default()}
373            };
374            let extension_key_expr = if let Some(extension_key_lit) = extension_key {
375                quote! {Some(icu_provider::_internal::locid::extensions::unicode::key!(#extension_key_lit))}
376            } else {
377                quote! {None}
378            };
379            let fallback_supplement_expr = if let Some(fallback_supplement_lit) =
380                fallback_supplement
381            {
382                match fallback_supplement_lit.value().as_str() {
383                    "collation" => {
384                        quote! {Some(icu_provider::_internal::LocaleFallbackSupplement::Collation)}
385                    }
386                    _ => panic!("Invalid value for fallback_supplement"),
387                }
388            } else {
389                quote! {None}
390            };
391            result.extend(quote!(
392                impl icu_provider::KeyedDataMarker for #marker_name {
393                    const KEY: icu_provider::DataKey = icu_provider::data_key!(#key_str, icu_provider::DataKeyMetadata::construct_internal(
394                        #fallback_by_expr,
395                        #extension_key_expr,
396                        #fallback_supplement_expr,
397                        #singleton,
398                    ));
399                }
400            ));
401        }
402    }
403
404    result.extend(quote!(
405        #[derive(icu_provider::prelude::yoke::Yokeable, icu_provider::prelude::zerofrom::ZeroFrom)]
406        #input
407    ));
408
409    result
410}