fraiseql_functions/triggers/http/
mod.rs1use std::collections::HashMap;
69
70use serde::{Deserialize, Serialize};
71
72#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
74pub struct HttpMethod(pub String);
75
76impl HttpMethod {
77 #[must_use]
79 pub fn new(method: &str) -> Self {
80 Self(method.to_uppercase())
81 }
82
83 #[must_use]
85 pub fn matches(&self, other: &str) -> bool {
86 self.0.eq_ignore_ascii_case(other)
87 }
88
89 #[must_use]
91 pub fn as_str(&self) -> &str {
92 &self.0
93 }
94}
95
96#[derive(Debug, Clone)]
106pub struct HttpTriggerRoute {
107 pub function_name: String,
109 pub method: String,
111 pub path: String,
113 pub requires_auth: bool,
115}
116
117impl HttpTriggerRoute {
118 #[must_use]
120 pub fn new(function_name: &str, method: &str, path: &str) -> Self {
121 Self {
122 function_name: function_name.to_string(),
123 method: method.to_string(),
124 path: path.to_string(),
125 requires_auth: false,
126 }
127 }
128
129 #[must_use = "builder method returns modified builder"]
131 pub const fn with_auth(mut self) -> Self {
132 self.requires_auth = true;
133 self
134 }
135
136 #[must_use = "builder method returns modified builder"]
138 pub const fn without_auth(mut self) -> Self {
139 self.requires_auth = false;
140 self
141 }
142
143 #[must_use]
145 pub fn matches(&self, method: &str, path: &str) -> bool {
146 self.method.eq_ignore_ascii_case(method) && self.path == path
147 }
148
149 #[must_use]
153 pub fn pattern_matches(&self, request_path: &str) -> bool {
154 let route_parts: Vec<&str> = self.path.split('/').collect();
155 let request_parts: Vec<&str> = request_path.split('/').collect();
156
157 if route_parts.len() != request_parts.len() {
158 return false;
159 }
160
161 route_parts.iter().zip(request_parts.iter()).all(|(route_part, request_part)| {
162 route_part == request_part || route_part.starts_with(':')
164 })
165 }
166
167 #[must_use]
171 pub fn extract_params(&self, request_path: &str) -> HashMap<String, String> {
172 let mut params = HashMap::new();
173
174 let route_parts: Vec<&str> = self.path.split('/').collect();
175 let request_parts: Vec<&str> = request_path.split('/').collect();
176
177 for (route_part, request_part) in route_parts.iter().zip(request_parts.iter()) {
178 if let Some(param_name) = route_part.strip_prefix(':') {
179 params.insert(param_name.to_string(), request_part.to_string());
180 }
181 }
182
183 params
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct HttpTriggerPayload {
192 pub method: String,
194 pub path: String,
196 pub headers: serde_json::Value,
198 pub query: serde_json::Value,
200 pub params: serde_json::Value,
202 pub body: Option<serde_json::Value>,
204}
205
206impl HttpTriggerPayload {
207 #[must_use]
209 pub fn new(
210 method: &str,
211 path: &str,
212 headers: serde_json::Value,
213 query: serde_json::Value,
214 body: Option<serde_json::Value>,
215 ) -> Self {
216 Self {
217 method: method.to_string(),
218 path: path.to_string(),
219 headers,
220 query,
221 params: serde_json::json!({}),
222 body,
223 }
224 }
225
226 #[must_use]
228 pub fn header(&self, name: &str) -> Option<String> {
229 let name_lower = name.to_lowercase();
230 if let serde_json::Value::Object(ref obj) = self.headers {
231 for (key, value) in obj {
232 if key.to_lowercase() == name_lower {
233 return value.as_str().map(|s| s.to_string());
234 }
235 }
236 }
237 None
238 }
239
240 #[must_use]
242 pub fn query_param(&self, name: &str) -> Option<String> {
243 self.query.get(name).and_then(|v| v.as_str().map(|s| s.to_string()))
244 }
245
246 #[must_use]
248 pub fn path_param(&self, name: &str) -> Option<String> {
249 self.params.get(name).and_then(|v| v.as_str().map(|s| s.to_string()))
250 }
251
252 #[must_use]
254 pub const fn json_body(&self) -> Option<&serde_json::Value> {
255 self.body.as_ref()
256 }
257
258 #[must_use]
260 pub fn is_get(&self) -> bool {
261 self.method.eq_ignore_ascii_case("GET")
262 }
263
264 #[must_use]
266 pub fn is_post(&self) -> bool {
267 self.method.eq_ignore_ascii_case("POST")
268 }
269
270 #[must_use]
272 pub fn is_put(&self) -> bool {
273 self.method.eq_ignore_ascii_case("PUT")
274 }
275
276 #[must_use]
278 pub fn is_delete(&self) -> bool {
279 self.method.eq_ignore_ascii_case("DELETE")
280 }
281
282 #[must_use]
284 pub fn is_patch(&self) -> bool {
285 self.method.eq_ignore_ascii_case("PATCH")
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct HttpTriggerResponse {
294 pub status: u16,
296 pub headers: serde_json::Value,
298 pub body: serde_json::Value,
300}
301
302impl HttpTriggerResponse {
303 #[must_use]
305 pub fn ok(body: serde_json::Value) -> Self {
306 Self {
307 status: 200,
308 headers: serde_json::json!({}),
309 body,
310 }
311 }
312
313 #[must_use]
315 pub fn with_status(status: u16, body: serde_json::Value) -> Self {
316 Self {
317 status,
318 headers: serde_json::json!({}),
319 body,
320 }
321 }
322
323 #[must_use]
325 pub fn created(body: serde_json::Value) -> Self {
326 Self::with_status(201, body)
327 }
328
329 #[must_use]
331 pub fn no_content() -> Self {
332 Self {
333 status: 204,
334 headers: serde_json::json!({}),
335 body: serde_json::json!({}),
336 }
337 }
338
339 #[must_use]
341 pub fn bad_request(message: &str) -> Self {
342 Self::with_status(400, serde_json::json!({"error": message}))
343 }
344
345 #[must_use]
347 pub fn unauthorized() -> Self {
348 Self::with_status(401, serde_json::json!({"error": "Unauthorized"}))
349 }
350
351 #[must_use]
353 pub fn forbidden() -> Self {
354 Self::with_status(403, serde_json::json!({"error": "Forbidden"}))
355 }
356
357 #[must_use]
359 pub fn not_found() -> Self {
360 Self::with_status(404, serde_json::json!({"error": "Not found"}))
361 }
362
363 #[must_use]
365 pub fn internal_error(message: &str) -> Self {
366 Self::with_status(500, serde_json::json!({"error": message}))
367 }
368
369 #[must_use = "builder method returns modified builder"]
371 pub fn with_header(mut self, key: String, value: String) -> Self {
372 if let serde_json::Value::Object(ref mut map) = self.headers {
373 map.insert(key, serde_json::Value::String(value));
374 }
375 self
376 }
377}
378
379#[derive(Debug, Clone, Default)]
383pub struct HttpTriggerMatcher {
384 routes: Vec<HttpTriggerRoute>,
386}
387
388impl HttpTriggerMatcher {
389 #[must_use]
391 pub const fn new() -> Self {
392 Self { routes: Vec::new() }
393 }
394
395 pub fn add(&mut self, route: HttpTriggerRoute) {
397 self.routes.push(route);
398 }
399
400 #[must_use]
402 pub fn find(&self, method: &str, path: &str) -> Option<HttpTriggerRoute> {
403 self.routes
404 .iter()
405 .find(|route| route.method.eq_ignore_ascii_case(method) && route.pattern_matches(path))
406 .cloned()
407 }
408
409 #[must_use]
411 pub fn routes(&self) -> &[HttpTriggerRoute] {
412 &self.routes
413 }
414}
415
416#[cfg(test)]
417mod tests;