fraiseql_functions/triggers/http/
mod.rs1use std::collections::HashMap;
58
59use serde::{Deserialize, Serialize};
60
61#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
63pub struct HttpMethod(pub String);
64
65impl HttpMethod {
66 #[must_use]
68 pub fn new(method: &str) -> Self {
69 Self(method.to_uppercase())
70 }
71
72 #[must_use]
74 pub fn matches(&self, other: &str) -> bool {
75 self.0.eq_ignore_ascii_case(other)
76 }
77
78 #[must_use]
80 pub fn as_str(&self) -> &str {
81 &self.0
82 }
83}
84
85#[derive(Debug, Clone)]
95pub struct HttpTriggerRoute {
96 pub function_name: String,
98 pub method: String,
100 pub path: String,
102 pub requires_auth: bool,
104}
105
106impl HttpTriggerRoute {
107 #[must_use]
109 pub fn new(function_name: &str, method: &str, path: &str) -> Self {
110 Self {
111 function_name: function_name.to_string(),
112 method: method.to_string(),
113 path: path.to_string(),
114 requires_auth: false,
115 }
116 }
117
118 #[must_use = "builder method returns modified builder"]
120 pub const fn with_auth(mut self) -> Self {
121 self.requires_auth = true;
122 self
123 }
124
125 #[must_use = "builder method returns modified builder"]
127 pub const fn without_auth(mut self) -> Self {
128 self.requires_auth = false;
129 self
130 }
131
132 #[must_use]
134 pub fn matches(&self, method: &str, path: &str) -> bool {
135 self.method.eq_ignore_ascii_case(method) && self.path == path
136 }
137
138 #[must_use]
142 pub fn pattern_matches(&self, request_path: &str) -> bool {
143 let route_parts: Vec<&str> = self.path.split('/').collect();
144 let request_parts: Vec<&str> = request_path.split('/').collect();
145
146 if route_parts.len() != request_parts.len() {
147 return false;
148 }
149
150 route_parts.iter().zip(request_parts.iter()).all(|(route_part, request_part)| {
151 route_part == request_part || route_part.starts_with(':')
153 })
154 }
155
156 #[must_use]
160 pub fn extract_params(&self, request_path: &str) -> HashMap<String, String> {
161 let mut params = HashMap::new();
162
163 let route_parts: Vec<&str> = self.path.split('/').collect();
164 let request_parts: Vec<&str> = request_path.split('/').collect();
165
166 for (route_part, request_part) in route_parts.iter().zip(request_parts.iter()) {
167 if let Some(param_name) = route_part.strip_prefix(':') {
168 params.insert(param_name.to_string(), request_part.to_string());
169 }
170 }
171
172 params
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct HttpTriggerPayload {
181 pub method: String,
183 pub path: String,
185 pub headers: serde_json::Value,
187 pub query: serde_json::Value,
189 pub params: serde_json::Value,
191 pub body: Option<serde_json::Value>,
193}
194
195impl HttpTriggerPayload {
196 #[must_use]
198 pub fn new(
199 method: &str,
200 path: &str,
201 headers: serde_json::Value,
202 query: serde_json::Value,
203 body: Option<serde_json::Value>,
204 ) -> Self {
205 Self {
206 method: method.to_string(),
207 path: path.to_string(),
208 headers,
209 query,
210 params: serde_json::json!({}),
211 body,
212 }
213 }
214
215 #[must_use]
217 pub fn header(&self, name: &str) -> Option<String> {
218 let name_lower = name.to_lowercase();
219 if let serde_json::Value::Object(ref obj) = self.headers {
220 for (key, value) in obj {
221 if key.to_lowercase() == name_lower {
222 return value.as_str().map(|s| s.to_string());
223 }
224 }
225 }
226 None
227 }
228
229 #[must_use]
231 pub fn query_param(&self, name: &str) -> Option<String> {
232 self.query.get(name).and_then(|v| v.as_str().map(|s| s.to_string()))
233 }
234
235 #[must_use]
237 pub fn path_param(&self, name: &str) -> Option<String> {
238 self.params.get(name).and_then(|v| v.as_str().map(|s| s.to_string()))
239 }
240
241 #[must_use]
243 pub const fn json_body(&self) -> Option<&serde_json::Value> {
244 self.body.as_ref()
245 }
246
247 #[must_use]
249 pub fn is_get(&self) -> bool {
250 self.method.eq_ignore_ascii_case("GET")
251 }
252
253 #[must_use]
255 pub fn is_post(&self) -> bool {
256 self.method.eq_ignore_ascii_case("POST")
257 }
258
259 #[must_use]
261 pub fn is_put(&self) -> bool {
262 self.method.eq_ignore_ascii_case("PUT")
263 }
264
265 #[must_use]
267 pub fn is_delete(&self) -> bool {
268 self.method.eq_ignore_ascii_case("DELETE")
269 }
270
271 #[must_use]
273 pub fn is_patch(&self) -> bool {
274 self.method.eq_ignore_ascii_case("PATCH")
275 }
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct HttpTriggerResponse {
283 pub status: u16,
285 pub headers: serde_json::Value,
287 pub body: serde_json::Value,
289}
290
291impl HttpTriggerResponse {
292 #[must_use]
294 pub fn ok(body: serde_json::Value) -> Self {
295 Self {
296 status: 200,
297 headers: serde_json::json!({}),
298 body,
299 }
300 }
301
302 #[must_use]
304 pub fn with_status(status: u16, body: serde_json::Value) -> Self {
305 Self {
306 status,
307 headers: serde_json::json!({}),
308 body,
309 }
310 }
311
312 #[must_use]
314 pub fn created(body: serde_json::Value) -> Self {
315 Self::with_status(201, body)
316 }
317
318 #[must_use]
320 pub fn no_content() -> Self {
321 Self {
322 status: 204,
323 headers: serde_json::json!({}),
324 body: serde_json::json!({}),
325 }
326 }
327
328 #[must_use]
330 pub fn bad_request(message: &str) -> Self {
331 Self::with_status(400, serde_json::json!({"error": message}))
332 }
333
334 #[must_use]
336 pub fn unauthorized() -> Self {
337 Self::with_status(401, serde_json::json!({"error": "Unauthorized"}))
338 }
339
340 #[must_use]
342 pub fn forbidden() -> Self {
343 Self::with_status(403, serde_json::json!({"error": "Forbidden"}))
344 }
345
346 #[must_use]
348 pub fn not_found() -> Self {
349 Self::with_status(404, serde_json::json!({"error": "Not found"}))
350 }
351
352 #[must_use]
354 pub fn internal_error(message: &str) -> Self {
355 Self::with_status(500, serde_json::json!({"error": message}))
356 }
357
358 #[must_use = "builder method returns modified builder"]
360 pub fn with_header(mut self, key: String, value: String) -> Self {
361 if let serde_json::Value::Object(ref mut map) = self.headers {
362 map.insert(key, serde_json::Value::String(value));
363 }
364 self
365 }
366}
367
368#[derive(Debug, Clone, Default)]
372pub struct HttpTriggerMatcher {
373 routes: Vec<HttpTriggerRoute>,
375}
376
377impl HttpTriggerMatcher {
378 #[must_use]
380 pub const fn new() -> Self {
381 Self { routes: Vec::new() }
382 }
383
384 pub fn add(&mut self, route: HttpTriggerRoute) {
386 self.routes.push(route);
387 }
388
389 #[must_use]
391 pub fn find(&self, method: &str, path: &str) -> Option<HttpTriggerRoute> {
392 self.routes
393 .iter()
394 .find(|route| route.method.eq_ignore_ascii_case(method) && route.pattern_matches(path))
395 .cloned()
396 }
397
398 #[must_use]
400 pub fn routes(&self) -> &[HttpTriggerRoute] {
401 &self.routes
402 }
403}
404
405#[cfg(test)]
406mod tests;