Skip to main content

bern_macros/
lib.rs

1//! Procedural macros for Bern declarations.
2//!
3//! The macros parse with `bern-syntax`, the same parser the build tool
4//! uses, check declarations against the shared component specifications
5//! and expand them into Rust that `rustc` type-checks. They never generate
6//! UI code: targets do that from the collected description.
7
8mod component;
9mod config;
10mod descriptor;
11mod features;
12mod players;
13mod protocol;
14mod session;
15mod types;
16mod values;
17
18use proc_macro::TokenStream;
19
20/// Declare an application protocol: value types, a read-only state struct
21/// and a command trait.
22///
23/// ```ignore
24/// #[bern::protocol(namespace = "player", version = 1)]
25/// pub mod player {
26///     use bern::prelude::*;
27///
28///     #[state]
29///     pub struct State { pub position_ms: i64 }
30///
31///     #[commands]
32///     pub trait Commands {
33///         /// Move the playback position
34///         fn seek(&mut self, position_ms: i64) -> Result<(), CommandError>;
35///     }
36/// }
37/// ```
38#[proc_macro_attribute]
39pub fn protocol(args: TokenStream, item: TokenStream) -> TokenStream {
40    protocol::expand(args.into(), item.into())
41        .unwrap_or_else(syn::Error::into_compile_error)
42        .into()
43}
44
45/// Declare an enum or record used by UI declarations outside a protocol.
46#[proc_macro_attribute]
47pub fn value(args: TokenStream, item: TokenStream) -> TokenStream {
48    // `pages = "slot"`: the enum gains a variant per page the modules add
49    // to the slot, as the build tool composes it.
50    let pages = match bern_syntax::parse_value_args(args.into()) {
51        Ok(pages) => pages,
52        Err(error) => return error.into_compile_error().into(),
53    };
54    values::expand_standalone(item.into(), pages.as_deref())
55        .unwrap_or_else(syn::Error::into_compile_error)
56        .into()
57}
58
59/// Declare typed session state owned by the window hosting a component.
60#[proc_macro_attribute]
61pub fn session(args: TokenStream, item: TokenStream) -> TokenStream {
62    if !args.is_empty() {
63        return syn::Error::new(
64            proc_macro2::Span::call_site(),
65            "`session` takes no arguments",
66        )
67        .into_compile_error()
68        .into();
69    }
70    session::expand(item.into())
71        .unwrap_or_else(syn::Error::into_compile_error)
72        .into()
73}
74
75/// Declare the media players owned by the application. Every field is a
76/// `Player`; its name is the stable `player_id` all entries use.
77///
78/// ```ignore
79/// #[bern::players]
80/// pub struct Players {
81///     /// Keeps playing across pages and windows.
82///     pub preview: Player,
83/// }
84/// ```
85#[proc_macro_attribute]
86pub fn players(args: TokenStream, item: TokenStream) -> TokenStream {
87    if !args.is_empty() {
88        return syn::Error::new(
89            proc_macro2::Span::call_site(),
90            "`players` takes no arguments",
91        )
92        .into_compile_error()
93        .into();
94    }
95    players::expand(item.into())
96        .unwrap_or_else(syn::Error::into_compile_error)
97        .into()
98}
99
100/// Declare an exportable component whose body is one `ui!` declaration.
101/// `#[component(features = X)]` makes the component the scope of the
102/// feature contract `X`.
103#[proc_macro_attribute]
104pub fn component(args: TokenStream, item: TokenStream) -> TokenStream {
105    component::expand(args.into(), item.into())
106        .unwrap_or_else(syn::Error::into_compile_error)
107        .into()
108}
109
110/// Declare a feature contract: what users can do within a component's
111/// scope, and the commands each feature is implemented by.
112///
113/// ```ignore
114/// #[bern::features]
115/// pub enum MediaFeature {
116///     /// Choose which clip plays.
117///     #[commands(feature)]
118///     Clip,
119/// }
120/// ```
121#[proc_macro_attribute]
122pub fn features(args: TokenStream, item: TokenStream) -> TokenStream {
123    if !args.is_empty() {
124        return syn::Error::new(
125            proc_macro2::Span::call_site(),
126            "`features` takes no arguments",
127        )
128        .into_compile_error()
129        .into();
130    }
131    features::expand(item.into())
132        .unwrap_or_else(syn::Error::into_compile_error)
133        .into()
134}
135
136/// The UI declaration grammar. Only valid as the body of a `#[component]`.
137#[proc_macro]
138pub fn ui(_input: TokenStream) -> TokenStream {
139    syn::Error::new(
140        proc_macro2::Span::call_site(),
141        "`ui!` is only valid as the whole body of a `#[component]` function",
142    )
143    .into_compile_error()
144    .into()
145}
146
147/// The application's style overrides and motion tokens. The build tool
148/// reads and checks them; for `rustc` the declaration only has to parse.
149#[proc_macro]
150pub fn style(input: TokenStream) -> TokenStream {
151    match bern_syntax::parse_style(input.into()) {
152        Ok(_) => TokenStream::new(),
153        Err(error) => error.into_compile_error().into(),
154    }
155}
156
157/// Localized text. Only valid inside `ui!` and `#[label(...)]`.
158#[proc_macro]
159pub fn tr(_input: TokenStream) -> TokenStream {
160    syn::Error::new(
161        proc_macro2::Span::call_site(),
162        "`tr!` is only valid inside `ui!` declarations and `#[label(...)]` attributes",
163    )
164    .into_compile_error()
165    .into()
166}