Skip to main content

telegram_webapp_sdk/
macros.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4//! Telegram WebApp SDK macros.
5//!
6//! This module provides declarative macros for building Telegram WebApp
7//! applications. They let you:
8//!
9//! * Register routable pages using [`telegram_page!`]
10//! * Define the WASM application entry point with Telegram SDK initialization
11//!   using [`telegram_app!`]
12//! * Build and start a router that collects all registered pages via
13//!   `inventory` using [`telegram_router!`]
14//!
15//! ## Requirements
16//!
17//! 1. A `Page` type and a global `inventory` collection in your crate, for
18//!    example:
19//!
20//! ```ignore
21//! pub mod pages {
22//!     /// Handler type for a page: a plain `fn()`.
23//!     pub type Handler = fn();
24//!
25//!     /// Routable page descriptor.
26//!     #[derive(Copy, Clone)]
27//!     pub struct Page {
28//!         pub path: &'static str,
29//!         pub handler: Handler;
30//!     }
31//!
32//!     // Collect all `Page` items via `inventory`.
33//!     inventory::collect!(Page);
34//!
35//!     /// Iterate over all collected pages as a real `Iterator`.
36//!     pub fn iter() -> impl Iterator<Item = &'static Page> {
37//!         inventory::iter::<Page>.into_iter()
38//!     }
39//! }
40//! ```
41//!
42//! 2. [`telegram_router!`] uses [`crate::router::Router`] by default. To supply
43//!    a custom router type, ensure it exposes:
44//!
45//! ```ignore
46//! impl Router {
47//!     fn new() -> Self;
48//!     fn register(self, path: &str, handler: fn()) -> Self;
49//!     fn start(self);
50//! }
51//! ```
52//!
53//! 3. For [`telegram_app!`], the following items must exist in your crate:
54//!
55//! * `utils::check_env::is_telegram_env() -> bool`
56//! * `mock::config::MockTelegramConfig::from_file(path) -> Result<_, _>`
57//! * `mock::init::mock_telegram_webapp(cfg) -> Result<_, _>`
58//! * `core::init::init_sdk() -> Result<(), wasm_bindgen::JsValue>`
59//!
60//! 4. `Cargo.toml`:
61//!
62//! ```toml
63//! [dependencies]
64//! inventory = "0.3"
65//! wasm-bindgen = "0.2"
66//! ```
67//!
68//! ## Quick example
69//!
70//! ```ignore
71//! use wasm_bindgen::prelude::JsValue;
72//!
73//! // Register a page.
74//! telegram_webapp_sdk::telegram_page!(
75//!     "/",
76//!     /// Home page handler.
77//!     pub fn index() {
78//!         // render something
79//!     }
80//! );
81//!
82//! // Application entry point.
83//! telegram_webapp_sdk::telegram_app!(
84//!     /// Application main entry.
85//!     pub fn main() -> Result<(), JsValue> {
86//!         telegram_webapp_sdk::telegram_router!();
87//!         Ok(())
88//!     }
89//! );
90//! ```
91
92#![allow(clippy::module_name_repetitions)]
93
94/// Register a routable page.
95///
96/// Expands into:
97/// * A function definition with the provided visibility, name, and body
98/// * A single registration item that submits a [`crate::pages::Page`] to
99///   `inventory`, wrapped in a hidden module to remain a valid item in any
100///   context
101///
102/// ### Handler signature
103///
104/// The handler must be a plain function `fn()` with no arguments. If you need
105/// state or context, encapsulate it externally (e.g. closures, singletons, DI),
106/// not as handler parameters.
107///
108/// ### Example
109///
110/// ```ignore
111/// use telegram_webapp_sdk::telegram_page;
112///
113/// telegram_page!(
114///     "/about",
115///     /// About page.
116///     pub fn about() {
117///         // render about page
118///     }
119/// );
120/// ```
121#[macro_export]
122macro_rules! telegram_page {
123    ($path:literal, $(#[$meta:meta])* $vis:vis fn $name:ident $($rest:tt)*) => {
124        $(#[$meta])*
125        $vis fn $name $($rest)*
126
127        #[doc(hidden)]
128        mod __telegram_page_register {
129            // Keep handler reachable while hiding helper names.
130            use super::$name as __handler;
131            #[allow(non_upper_case_globals)]
132            const _: () = {
133                $crate::inventory::submit! {
134                    $crate::pages::Page { path: $path, handler: __handler }
135                }
136            };
137        }
138    };
139}
140
141/// Define the WASM application entry point with Telegram SDK initialization.
142///
143/// The generated function is annotated with `#[wasm_bindgen(start)]`.
144/// It performs:
145///
146/// * Environment detection via `utils::check_env::is_telegram_env()`
147/// * Debug-only mock initialization when not in Telegram
148/// * SDK initialization via `core::init::init_sdk()?`
149///
150/// After these steps, the provided function body is executed.
151///
152/// ### Return type
153///
154/// The function may return either `()` or `Result<(), wasm_bindgen::JsValue>`.
155///
156/// ### Example
157///
158/// ```ignore
159/// use telegram_webapp_sdk::telegram_app;
160/// use wasm_bindgen::JsValue;
161///
162/// telegram_app!(
163///     /// Application entry point.
164///     pub fn main() -> Result<(), JsValue> {
165///         telegram_webapp_sdk::telegram_router!();
166///         Ok(())
167///     }
168/// );
169/// ```
170#[macro_export]
171macro_rules! telegram_app {
172    ($(#[$meta:meta])* $vis:vis fn $name:ident($($arg:tt)*) $(-> $ret:ty)? $body:block) => {
173        $(#[$meta])*
174        #[wasm_bindgen::prelude::wasm_bindgen(start)]
175        $vis fn $name($($arg)*) $(-> $ret)? {
176            if !$crate::utils::check_env::is_telegram_env() {
177                #[cfg(debug_assertions)]
178                if let Ok(cfg) = $crate::mock::config::MockTelegramConfig::from_file("telegram-webapp.toml") {
179                    let _ = $crate::mock::init::mock_telegram_webapp(cfg);
180                }
181            }
182            $crate::core::init::init_sdk()?;
183            $body
184        }
185    };
186}
187
188/// Build and start a router from all registered pages.
189///
190/// By default it uses [`crate::router::Router`]. A custom router type can be
191/// supplied as the first argument. The router type must expose:
192///
193/// * `fn new() -> Self`
194/// * `fn register(self, path: &str, handler: fn()) -> Self`
195/// * `fn start(self)`
196///
197/// ### Examples
198///
199/// Using the default router:
200///
201/// ```ignore
202/// use telegram_webapp_sdk::{telegram_page, telegram_router};
203///
204/// telegram_page!("/", pub fn index() {});
205/// telegram_router!();
206/// ```
207///
208/// Providing a custom router type:
209///
210/// ```ignore
211/// use telegram_webapp_sdk::telegram_router;
212///
213/// struct CustomRouter;
214/// impl CustomRouter {
215///     fn new() -> Self { CustomRouter }
216///     fn register(self, _path: &str, _handler: fn()) -> Self { self }
217///     fn start(self) {}
218/// }
219///
220/// telegram_router!(CustomRouter);
221/// ```
222#[macro_export]
223macro_rules! telegram_router {
224    () => {
225        $crate::telegram_router!($crate::router::Router);
226    };
227    ($router:ty) => {{
228        let mut router = <$router>::new();
229        for page in $crate::pages::iter() {
230            router = router.register(page.path, page.handler);
231        }
232        router.start();
233    }};
234}
235
236/// Create a `<button>` element.
237///
238/// Generates a [`web_sys::HtmlElement`] with the provided text, optional CSS
239/// class and arbitrary attributes. The macro evaluates to
240/// `Result<web_sys::HtmlElement, wasm_bindgen::JsValue>` so it can be used with
241/// the `?` operator inside functions returning `Result`.
242///
243/// # Examples
244///
245/// ```ignore
246/// use telegram_webapp_sdk::telegram_button;
247/// use wasm_bindgen::JsValue;
248///
249/// # fn example() -> Result<(), JsValue> {
250/// let document = web_sys::window()
251///     .and_then(|w| w.document())
252///     .ok_or_else(|| JsValue::from_str("no document"))?;
253/// let button = telegram_button!(document, "Click", class = "primary", "type" = "button")?;
254/// assert_eq!(button.tag_name(), "BUTTON");
255/// # Ok(())
256/// # }
257/// ```
258#[macro_export]
259macro_rules! telegram_button {
260    ($doc:expr, $text:expr $(, class = $class:expr)? $(, $attr:literal = $value:expr)* $(,)?) => {{
261        || -> Result<web_sys::HtmlElement, wasm_bindgen::JsValue> {
262            use wasm_bindgen::JsCast;
263            let element = $doc.create_element("button")?;
264            element.set_inner_html($text);
265            $(element.set_class_name($class);)?
266            $(
267                element.set_attribute($attr, $value)?;
268            )*
269            element
270                .dyn_into::<web_sys::HtmlElement>()
271                .map_err(wasm_bindgen::JsValue::from)
272        }()
273    }};
274}
275
276/// Create an `<img>` element.
277///
278/// Generates a [`web_sys::HtmlImageElement`] with the provided `src`, optional
279/// CSS class, `alt` text and additional attributes. Like
280/// [`telegram_button!`], this macro yields a `Result` for ergonomic error
281/// propagation.
282///
283/// # Examples
284///
285/// ```ignore
286/// use telegram_webapp_sdk::telegram_image;
287/// use wasm_bindgen::JsValue;
288///
289/// # fn example() -> Result<(), JsValue> {
290/// let document = web_sys::window()
291///     .and_then(|w| w.document())
292///     .ok_or_else(|| JsValue::from_str("no document"))?;
293/// let image = telegram_image!(document, "/logo.png", class = "logo", alt = "Logo")?;
294/// assert_eq!(image.tag_name(), "IMG");
295/// # Ok(())
296/// # }
297/// ```
298#[macro_export]
299macro_rules! telegram_image {
300    ($doc:expr, $src:expr $(, class = $class:expr)? $(, alt = $alt:expr)? $(, $attr:literal = $value:expr)* $(,)?) => {{
301        || -> Result<web_sys::HtmlImageElement, wasm_bindgen::JsValue> {
302            use wasm_bindgen::JsCast;
303            let element = $doc.create_element("img")?;
304            element.set_attribute("src", $src)?;
305            $(element.set_class_name($class);)?
306            $(element.set_attribute("alt", $alt)?;)?
307            $(
308                element.set_attribute($attr, $value)?;
309            )*
310            element
311                .dyn_into::<web_sys::HtmlImageElement>()
312                .map_err(wasm_bindgen::JsValue::from)
313        }()
314    }};
315}