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 fn current_time() -> i32;
79}
80
81#[derive(Debug, Clone)]
102pub struct Auth {
103 pub sub: String,
105 pub preferred_username: Option<String>,
107 pub name: Option<String>,
109 pub email: Option<String>,
111 pub realm_roles: Vec<String>,
113 pub claims: HashMap<String, Value>,
115 pub permissions: Vec<String>,
117 pub role_names: Vec<String>,
119 pub is_service_account: bool,
122}
123
124impl Auth {
125 pub fn has_role(&self, role: &str) -> bool {
127 self.realm_roles.iter().any(|r| r == role)
128 }
129
130 pub fn can(&self, area: &str, operation: &str) -> bool {
141 let required = format!("{}:{}", area, operation);
142 let wildcard = format!("{}:*", area);
143 self.permissions
144 .iter()
145 .any(|p| p == &required || p == &wildcard || p == "*")
146 }
147
148 pub fn has_cufflink_role(&self, role: &str) -> bool {
150 self.role_names.iter().any(|r| r == role)
151 }
152
153 pub fn claim(&self, key: &str) -> Option<&Value> {
155 self.claims.get(key)
156 }
157}
158
159#[derive(Debug, Clone)]
166pub struct Request {
167 method: String,
168 handler: String,
169 headers: HashMap<String, String>,
170 body: Value,
171 tenant: String,
172 service: String,
173 auth: Option<Auth>,
174}
175
176impl Request {
177 pub fn from_json(json: &str) -> Option<Self> {
179 let v: Value = serde_json::from_str(json).ok()?;
180 let headers = v["headers"]
181 .as_object()
182 .map(|m| {
183 m.iter()
184 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
185 .collect()
186 })
187 .unwrap_or_default();
188
189 let auth = v["auth"].as_object().map(|auth_obj| {
190 let a = Value::Object(auth_obj.clone());
191 Auth {
192 sub: a["sub"].as_str().unwrap_or("").to_string(),
193 preferred_username: a["preferred_username"].as_str().map(|s| s.to_string()),
194 name: a["name"].as_str().map(|s| s.to_string()),
195 email: a["email"].as_str().map(|s| s.to_string()),
196 realm_roles: a["realm_roles"]
197 .as_array()
198 .map(|arr| {
199 arr.iter()
200 .filter_map(|v| v.as_str().map(|s| s.to_string()))
201 .collect()
202 })
203 .unwrap_or_default(),
204 claims: a["claims"]
205 .as_object()
206 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
207 .unwrap_or_default(),
208 permissions: a["permissions"]
209 .as_array()
210 .map(|arr| {
211 arr.iter()
212 .filter_map(|v| v.as_str().map(|s| s.to_string()))
213 .collect()
214 })
215 .unwrap_or_default(),
216 role_names: a["role_names"]
217 .as_array()
218 .map(|arr| {
219 arr.iter()
220 .filter_map(|v| v.as_str().map(|s| s.to_string()))
221 .collect()
222 })
223 .unwrap_or_default(),
224 is_service_account: a["is_service_account"].as_bool().unwrap_or(false),
225 }
226 });
227
228 Some(Self {
229 method: v["method"].as_str().unwrap_or("GET").to_string(),
230 handler: v["handler"].as_str().unwrap_or("").to_string(),
231 headers,
232 body: v["body"].clone(),
233 tenant: v["tenant"].as_str().unwrap_or("").to_string(),
234 service: v["service"].as_str().unwrap_or("").to_string(),
235 auth,
236 })
237 }
238
239 pub fn method(&self) -> &str {
241 &self.method
242 }
243
244 pub fn handler(&self) -> &str {
246 &self.handler
247 }
248
249 pub fn headers(&self) -> &HashMap<String, String> {
251 &self.headers
252 }
253
254 pub fn header(&self, name: &str) -> Option<&str> {
256 self.headers.get(name).map(|s| s.as_str())
257 }
258
259 pub fn body(&self) -> &Value {
261 &self.body
262 }
263
264 pub fn tenant(&self) -> &str {
266 &self.tenant
267 }
268
269 pub fn service(&self) -> &str {
271 &self.service
272 }
273
274 pub fn auth(&self) -> Option<&Auth> {
278 self.auth.as_ref()
279 }
280
281 pub fn require_auth(&self) -> Result<&Auth, Response> {
293 self.auth.as_ref().ok_or_else(|| {
294 Response::json(&serde_json::json!({
295 "error": "Authentication required",
296 "status": 401
297 }))
298 })
299 }
300}
301
302#[derive(Debug, Clone)]
306pub struct Response {
307 data: String,
308 status: u16,
309}
310
311impl Response {
312 pub fn json(value: &Value) -> Self {
314 Self {
315 data: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
316 status: 200,
317 }
318 }
319
320 pub fn text(s: &str) -> Self {
322 Self::json(&Value::String(s.to_string()))
323 }
324
325 pub fn error(message: &str) -> Self {
327 Self {
328 data: serde_json::json!({"error": message}).to_string(),
329 status: 400,
330 }
331 }
332
333 pub fn not_found(message: &str) -> Self {
335 Self {
336 data: serde_json::json!({"error": message}).to_string(),
337 status: 404,
338 }
339 }
340
341 pub fn forbidden(message: &str) -> Self {
343 Self {
344 data: serde_json::json!({"error": message}).to_string(),
345 status: 403,
346 }
347 }
348
349 pub fn empty() -> Self {
351 Self::json(&serde_json::json!({"ok": true}))
352 }
353
354 pub fn with_status(mut self, status: u16) -> Self {
356 self.status = status;
357 self
358 }
359
360 pub fn into_data(self) -> String {
363 if self.status == 200 {
364 self.data
366 } else {
367 serde_json::json!({
369 "__status": self.status,
370 "__body": serde_json::from_str::<Value>(&self.data).unwrap_or(Value::String(self.data)),
371 })
372 .to_string()
373 }
374 }
375}
376
377pub mod db {
384 use super::*;
385
386 pub fn query(sql: &str) -> Vec<Value> {
397 #[cfg(target_arch = "wasm32")]
398 {
399 let bytes = sql.as_bytes();
400 let result = unsafe { db_query(bytes.as_ptr() as i32, bytes.len() as i32) };
401 if result < 0 {
402 return vec![];
403 }
404 read_host_response()
405 }
406 #[cfg(not(target_arch = "wasm32"))]
407 {
408 let _ = sql;
409 vec![]
410 }
411 }
412
413 pub fn query_one(sql: &str) -> Option<Value> {
421 query(sql).into_iter().next()
422 }
423
424 pub fn execute(sql: &str) -> i32 {
433 #[cfg(target_arch = "wasm32")]
434 {
435 let bytes = sql.as_bytes();
436 unsafe { db_execute(bytes.as_ptr() as i32, bytes.len() as i32) }
437 }
438 #[cfg(not(target_arch = "wasm32"))]
439 {
440 let _ = sql;
441 0
442 }
443 }
444
445 #[cfg(target_arch = "wasm32")]
447 fn read_host_response() -> Vec<Value> {
448 let len = unsafe { get_host_response_len() };
449 if len <= 0 {
450 return vec![];
451 }
452 let mut buf = vec![0u8; len as usize];
453 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
454 if read <= 0 {
455 return vec![];
456 }
457 buf.truncate(read as usize);
458 let json_str = String::from_utf8_lossy(&buf);
459 serde_json::from_str(&json_str).unwrap_or_default()
460 }
461}
462
463pub mod nats {
470 #[allow(unused_imports)]
471 use super::*;
472
473 pub fn publish(subject: &str, payload: &str) -> bool {
484 #[cfg(target_arch = "wasm32")]
485 {
486 let subj_bytes = subject.as_bytes();
487 let payload_bytes = payload.as_bytes();
488 let result = unsafe {
489 nats_publish(
490 subj_bytes.as_ptr() as i32,
491 subj_bytes.len() as i32,
492 payload_bytes.as_ptr() as i32,
493 payload_bytes.len() as i32,
494 )
495 };
496 result == 0
497 }
498 #[cfg(not(target_arch = "wasm32"))]
499 {
500 let _ = (subject, payload);
501 true
502 }
503 }
504
505 pub fn request(subject: &str, payload: &str, timeout_ms: i32) -> Option<String> {
517 #[cfg(target_arch = "wasm32")]
518 {
519 let subj_bytes = subject.as_bytes();
520 let payload_bytes = payload.as_bytes();
521 let result = unsafe {
522 nats_request(
523 subj_bytes.as_ptr() as i32,
524 subj_bytes.len() as i32,
525 payload_bytes.as_ptr() as i32,
526 payload_bytes.len() as i32,
527 timeout_ms,
528 )
529 };
530 if result != 0 {
531 return None;
532 }
533 let len = unsafe { get_host_response_len() };
534 if len <= 0 {
535 return None;
536 }
537 let mut buf = vec![0u8; len as usize];
538 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
539 if read <= 0 {
540 return None;
541 }
542 String::from_utf8(buf[..read as usize].to_vec()).ok()
543 }
544 #[cfg(not(target_arch = "wasm32"))]
545 {
546 let _ = (subject, payload, timeout_ms);
547 None
548 }
549 }
550}
551
552pub mod log {
558 #[allow(unused_imports)]
559 use super::*;
560
561 pub fn error(msg: &str) {
563 write(0, msg);
564 }
565
566 pub fn warn(msg: &str) {
568 write(1, msg);
569 }
570
571 pub fn info(msg: &str) {
573 write(2, msg);
574 }
575
576 pub fn debug(msg: &str) {
578 write(3, msg);
579 }
580
581 fn write(level: i32, msg: &str) {
582 #[cfg(target_arch = "wasm32")]
583 {
584 let bytes = msg.as_bytes();
585 unsafe {
586 super::cufflink_log(level, bytes.as_ptr() as i32, bytes.len() as i32);
587 }
588 }
589 #[cfg(not(target_arch = "wasm32"))]
590 {
591 let _ = (level, msg);
592 }
593 }
594}
595
596pub mod http {
603 #[allow(unused_imports)]
604 use super::*;
605
606 #[derive(Debug, Clone)]
608 pub struct FetchResponse {
609 pub status: i32,
611 pub body: String,
613 pub body_encoding: String,
615 pub headers: HashMap<String, String>,
617 }
618
619 impl FetchResponse {
620 pub fn json(&self) -> Option<Value> {
622 serde_json::from_str(&self.body).ok()
623 }
624
625 pub fn is_success(&self) -> bool {
627 (200..300).contains(&self.status)
628 }
629
630 pub fn is_base64(&self) -> bool {
632 self.body_encoding == "base64"
633 }
634 }
635
636 pub fn fetch(
647 method: &str,
648 url: &str,
649 headers: &[(&str, &str)],
650 body: Option<&str>,
651 ) -> Option<FetchResponse> {
652 #[cfg(target_arch = "wasm32")]
653 {
654 let method_bytes = method.as_bytes();
655 let url_bytes = url.as_bytes();
656 let headers_map: HashMap<&str, &str> = headers.iter().copied().collect();
657 let headers_json = serde_json::to_string(&headers_map).unwrap_or_default();
658 let headers_bytes = headers_json.as_bytes();
659 let body_bytes = body.unwrap_or("").as_bytes();
660 let body_len = body.map(|b| b.len()).unwrap_or(0);
661
662 let result = unsafe {
663 http_fetch(
664 method_bytes.as_ptr() as i32,
665 method_bytes.len() as i32,
666 url_bytes.as_ptr() as i32,
667 url_bytes.len() as i32,
668 headers_bytes.as_ptr() as i32,
669 headers_bytes.len() as i32,
670 body_bytes.as_ptr() as i32,
671 body_len as i32,
672 )
673 };
674
675 if result < 0 {
676 return None;
677 }
678
679 read_fetch_response()
680 }
681 #[cfg(not(target_arch = "wasm32"))]
682 {
683 let _ = (method, url, headers, body);
684 None
685 }
686 }
687
688 pub fn get(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
690 fetch("GET", url, headers, None)
691 }
692
693 pub fn post(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
695 fetch("POST", url, headers, Some(body))
696 }
697
698 pub fn put(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
700 fetch("PUT", url, headers, Some(body))
701 }
702
703 pub fn delete(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
705 fetch("DELETE", url, headers, None)
706 }
707
708 pub fn patch(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
710 fetch("PATCH", url, headers, Some(body))
711 }
712
713 #[cfg(target_arch = "wasm32")]
715 fn read_fetch_response() -> Option<FetchResponse> {
716 let len = unsafe { get_host_response_len() };
717 if len <= 0 {
718 return None;
719 }
720 let mut buf = vec![0u8; len as usize];
721 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
722 if read <= 0 {
723 return None;
724 }
725 buf.truncate(read as usize);
726 let json_str = String::from_utf8_lossy(&buf);
727 let v: Value = serde_json::from_str(&json_str).ok()?;
728 Some(FetchResponse {
729 status: v["status"].as_i64().unwrap_or(0) as i32,
730 body: v["body"].as_str().unwrap_or("").to_string(),
731 body_encoding: v["body_encoding"].as_str().unwrap_or("utf8").to_string(),
732 headers: v["headers"]
733 .as_object()
734 .map(|m| {
735 m.iter()
736 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
737 .collect()
738 })
739 .unwrap_or_default(),
740 })
741 }
742}
743
744pub mod config {
752 #[allow(unused_imports)]
753 use super::*;
754
755 pub fn get(key: &str) -> Option<String> {
764 #[cfg(target_arch = "wasm32")]
765 {
766 let bytes = key.as_bytes();
767 let result = unsafe { get_config(bytes.as_ptr() as i32, bytes.len() as i32) };
768 if result < 0 {
769 return None;
770 }
771 read_config_response()
772 }
773 #[cfg(not(target_arch = "wasm32"))]
774 {
775 let _ = key;
776 None
777 }
778 }
779
780 #[cfg(target_arch = "wasm32")]
782 fn read_config_response() -> Option<String> {
783 let len = unsafe { get_host_response_len() };
784 if len <= 0 {
785 return None;
786 }
787 let mut buf = vec![0u8; len as usize];
788 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
789 if read <= 0 {
790 return None;
791 }
792 buf.truncate(read as usize);
793 String::from_utf8(buf).ok()
794 }
795}
796
797pub mod storage {
805 #[allow(unused_imports)]
806 use super::*;
807
808 pub fn download(bucket: &str, key: &str) -> Option<String> {
819 #[cfg(target_arch = "wasm32")]
820 {
821 let bucket_bytes = bucket.as_bytes();
822 let key_bytes = key.as_bytes();
823 let result = unsafe {
824 s3_download(
825 bucket_bytes.as_ptr() as i32,
826 bucket_bytes.len() as i32,
827 key_bytes.as_ptr() as i32,
828 key_bytes.len() as i32,
829 )
830 };
831 if result < 0 {
832 return None;
833 }
834 read_storage_response()
835 }
836 #[cfg(not(target_arch = "wasm32"))]
837 {
838 let _ = (bucket, key);
839 None
840 }
841 }
842
843 pub fn presign_upload(
856 bucket: &str,
857 key: &str,
858 content_type: &str,
859 expires_secs: u64,
860 ) -> Option<String> {
861 #[cfg(target_arch = "wasm32")]
862 {
863 let bucket_bytes = bucket.as_bytes();
864 let key_bytes = key.as_bytes();
865 let ct_bytes = content_type.as_bytes();
866 let result = unsafe {
867 s3_presign_upload(
868 bucket_bytes.as_ptr() as i32,
869 bucket_bytes.len() as i32,
870 key_bytes.as_ptr() as i32,
871 key_bytes.len() as i32,
872 ct_bytes.as_ptr() as i32,
873 ct_bytes.len() as i32,
874 expires_secs as i32,
875 )
876 };
877 if result < 0 {
878 return None;
879 }
880 read_storage_response()
881 }
882 #[cfg(not(target_arch = "wasm32"))]
883 {
884 let _ = (bucket, key, content_type, expires_secs);
885 None
886 }
887 }
888
889 #[cfg(target_arch = "wasm32")]
891 fn read_storage_response() -> Option<String> {
892 let len = unsafe { get_host_response_len() };
893 if len <= 0 {
894 return None;
895 }
896 let mut buf = vec![0u8; len as usize];
897 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
898 if read <= 0 {
899 return None;
900 }
901 buf.truncate(read as usize);
902 String::from_utf8(buf).ok()
903 }
904}
905
906pub mod redis {
913 #[allow(unused_imports)]
914 use super::*;
915
916 pub fn get(key: &str) -> Option<String> {
925 #[cfg(target_arch = "wasm32")]
926 {
927 let bytes = key.as_bytes();
928 let result = unsafe { redis_get(bytes.as_ptr() as i32, bytes.len() as i32) };
929 if result < 0 {
930 return None;
931 }
932 read_redis_response()
933 }
934 #[cfg(not(target_arch = "wasm32"))]
935 {
936 let _ = key;
937 None
938 }
939 }
940
941 pub fn set(key: &str, value: &str, ttl_secs: i32) -> bool {
949 #[cfg(target_arch = "wasm32")]
950 {
951 let key_bytes = key.as_bytes();
952 let val_bytes = value.as_bytes();
953 let result = unsafe {
954 redis_set(
955 key_bytes.as_ptr() as i32,
956 key_bytes.len() as i32,
957 val_bytes.as_ptr() as i32,
958 val_bytes.len() as i32,
959 ttl_secs,
960 )
961 };
962 result == 0
963 }
964 #[cfg(not(target_arch = "wasm32"))]
965 {
966 let _ = (key, value, ttl_secs);
967 true
968 }
969 }
970
971 pub fn del(key: &str) -> bool {
979 #[cfg(target_arch = "wasm32")]
980 {
981 let bytes = key.as_bytes();
982 let result = unsafe { redis_del(bytes.as_ptr() as i32, bytes.len() as i32) };
983 result == 0
984 }
985 #[cfg(not(target_arch = "wasm32"))]
986 {
987 let _ = key;
988 true
989 }
990 }
991
992 #[cfg(target_arch = "wasm32")]
994 fn read_redis_response() -> Option<String> {
995 let len = unsafe { get_host_response_len() };
996 if len <= 0 {
997 return None;
998 }
999 let mut buf = vec![0u8; len as usize];
1000 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1001 if read <= 0 {
1002 return None;
1003 }
1004 buf.truncate(read as usize);
1005 String::from_utf8(buf).ok()
1006 }
1007}
1008
1009pub mod util {
1013 #[allow(unused_imports)]
1014 use super::*;
1015
1016 pub fn current_time() -> String {
1031 #[cfg(target_arch = "wasm32")]
1032 {
1033 let result = unsafe { super::current_time() };
1034 if result < 0 {
1035 return String::new();
1036 }
1037 let len = unsafe { get_host_response_len() };
1038 if len <= 0 {
1039 return String::new();
1040 }
1041 let mut buf = vec![0u8; len as usize];
1042 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1043 if read <= 0 {
1044 return String::new();
1045 }
1046 buf.truncate(read as usize);
1047 String::from_utf8(buf).unwrap_or_default()
1048 }
1049
1050 #[cfg(not(target_arch = "wasm32"))]
1051 {
1052 let secs = std::time::SystemTime::now()
1053 .duration_since(std::time::UNIX_EPOCH)
1054 .map(|d| d.as_secs())
1055 .unwrap_or(0);
1056 format!("1970-01-01T00:00:00Z+{}", secs)
1057 }
1058 }
1059
1060 pub fn generate_uuid() -> String {
1061 #[cfg(target_arch = "wasm32")]
1062 {
1063 let result = unsafe { super::generate_uuid() };
1064 if result < 0 {
1065 return String::new();
1066 }
1067 let len = unsafe { get_host_response_len() };
1068 if len <= 0 {
1069 return String::new();
1070 }
1071 let mut buf = vec![0u8; len as usize];
1072 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1073 if read <= 0 {
1074 return String::new();
1075 }
1076 buf.truncate(read as usize);
1077 String::from_utf8(buf).unwrap_or_default()
1078 }
1079
1080 #[cfg(not(target_arch = "wasm32"))]
1081 {
1082 format!(
1083 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
1084 std::time::SystemTime::now()
1085 .duration_since(std::time::UNIX_EPOCH)
1086 .map(|d| d.as_nanos() as u32)
1087 .unwrap_or(0),
1088 std::process::id() as u16,
1089 0u16,
1090 0x8000u16,
1091 0u64,
1092 )
1093 }
1094 }
1095}
1096
1097#[doc(hidden)]
1101pub fn __run_handler<F>(ptr: i32, len: i32, f: F) -> i32
1102where
1103 F: FnOnce(Request) -> Response,
1104{
1105 let request_json = unsafe {
1107 let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
1108 String::from_utf8_lossy(slice).into_owned()
1109 };
1110
1111 let request = Request::from_json(&request_json).unwrap_or_else(|| Request {
1113 method: "GET".to_string(),
1114 handler: String::new(),
1115 headers: HashMap::new(),
1116 body: Value::Null,
1117 tenant: String::new(),
1118 service: String::new(),
1119 auth: None,
1120 });
1121
1122 let response = f(request);
1124 let response_bytes = response.into_data().into_bytes();
1125
1126 let total = 4 + response_bytes.len();
1128 let layout = std::alloc::Layout::from_size_align(total, 1).expect("invalid layout");
1129 let out_ptr = unsafe { std::alloc::alloc(layout) };
1130
1131 unsafe {
1132 let len_bytes = (response_bytes.len() as u32).to_le_bytes();
1133 std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
1134 std::ptr::copy_nonoverlapping(
1135 response_bytes.as_ptr(),
1136 out_ptr.add(4),
1137 response_bytes.len(),
1138 );
1139 }
1140
1141 out_ptr as i32
1142}
1143
1144#[macro_export]
1157macro_rules! init {
1158 () => {
1159 #[no_mangle]
1160 pub extern "C" fn alloc(size: i32) -> i32 {
1161 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
1162 unsafe { std::alloc::alloc(layout) as i32 }
1163 }
1164 };
1165}
1166
1167#[macro_export]
1194macro_rules! handler {
1195 ($name:ident, |$req:ident : Request| $body:expr) => {
1196 #[no_mangle]
1197 pub extern "C" fn $name(ptr: i32, len: i32) -> i32 {
1198 $crate::__run_handler(ptr, len, |$req: $crate::Request| $body)
1199 }
1200 };
1201}
1202
1203pub mod migrate {
1242 use super::{Request, Response};
1243 pub use cufflink_types::SchemaDiff;
1244
1245 pub fn run<F>(req: Request, handler: F) -> Response
1252 where
1253 F: FnOnce(SchemaDiff) -> Result<(), String>,
1254 {
1255 match serde_json::from_value::<SchemaDiff>(req.body().clone()) {
1256 Ok(diff) => match handler(diff) {
1257 Ok(()) => Response::json(&serde_json::json!({"ok": true})),
1258 Err(e) => Response::error(&e),
1259 },
1260 Err(e) => Response::error(&format!(
1261 "on_migrate: failed to parse SchemaDiff payload: {}",
1262 e
1263 )),
1264 }
1265 }
1266}
1267
1268pub mod prelude {
1276 pub use crate::config;
1277 pub use crate::db;
1278 pub use crate::http;
1279 pub use crate::log;
1280 pub use crate::migrate;
1281 pub use crate::nats;
1282 pub use crate::redis;
1283 pub use crate::storage;
1284 pub use crate::util;
1285 pub use crate::Auth;
1286 pub use crate::Request;
1287 pub use crate::Response;
1288 pub use serde_json::{json, Value};
1289}
1290
1291#[cfg(test)]
1294mod tests {
1295 use super::*;
1296 use serde_json::json;
1297
1298 #[test]
1299 fn test_request_parsing() {
1300 let json = serde_json::to_string(&json!({
1301 "method": "POST",
1302 "handler": "checkout",
1303 "headers": {"content-type": "application/json"},
1304 "body": {"item": "widget", "qty": 3},
1305 "tenant": "acme",
1306 "service": "shop"
1307 }))
1308 .unwrap();
1309
1310 let req = Request::from_json(&json).unwrap();
1311 assert_eq!(req.method(), "POST");
1312 assert_eq!(req.handler(), "checkout");
1313 assert_eq!(req.tenant(), "acme");
1314 assert_eq!(req.service(), "shop");
1315 assert_eq!(req.body()["item"], "widget");
1316 assert_eq!(req.body()["qty"], 3);
1317 assert_eq!(req.header("content-type"), Some("application/json"));
1318 }
1319
1320 #[test]
1321 fn test_request_missing_fields() {
1322 let json = r#"{"method": "GET"}"#;
1323 let req = Request::from_json(json).unwrap();
1324 assert_eq!(req.method(), "GET");
1325 assert_eq!(req.handler(), "");
1326 assert_eq!(req.tenant(), "");
1327 assert_eq!(req.body(), &Value::Null);
1328 }
1329
1330 #[test]
1331 fn test_response_json() {
1332 let resp = Response::json(&json!({"status": "ok", "count": 42}));
1333 let data = resp.into_data();
1334 let parsed: Value = serde_json::from_str(&data).unwrap();
1335 assert_eq!(parsed["status"], "ok");
1336 assert_eq!(parsed["count"], 42);
1337 }
1338
1339 #[test]
1340 fn test_response_error() {
1341 let resp = Response::error("something went wrong");
1342 let data = resp.into_data();
1343 let parsed: Value = serde_json::from_str(&data).unwrap();
1344 assert_eq!(parsed["__status"], 400);
1346 assert_eq!(parsed["__body"]["error"], "something went wrong");
1347 }
1348
1349 #[test]
1350 fn test_response_not_found() {
1351 let resp = Response::not_found("item not found");
1352 let data = resp.into_data();
1353 let parsed: Value = serde_json::from_str(&data).unwrap();
1354 assert_eq!(parsed["__status"], 404);
1355 assert_eq!(parsed["__body"]["error"], "item not found");
1356 }
1357
1358 #[test]
1359 fn test_response_with_status() {
1360 let resp = Response::json(&serde_json::json!({"ok": true})).with_status(201);
1361 let data = resp.into_data();
1362 let parsed: Value = serde_json::from_str(&data).unwrap();
1363 assert_eq!(parsed["__status"], 201);
1364 assert_eq!(parsed["__body"]["ok"], true);
1365 }
1366
1367 fn migrate_request(diff: serde_json::Value) -> Request {
1368 let payload = serde_json::to_string(&json!({
1369 "method": "POST",
1370 "handler": "handle_on_migrate",
1371 "headers": {},
1372 "body": diff,
1373 "tenant": "default",
1374 "service": "logistics-service",
1375 }))
1376 .unwrap();
1377 Request::from_json(&payload).unwrap()
1378 }
1379
1380 #[test]
1381 fn test_migrate_run_success() {
1382 let req = migrate_request(json!({
1383 "added_columns": [["pickups", "min"]],
1384 "dropped_columns": [["pickups", "midpoint"]],
1385 }));
1386 let resp = migrate::run(req, |diff| {
1387 assert!(diff.added_column("pickups", "min"));
1388 assert!(diff.dropped_column("pickups", "midpoint"));
1389 Ok(())
1390 });
1391 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1392 assert_eq!(parsed["ok"], true);
1393 }
1394
1395 #[test]
1396 fn test_migrate_run_handler_error() {
1397 let req = migrate_request(json!({}));
1398 let resp = migrate::run(req, |_| Err("backfill failed".into()));
1399 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1400 assert_eq!(parsed["__status"], 400);
1401 assert_eq!(parsed["__body"]["error"], "backfill failed");
1402 }
1403
1404 #[test]
1405 fn test_migrate_run_invalid_payload() {
1406 let req = migrate_request(json!("not a diff"));
1408 let resp = migrate::run(req, |_| {
1409 panic!("closure should not be called for invalid payload")
1410 });
1411 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1412 assert_eq!(parsed["__status"], 400);
1413 assert!(parsed["__body"]["error"]
1414 .as_str()
1415 .unwrap()
1416 .contains("on_migrate: failed to parse SchemaDiff payload"));
1417 }
1418
1419 #[test]
1420 fn test_migrate_run_empty_diff() {
1421 let req = migrate_request(json!({}));
1422 let mut called = false;
1423 let resp = migrate::run(req, |diff| {
1424 assert!(diff.is_empty());
1425 called = true;
1426 Ok(())
1427 });
1428 assert!(called);
1429 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1430 assert_eq!(parsed["ok"], true);
1431 }
1432
1433 #[test]
1434 fn test_response_200_no_wrapper() {
1435 let resp = Response::json(&serde_json::json!({"data": "test"}));
1436 let data = resp.into_data();
1437 let parsed: Value = serde_json::from_str(&data).unwrap();
1438 assert_eq!(parsed["data"], "test");
1440 assert!(parsed.get("__status").is_none());
1441 }
1442
1443 #[test]
1444 fn test_response_empty() {
1445 let resp = Response::empty();
1446 let data = resp.into_data();
1447 let parsed: Value = serde_json::from_str(&data).unwrap();
1448 assert_eq!(parsed["ok"], true);
1449 }
1450
1451 #[test]
1452 fn test_response_text() {
1453 let resp = Response::text("hello world");
1454 let data = resp.into_data();
1455 let parsed: Value = serde_json::from_str(&data).unwrap();
1456 assert_eq!(parsed, "hello world");
1457 }
1458
1459 #[test]
1460 fn test_db_query_noop_on_native() {
1461 let rows = db::query("SELECT 1");
1463 assert!(rows.is_empty());
1464 }
1465
1466 #[test]
1467 fn test_db_query_one_noop_on_native() {
1468 let row = db::query_one("SELECT 1");
1469 assert!(row.is_none());
1470 }
1471
1472 #[test]
1473 fn test_db_execute_noop_on_native() {
1474 let affected = db::execute("INSERT INTO x VALUES (1)");
1475 assert_eq!(affected, 0);
1476 }
1477
1478 #[test]
1479 fn test_nats_publish_noop_on_native() {
1480 let ok = nats::publish("test.subject", "payload");
1481 assert!(ok);
1482 }
1483
1484 #[test]
1485 fn test_request_with_auth() {
1486 let json = serde_json::to_string(&json!({
1487 "method": "POST",
1488 "handler": "checkout",
1489 "headers": {},
1490 "body": {},
1491 "tenant": "acme",
1492 "service": "shop",
1493 "auth": {
1494 "sub": "user-123",
1495 "preferred_username": "john",
1496 "name": "John Doe",
1497 "email": "john@example.com",
1498 "realm_roles": ["admin", "manager"],
1499 "claims": {"department": "engineering"}
1500 }
1501 }))
1502 .unwrap();
1503
1504 let req = Request::from_json(&json).unwrap();
1505 let auth = req.auth().unwrap();
1506 assert_eq!(auth.sub, "user-123");
1507 assert_eq!(auth.preferred_username.as_deref(), Some("john"));
1508 assert_eq!(auth.name.as_deref(), Some("John Doe"));
1509 assert_eq!(auth.email.as_deref(), Some("john@example.com"));
1510 assert!(auth.has_role("admin"));
1511 assert!(auth.has_role("manager"));
1512 assert!(!auth.has_role("viewer"));
1513 assert_eq!(
1514 auth.claim("department").and_then(|v| v.as_str()),
1515 Some("engineering")
1516 );
1517 }
1518
1519 #[test]
1520 fn test_request_without_auth() {
1521 let json = r#"{"method": "GET"}"#;
1522 let req = Request::from_json(json).unwrap();
1523 assert!(req.auth().is_none());
1524 }
1525
1526 #[test]
1527 fn test_request_null_auth() {
1528 let json = serde_json::to_string(&json!({
1529 "method": "GET",
1530 "auth": null
1531 }))
1532 .unwrap();
1533 let req = Request::from_json(&json).unwrap();
1534 assert!(req.auth().is_none());
1535 }
1536
1537 #[test]
1538 fn test_require_auth_success() {
1539 let json = serde_json::to_string(&json!({
1540 "method": "GET",
1541 "auth": {"sub": "user-1", "realm_roles": [], "claims": {}}
1542 }))
1543 .unwrap();
1544 let req = Request::from_json(&json).unwrap();
1545 assert!(req.require_auth().is_ok());
1546 assert_eq!(req.require_auth().unwrap().sub, "user-1");
1547 }
1548
1549 #[test]
1550 fn test_require_auth_fails_when_unauthenticated() {
1551 let json = r#"{"method": "GET"}"#;
1552 let req = Request::from_json(json).unwrap();
1553 assert!(req.require_auth().is_err());
1554 }
1555
1556 #[test]
1557 fn test_http_fetch_noop_on_native() {
1558 let resp = http::fetch("GET", "https://example.com", &[], None);
1559 assert!(resp.is_none());
1560 }
1561
1562 #[test]
1563 fn test_http_get_noop_on_native() {
1564 let resp = http::get("https://example.com", &[]);
1565 assert!(resp.is_none());
1566 }
1567
1568 #[test]
1569 fn test_http_post_noop_on_native() {
1570 let resp = http::post("https://example.com", &[], "{}");
1571 assert!(resp.is_none());
1572 }
1573
1574 #[test]
1575 fn test_storage_download_noop_on_native() {
1576 let data = storage::download("my-bucket", "images/photo.jpg");
1577 assert!(data.is_none());
1578 }
1579
1580 #[test]
1581 fn test_auth_permissions() {
1582 let json = serde_json::to_string(&json!({
1583 "method": "POST",
1584 "handler": "test",
1585 "headers": {},
1586 "body": {},
1587 "tenant": "acme",
1588 "service": "shop",
1589 "auth": {
1590 "sub": "user-1",
1591 "realm_roles": ["admin"],
1592 "claims": {},
1593 "permissions": ["staff:create", "staff:view", "items:*"],
1594 "role_names": ["admin", "manager"]
1595 }
1596 }))
1597 .unwrap();
1598
1599 let req = Request::from_json(&json).unwrap();
1600 let auth = req.auth().unwrap();
1601
1602 assert!(auth.can("staff", "create"));
1604 assert!(auth.can("staff", "view"));
1605 assert!(!auth.can("staff", "delete"));
1606
1607 assert!(auth.can("items", "create"));
1609 assert!(auth.can("items", "view"));
1610 assert!(auth.can("items", "delete"));
1611
1612 assert!(!auth.can("batches", "view"));
1614
1615 assert!(auth.has_cufflink_role("admin"));
1617 assert!(auth.has_cufflink_role("manager"));
1618 assert!(!auth.has_cufflink_role("viewer"));
1619 }
1620
1621 #[test]
1622 fn test_auth_super_wildcard() {
1623 let auth = Auth {
1624 sub: "user-1".to_string(),
1625 preferred_username: None,
1626 name: None,
1627 email: None,
1628 realm_roles: vec![],
1629 claims: HashMap::new(),
1630 permissions: vec!["*".to_string()],
1631 role_names: vec!["superadmin".to_string()],
1632 is_service_account: false,
1633 };
1634
1635 assert!(auth.can("anything", "everything"));
1636 assert!(auth.can("staff", "create"));
1637 }
1638
1639 #[test]
1640 fn test_auth_empty_permissions() {
1641 let auth = Auth {
1642 sub: "user-1".to_string(),
1643 preferred_username: None,
1644 name: None,
1645 email: None,
1646 realm_roles: vec![],
1647 claims: HashMap::new(),
1648 permissions: vec![],
1649 role_names: vec![],
1650 is_service_account: false,
1651 };
1652
1653 assert!(!auth.can("staff", "create"));
1654 assert!(!auth.has_cufflink_role("admin"));
1655 }
1656
1657 #[test]
1658 fn test_redis_get_noop_on_native() {
1659 let val = redis::get("some-key");
1660 assert!(val.is_none());
1661 }
1662
1663 #[test]
1664 fn test_redis_set_noop_on_native() {
1665 let ok = redis::set("key", "value", 3600);
1666 assert!(ok);
1667 }
1668
1669 #[test]
1670 fn test_redis_del_noop_on_native() {
1671 let ok = redis::del("key");
1672 assert!(ok);
1673 }
1674
1675 #[test]
1676 fn test_http_fetch_response_helpers() {
1677 let resp = http::FetchResponse {
1678 status: 200,
1679 body: r#"{"key": "value"}"#.to_string(),
1680 body_encoding: "utf8".to_string(),
1681 headers: HashMap::new(),
1682 };
1683 assert!(resp.is_success());
1684 assert!(!resp.is_base64());
1685 let json = resp.json().unwrap();
1686 assert_eq!(json["key"], "value");
1687
1688 let err_resp = http::FetchResponse {
1689 status: 404,
1690 body: "not found".to_string(),
1691 body_encoding: "utf8".to_string(),
1692 headers: HashMap::new(),
1693 };
1694 assert!(!err_resp.is_success());
1695
1696 let binary_resp = http::FetchResponse {
1697 status: 200,
1698 body: "aW1hZ2VkYXRh".to_string(),
1699 body_encoding: "base64".to_string(),
1700 headers: HashMap::new(),
1701 };
1702 assert!(binary_resp.is_base64());
1703 }
1704}