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