armature_macros/lib.rs
1//! Declarative macros for the Armature framework
2//!
3//! This crate provides convenient declarative macros (macro_rules!) for
4//! common patterns in Armature applications.
5//!
6//! # Response Macros
7//!
8//! Quick HTTP responses:
9//! ```ignore
10//! json_response!({ "message": "Success" })
11//! ok_json!({ "id": 123 })
12//! error_json!("User not found")
13//! ```
14//!
15//! # Route Grouping
16//!
17//! ```ignore
18//! routes! {
19//! GET "/users" => list_users,
20//! POST "/users" => create_user,
21//! GET "/users/:id" => get_user,
22//! }
23//! ```
24
25/// Create a JSON HTTP response with status code
26///
27/// # Examples
28///
29/// ```ignore
30/// // With explicit status
31/// json_response!(200, { "message": "Success", "id": 123 })
32///
33/// // With status constant
34/// json_response!(StatusCode::OK, { "data": data })
35/// ```
36#[macro_export]
37macro_rules! json_response {
38 ($status:expr, $value:expr) => {{
39 use armature_core::HttpResponse;
40 HttpResponse::new($status).with_json(&$value)
41 }};
42}
43
44/// Create a 200 OK JSON response
45///
46/// # Examples
47///
48/// ```ignore
49/// ok_json!({ "message": "Success" })
50/// ok_json!({ "users": users_vec })
51/// ```
52#[macro_export]
53macro_rules! ok_json {
54 ($value:expr) => {{
55 use armature_core::HttpResponse;
56 HttpResponse::ok().with_json(&$value)
57 }};
58}
59
60/// Create a 201 Created JSON response
61///
62/// # Examples
63///
64/// ```ignore
65/// created_json!({ "id": new_id, "message": "User created" })
66/// ```
67#[macro_export]
68macro_rules! created_json {
69 ($value:expr) => {{
70 use armature_core::HttpResponse;
71 HttpResponse::created().with_json(&$value)
72 }};
73}
74
75/// Create a 400 Bad Request JSON error response
76///
77/// # Examples
78///
79/// ```ignore
80/// bad_request!("Invalid input")
81/// bad_request!("Field '{}' is required", field_name)
82/// ```
83#[macro_export]
84macro_rules! bad_request {
85 ($msg:expr) => {
86 {
87 use armature_core::HttpResponse;
88 HttpResponse::bad_request().with_json(&serde_json::json!({
89 "error": $msg,
90 "status": 400
91 }))
92 }
93 };
94 ($fmt:expr, $($arg:tt)*) => {
95 bad_request!(format!($fmt, $($arg)*))
96 };
97}
98
99/// Create a 404 Not Found JSON error response
100///
101/// # Examples
102///
103/// ```ignore
104/// not_found!("User not found")
105/// not_found!("Resource '{}' not found", resource_id)
106/// ```
107#[macro_export]
108macro_rules! not_found {
109 ($msg:expr) => {
110 {
111 use armature_core::HttpResponse;
112 HttpResponse::not_found().with_json(&serde_json::json!({
113 "error": $msg,
114 "status": 404
115 }))
116 }
117 };
118 ($fmt:expr, $($arg:tt)*) => {
119 not_found!(format!($fmt, $($arg)*))
120 };
121}
122
123/// Create a 500 Internal Server Error JSON response
124///
125/// # Examples
126///
127/// ```ignore
128/// internal_error!("Database connection failed")
129/// ```
130#[macro_export]
131macro_rules! internal_error {
132 ($msg:expr) => {
133 {
134 use armature_core::HttpResponse;
135 HttpResponse::internal_server_error().with_json(&serde_json::json!({
136 "error": $msg,
137 "status": 500
138 }))
139 }
140 };
141}
142
143/// Extract and validate path parameters
144///
145/// # Examples
146///
147/// ```ignore
148/// let id: i64 = path_param!(request, "id")?;
149/// let slug: String = path_param!(request, "slug")?;
150/// ```
151#[macro_export]
152macro_rules! path_param {
153 ($req:expr, $name:expr) => {{
154 $req.path_params
155 .get($name)
156 .ok_or_else(|| {
157 armature_core::Error::BadRequest(format!("Missing path parameter: {}", $name))
158 })?
159 .parse()
160 .map_err(|e| {
161 armature_core::Error::BadRequest(format!(
162 "Invalid path parameter '{}': {}",
163 $name, e
164 ))
165 })
166 }};
167}
168
169/// Extract and validate query parameters
170///
171/// # Examples
172///
173/// ```ignore
174/// let page: u32 = query_param!(request, "page").unwrap_or(1);
175/// let limit: u32 = query_param!(request, "limit").unwrap_or(20);
176/// ```
177#[macro_export]
178macro_rules! query_param {
179 ($req:expr, $name:expr) => {{ $req.query_params.get($name).and_then(|v| v.parse().ok()) }};
180}
181
182/// Extract header value
183///
184/// # Examples
185///
186/// ```ignore
187/// let auth = header!(request, "Authorization")?;
188/// let content_type = header!(request, "Content-Type").unwrap_or("text/plain");
189/// ```
190#[macro_export]
191macro_rules! header {
192 ($req:expr, $name:expr) => {{
193 $req.headers
194 .get($name)
195 .ok_or_else(|| armature_core::Error::BadRequest(format!("Missing header: {}", $name)))
196 }};
197}
198
199/// Create a validation error
200///
201/// # Examples
202///
203/// ```ignore
204/// return validation_error!("Email is required");
205/// return validation_error!("Age must be at least {}", min_age);
206/// ```
207#[macro_export]
208macro_rules! validation_error {
209 ($msg:expr) => {
210 Err(armature_core::Error::Validation($msg.to_string()))
211 };
212 ($fmt:expr, $($arg:tt)*) => {
213 validation_error!(format!($fmt, $($arg)*))
214 };
215}
216
217/// Guard a condition, returning an error if false
218///
219/// # Examples
220///
221/// ```ignore
222/// guard!(user.is_admin(), "Admin access required");
223/// guard!(age >= 18, "Must be 18 or older");
224/// ```
225#[macro_export]
226macro_rules! guard {
227 ($cond:expr, $msg:expr) => {
228 if !($cond) {
229 return Err(armature_core::Error::Forbidden($msg.to_string()));
230 }
231 };
232}
233
234/// Define multiple routes concisely
235///
236/// # Examples
237///
238/// ```ignore
239/// routes! {
240/// GET "/users" => list_users,
241/// POST "/users" => create_user,
242/// GET "/users/:id" => get_user,
243/// PUT "/users/:id" => update_user,
244/// DELETE "/users/:id" => delete_user,
245/// }
246/// ```
247#[macro_export]
248macro_rules! routes {
249 ($($method:ident $path:literal => $handler:expr),* $(,)?) => {
250 vec![
251 $(
252 armature_core::Route {
253 method: armature_core::HttpMethod::from_str(stringify!($method)).unwrap(),
254 path: $path.to_string(),
255 handler: std::sync::Arc::new($handler),
256 constraints: None,
257 },
258 )*
259 ]
260 };
261}
262
263/// Build JSON objects with type safety
264///
265/// # Examples
266///
267/// ```ignore
268/// let response = json_object! {
269/// "id" => user.id,
270/// "name" => user.name,
271/// "email" => user.email,
272/// };
273/// ```
274#[macro_export]
275macro_rules! json_object {
276 ($($key:literal => $value:expr),* $(,)?) => {
277 {
278 let mut map = serde_json::Map::new();
279 $(
280 map.insert($key.to_string(), serde_json::json!($value));
281 )*
282 serde_json::Value::Object(map)
283 }
284 };
285}
286
287/// Extract multiple path params at once
288///
289/// # Examples
290///
291/// ```ignore
292/// let (user_id, post_id) = path_params!(request, "user_id": i64, "post_id": i64)?;
293/// ```
294#[macro_export]
295macro_rules! path_params {
296 ($req:expr, $($name:literal : $type:ty),+ $(,)?) => {
297 {
298 (
299 $(
300 {
301 $req.path_params
302 .get($name)
303 .ok_or_else(|| armature_core::Error::BadRequest(
304 format!("Missing path parameter: {}", $name)
305 ))?
306 .parse::<$type>()
307 .map_err(|e| armature_core::Error::BadRequest(
308 format!("Invalid path parameter '{}': {}", $name, e)
309 ))?
310 }
311 ),+
312 )
313 }
314 };
315}
316
317/// Log and return an error
318///
319/// # Examples
320///
321/// ```ignore
322/// log_error!("Failed to connect to database: {}", err);
323/// ```
324#[macro_export]
325macro_rules! log_error {
326 ($($arg:tt)*) => {
327 {
328 tracing::error!($($arg)*);
329 Err(armature_core::Error::InternalServerError(
330 format!($($arg)*)
331 ))
332 }
333 };
334}
335
336/// Create a paginated response
337///
338/// # Examples
339///
340/// ```ignore
341/// paginated_response!(users, page, total_count)
342/// ```
343#[macro_export]
344macro_rules! paginated_response {
345 ($data:expr, $page:expr, $total:expr) => {
346 {
347 use armature_core::HttpResponse;
348 HttpResponse::ok().with_json(&serde_json::json!({
349 "data": $data,
350 "pagination": {
351 "page": $page,
352 "total": $total,
353 "per_page": $data.len(),
354 }
355 }))
356 }
357 };
358}
359
360pub mod prelude;
361
362#[cfg(test)]
363mod tests {
364 #[test]
365 fn test_macros_compile() {
366 // Ensure macros are exported
367 }
368}