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 image_transform_jpeg(
66 bucket_ptr: i32,
67 bucket_len: i32,
68 in_key_ptr: i32,
69 in_key_len: i32,
70 out_key_ptr: i32,
71 out_key_len: i32,
72 max_dim: i32,
73 quality: i32,
74 ) -> i32;
75 fn s3_presign_upload(
76 bucket_ptr: i32,
77 bucket_len: i32,
78 key_ptr: i32,
79 key_len: i32,
80 content_type_ptr: i32,
81 content_type_len: i32,
82 expires_secs: i32,
83 ) -> i32;
84 fn redis_get(key_ptr: i32, key_len: i32) -> i32;
85 fn redis_set(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32, ttl_secs: i32) -> i32;
86 fn redis_del(key_ptr: i32, key_len: i32) -> i32;
87 fn generate_uuid() -> i32;
88 fn current_time() -> i32;
89}
90
91#[derive(Debug, Clone)]
112pub struct Auth {
113 pub sub: String,
115 pub preferred_username: Option<String>,
117 pub name: Option<String>,
119 pub email: Option<String>,
121 pub realm_roles: Vec<String>,
123 pub claims: HashMap<String, Value>,
125 pub permissions: Vec<String>,
127 pub role_names: Vec<String>,
129 pub is_service_account: bool,
132}
133
134impl Auth {
135 pub fn has_role(&self, role: &str) -> bool {
137 self.realm_roles.iter().any(|r| r == role)
138 }
139
140 pub fn can(&self, area: &str, operation: &str) -> bool {
151 let required = format!("{}:{}", area, operation);
152 let wildcard = format!("{}:*", area);
153 self.permissions
154 .iter()
155 .any(|p| p == &required || p == &wildcard || p == "*")
156 }
157
158 pub fn has_cufflink_role(&self, role: &str) -> bool {
160 self.role_names.iter().any(|r| r == role)
161 }
162
163 pub fn claim(&self, key: &str) -> Option<&Value> {
165 self.claims.get(key)
166 }
167}
168
169#[derive(Debug, Clone)]
176pub struct Request {
177 method: String,
178 handler: String,
179 headers: HashMap<String, String>,
180 body: Value,
181 tenant: String,
182 service: String,
183 auth: Option<Auth>,
184}
185
186impl Request {
187 pub fn from_json(json: &str) -> Option<Self> {
189 let v: Value = serde_json::from_str(json).ok()?;
190 let headers = v["headers"]
191 .as_object()
192 .map(|m| {
193 m.iter()
194 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
195 .collect()
196 })
197 .unwrap_or_default();
198
199 let auth = v["auth"].as_object().map(|auth_obj| {
200 let a = Value::Object(auth_obj.clone());
201 Auth {
202 sub: a["sub"].as_str().unwrap_or("").to_string(),
203 preferred_username: a["preferred_username"].as_str().map(|s| s.to_string()),
204 name: a["name"].as_str().map(|s| s.to_string()),
205 email: a["email"].as_str().map(|s| s.to_string()),
206 realm_roles: a["realm_roles"]
207 .as_array()
208 .map(|arr| {
209 arr.iter()
210 .filter_map(|v| v.as_str().map(|s| s.to_string()))
211 .collect()
212 })
213 .unwrap_or_default(),
214 claims: a["claims"]
215 .as_object()
216 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
217 .unwrap_or_default(),
218 permissions: a["permissions"]
219 .as_array()
220 .map(|arr| {
221 arr.iter()
222 .filter_map(|v| v.as_str().map(|s| s.to_string()))
223 .collect()
224 })
225 .unwrap_or_default(),
226 role_names: a["role_names"]
227 .as_array()
228 .map(|arr| {
229 arr.iter()
230 .filter_map(|v| v.as_str().map(|s| s.to_string()))
231 .collect()
232 })
233 .unwrap_or_default(),
234 is_service_account: a["is_service_account"].as_bool().unwrap_or(false),
235 }
236 });
237
238 Some(Self {
239 method: v["method"].as_str().unwrap_or("GET").to_string(),
240 handler: v["handler"].as_str().unwrap_or("").to_string(),
241 headers,
242 body: v["body"].clone(),
243 tenant: v["tenant"].as_str().unwrap_or("").to_string(),
244 service: v["service"].as_str().unwrap_or("").to_string(),
245 auth,
246 })
247 }
248
249 pub fn method(&self) -> &str {
251 &self.method
252 }
253
254 pub fn handler(&self) -> &str {
256 &self.handler
257 }
258
259 pub fn headers(&self) -> &HashMap<String, String> {
261 &self.headers
262 }
263
264 pub fn header(&self, name: &str) -> Option<&str> {
266 self.headers.get(name).map(|s| s.as_str())
267 }
268
269 pub fn body(&self) -> &Value {
271 &self.body
272 }
273
274 pub fn tenant(&self) -> &str {
276 &self.tenant
277 }
278
279 pub fn service(&self) -> &str {
281 &self.service
282 }
283
284 pub fn auth(&self) -> Option<&Auth> {
288 self.auth.as_ref()
289 }
290
291 pub fn require_auth(&self) -> Result<&Auth, Response> {
303 self.auth.as_ref().ok_or_else(|| {
304 Response::json(&serde_json::json!({
305 "error": "Authentication required",
306 "status": 401
307 }))
308 })
309 }
310}
311
312#[derive(Debug, Clone)]
316pub struct Response {
317 data: String,
318 status: u16,
319}
320
321impl Response {
322 pub fn json(value: &Value) -> Self {
324 Self {
325 data: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
326 status: 200,
327 }
328 }
329
330 pub fn text(s: &str) -> Self {
332 Self::json(&Value::String(s.to_string()))
333 }
334
335 pub fn error(message: &str) -> Self {
337 Self {
338 data: serde_json::json!({"error": message}).to_string(),
339 status: 400,
340 }
341 }
342
343 pub fn not_found(message: &str) -> Self {
345 Self {
346 data: serde_json::json!({"error": message}).to_string(),
347 status: 404,
348 }
349 }
350
351 pub fn forbidden(message: &str) -> Self {
353 Self {
354 data: serde_json::json!({"error": message}).to_string(),
355 status: 403,
356 }
357 }
358
359 pub fn empty() -> Self {
361 Self::json(&serde_json::json!({"ok": true}))
362 }
363
364 pub fn with_status(mut self, status: u16) -> Self {
366 self.status = status;
367 self
368 }
369
370 pub fn into_data(self) -> String {
373 if self.status == 200 {
374 self.data
376 } else {
377 serde_json::json!({
379 "__status": self.status,
380 "__body": serde_json::from_str::<Value>(&self.data).unwrap_or(Value::String(self.data)),
381 })
382 .to_string()
383 }
384 }
385}
386
387pub mod db {
394 use super::*;
395
396 pub fn query(sql: &str) -> Vec<Value> {
407 #[cfg(target_arch = "wasm32")]
408 {
409 let bytes = sql.as_bytes();
410 let result = unsafe { db_query(bytes.as_ptr() as i32, bytes.len() as i32) };
411 if result < 0 {
412 return vec![];
413 }
414 read_host_response()
415 }
416 #[cfg(not(target_arch = "wasm32"))]
417 {
418 let _ = sql;
419 vec![]
420 }
421 }
422
423 pub fn query_one(sql: &str) -> Option<Value> {
431 query(sql).into_iter().next()
432 }
433
434 pub fn execute(sql: &str) -> i32 {
443 #[cfg(target_arch = "wasm32")]
444 {
445 let bytes = sql.as_bytes();
446 unsafe { db_execute(bytes.as_ptr() as i32, bytes.len() as i32) }
447 }
448 #[cfg(not(target_arch = "wasm32"))]
449 {
450 let _ = sql;
451 0
452 }
453 }
454
455 #[cfg(target_arch = "wasm32")]
457 fn read_host_response() -> Vec<Value> {
458 let len = unsafe { get_host_response_len() };
459 if len <= 0 {
460 return vec![];
461 }
462 let mut buf = vec![0u8; len as usize];
463 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
464 if read <= 0 {
465 return vec![];
466 }
467 buf.truncate(read as usize);
468 let json_str = String::from_utf8_lossy(&buf);
469 serde_json::from_str(&json_str).unwrap_or_default()
470 }
471}
472
473pub mod nats {
480 #[allow(unused_imports)]
481 use super::*;
482
483 pub fn publish(subject: &str, payload: &str) -> bool {
494 #[cfg(target_arch = "wasm32")]
495 {
496 let subj_bytes = subject.as_bytes();
497 let payload_bytes = payload.as_bytes();
498 let result = unsafe {
499 nats_publish(
500 subj_bytes.as_ptr() as i32,
501 subj_bytes.len() as i32,
502 payload_bytes.as_ptr() as i32,
503 payload_bytes.len() as i32,
504 )
505 };
506 result == 0
507 }
508 #[cfg(not(target_arch = "wasm32"))]
509 {
510 let _ = (subject, payload);
511 true
512 }
513 }
514
515 pub fn request(subject: &str, payload: &str, timeout_ms: i32) -> Option<String> {
527 #[cfg(target_arch = "wasm32")]
528 {
529 let subj_bytes = subject.as_bytes();
530 let payload_bytes = payload.as_bytes();
531 let result = unsafe {
532 nats_request(
533 subj_bytes.as_ptr() as i32,
534 subj_bytes.len() as i32,
535 payload_bytes.as_ptr() as i32,
536 payload_bytes.len() as i32,
537 timeout_ms,
538 )
539 };
540 if result != 0 {
541 return None;
542 }
543 let len = unsafe { get_host_response_len() };
544 if len <= 0 {
545 return None;
546 }
547 let mut buf = vec![0u8; len as usize];
548 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
549 if read <= 0 {
550 return None;
551 }
552 String::from_utf8(buf[..read as usize].to_vec()).ok()
553 }
554 #[cfg(not(target_arch = "wasm32"))]
555 {
556 let _ = (subject, payload, timeout_ms);
557 None
558 }
559 }
560}
561
562pub mod log {
568 #[allow(unused_imports)]
569 use super::*;
570
571 pub fn error(msg: &str) {
573 write(0, msg);
574 }
575
576 pub fn warn(msg: &str) {
578 write(1, msg);
579 }
580
581 pub fn info(msg: &str) {
583 write(2, msg);
584 }
585
586 pub fn debug(msg: &str) {
588 write(3, msg);
589 }
590
591 fn write(level: i32, msg: &str) {
592 #[cfg(target_arch = "wasm32")]
593 {
594 let bytes = msg.as_bytes();
595 unsafe {
596 super::cufflink_log(level, bytes.as_ptr() as i32, bytes.len() as i32);
597 }
598 }
599 #[cfg(not(target_arch = "wasm32"))]
600 {
601 let _ = (level, msg);
602 }
603 }
604}
605
606pub mod http {
613 #[allow(unused_imports)]
614 use super::*;
615
616 #[derive(Debug, Clone)]
618 pub struct FetchResponse {
619 pub status: i32,
621 pub body: String,
623 pub body_encoding: String,
625 pub headers: HashMap<String, String>,
627 }
628
629 impl FetchResponse {
630 pub fn json(&self) -> Option<Value> {
632 serde_json::from_str(&self.body).ok()
633 }
634
635 pub fn is_success(&self) -> bool {
637 (200..300).contains(&self.status)
638 }
639
640 pub fn is_base64(&self) -> bool {
642 self.body_encoding == "base64"
643 }
644 }
645
646 pub fn fetch(
657 method: &str,
658 url: &str,
659 headers: &[(&str, &str)],
660 body: Option<&str>,
661 ) -> Option<FetchResponse> {
662 #[cfg(target_arch = "wasm32")]
663 {
664 let method_bytes = method.as_bytes();
665 let url_bytes = url.as_bytes();
666 let headers_map: HashMap<&str, &str> = headers.iter().copied().collect();
667 let headers_json = serde_json::to_string(&headers_map).unwrap_or_default();
668 let headers_bytes = headers_json.as_bytes();
669 let body_bytes = body.unwrap_or("").as_bytes();
670 let body_len = body.map(|b| b.len()).unwrap_or(0);
671
672 let result = unsafe {
673 http_fetch(
674 method_bytes.as_ptr() as i32,
675 method_bytes.len() as i32,
676 url_bytes.as_ptr() as i32,
677 url_bytes.len() as i32,
678 headers_bytes.as_ptr() as i32,
679 headers_bytes.len() as i32,
680 body_bytes.as_ptr() as i32,
681 body_len as i32,
682 )
683 };
684
685 if result < 0 {
686 return None;
687 }
688
689 read_fetch_response()
690 }
691 #[cfg(not(target_arch = "wasm32"))]
692 {
693 let _ = (method, url, headers, body);
694 None
695 }
696 }
697
698 pub fn get(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
700 fetch("GET", url, headers, None)
701 }
702
703 pub fn post(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
705 fetch("POST", url, headers, Some(body))
706 }
707
708 pub fn put(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
710 fetch("PUT", url, headers, Some(body))
711 }
712
713 pub fn delete(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
715 fetch("DELETE", url, headers, None)
716 }
717
718 pub fn patch(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
720 fetch("PATCH", url, headers, Some(body))
721 }
722
723 #[cfg(target_arch = "wasm32")]
725 fn read_fetch_response() -> Option<FetchResponse> {
726 let len = unsafe { get_host_response_len() };
727 if len <= 0 {
728 return None;
729 }
730 let mut buf = vec![0u8; len as usize];
731 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
732 if read <= 0 {
733 return None;
734 }
735 buf.truncate(read as usize);
736 let json_str = String::from_utf8_lossy(&buf);
737 let v: Value = serde_json::from_str(&json_str).ok()?;
738 Some(FetchResponse {
739 status: v["status"].as_i64().unwrap_or(0) as i32,
740 body: v["body"].as_str().unwrap_or("").to_string(),
741 body_encoding: v["body_encoding"].as_str().unwrap_or("utf8").to_string(),
742 headers: v["headers"]
743 .as_object()
744 .map(|m| {
745 m.iter()
746 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
747 .collect()
748 })
749 .unwrap_or_default(),
750 })
751 }
752}
753
754pub mod config {
762 #[allow(unused_imports)]
763 use super::*;
764
765 pub fn get(key: &str) -> Option<String> {
774 #[cfg(target_arch = "wasm32")]
775 {
776 let bytes = key.as_bytes();
777 let result = unsafe { get_config(bytes.as_ptr() as i32, bytes.len() as i32) };
778 if result < 0 {
779 return None;
780 }
781 read_config_response()
782 }
783 #[cfg(not(target_arch = "wasm32"))]
784 {
785 let _ = key;
786 None
787 }
788 }
789
790 #[cfg(target_arch = "wasm32")]
792 fn read_config_response() -> Option<String> {
793 let len = unsafe { get_host_response_len() };
794 if len <= 0 {
795 return None;
796 }
797 let mut buf = vec![0u8; len as usize];
798 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
799 if read <= 0 {
800 return None;
801 }
802 buf.truncate(read as usize);
803 String::from_utf8(buf).ok()
804 }
805}
806
807pub mod storage {
815 #[allow(unused_imports)]
816 use super::*;
817
818 pub fn download(bucket: &str, key: &str) -> Option<String> {
829 #[cfg(target_arch = "wasm32")]
830 {
831 let bucket_bytes = bucket.as_bytes();
832 let key_bytes = key.as_bytes();
833 let result = unsafe {
834 s3_download(
835 bucket_bytes.as_ptr() as i32,
836 bucket_bytes.len() as i32,
837 key_bytes.as_ptr() as i32,
838 key_bytes.len() as i32,
839 )
840 };
841 if result < 0 {
842 return None;
843 }
844 read_storage_response()
845 }
846 #[cfg(not(target_arch = "wasm32"))]
847 {
848 let _ = (bucket, key);
849 None
850 }
851 }
852
853 pub fn presign_upload(
866 bucket: &str,
867 key: &str,
868 content_type: &str,
869 expires_secs: u64,
870 ) -> Option<String> {
871 #[cfg(target_arch = "wasm32")]
872 {
873 let bucket_bytes = bucket.as_bytes();
874 let key_bytes = key.as_bytes();
875 let ct_bytes = content_type.as_bytes();
876 let result = unsafe {
877 s3_presign_upload(
878 bucket_bytes.as_ptr() as i32,
879 bucket_bytes.len() as i32,
880 key_bytes.as_ptr() as i32,
881 key_bytes.len() as i32,
882 ct_bytes.as_ptr() as i32,
883 ct_bytes.len() as i32,
884 expires_secs as i32,
885 )
886 };
887 if result < 0 {
888 return None;
889 }
890 read_storage_response()
891 }
892 #[cfg(not(target_arch = "wasm32"))]
893 {
894 let _ = (bucket, key, content_type, expires_secs);
895 None
896 }
897 }
898
899 #[cfg(target_arch = "wasm32")]
901 fn read_storage_response() -> Option<String> {
902 let len = unsafe { get_host_response_len() };
903 if len <= 0 {
904 return None;
905 }
906 let mut buf = vec![0u8; len as usize];
907 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
908 if read <= 0 {
909 return None;
910 }
911 buf.truncate(read as usize);
912 String::from_utf8(buf).ok()
913 }
914}
915
916pub mod image {
926 #[allow(unused_imports)]
927 use super::*;
928
929 pub fn transform_jpeg(
940 bucket: &str,
941 in_key: &str,
942 out_key: &str,
943 max_dim: u32,
944 quality: u8,
945 ) -> Option<u32> {
946 #[cfg(target_arch = "wasm32")]
947 {
948 let bucket_bytes = bucket.as_bytes();
949 let in_key_bytes = in_key.as_bytes();
950 let out_key_bytes = out_key.as_bytes();
951 let result = unsafe {
952 super::image_transform_jpeg(
953 bucket_bytes.as_ptr() as i32,
954 bucket_bytes.len() as i32,
955 in_key_bytes.as_ptr() as i32,
956 in_key_bytes.len() as i32,
957 out_key_bytes.as_ptr() as i32,
958 out_key_bytes.len() as i32,
959 max_dim as i32,
960 quality as i32,
961 )
962 };
963 if result < 0 {
964 return None;
965 }
966 Some(result as u32)
967 }
968 #[cfg(not(target_arch = "wasm32"))]
969 {
970 let _ = (bucket, in_key, out_key, max_dim, quality);
971 None
972 }
973 }
974}
975
976pub mod redis {
983 #[allow(unused_imports)]
984 use super::*;
985
986 pub fn get(key: &str) -> Option<String> {
995 #[cfg(target_arch = "wasm32")]
996 {
997 let bytes = key.as_bytes();
998 let result = unsafe { redis_get(bytes.as_ptr() as i32, bytes.len() as i32) };
999 if result < 0 {
1000 return None;
1001 }
1002 read_redis_response()
1003 }
1004 #[cfg(not(target_arch = "wasm32"))]
1005 {
1006 let _ = key;
1007 None
1008 }
1009 }
1010
1011 pub fn set(key: &str, value: &str, ttl_secs: i32) -> bool {
1019 #[cfg(target_arch = "wasm32")]
1020 {
1021 let key_bytes = key.as_bytes();
1022 let val_bytes = value.as_bytes();
1023 let result = unsafe {
1024 redis_set(
1025 key_bytes.as_ptr() as i32,
1026 key_bytes.len() as i32,
1027 val_bytes.as_ptr() as i32,
1028 val_bytes.len() as i32,
1029 ttl_secs,
1030 )
1031 };
1032 result == 0
1033 }
1034 #[cfg(not(target_arch = "wasm32"))]
1035 {
1036 let _ = (key, value, ttl_secs);
1037 true
1038 }
1039 }
1040
1041 pub fn del(key: &str) -> bool {
1049 #[cfg(target_arch = "wasm32")]
1050 {
1051 let bytes = key.as_bytes();
1052 let result = unsafe { redis_del(bytes.as_ptr() as i32, bytes.len() as i32) };
1053 result == 0
1054 }
1055 #[cfg(not(target_arch = "wasm32"))]
1056 {
1057 let _ = key;
1058 true
1059 }
1060 }
1061
1062 #[cfg(target_arch = "wasm32")]
1064 fn read_redis_response() -> Option<String> {
1065 let len = unsafe { get_host_response_len() };
1066 if len <= 0 {
1067 return None;
1068 }
1069 let mut buf = vec![0u8; len as usize];
1070 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1071 if read <= 0 {
1072 return None;
1073 }
1074 buf.truncate(read as usize);
1075 String::from_utf8(buf).ok()
1076 }
1077}
1078
1079pub mod util {
1083 #[allow(unused_imports)]
1084 use super::*;
1085
1086 pub fn current_time() -> String {
1101 #[cfg(target_arch = "wasm32")]
1102 {
1103 let result = unsafe { super::current_time() };
1104 if result < 0 {
1105 return String::new();
1106 }
1107 let len = unsafe { get_host_response_len() };
1108 if len <= 0 {
1109 return String::new();
1110 }
1111 let mut buf = vec![0u8; len as usize];
1112 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1113 if read <= 0 {
1114 return String::new();
1115 }
1116 buf.truncate(read as usize);
1117 String::from_utf8(buf).unwrap_or_default()
1118 }
1119
1120 #[cfg(not(target_arch = "wasm32"))]
1121 {
1122 let secs = std::time::SystemTime::now()
1123 .duration_since(std::time::UNIX_EPOCH)
1124 .map(|d| d.as_secs())
1125 .unwrap_or(0);
1126 format!("1970-01-01T00:00:00Z+{}", secs)
1127 }
1128 }
1129
1130 pub fn generate_uuid() -> String {
1131 #[cfg(target_arch = "wasm32")]
1132 {
1133 let result = unsafe { super::generate_uuid() };
1134 if result < 0 {
1135 return String::new();
1136 }
1137 let len = unsafe { get_host_response_len() };
1138 if len <= 0 {
1139 return String::new();
1140 }
1141 let mut buf = vec![0u8; len as usize];
1142 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1143 if read <= 0 {
1144 return String::new();
1145 }
1146 buf.truncate(read as usize);
1147 String::from_utf8(buf).unwrap_or_default()
1148 }
1149
1150 #[cfg(not(target_arch = "wasm32"))]
1151 {
1152 format!(
1153 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
1154 std::time::SystemTime::now()
1155 .duration_since(std::time::UNIX_EPOCH)
1156 .map(|d| d.as_nanos() as u32)
1157 .unwrap_or(0),
1158 std::process::id() as u16,
1159 0u16,
1160 0x8000u16,
1161 0u64,
1162 )
1163 }
1164 }
1165}
1166
1167#[doc(hidden)]
1171pub fn __run_handler<F>(ptr: i32, len: i32, f: F) -> i32
1172where
1173 F: FnOnce(Request) -> Response,
1174{
1175 let request_json = unsafe {
1177 let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
1178 String::from_utf8_lossy(slice).into_owned()
1179 };
1180
1181 let request = Request::from_json(&request_json).unwrap_or_else(|| Request {
1183 method: "GET".to_string(),
1184 handler: String::new(),
1185 headers: HashMap::new(),
1186 body: Value::Null,
1187 tenant: String::new(),
1188 service: String::new(),
1189 auth: None,
1190 });
1191
1192 let response = f(request);
1194 let response_bytes = response.into_data().into_bytes();
1195
1196 let total = 4 + response_bytes.len();
1198 let layout = std::alloc::Layout::from_size_align(total, 1).expect("invalid layout");
1199 let out_ptr = unsafe { std::alloc::alloc(layout) };
1200
1201 unsafe {
1202 let len_bytes = (response_bytes.len() as u32).to_le_bytes();
1203 std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
1204 std::ptr::copy_nonoverlapping(
1205 response_bytes.as_ptr(),
1206 out_ptr.add(4),
1207 response_bytes.len(),
1208 );
1209 }
1210
1211 out_ptr as i32
1212}
1213
1214#[macro_export]
1227macro_rules! init {
1228 () => {
1229 #[no_mangle]
1230 pub extern "C" fn alloc(size: i32) -> i32 {
1231 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
1232 unsafe { std::alloc::alloc(layout) as i32 }
1233 }
1234 };
1235}
1236
1237#[macro_export]
1264macro_rules! handler {
1265 ($name:ident, |$req:ident : Request| $body:expr) => {
1266 #[no_mangle]
1267 pub extern "C" fn $name(ptr: i32, len: i32) -> i32 {
1268 $crate::__run_handler(ptr, len, |$req: $crate::Request| $body)
1269 }
1270 };
1271}
1272
1273pub mod migrate {
1312 use super::{Request, Response};
1313 pub use cufflink_types::SchemaDiff;
1314
1315 pub fn run<F>(req: Request, handler: F) -> Response
1322 where
1323 F: FnOnce(SchemaDiff) -> Result<(), String>,
1324 {
1325 match serde_json::from_value::<SchemaDiff>(req.body().clone()) {
1326 Ok(diff) => match handler(diff) {
1327 Ok(()) => Response::json(&serde_json::json!({"ok": true})),
1328 Err(e) => Response::error(&e),
1329 },
1330 Err(e) => Response::error(&format!(
1331 "on_migrate: failed to parse SchemaDiff payload: {}",
1332 e
1333 )),
1334 }
1335 }
1336}
1337
1338pub mod prelude {
1346 pub use crate::config;
1347 pub use crate::db;
1348 pub use crate::http;
1349 pub use crate::image;
1350 pub use crate::log;
1351 pub use crate::migrate;
1352 pub use crate::nats;
1353 pub use crate::redis;
1354 pub use crate::storage;
1355 pub use crate::util;
1356 pub use crate::Auth;
1357 pub use crate::Request;
1358 pub use crate::Response;
1359 pub use serde_json::{json, Value};
1360}
1361
1362#[cfg(test)]
1365mod tests {
1366 use super::*;
1367 use serde_json::json;
1368
1369 #[test]
1370 fn test_request_parsing() {
1371 let json = serde_json::to_string(&json!({
1372 "method": "POST",
1373 "handler": "checkout",
1374 "headers": {"content-type": "application/json"},
1375 "body": {"item": "widget", "qty": 3},
1376 "tenant": "acme",
1377 "service": "shop"
1378 }))
1379 .unwrap();
1380
1381 let req = Request::from_json(&json).unwrap();
1382 assert_eq!(req.method(), "POST");
1383 assert_eq!(req.handler(), "checkout");
1384 assert_eq!(req.tenant(), "acme");
1385 assert_eq!(req.service(), "shop");
1386 assert_eq!(req.body()["item"], "widget");
1387 assert_eq!(req.body()["qty"], 3);
1388 assert_eq!(req.header("content-type"), Some("application/json"));
1389 }
1390
1391 #[test]
1392 fn test_request_missing_fields() {
1393 let json = r#"{"method": "GET"}"#;
1394 let req = Request::from_json(json).unwrap();
1395 assert_eq!(req.method(), "GET");
1396 assert_eq!(req.handler(), "");
1397 assert_eq!(req.tenant(), "");
1398 assert_eq!(req.body(), &Value::Null);
1399 }
1400
1401 #[test]
1402 fn test_response_json() {
1403 let resp = Response::json(&json!({"status": "ok", "count": 42}));
1404 let data = resp.into_data();
1405 let parsed: Value = serde_json::from_str(&data).unwrap();
1406 assert_eq!(parsed["status"], "ok");
1407 assert_eq!(parsed["count"], 42);
1408 }
1409
1410 #[test]
1411 fn test_response_error() {
1412 let resp = Response::error("something went wrong");
1413 let data = resp.into_data();
1414 let parsed: Value = serde_json::from_str(&data).unwrap();
1415 assert_eq!(parsed["__status"], 400);
1417 assert_eq!(parsed["__body"]["error"], "something went wrong");
1418 }
1419
1420 #[test]
1421 fn test_response_not_found() {
1422 let resp = Response::not_found("item not found");
1423 let data = resp.into_data();
1424 let parsed: Value = serde_json::from_str(&data).unwrap();
1425 assert_eq!(parsed["__status"], 404);
1426 assert_eq!(parsed["__body"]["error"], "item not found");
1427 }
1428
1429 #[test]
1430 fn test_response_with_status() {
1431 let resp = Response::json(&serde_json::json!({"ok": true})).with_status(201);
1432 let data = resp.into_data();
1433 let parsed: Value = serde_json::from_str(&data).unwrap();
1434 assert_eq!(parsed["__status"], 201);
1435 assert_eq!(parsed["__body"]["ok"], true);
1436 }
1437
1438 fn migrate_request(diff: serde_json::Value) -> Request {
1439 let payload = serde_json::to_string(&json!({
1440 "method": "POST",
1441 "handler": "handle_on_migrate",
1442 "headers": {},
1443 "body": diff,
1444 "tenant": "default",
1445 "service": "logistics-service",
1446 }))
1447 .unwrap();
1448 Request::from_json(&payload).unwrap()
1449 }
1450
1451 #[test]
1452 fn test_migrate_run_success() {
1453 let req = migrate_request(json!({
1454 "added_columns": [["pickups", "min"]],
1455 "dropped_columns": [["pickups", "midpoint"]],
1456 }));
1457 let resp = migrate::run(req, |diff| {
1458 assert!(diff.added_column("pickups", "min"));
1459 assert!(diff.dropped_column("pickups", "midpoint"));
1460 Ok(())
1461 });
1462 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1463 assert_eq!(parsed["ok"], true);
1464 }
1465
1466 #[test]
1467 fn test_migrate_run_handler_error() {
1468 let req = migrate_request(json!({}));
1469 let resp = migrate::run(req, |_| Err("backfill failed".into()));
1470 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1471 assert_eq!(parsed["__status"], 400);
1472 assert_eq!(parsed["__body"]["error"], "backfill failed");
1473 }
1474
1475 #[test]
1476 fn test_migrate_run_invalid_payload() {
1477 let req = migrate_request(json!("not a diff"));
1479 let resp = migrate::run(req, |_| {
1480 panic!("closure should not be called for invalid payload")
1481 });
1482 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1483 assert_eq!(parsed["__status"], 400);
1484 assert!(parsed["__body"]["error"]
1485 .as_str()
1486 .unwrap()
1487 .contains("on_migrate: failed to parse SchemaDiff payload"));
1488 }
1489
1490 #[test]
1491 fn test_migrate_run_empty_diff() {
1492 let req = migrate_request(json!({}));
1493 let mut called = false;
1494 let resp = migrate::run(req, |diff| {
1495 assert!(diff.is_empty());
1496 called = true;
1497 Ok(())
1498 });
1499 assert!(called);
1500 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1501 assert_eq!(parsed["ok"], true);
1502 }
1503
1504 #[test]
1505 fn test_response_200_no_wrapper() {
1506 let resp = Response::json(&serde_json::json!({"data": "test"}));
1507 let data = resp.into_data();
1508 let parsed: Value = serde_json::from_str(&data).unwrap();
1509 assert_eq!(parsed["data"], "test");
1511 assert!(parsed.get("__status").is_none());
1512 }
1513
1514 #[test]
1515 fn test_response_empty() {
1516 let resp = Response::empty();
1517 let data = resp.into_data();
1518 let parsed: Value = serde_json::from_str(&data).unwrap();
1519 assert_eq!(parsed["ok"], true);
1520 }
1521
1522 #[test]
1523 fn test_response_text() {
1524 let resp = Response::text("hello world");
1525 let data = resp.into_data();
1526 let parsed: Value = serde_json::from_str(&data).unwrap();
1527 assert_eq!(parsed, "hello world");
1528 }
1529
1530 #[test]
1531 fn test_db_query_noop_on_native() {
1532 let rows = db::query("SELECT 1");
1534 assert!(rows.is_empty());
1535 }
1536
1537 #[test]
1538 fn test_db_query_one_noop_on_native() {
1539 let row = db::query_one("SELECT 1");
1540 assert!(row.is_none());
1541 }
1542
1543 #[test]
1544 fn test_db_execute_noop_on_native() {
1545 let affected = db::execute("INSERT INTO x VALUES (1)");
1546 assert_eq!(affected, 0);
1547 }
1548
1549 #[test]
1550 fn test_nats_publish_noop_on_native() {
1551 let ok = nats::publish("test.subject", "payload");
1552 assert!(ok);
1553 }
1554
1555 #[test]
1556 fn test_request_with_auth() {
1557 let json = serde_json::to_string(&json!({
1558 "method": "POST",
1559 "handler": "checkout",
1560 "headers": {},
1561 "body": {},
1562 "tenant": "acme",
1563 "service": "shop",
1564 "auth": {
1565 "sub": "user-123",
1566 "preferred_username": "john",
1567 "name": "John Doe",
1568 "email": "john@example.com",
1569 "realm_roles": ["admin", "manager"],
1570 "claims": {"department": "engineering"}
1571 }
1572 }))
1573 .unwrap();
1574
1575 let req = Request::from_json(&json).unwrap();
1576 let auth = req.auth().unwrap();
1577 assert_eq!(auth.sub, "user-123");
1578 assert_eq!(auth.preferred_username.as_deref(), Some("john"));
1579 assert_eq!(auth.name.as_deref(), Some("John Doe"));
1580 assert_eq!(auth.email.as_deref(), Some("john@example.com"));
1581 assert!(auth.has_role("admin"));
1582 assert!(auth.has_role("manager"));
1583 assert!(!auth.has_role("viewer"));
1584 assert_eq!(
1585 auth.claim("department").and_then(|v| v.as_str()),
1586 Some("engineering")
1587 );
1588 }
1589
1590 #[test]
1591 fn test_request_without_auth() {
1592 let json = r#"{"method": "GET"}"#;
1593 let req = Request::from_json(json).unwrap();
1594 assert!(req.auth().is_none());
1595 }
1596
1597 #[test]
1598 fn test_request_null_auth() {
1599 let json = serde_json::to_string(&json!({
1600 "method": "GET",
1601 "auth": null
1602 }))
1603 .unwrap();
1604 let req = Request::from_json(&json).unwrap();
1605 assert!(req.auth().is_none());
1606 }
1607
1608 #[test]
1609 fn test_require_auth_success() {
1610 let json = serde_json::to_string(&json!({
1611 "method": "GET",
1612 "auth": {"sub": "user-1", "realm_roles": [], "claims": {}}
1613 }))
1614 .unwrap();
1615 let req = Request::from_json(&json).unwrap();
1616 assert!(req.require_auth().is_ok());
1617 assert_eq!(req.require_auth().unwrap().sub, "user-1");
1618 }
1619
1620 #[test]
1621 fn test_require_auth_fails_when_unauthenticated() {
1622 let json = r#"{"method": "GET"}"#;
1623 let req = Request::from_json(json).unwrap();
1624 assert!(req.require_auth().is_err());
1625 }
1626
1627 #[test]
1628 fn test_http_fetch_noop_on_native() {
1629 let resp = http::fetch("GET", "https://example.com", &[], None);
1630 assert!(resp.is_none());
1631 }
1632
1633 #[test]
1634 fn test_http_get_noop_on_native() {
1635 let resp = http::get("https://example.com", &[]);
1636 assert!(resp.is_none());
1637 }
1638
1639 #[test]
1640 fn test_http_post_noop_on_native() {
1641 let resp = http::post("https://example.com", &[], "{}");
1642 assert!(resp.is_none());
1643 }
1644
1645 #[test]
1646 fn test_storage_download_noop_on_native() {
1647 let data = storage::download("my-bucket", "images/photo.jpg");
1648 assert!(data.is_none());
1649 }
1650
1651 #[test]
1652 fn test_image_transform_jpeg_noop_on_native() {
1653 let bytes = image::transform_jpeg("my-bucket", "in.jpg", "out.jpg", 1024, 80);
1654 assert!(bytes.is_none());
1655 }
1656
1657 #[test]
1658 fn test_auth_permissions() {
1659 let json = serde_json::to_string(&json!({
1660 "method": "POST",
1661 "handler": "test",
1662 "headers": {},
1663 "body": {},
1664 "tenant": "acme",
1665 "service": "shop",
1666 "auth": {
1667 "sub": "user-1",
1668 "realm_roles": ["admin"],
1669 "claims": {},
1670 "permissions": ["staff:create", "staff:view", "items:*"],
1671 "role_names": ["admin", "manager"]
1672 }
1673 }))
1674 .unwrap();
1675
1676 let req = Request::from_json(&json).unwrap();
1677 let auth = req.auth().unwrap();
1678
1679 assert!(auth.can("staff", "create"));
1681 assert!(auth.can("staff", "view"));
1682 assert!(!auth.can("staff", "delete"));
1683
1684 assert!(auth.can("items", "create"));
1686 assert!(auth.can("items", "view"));
1687 assert!(auth.can("items", "delete"));
1688
1689 assert!(!auth.can("batches", "view"));
1691
1692 assert!(auth.has_cufflink_role("admin"));
1694 assert!(auth.has_cufflink_role("manager"));
1695 assert!(!auth.has_cufflink_role("viewer"));
1696 }
1697
1698 #[test]
1699 fn test_auth_super_wildcard() {
1700 let auth = Auth {
1701 sub: "user-1".to_string(),
1702 preferred_username: None,
1703 name: None,
1704 email: None,
1705 realm_roles: vec![],
1706 claims: HashMap::new(),
1707 permissions: vec!["*".to_string()],
1708 role_names: vec!["superadmin".to_string()],
1709 is_service_account: false,
1710 };
1711
1712 assert!(auth.can("anything", "everything"));
1713 assert!(auth.can("staff", "create"));
1714 }
1715
1716 #[test]
1717 fn test_auth_empty_permissions() {
1718 let auth = Auth {
1719 sub: "user-1".to_string(),
1720 preferred_username: None,
1721 name: None,
1722 email: None,
1723 realm_roles: vec![],
1724 claims: HashMap::new(),
1725 permissions: vec![],
1726 role_names: vec![],
1727 is_service_account: false,
1728 };
1729
1730 assert!(!auth.can("staff", "create"));
1731 assert!(!auth.has_cufflink_role("admin"));
1732 }
1733
1734 #[test]
1735 fn test_redis_get_noop_on_native() {
1736 let val = redis::get("some-key");
1737 assert!(val.is_none());
1738 }
1739
1740 #[test]
1741 fn test_redis_set_noop_on_native() {
1742 let ok = redis::set("key", "value", 3600);
1743 assert!(ok);
1744 }
1745
1746 #[test]
1747 fn test_redis_del_noop_on_native() {
1748 let ok = redis::del("key");
1749 assert!(ok);
1750 }
1751
1752 #[test]
1753 fn test_http_fetch_response_helpers() {
1754 let resp = http::FetchResponse {
1755 status: 200,
1756 body: r#"{"key": "value"}"#.to_string(),
1757 body_encoding: "utf8".to_string(),
1758 headers: HashMap::new(),
1759 };
1760 assert!(resp.is_success());
1761 assert!(!resp.is_base64());
1762 let json = resp.json().unwrap();
1763 assert_eq!(json["key"], "value");
1764
1765 let err_resp = http::FetchResponse {
1766 status: 404,
1767 body: "not found".to_string(),
1768 body_encoding: "utf8".to_string(),
1769 headers: HashMap::new(),
1770 };
1771 assert!(!err_resp.is_success());
1772
1773 let binary_resp = http::FetchResponse {
1774 status: 200,
1775 body: "aW1hZ2VkYXRh".to_string(),
1776 body_encoding: "base64".to_string(),
1777 headers: HashMap::new(),
1778 };
1779 assert!(binary_resp.is_base64());
1780 }
1781}