1use serde_json::Value;
31use std::collections::HashMap;
32
33#[cfg(target_arch = "wasm32")]
38extern "C" {
39 #[link_name = "cufflink_log"]
40 fn cufflink_log(level: i32, msg_ptr: i32, msg_len: i32);
41 fn db_query(sql_ptr: i32, sql_len: i32) -> i32;
42 fn db_execute(sql_ptr: i32, sql_len: i32) -> i32;
43 fn get_host_response_len() -> i32;
44 fn get_host_response(buf_ptr: i32, buf_len: i32) -> i32;
45 fn nats_publish(subj_ptr: i32, subj_len: i32, payload_ptr: i32, payload_len: i32) -> i32;
46 fn nats_request(
47 subj_ptr: i32,
48 subj_len: i32,
49 payload_ptr: i32,
50 payload_len: i32,
51 timeout_ms: i32,
52 ) -> i32;
53 fn http_fetch(
54 method_ptr: i32,
55 method_len: i32,
56 url_ptr: i32,
57 url_len: i32,
58 headers_ptr: i32,
59 headers_len: i32,
60 body_ptr: i32,
61 body_len: i32,
62 ) -> i32;
63 fn get_config(key_ptr: i32, key_len: i32) -> i32;
64 fn s3_download(bucket_ptr: i32, bucket_len: i32, key_ptr: i32, key_len: i32) -> i32;
65 fn s3_presign_upload(
66 bucket_ptr: i32,
67 bucket_len: i32,
68 key_ptr: i32,
69 key_len: i32,
70 content_type_ptr: i32,
71 content_type_len: i32,
72 expires_secs: i32,
73 ) -> i32;
74 fn redis_get(key_ptr: i32, key_len: i32) -> i32;
75 fn redis_set(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32, ttl_secs: i32) -> i32;
76 fn redis_del(key_ptr: i32, key_len: i32) -> i32;
77 fn generate_uuid() -> i32;
78}
79
80#[derive(Debug, Clone)]
101pub struct Auth {
102 pub sub: String,
104 pub preferred_username: Option<String>,
106 pub name: Option<String>,
108 pub email: Option<String>,
110 pub realm_roles: Vec<String>,
112 pub claims: HashMap<String, Value>,
114 pub permissions: Vec<String>,
116 pub role_names: Vec<String>,
118 pub is_service_account: bool,
121}
122
123impl Auth {
124 pub fn has_role(&self, role: &str) -> bool {
126 self.realm_roles.iter().any(|r| r == role)
127 }
128
129 pub fn can(&self, area: &str, operation: &str) -> bool {
140 let required = format!("{}:{}", area, operation);
141 let wildcard = format!("{}:*", area);
142 self.permissions
143 .iter()
144 .any(|p| p == &required || p == &wildcard || p == "*")
145 }
146
147 pub fn has_cufflink_role(&self, role: &str) -> bool {
149 self.role_names.iter().any(|r| r == role)
150 }
151
152 pub fn claim(&self, key: &str) -> Option<&Value> {
154 self.claims.get(key)
155 }
156}
157
158#[derive(Debug, Clone)]
165pub struct Request {
166 method: String,
167 handler: String,
168 headers: HashMap<String, String>,
169 body: Value,
170 tenant: String,
171 service: String,
172 auth: Option<Auth>,
173}
174
175impl Request {
176 pub fn from_json(json: &str) -> Option<Self> {
178 let v: Value = serde_json::from_str(json).ok()?;
179 let headers = v["headers"]
180 .as_object()
181 .map(|m| {
182 m.iter()
183 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
184 .collect()
185 })
186 .unwrap_or_default();
187
188 let auth = v["auth"].as_object().map(|auth_obj| {
189 let a = Value::Object(auth_obj.clone());
190 Auth {
191 sub: a["sub"].as_str().unwrap_or("").to_string(),
192 preferred_username: a["preferred_username"].as_str().map(|s| s.to_string()),
193 name: a["name"].as_str().map(|s| s.to_string()),
194 email: a["email"].as_str().map(|s| s.to_string()),
195 realm_roles: a["realm_roles"]
196 .as_array()
197 .map(|arr| {
198 arr.iter()
199 .filter_map(|v| v.as_str().map(|s| s.to_string()))
200 .collect()
201 })
202 .unwrap_or_default(),
203 claims: a["claims"]
204 .as_object()
205 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
206 .unwrap_or_default(),
207 permissions: a["permissions"]
208 .as_array()
209 .map(|arr| {
210 arr.iter()
211 .filter_map(|v| v.as_str().map(|s| s.to_string()))
212 .collect()
213 })
214 .unwrap_or_default(),
215 role_names: a["role_names"]
216 .as_array()
217 .map(|arr| {
218 arr.iter()
219 .filter_map(|v| v.as_str().map(|s| s.to_string()))
220 .collect()
221 })
222 .unwrap_or_default(),
223 is_service_account: a["is_service_account"].as_bool().unwrap_or(false),
224 }
225 });
226
227 Some(Self {
228 method: v["method"].as_str().unwrap_or("GET").to_string(),
229 handler: v["handler"].as_str().unwrap_or("").to_string(),
230 headers,
231 body: v["body"].clone(),
232 tenant: v["tenant"].as_str().unwrap_or("").to_string(),
233 service: v["service"].as_str().unwrap_or("").to_string(),
234 auth,
235 })
236 }
237
238 pub fn method(&self) -> &str {
240 &self.method
241 }
242
243 pub fn handler(&self) -> &str {
245 &self.handler
246 }
247
248 pub fn headers(&self) -> &HashMap<String, String> {
250 &self.headers
251 }
252
253 pub fn header(&self, name: &str) -> Option<&str> {
255 self.headers.get(name).map(|s| s.as_str())
256 }
257
258 pub fn body(&self) -> &Value {
260 &self.body
261 }
262
263 pub fn tenant(&self) -> &str {
265 &self.tenant
266 }
267
268 pub fn service(&self) -> &str {
270 &self.service
271 }
272
273 pub fn auth(&self) -> Option<&Auth> {
277 self.auth.as_ref()
278 }
279
280 pub fn require_auth(&self) -> Result<&Auth, Response> {
292 self.auth.as_ref().ok_or_else(|| {
293 Response::json(&serde_json::json!({
294 "error": "Authentication required",
295 "status": 401
296 }))
297 })
298 }
299}
300
301#[derive(Debug, Clone)]
305pub struct Response {
306 data: String,
307 status: u16,
308}
309
310impl Response {
311 pub fn json(value: &Value) -> Self {
313 Self {
314 data: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
315 status: 200,
316 }
317 }
318
319 pub fn text(s: &str) -> Self {
321 Self::json(&Value::String(s.to_string()))
322 }
323
324 pub fn error(message: &str) -> Self {
326 Self {
327 data: serde_json::json!({"error": message}).to_string(),
328 status: 400,
329 }
330 }
331
332 pub fn not_found(message: &str) -> Self {
334 Self {
335 data: serde_json::json!({"error": message}).to_string(),
336 status: 404,
337 }
338 }
339
340 pub fn forbidden(message: &str) -> Self {
342 Self {
343 data: serde_json::json!({"error": message}).to_string(),
344 status: 403,
345 }
346 }
347
348 pub fn empty() -> Self {
350 Self::json(&serde_json::json!({"ok": true}))
351 }
352
353 pub fn with_status(mut self, status: u16) -> Self {
355 self.status = status;
356 self
357 }
358
359 pub fn into_data(self) -> String {
362 if self.status == 200 {
363 self.data
365 } else {
366 serde_json::json!({
368 "__status": self.status,
369 "__body": serde_json::from_str::<Value>(&self.data).unwrap_or(Value::String(self.data)),
370 })
371 .to_string()
372 }
373 }
374}
375
376pub mod db {
383 use super::*;
384
385 pub fn query(sql: &str) -> Vec<Value> {
396 #[cfg(target_arch = "wasm32")]
397 {
398 let bytes = sql.as_bytes();
399 let result = unsafe { db_query(bytes.as_ptr() as i32, bytes.len() as i32) };
400 if result < 0 {
401 return vec![];
402 }
403 read_host_response()
404 }
405 #[cfg(not(target_arch = "wasm32"))]
406 {
407 let _ = sql;
408 vec![]
409 }
410 }
411
412 pub fn query_one(sql: &str) -> Option<Value> {
420 query(sql).into_iter().next()
421 }
422
423 pub fn execute(sql: &str) -> i32 {
432 #[cfg(target_arch = "wasm32")]
433 {
434 let bytes = sql.as_bytes();
435 unsafe { db_execute(bytes.as_ptr() as i32, bytes.len() as i32) }
436 }
437 #[cfg(not(target_arch = "wasm32"))]
438 {
439 let _ = sql;
440 0
441 }
442 }
443
444 #[cfg(target_arch = "wasm32")]
446 fn read_host_response() -> Vec<Value> {
447 let len = unsafe { get_host_response_len() };
448 if len <= 0 {
449 return vec![];
450 }
451 let mut buf = vec![0u8; len as usize];
452 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
453 if read <= 0 {
454 return vec![];
455 }
456 buf.truncate(read as usize);
457 let json_str = String::from_utf8_lossy(&buf);
458 serde_json::from_str(&json_str).unwrap_or_default()
459 }
460}
461
462pub mod nats {
469 #[allow(unused_imports)]
470 use super::*;
471
472 pub fn publish(subject: &str, payload: &str) -> bool {
483 #[cfg(target_arch = "wasm32")]
484 {
485 let subj_bytes = subject.as_bytes();
486 let payload_bytes = payload.as_bytes();
487 let result = unsafe {
488 nats_publish(
489 subj_bytes.as_ptr() as i32,
490 subj_bytes.len() as i32,
491 payload_bytes.as_ptr() as i32,
492 payload_bytes.len() as i32,
493 )
494 };
495 result == 0
496 }
497 #[cfg(not(target_arch = "wasm32"))]
498 {
499 let _ = (subject, payload);
500 true
501 }
502 }
503
504 pub fn request(subject: &str, payload: &str, timeout_ms: i32) -> Option<String> {
516 #[cfg(target_arch = "wasm32")]
517 {
518 let subj_bytes = subject.as_bytes();
519 let payload_bytes = payload.as_bytes();
520 let result = unsafe {
521 nats_request(
522 subj_bytes.as_ptr() as i32,
523 subj_bytes.len() as i32,
524 payload_bytes.as_ptr() as i32,
525 payload_bytes.len() as i32,
526 timeout_ms,
527 )
528 };
529 if result != 0 {
530 return None;
531 }
532 let len = unsafe { get_host_response_len() };
533 if len <= 0 {
534 return None;
535 }
536 let mut buf = vec![0u8; len as usize];
537 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
538 if read <= 0 {
539 return None;
540 }
541 String::from_utf8(buf[..read as usize].to_vec()).ok()
542 }
543 #[cfg(not(target_arch = "wasm32"))]
544 {
545 let _ = (subject, payload, timeout_ms);
546 None
547 }
548 }
549}
550
551pub mod log {
557 #[allow(unused_imports)]
558 use super::*;
559
560 pub fn error(msg: &str) {
562 write(0, msg);
563 }
564
565 pub fn warn(msg: &str) {
567 write(1, msg);
568 }
569
570 pub fn info(msg: &str) {
572 write(2, msg);
573 }
574
575 pub fn debug(msg: &str) {
577 write(3, msg);
578 }
579
580 fn write(level: i32, msg: &str) {
581 #[cfg(target_arch = "wasm32")]
582 {
583 let bytes = msg.as_bytes();
584 unsafe {
585 super::cufflink_log(level, bytes.as_ptr() as i32, bytes.len() as i32);
586 }
587 }
588 #[cfg(not(target_arch = "wasm32"))]
589 {
590 let _ = (level, msg);
591 }
592 }
593}
594
595pub mod http {
602 #[allow(unused_imports)]
603 use super::*;
604
605 #[derive(Debug, Clone)]
607 pub struct FetchResponse {
608 pub status: i32,
610 pub body: String,
612 pub body_encoding: String,
614 pub headers: HashMap<String, String>,
616 }
617
618 impl FetchResponse {
619 pub fn json(&self) -> Option<Value> {
621 serde_json::from_str(&self.body).ok()
622 }
623
624 pub fn is_success(&self) -> bool {
626 (200..300).contains(&self.status)
627 }
628
629 pub fn is_base64(&self) -> bool {
631 self.body_encoding == "base64"
632 }
633 }
634
635 pub fn fetch(
646 method: &str,
647 url: &str,
648 headers: &[(&str, &str)],
649 body: Option<&str>,
650 ) -> Option<FetchResponse> {
651 #[cfg(target_arch = "wasm32")]
652 {
653 let method_bytes = method.as_bytes();
654 let url_bytes = url.as_bytes();
655 let headers_map: HashMap<&str, &str> = headers.iter().copied().collect();
656 let headers_json = serde_json::to_string(&headers_map).unwrap_or_default();
657 let headers_bytes = headers_json.as_bytes();
658 let body_bytes = body.unwrap_or("").as_bytes();
659 let body_len = body.map(|b| b.len()).unwrap_or(0);
660
661 let result = unsafe {
662 http_fetch(
663 method_bytes.as_ptr() as i32,
664 method_bytes.len() as i32,
665 url_bytes.as_ptr() as i32,
666 url_bytes.len() as i32,
667 headers_bytes.as_ptr() as i32,
668 headers_bytes.len() as i32,
669 body_bytes.as_ptr() as i32,
670 body_len as i32,
671 )
672 };
673
674 if result < 0 {
675 return None;
676 }
677
678 read_fetch_response()
679 }
680 #[cfg(not(target_arch = "wasm32"))]
681 {
682 let _ = (method, url, headers, body);
683 None
684 }
685 }
686
687 pub fn get(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
689 fetch("GET", url, headers, None)
690 }
691
692 pub fn post(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
694 fetch("POST", url, headers, Some(body))
695 }
696
697 pub fn put(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
699 fetch("PUT", url, headers, Some(body))
700 }
701
702 pub fn delete(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
704 fetch("DELETE", url, headers, None)
705 }
706
707 pub fn patch(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
709 fetch("PATCH", url, headers, Some(body))
710 }
711
712 #[cfg(target_arch = "wasm32")]
714 fn read_fetch_response() -> Option<FetchResponse> {
715 let len = unsafe { get_host_response_len() };
716 if len <= 0 {
717 return None;
718 }
719 let mut buf = vec![0u8; len as usize];
720 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
721 if read <= 0 {
722 return None;
723 }
724 buf.truncate(read as usize);
725 let json_str = String::from_utf8_lossy(&buf);
726 let v: Value = serde_json::from_str(&json_str).ok()?;
727 Some(FetchResponse {
728 status: v["status"].as_i64().unwrap_or(0) as i32,
729 body: v["body"].as_str().unwrap_or("").to_string(),
730 body_encoding: v["body_encoding"].as_str().unwrap_or("utf8").to_string(),
731 headers: v["headers"]
732 .as_object()
733 .map(|m| {
734 m.iter()
735 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
736 .collect()
737 })
738 .unwrap_or_default(),
739 })
740 }
741}
742
743pub mod config {
751 #[allow(unused_imports)]
752 use super::*;
753
754 pub fn get(key: &str) -> Option<String> {
763 #[cfg(target_arch = "wasm32")]
764 {
765 let bytes = key.as_bytes();
766 let result = unsafe { get_config(bytes.as_ptr() as i32, bytes.len() as i32) };
767 if result < 0 {
768 return None;
769 }
770 read_config_response()
771 }
772 #[cfg(not(target_arch = "wasm32"))]
773 {
774 let _ = key;
775 None
776 }
777 }
778
779 #[cfg(target_arch = "wasm32")]
781 fn read_config_response() -> Option<String> {
782 let len = unsafe { get_host_response_len() };
783 if len <= 0 {
784 return None;
785 }
786 let mut buf = vec![0u8; len as usize];
787 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
788 if read <= 0 {
789 return None;
790 }
791 buf.truncate(read as usize);
792 String::from_utf8(buf).ok()
793 }
794}
795
796pub mod storage {
804 #[allow(unused_imports)]
805 use super::*;
806
807 pub fn download(bucket: &str, key: &str) -> Option<String> {
818 #[cfg(target_arch = "wasm32")]
819 {
820 let bucket_bytes = bucket.as_bytes();
821 let key_bytes = key.as_bytes();
822 let result = unsafe {
823 s3_download(
824 bucket_bytes.as_ptr() as i32,
825 bucket_bytes.len() as i32,
826 key_bytes.as_ptr() as i32,
827 key_bytes.len() as i32,
828 )
829 };
830 if result < 0 {
831 return None;
832 }
833 read_storage_response()
834 }
835 #[cfg(not(target_arch = "wasm32"))]
836 {
837 let _ = (bucket, key);
838 None
839 }
840 }
841
842 pub fn presign_upload(
855 bucket: &str,
856 key: &str,
857 content_type: &str,
858 expires_secs: u64,
859 ) -> Option<String> {
860 #[cfg(target_arch = "wasm32")]
861 {
862 let bucket_bytes = bucket.as_bytes();
863 let key_bytes = key.as_bytes();
864 let ct_bytes = content_type.as_bytes();
865 let result = unsafe {
866 s3_presign_upload(
867 bucket_bytes.as_ptr() as i32,
868 bucket_bytes.len() as i32,
869 key_bytes.as_ptr() as i32,
870 key_bytes.len() as i32,
871 ct_bytes.as_ptr() as i32,
872 ct_bytes.len() as i32,
873 expires_secs as i32,
874 )
875 };
876 if result < 0 {
877 return None;
878 }
879 read_storage_response()
880 }
881 #[cfg(not(target_arch = "wasm32"))]
882 {
883 let _ = (bucket, key, content_type, expires_secs);
884 None
885 }
886 }
887
888 #[cfg(target_arch = "wasm32")]
890 fn read_storage_response() -> Option<String> {
891 let len = unsafe { get_host_response_len() };
892 if len <= 0 {
893 return None;
894 }
895 let mut buf = vec![0u8; len as usize];
896 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
897 if read <= 0 {
898 return None;
899 }
900 buf.truncate(read as usize);
901 String::from_utf8(buf).ok()
902 }
903}
904
905pub mod redis {
912 #[allow(unused_imports)]
913 use super::*;
914
915 pub fn get(key: &str) -> Option<String> {
924 #[cfg(target_arch = "wasm32")]
925 {
926 let bytes = key.as_bytes();
927 let result = unsafe { redis_get(bytes.as_ptr() as i32, bytes.len() as i32) };
928 if result < 0 {
929 return None;
930 }
931 read_redis_response()
932 }
933 #[cfg(not(target_arch = "wasm32"))]
934 {
935 let _ = key;
936 None
937 }
938 }
939
940 pub fn set(key: &str, value: &str, ttl_secs: i32) -> bool {
948 #[cfg(target_arch = "wasm32")]
949 {
950 let key_bytes = key.as_bytes();
951 let val_bytes = value.as_bytes();
952 let result = unsafe {
953 redis_set(
954 key_bytes.as_ptr() as i32,
955 key_bytes.len() as i32,
956 val_bytes.as_ptr() as i32,
957 val_bytes.len() as i32,
958 ttl_secs,
959 )
960 };
961 result == 0
962 }
963 #[cfg(not(target_arch = "wasm32"))]
964 {
965 let _ = (key, value, ttl_secs);
966 true
967 }
968 }
969
970 pub fn del(key: &str) -> bool {
978 #[cfg(target_arch = "wasm32")]
979 {
980 let bytes = key.as_bytes();
981 let result = unsafe { redis_del(bytes.as_ptr() as i32, bytes.len() as i32) };
982 result == 0
983 }
984 #[cfg(not(target_arch = "wasm32"))]
985 {
986 let _ = key;
987 true
988 }
989 }
990
991 #[cfg(target_arch = "wasm32")]
993 fn read_redis_response() -> Option<String> {
994 let len = unsafe { get_host_response_len() };
995 if len <= 0 {
996 return None;
997 }
998 let mut buf = vec![0u8; len as usize];
999 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1000 if read <= 0 {
1001 return None;
1002 }
1003 buf.truncate(read as usize);
1004 String::from_utf8(buf).ok()
1005 }
1006}
1007
1008pub mod util {
1012 #[allow(unused_imports)]
1013 use super::*;
1014
1015 pub fn generate_uuid() -> String {
1022 #[cfg(target_arch = "wasm32")]
1023 {
1024 let result = unsafe { super::generate_uuid() };
1025 if result < 0 {
1026 return String::new();
1027 }
1028 let len = unsafe { get_host_response_len() };
1029 if len <= 0 {
1030 return String::new();
1031 }
1032 let mut buf = vec![0u8; len as usize];
1033 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1034 if read <= 0 {
1035 return String::new();
1036 }
1037 buf.truncate(read as usize);
1038 String::from_utf8(buf).unwrap_or_default()
1039 }
1040
1041 #[cfg(not(target_arch = "wasm32"))]
1042 {
1043 format!(
1044 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
1045 std::time::SystemTime::now()
1046 .duration_since(std::time::UNIX_EPOCH)
1047 .map(|d| d.as_nanos() as u32)
1048 .unwrap_or(0),
1049 std::process::id() as u16,
1050 0u16,
1051 0x8000u16,
1052 0u64,
1053 )
1054 }
1055 }
1056}
1057
1058#[doc(hidden)]
1062pub fn __run_handler<F>(ptr: i32, len: i32, f: F) -> i32
1063where
1064 F: FnOnce(Request) -> Response,
1065{
1066 let request_json = unsafe {
1068 let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
1069 String::from_utf8_lossy(slice).into_owned()
1070 };
1071
1072 let request = Request::from_json(&request_json).unwrap_or_else(|| Request {
1074 method: "GET".to_string(),
1075 handler: String::new(),
1076 headers: HashMap::new(),
1077 body: Value::Null,
1078 tenant: String::new(),
1079 service: String::new(),
1080 auth: None,
1081 });
1082
1083 let response = f(request);
1085 let response_bytes = response.into_data().into_bytes();
1086
1087 let total = 4 + response_bytes.len();
1089 let layout = std::alloc::Layout::from_size_align(total, 1).expect("invalid layout");
1090 let out_ptr = unsafe { std::alloc::alloc(layout) };
1091
1092 unsafe {
1093 let len_bytes = (response_bytes.len() as u32).to_le_bytes();
1094 std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
1095 std::ptr::copy_nonoverlapping(
1096 response_bytes.as_ptr(),
1097 out_ptr.add(4),
1098 response_bytes.len(),
1099 );
1100 }
1101
1102 out_ptr as i32
1103}
1104
1105#[macro_export]
1118macro_rules! init {
1119 () => {
1120 #[no_mangle]
1121 pub extern "C" fn alloc(size: i32) -> i32 {
1122 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
1123 unsafe { std::alloc::alloc(layout) as i32 }
1124 }
1125 };
1126}
1127
1128#[macro_export]
1155macro_rules! handler {
1156 ($name:ident, |$req:ident : Request| $body:expr) => {
1157 #[no_mangle]
1158 pub extern "C" fn $name(ptr: i32, len: i32) -> i32 {
1159 $crate::__run_handler(ptr, len, |$req: $crate::Request| $body)
1160 }
1161 };
1162}
1163
1164pub mod prelude {
1172 pub use crate::config;
1173 pub use crate::db;
1174 pub use crate::http;
1175 pub use crate::log;
1176 pub use crate::nats;
1177 pub use crate::redis;
1178 pub use crate::storage;
1179 pub use crate::util;
1180 pub use crate::Auth;
1181 pub use crate::Request;
1182 pub use crate::Response;
1183 pub use serde_json::{json, Value};
1184}
1185
1186#[cfg(test)]
1189mod tests {
1190 use super::*;
1191 use serde_json::json;
1192
1193 #[test]
1194 fn test_request_parsing() {
1195 let json = serde_json::to_string(&json!({
1196 "method": "POST",
1197 "handler": "checkout",
1198 "headers": {"content-type": "application/json"},
1199 "body": {"item": "widget", "qty": 3},
1200 "tenant": "acme",
1201 "service": "shop"
1202 }))
1203 .unwrap();
1204
1205 let req = Request::from_json(&json).unwrap();
1206 assert_eq!(req.method(), "POST");
1207 assert_eq!(req.handler(), "checkout");
1208 assert_eq!(req.tenant(), "acme");
1209 assert_eq!(req.service(), "shop");
1210 assert_eq!(req.body()["item"], "widget");
1211 assert_eq!(req.body()["qty"], 3);
1212 assert_eq!(req.header("content-type"), Some("application/json"));
1213 }
1214
1215 #[test]
1216 fn test_request_missing_fields() {
1217 let json = r#"{"method": "GET"}"#;
1218 let req = Request::from_json(json).unwrap();
1219 assert_eq!(req.method(), "GET");
1220 assert_eq!(req.handler(), "");
1221 assert_eq!(req.tenant(), "");
1222 assert_eq!(req.body(), &Value::Null);
1223 }
1224
1225 #[test]
1226 fn test_response_json() {
1227 let resp = Response::json(&json!({"status": "ok", "count": 42}));
1228 let data = resp.into_data();
1229 let parsed: Value = serde_json::from_str(&data).unwrap();
1230 assert_eq!(parsed["status"], "ok");
1231 assert_eq!(parsed["count"], 42);
1232 }
1233
1234 #[test]
1235 fn test_response_error() {
1236 let resp = Response::error("something went wrong");
1237 let data = resp.into_data();
1238 let parsed: Value = serde_json::from_str(&data).unwrap();
1239 assert_eq!(parsed["__status"], 400);
1241 assert_eq!(parsed["__body"]["error"], "something went wrong");
1242 }
1243
1244 #[test]
1245 fn test_response_not_found() {
1246 let resp = Response::not_found("item not found");
1247 let data = resp.into_data();
1248 let parsed: Value = serde_json::from_str(&data).unwrap();
1249 assert_eq!(parsed["__status"], 404);
1250 assert_eq!(parsed["__body"]["error"], "item not found");
1251 }
1252
1253 #[test]
1254 fn test_response_with_status() {
1255 let resp = Response::json(&serde_json::json!({"ok": true})).with_status(201);
1256 let data = resp.into_data();
1257 let parsed: Value = serde_json::from_str(&data).unwrap();
1258 assert_eq!(parsed["__status"], 201);
1259 assert_eq!(parsed["__body"]["ok"], true);
1260 }
1261
1262 #[test]
1263 fn test_response_200_no_wrapper() {
1264 let resp = Response::json(&serde_json::json!({"data": "test"}));
1265 let data = resp.into_data();
1266 let parsed: Value = serde_json::from_str(&data).unwrap();
1267 assert_eq!(parsed["data"], "test");
1269 assert!(parsed.get("__status").is_none());
1270 }
1271
1272 #[test]
1273 fn test_response_empty() {
1274 let resp = Response::empty();
1275 let data = resp.into_data();
1276 let parsed: Value = serde_json::from_str(&data).unwrap();
1277 assert_eq!(parsed["ok"], true);
1278 }
1279
1280 #[test]
1281 fn test_response_text() {
1282 let resp = Response::text("hello world");
1283 let data = resp.into_data();
1284 let parsed: Value = serde_json::from_str(&data).unwrap();
1285 assert_eq!(parsed, "hello world");
1286 }
1287
1288 #[test]
1289 fn test_db_query_noop_on_native() {
1290 let rows = db::query("SELECT 1");
1292 assert!(rows.is_empty());
1293 }
1294
1295 #[test]
1296 fn test_db_query_one_noop_on_native() {
1297 let row = db::query_one("SELECT 1");
1298 assert!(row.is_none());
1299 }
1300
1301 #[test]
1302 fn test_db_execute_noop_on_native() {
1303 let affected = db::execute("INSERT INTO x VALUES (1)");
1304 assert_eq!(affected, 0);
1305 }
1306
1307 #[test]
1308 fn test_nats_publish_noop_on_native() {
1309 let ok = nats::publish("test.subject", "payload");
1310 assert!(ok);
1311 }
1312
1313 #[test]
1314 fn test_request_with_auth() {
1315 let json = serde_json::to_string(&json!({
1316 "method": "POST",
1317 "handler": "checkout",
1318 "headers": {},
1319 "body": {},
1320 "tenant": "acme",
1321 "service": "shop",
1322 "auth": {
1323 "sub": "user-123",
1324 "preferred_username": "john",
1325 "name": "John Doe",
1326 "email": "john@example.com",
1327 "realm_roles": ["admin", "manager"],
1328 "claims": {"department": "engineering"}
1329 }
1330 }))
1331 .unwrap();
1332
1333 let req = Request::from_json(&json).unwrap();
1334 let auth = req.auth().unwrap();
1335 assert_eq!(auth.sub, "user-123");
1336 assert_eq!(auth.preferred_username.as_deref(), Some("john"));
1337 assert_eq!(auth.name.as_deref(), Some("John Doe"));
1338 assert_eq!(auth.email.as_deref(), Some("john@example.com"));
1339 assert!(auth.has_role("admin"));
1340 assert!(auth.has_role("manager"));
1341 assert!(!auth.has_role("viewer"));
1342 assert_eq!(
1343 auth.claim("department").and_then(|v| v.as_str()),
1344 Some("engineering")
1345 );
1346 }
1347
1348 #[test]
1349 fn test_request_without_auth() {
1350 let json = r#"{"method": "GET"}"#;
1351 let req = Request::from_json(json).unwrap();
1352 assert!(req.auth().is_none());
1353 }
1354
1355 #[test]
1356 fn test_request_null_auth() {
1357 let json = serde_json::to_string(&json!({
1358 "method": "GET",
1359 "auth": null
1360 }))
1361 .unwrap();
1362 let req = Request::from_json(&json).unwrap();
1363 assert!(req.auth().is_none());
1364 }
1365
1366 #[test]
1367 fn test_require_auth_success() {
1368 let json = serde_json::to_string(&json!({
1369 "method": "GET",
1370 "auth": {"sub": "user-1", "realm_roles": [], "claims": {}}
1371 }))
1372 .unwrap();
1373 let req = Request::from_json(&json).unwrap();
1374 assert!(req.require_auth().is_ok());
1375 assert_eq!(req.require_auth().unwrap().sub, "user-1");
1376 }
1377
1378 #[test]
1379 fn test_require_auth_fails_when_unauthenticated() {
1380 let json = r#"{"method": "GET"}"#;
1381 let req = Request::from_json(json).unwrap();
1382 assert!(req.require_auth().is_err());
1383 }
1384
1385 #[test]
1386 fn test_http_fetch_noop_on_native() {
1387 let resp = http::fetch("GET", "https://example.com", &[], None);
1388 assert!(resp.is_none());
1389 }
1390
1391 #[test]
1392 fn test_http_get_noop_on_native() {
1393 let resp = http::get("https://example.com", &[]);
1394 assert!(resp.is_none());
1395 }
1396
1397 #[test]
1398 fn test_http_post_noop_on_native() {
1399 let resp = http::post("https://example.com", &[], "{}");
1400 assert!(resp.is_none());
1401 }
1402
1403 #[test]
1404 fn test_storage_download_noop_on_native() {
1405 let data = storage::download("my-bucket", "images/photo.jpg");
1406 assert!(data.is_none());
1407 }
1408
1409 #[test]
1410 fn test_auth_permissions() {
1411 let json = serde_json::to_string(&json!({
1412 "method": "POST",
1413 "handler": "test",
1414 "headers": {},
1415 "body": {},
1416 "tenant": "acme",
1417 "service": "shop",
1418 "auth": {
1419 "sub": "user-1",
1420 "realm_roles": ["admin"],
1421 "claims": {},
1422 "permissions": ["staff:create", "staff:view", "items:*"],
1423 "role_names": ["admin", "manager"]
1424 }
1425 }))
1426 .unwrap();
1427
1428 let req = Request::from_json(&json).unwrap();
1429 let auth = req.auth().unwrap();
1430
1431 assert!(auth.can("staff", "create"));
1433 assert!(auth.can("staff", "view"));
1434 assert!(!auth.can("staff", "delete"));
1435
1436 assert!(auth.can("items", "create"));
1438 assert!(auth.can("items", "view"));
1439 assert!(auth.can("items", "delete"));
1440
1441 assert!(!auth.can("batches", "view"));
1443
1444 assert!(auth.has_cufflink_role("admin"));
1446 assert!(auth.has_cufflink_role("manager"));
1447 assert!(!auth.has_cufflink_role("viewer"));
1448 }
1449
1450 #[test]
1451 fn test_auth_super_wildcard() {
1452 let auth = Auth {
1453 sub: "user-1".to_string(),
1454 preferred_username: None,
1455 name: None,
1456 email: None,
1457 realm_roles: vec![],
1458 claims: HashMap::new(),
1459 permissions: vec!["*".to_string()],
1460 role_names: vec!["superadmin".to_string()],
1461 is_service_account: false,
1462 };
1463
1464 assert!(auth.can("anything", "everything"));
1465 assert!(auth.can("staff", "create"));
1466 }
1467
1468 #[test]
1469 fn test_auth_empty_permissions() {
1470 let auth = Auth {
1471 sub: "user-1".to_string(),
1472 preferred_username: None,
1473 name: None,
1474 email: None,
1475 realm_roles: vec![],
1476 claims: HashMap::new(),
1477 permissions: vec![],
1478 role_names: vec![],
1479 is_service_account: false,
1480 };
1481
1482 assert!(!auth.can("staff", "create"));
1483 assert!(!auth.has_cufflink_role("admin"));
1484 }
1485
1486 #[test]
1487 fn test_redis_get_noop_on_native() {
1488 let val = redis::get("some-key");
1489 assert!(val.is_none());
1490 }
1491
1492 #[test]
1493 fn test_redis_set_noop_on_native() {
1494 let ok = redis::set("key", "value", 3600);
1495 assert!(ok);
1496 }
1497
1498 #[test]
1499 fn test_redis_del_noop_on_native() {
1500 let ok = redis::del("key");
1501 assert!(ok);
1502 }
1503
1504 #[test]
1505 fn test_http_fetch_response_helpers() {
1506 let resp = http::FetchResponse {
1507 status: 200,
1508 body: r#"{"key": "value"}"#.to_string(),
1509 body_encoding: "utf8".to_string(),
1510 headers: HashMap::new(),
1511 };
1512 assert!(resp.is_success());
1513 assert!(!resp.is_base64());
1514 let json = resp.json().unwrap();
1515 assert_eq!(json["key"], "value");
1516
1517 let err_resp = http::FetchResponse {
1518 status: 404,
1519 body: "not found".to_string(),
1520 body_encoding: "utf8".to_string(),
1521 headers: HashMap::new(),
1522 };
1523 assert!(!err_resp.is_success());
1524
1525 let binary_resp = http::FetchResponse {
1526 status: 200,
1527 body: "aW1hZ2VkYXRh".to_string(),
1528 body_encoding: "base64".to_string(),
1529 headers: HashMap::new(),
1530 };
1531 assert!(binary_resp.is_base64());
1532 }
1533}