1use serde_json::Value;
31use std::collections::HashMap;
32
33#[cfg(target_arch = "wasm32")]
36pub(crate) fn read_batch_response() -> Option<Vec<Value>> {
37 let len = unsafe { get_host_response_len() };
38 if len <= 0 {
39 return None;
40 }
41 let mut buf = vec![0u8; len as usize];
42 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
43 if read <= 0 {
44 return None;
45 }
46 buf.truncate(read as usize);
47 let s = String::from_utf8(buf).ok()?;
48 let v: Value = serde_json::from_str(&s).ok()?;
49 v.as_array().cloned()
50}
51
52#[cfg(target_arch = "wasm32")]
57extern "C" {
58 #[link_name = "cufflink_log"]
59 fn cufflink_log(level: i32, msg_ptr: i32, msg_len: i32);
60 fn db_query(sql_ptr: i32, sql_len: i32) -> i32;
61 fn db_execute(sql_ptr: i32, sql_len: i32) -> i32;
62 fn get_host_response_len() -> i32;
63 fn get_host_response(buf_ptr: i32, buf_len: i32) -> i32;
64 fn nats_publish(subj_ptr: i32, subj_len: i32, payload_ptr: i32, payload_len: i32) -> i32;
65 fn nats_request(
66 subj_ptr: i32,
67 subj_len: i32,
68 payload_ptr: i32,
69 payload_len: i32,
70 timeout_ms: i32,
71 ) -> i32;
72 fn http_fetch(
73 method_ptr: i32,
74 method_len: i32,
75 url_ptr: i32,
76 url_len: i32,
77 headers_ptr: i32,
78 headers_len: i32,
79 body_ptr: i32,
80 body_len: i32,
81 ) -> i32;
82 fn get_config(key_ptr: i32, key_len: i32) -> i32;
83 fn s3_download(bucket_ptr: i32, bucket_len: i32, key_ptr: i32, key_len: i32) -> i32;
84 fn s3_download_many(items_ptr: i32, items_len: i32) -> i32;
85 fn http_fetch_many(items_ptr: i32, items_len: i32) -> i32;
86 fn image_transform_jpeg(
87 bucket_ptr: i32,
88 bucket_len: i32,
89 in_key_ptr: i32,
90 in_key_len: i32,
91 out_key_ptr: i32,
92 out_key_len: i32,
93 max_dim: i32,
94 quality: i32,
95 ) -> i32;
96 fn s3_presign_upload(
97 bucket_ptr: i32,
98 bucket_len: i32,
99 key_ptr: i32,
100 key_len: i32,
101 content_type_ptr: i32,
102 content_type_len: i32,
103 expires_secs: i32,
104 ) -> i32;
105 fn redis_get(key_ptr: i32, key_len: i32) -> i32;
106 fn redis_set(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32, ttl_secs: i32) -> i32;
107 fn redis_del(key_ptr: i32, key_len: i32) -> i32;
108 fn generate_uuid() -> i32;
109 fn current_time() -> i32;
110}
111
112#[derive(Debug, Clone)]
133pub struct Auth {
134 pub sub: String,
136 pub preferred_username: Option<String>,
138 pub name: Option<String>,
140 pub email: Option<String>,
142 pub realm_roles: Vec<String>,
144 pub claims: HashMap<String, Value>,
146 pub permissions: Vec<String>,
148 pub role_names: Vec<String>,
150 pub is_service_account: bool,
153}
154
155impl Auth {
156 pub fn has_role(&self, role: &str) -> bool {
158 self.realm_roles.iter().any(|r| r == role)
159 }
160
161 pub fn can(&self, area: &str, operation: &str) -> bool {
172 let required = format!("{}:{}", area, operation);
173 let wildcard = format!("{}:*", area);
174 self.permissions
175 .iter()
176 .any(|p| p == &required || p == &wildcard || p == "*")
177 }
178
179 pub fn has_cufflink_role(&self, role: &str) -> bool {
181 self.role_names.iter().any(|r| r == role)
182 }
183
184 pub fn claim(&self, key: &str) -> Option<&Value> {
186 self.claims.get(key)
187 }
188}
189
190#[derive(Debug, Clone)]
199pub struct JobContext {
200 pub id: String,
201 pub attempt: u32,
202 pub max_attempts: u32,
203}
204
205impl JobContext {
206 pub fn is_retry(&self) -> bool {
208 self.attempt > 1
209 }
210}
211
212#[derive(Debug, Clone)]
219pub struct Request {
220 method: String,
221 handler: String,
222 headers: HashMap<String, String>,
223 body: Value,
224 raw_body: Vec<u8>,
225 tenant: String,
226 service: String,
227 auth: Option<Auth>,
228 job: Option<JobContext>,
229}
230
231impl Request {
232 pub fn from_json(json: &str) -> Option<Self> {
234 let v: Value = serde_json::from_str(json).ok()?;
235 let headers = v["headers"]
236 .as_object()
237 .map(|m| {
238 m.iter()
239 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
240 .collect()
241 })
242 .unwrap_or_default();
243
244 let auth = v["auth"].as_object().map(|auth_obj| {
245 let a = Value::Object(auth_obj.clone());
246 Auth {
247 sub: a["sub"].as_str().unwrap_or("").to_string(),
248 preferred_username: a["preferred_username"].as_str().map(|s| s.to_string()),
249 name: a["name"].as_str().map(|s| s.to_string()),
250 email: a["email"].as_str().map(|s| s.to_string()),
251 realm_roles: a["realm_roles"]
252 .as_array()
253 .map(|arr| {
254 arr.iter()
255 .filter_map(|v| v.as_str().map(|s| s.to_string()))
256 .collect()
257 })
258 .unwrap_or_default(),
259 claims: a["claims"]
260 .as_object()
261 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
262 .unwrap_or_default(),
263 permissions: a["permissions"]
264 .as_array()
265 .map(|arr| {
266 arr.iter()
267 .filter_map(|v| v.as_str().map(|s| s.to_string()))
268 .collect()
269 })
270 .unwrap_or_default(),
271 role_names: a["role_names"]
272 .as_array()
273 .map(|arr| {
274 arr.iter()
275 .filter_map(|v| v.as_str().map(|s| s.to_string()))
276 .collect()
277 })
278 .unwrap_or_default(),
279 is_service_account: a["is_service_account"].as_bool().unwrap_or(false),
280 }
281 });
282
283 let raw_body = v["body_raw_b64"]
284 .as_str()
285 .filter(|s| !s.is_empty())
286 .and_then(|s| {
287 use base64::{engine::general_purpose, Engine};
288 general_purpose::STANDARD.decode(s).ok()
289 })
290 .unwrap_or_default();
291
292 let job = v["_job"].as_object().and_then(|j| {
293 let id = j.get("id")?.as_str()?.to_string();
294 let attempt = u32::try_from(j.get("attempt")?.as_u64()?).ok()?;
295 let max_attempts = u32::try_from(j.get("max_attempts")?.as_u64()?).ok()?;
296 Some(JobContext {
297 id,
298 attempt,
299 max_attempts,
300 })
301 });
302
303 Some(Self {
304 method: v["method"].as_str().unwrap_or("GET").to_string(),
305 handler: v["handler"].as_str().unwrap_or("").to_string(),
306 headers,
307 body: v["body"].clone(),
308 raw_body,
309 tenant: v["tenant"].as_str().unwrap_or("").to_string(),
310 service: v["service"].as_str().unwrap_or("").to_string(),
311 auth,
312 job,
313 })
314 }
315
316 pub fn method(&self) -> &str {
318 &self.method
319 }
320
321 pub fn handler(&self) -> &str {
323 &self.handler
324 }
325
326 pub fn headers(&self) -> &HashMap<String, String> {
328 &self.headers
329 }
330
331 pub fn header(&self, name: &str) -> Option<&str> {
333 self.headers.get(name).map(|s| s.as_str())
334 }
335
336 pub fn body(&self) -> &Value {
338 &self.body
339 }
340
341 pub fn raw_body(&self) -> &[u8] {
349 &self.raw_body
350 }
351
352 pub fn tenant(&self) -> &str {
354 &self.tenant
355 }
356
357 pub fn service(&self) -> &str {
359 &self.service
360 }
361
362 pub fn auth(&self) -> Option<&Auth> {
366 self.auth.as_ref()
367 }
368
369 pub fn job(&self) -> Option<&JobContext> {
374 self.job.as_ref()
375 }
376
377 pub fn require_auth(&self) -> Result<&Auth, Response> {
389 self.auth.as_ref().ok_or_else(|| {
390 Response::json(&serde_json::json!({
391 "error": "Authentication required",
392 "status": 401
393 }))
394 })
395 }
396}
397
398#[derive(Debug, Clone)]
402pub struct Response {
403 data: String,
404 status: u16,
405}
406
407impl Response {
408 pub fn json(value: &Value) -> Self {
410 Self {
411 data: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
412 status: 200,
413 }
414 }
415
416 pub fn text(s: &str) -> Self {
418 Self::json(&Value::String(s.to_string()))
419 }
420
421 pub fn error(message: &str) -> Self {
423 Self {
424 data: serde_json::json!({"error": message}).to_string(),
425 status: 400,
426 }
427 }
428
429 pub fn not_found(message: &str) -> Self {
431 Self {
432 data: serde_json::json!({"error": message}).to_string(),
433 status: 404,
434 }
435 }
436
437 pub fn forbidden(message: &str) -> Self {
439 Self {
440 data: serde_json::json!({"error": message}).to_string(),
441 status: 403,
442 }
443 }
444
445 pub fn empty() -> Self {
447 Self::json(&serde_json::json!({"ok": true}))
448 }
449
450 pub fn with_status(mut self, status: u16) -> Self {
452 self.status = status;
453 self
454 }
455
456 pub fn into_data(self) -> String {
459 if self.status == 200 {
460 self.data
462 } else {
463 serde_json::json!({
465 "__status": self.status,
466 "__body": serde_json::from_str::<Value>(&self.data).unwrap_or(Value::String(self.data)),
467 })
468 .to_string()
469 }
470 }
471}
472
473pub mod db {
480 use super::*;
481
482 pub fn query(sql: &str) -> Vec<Value> {
493 #[cfg(target_arch = "wasm32")]
494 {
495 let bytes = sql.as_bytes();
496 let result = unsafe { db_query(bytes.as_ptr() as i32, bytes.len() as i32) };
497 if result < 0 {
498 return vec![];
499 }
500 read_host_response()
501 }
502 #[cfg(not(target_arch = "wasm32"))]
503 {
504 let _ = sql;
505 vec![]
506 }
507 }
508
509 pub fn query_one(sql: &str) -> Option<Value> {
517 query(sql).into_iter().next()
518 }
519
520 pub fn execute(sql: &str) -> i32 {
529 #[cfg(target_arch = "wasm32")]
530 {
531 let bytes = sql.as_bytes();
532 unsafe { db_execute(bytes.as_ptr() as i32, bytes.len() as i32) }
533 }
534 #[cfg(not(target_arch = "wasm32"))]
535 {
536 let _ = sql;
537 0
538 }
539 }
540
541 #[cfg(target_arch = "wasm32")]
543 fn read_host_response() -> Vec<Value> {
544 let len = unsafe { get_host_response_len() };
545 if len <= 0 {
546 return vec![];
547 }
548 let mut buf = vec![0u8; len as usize];
549 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
550 if read <= 0 {
551 return vec![];
552 }
553 buf.truncate(read as usize);
554 let json_str = String::from_utf8_lossy(&buf);
555 serde_json::from_str(&json_str).unwrap_or_default()
556 }
557}
558
559pub mod nats {
566 #[allow(unused_imports)]
567 use super::*;
568
569 pub fn publish(subject: &str, payload: &str) -> bool {
580 #[cfg(target_arch = "wasm32")]
581 {
582 let subj_bytes = subject.as_bytes();
583 let payload_bytes = payload.as_bytes();
584 let result = unsafe {
585 nats_publish(
586 subj_bytes.as_ptr() as i32,
587 subj_bytes.len() as i32,
588 payload_bytes.as_ptr() as i32,
589 payload_bytes.len() as i32,
590 )
591 };
592 result == 0
593 }
594 #[cfg(not(target_arch = "wasm32"))]
595 {
596 let _ = (subject, payload);
597 true
598 }
599 }
600
601 pub fn request(subject: &str, payload: &str, timeout_ms: i32) -> Option<String> {
613 #[cfg(target_arch = "wasm32")]
614 {
615 let subj_bytes = subject.as_bytes();
616 let payload_bytes = payload.as_bytes();
617 let result = unsafe {
618 nats_request(
619 subj_bytes.as_ptr() as i32,
620 subj_bytes.len() as i32,
621 payload_bytes.as_ptr() as i32,
622 payload_bytes.len() as i32,
623 timeout_ms,
624 )
625 };
626 if result != 0 {
627 return None;
628 }
629 let len = unsafe { get_host_response_len() };
630 if len <= 0 {
631 return None;
632 }
633 let mut buf = vec![0u8; len as usize];
634 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
635 if read <= 0 {
636 return None;
637 }
638 String::from_utf8(buf[..read as usize].to_vec()).ok()
639 }
640 #[cfg(not(target_arch = "wasm32"))]
641 {
642 let _ = (subject, payload, timeout_ms);
643 None
644 }
645 }
646}
647
648pub mod log {
654 #[allow(unused_imports)]
655 use super::*;
656
657 pub fn error(msg: &str) {
659 write(0, msg);
660 }
661
662 pub fn warn(msg: &str) {
664 write(1, msg);
665 }
666
667 pub fn info(msg: &str) {
669 write(2, msg);
670 }
671
672 pub fn debug(msg: &str) {
674 write(3, msg);
675 }
676
677 fn write(level: i32, msg: &str) {
678 #[cfg(target_arch = "wasm32")]
679 {
680 let bytes = msg.as_bytes();
681 unsafe {
682 super::cufflink_log(level, bytes.as_ptr() as i32, bytes.len() as i32);
683 }
684 }
685 #[cfg(not(target_arch = "wasm32"))]
686 {
687 let _ = (level, msg);
688 }
689 }
690}
691
692pub mod http {
699 #[allow(unused_imports)]
700 use super::*;
701
702 #[derive(Debug, Clone)]
704 pub struct FetchResponse {
705 pub status: i32,
707 pub body: String,
709 pub body_encoding: String,
711 pub headers: HashMap<String, String>,
713 }
714
715 impl FetchResponse {
716 pub fn json(&self) -> Option<Value> {
718 serde_json::from_str(&self.body).ok()
719 }
720
721 pub fn is_success(&self) -> bool {
723 (200..300).contains(&self.status)
724 }
725
726 pub fn is_base64(&self) -> bool {
728 self.body_encoding == "base64"
729 }
730 }
731
732 pub fn fetch(
743 method: &str,
744 url: &str,
745 headers: &[(&str, &str)],
746 body: Option<&str>,
747 ) -> Option<FetchResponse> {
748 #[cfg(target_arch = "wasm32")]
749 {
750 let method_bytes = method.as_bytes();
751 let url_bytes = url.as_bytes();
752 let headers_map: HashMap<&str, &str> = headers.iter().copied().collect();
753 let headers_json = serde_json::to_string(&headers_map).unwrap_or_default();
754 let headers_bytes = headers_json.as_bytes();
755 let body_bytes = body.unwrap_or("").as_bytes();
756 let body_len = body.map(|b| b.len()).unwrap_or(0);
757
758 let result = unsafe {
759 http_fetch(
760 method_bytes.as_ptr() as i32,
761 method_bytes.len() as i32,
762 url_bytes.as_ptr() as i32,
763 url_bytes.len() as i32,
764 headers_bytes.as_ptr() as i32,
765 headers_bytes.len() as i32,
766 body_bytes.as_ptr() as i32,
767 body_len as i32,
768 )
769 };
770
771 if result < 0 {
772 return None;
773 }
774
775 read_fetch_response()
776 }
777 #[cfg(not(target_arch = "wasm32"))]
778 {
779 let _ = (method, url, headers, body);
780 None
781 }
782 }
783
784 pub fn get(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
786 fetch("GET", url, headers, None)
787 }
788
789 pub fn post(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
791 fetch("POST", url, headers, Some(body))
792 }
793
794 pub fn put(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
796 fetch("PUT", url, headers, Some(body))
797 }
798
799 pub fn delete(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
801 fetch("DELETE", url, headers, None)
802 }
803
804 pub fn patch(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
806 fetch("PATCH", url, headers, Some(body))
807 }
808
809 #[derive(Debug, Clone)]
811 pub struct FetchRequest<'a> {
812 pub method: &'a str,
813 pub url: &'a str,
814 pub headers: &'a [(&'a str, &'a str)],
815 pub body: Option<&'a str>,
816 }
817
818 pub fn fetch_many(requests: &[FetchRequest<'_>]) -> Vec<Result<FetchResponse, String>> {
833 #[cfg(target_arch = "wasm32")]
834 {
835 let items: Vec<Value> = requests
836 .iter()
837 .map(|r| {
838 let h: HashMap<&str, &str> = r.headers.iter().copied().collect();
839 serde_json::json!({
840 "method": r.method,
841 "url": r.url,
842 "headers": h,
843 "body": r.body,
844 })
845 })
846 .collect();
847 let payload = serde_json::to_string(&items).unwrap_or_else(|_| "[]".to_string());
848 let bytes = payload.as_bytes();
849 let rc = unsafe { super::http_fetch_many(bytes.as_ptr() as i32, bytes.len() as i32) };
850 if rc < 0 {
851 return requests
852 .iter()
853 .map(|_| Err("http_fetch_many host call failed".to_string()))
854 .collect();
855 }
856 super::read_batch_response()
857 .map(|v| v.into_iter().map(parse_fetch_slot).collect())
858 .unwrap_or_else(|| {
859 requests
860 .iter()
861 .map(|_| Err("malformed host response".to_string()))
862 .collect()
863 })
864 }
865 #[cfg(not(target_arch = "wasm32"))]
866 {
867 let _ = requests;
868 vec![]
869 }
870 }
871
872 #[cfg(target_arch = "wasm32")]
873 fn fetch_response_from_json(v: &Value) -> FetchResponse {
874 FetchResponse {
875 status: v["status"].as_i64().unwrap_or(0) as i32,
876 body: v["body"].as_str().unwrap_or("").to_string(),
877 body_encoding: v["body_encoding"].as_str().unwrap_or("utf8").to_string(),
878 headers: v["headers"]
879 .as_object()
880 .map(|m| {
881 m.iter()
882 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
883 .collect()
884 })
885 .unwrap_or_default(),
886 }
887 }
888
889 #[cfg(target_arch = "wasm32")]
890 fn parse_fetch_slot(slot: Value) -> Result<FetchResponse, String> {
891 if !slot["ok"].as_bool().unwrap_or(false) {
892 return Err(slot["error"]
893 .as_str()
894 .unwrap_or("unknown error")
895 .to_string());
896 }
897 Ok(fetch_response_from_json(&slot))
898 }
899
900 #[cfg(target_arch = "wasm32")]
902 fn read_fetch_response() -> Option<FetchResponse> {
903 let len = unsafe { get_host_response_len() };
904 if len <= 0 {
905 return None;
906 }
907 let mut buf = vec![0u8; len as usize];
908 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
909 if read <= 0 {
910 return None;
911 }
912 buf.truncate(read as usize);
913 let json_str = String::from_utf8_lossy(&buf);
914 let v: Value = serde_json::from_str(&json_str).ok()?;
915 Some(fetch_response_from_json(&v))
916 }
917}
918
919pub mod config {
927 #[allow(unused_imports)]
928 use super::*;
929
930 pub fn get(key: &str) -> Option<String> {
939 #[cfg(target_arch = "wasm32")]
940 {
941 let bytes = key.as_bytes();
942 let result = unsafe { get_config(bytes.as_ptr() as i32, bytes.len() as i32) };
943 if result < 0 {
944 return None;
945 }
946 read_config_response()
947 }
948 #[cfg(not(target_arch = "wasm32"))]
949 {
950 let _ = key;
951 None
952 }
953 }
954
955 #[cfg(target_arch = "wasm32")]
957 fn read_config_response() -> Option<String> {
958 let len = unsafe { get_host_response_len() };
959 if len <= 0 {
960 return None;
961 }
962 let mut buf = vec![0u8; len as usize];
963 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
964 if read <= 0 {
965 return None;
966 }
967 buf.truncate(read as usize);
968 String::from_utf8(buf).ok()
969 }
970}
971
972pub mod storage {
980 #[allow(unused_imports)]
981 use super::*;
982
983 pub fn download(bucket: &str, key: &str) -> Option<String> {
994 #[cfg(target_arch = "wasm32")]
995 {
996 let bucket_bytes = bucket.as_bytes();
997 let key_bytes = key.as_bytes();
998 let result = unsafe {
999 s3_download(
1000 bucket_bytes.as_ptr() as i32,
1001 bucket_bytes.len() as i32,
1002 key_bytes.as_ptr() as i32,
1003 key_bytes.len() as i32,
1004 )
1005 };
1006 if result < 0 {
1007 return None;
1008 }
1009 read_storage_response()
1010 }
1011 #[cfg(not(target_arch = "wasm32"))]
1012 {
1013 let _ = (bucket, key);
1014 None
1015 }
1016 }
1017
1018 pub fn presign_upload(
1031 bucket: &str,
1032 key: &str,
1033 content_type: &str,
1034 expires_secs: u64,
1035 ) -> Option<String> {
1036 #[cfg(target_arch = "wasm32")]
1037 {
1038 let bucket_bytes = bucket.as_bytes();
1039 let key_bytes = key.as_bytes();
1040 let ct_bytes = content_type.as_bytes();
1041 let result = unsafe {
1042 s3_presign_upload(
1043 bucket_bytes.as_ptr() as i32,
1044 bucket_bytes.len() as i32,
1045 key_bytes.as_ptr() as i32,
1046 key_bytes.len() as i32,
1047 ct_bytes.as_ptr() as i32,
1048 ct_bytes.len() as i32,
1049 expires_secs as i32,
1050 )
1051 };
1052 if result < 0 {
1053 return None;
1054 }
1055 read_storage_response()
1056 }
1057 #[cfg(not(target_arch = "wasm32"))]
1058 {
1059 let _ = (bucket, key, content_type, expires_secs);
1060 None
1061 }
1062 }
1063
1064 #[cfg(target_arch = "wasm32")]
1066 fn read_storage_response() -> Option<String> {
1067 let len = unsafe { get_host_response_len() };
1068 if len <= 0 {
1069 return None;
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 None;
1075 }
1076 buf.truncate(read as usize);
1077 String::from_utf8(buf).ok()
1078 }
1079
1080 pub fn download_many(items: &[(&str, &str)]) -> Vec<Result<Vec<u8>, String>> {
1085 #[cfg(target_arch = "wasm32")]
1086 {
1087 let payload_items: Vec<Value> = items
1088 .iter()
1089 .map(|(b, k)| serde_json::json!({ "bucket": b, "key": k }))
1090 .collect();
1091 let payload = serde_json::to_string(&payload_items).unwrap_or_else(|_| "[]".into());
1092 let bytes = payload.as_bytes();
1093 let rc = unsafe { super::s3_download_many(bytes.as_ptr() as i32, bytes.len() as i32) };
1094 if rc < 0 {
1095 return items
1096 .iter()
1097 .map(|_| Err("s3_download_many host call failed".to_string()))
1098 .collect();
1099 }
1100 super::read_batch_response()
1101 .map(|v| v.into_iter().map(parse_download_slot).collect())
1102 .unwrap_or_else(|| {
1103 items
1104 .iter()
1105 .map(|_| Err("malformed host response".to_string()))
1106 .collect()
1107 })
1108 }
1109 #[cfg(not(target_arch = "wasm32"))]
1110 {
1111 let _ = items;
1112 vec![]
1113 }
1114 }
1115
1116 #[cfg(target_arch = "wasm32")]
1117 fn parse_download_slot(slot: Value) -> Result<Vec<u8>, String> {
1118 if !slot["ok"].as_bool().unwrap_or(false) {
1119 return Err(slot["error"]
1120 .as_str()
1121 .unwrap_or("unknown error")
1122 .to_string());
1123 }
1124 let b64 = slot["data_b64"]
1125 .as_str()
1126 .ok_or_else(|| "missing data_b64".to_string())?;
1127 use base64::{engine::general_purpose, Engine};
1128 general_purpose::STANDARD
1129 .decode(b64)
1130 .map_err(|e| format!("base64 decode failed: {}", e))
1131 }
1132}
1133
1134pub mod image {
1144 #[allow(unused_imports)]
1145 use super::*;
1146
1147 pub fn transform_jpeg(
1158 bucket: &str,
1159 in_key: &str,
1160 out_key: &str,
1161 max_dim: u32,
1162 quality: u8,
1163 ) -> Option<u32> {
1164 #[cfg(target_arch = "wasm32")]
1165 {
1166 let bucket_bytes = bucket.as_bytes();
1167 let in_key_bytes = in_key.as_bytes();
1168 let out_key_bytes = out_key.as_bytes();
1169 let result = unsafe {
1170 super::image_transform_jpeg(
1171 bucket_bytes.as_ptr() as i32,
1172 bucket_bytes.len() as i32,
1173 in_key_bytes.as_ptr() as i32,
1174 in_key_bytes.len() as i32,
1175 out_key_bytes.as_ptr() as i32,
1176 out_key_bytes.len() as i32,
1177 max_dim as i32,
1178 quality as i32,
1179 )
1180 };
1181 if result < 0 {
1182 return None;
1183 }
1184 Some(result as u32)
1185 }
1186 #[cfg(not(target_arch = "wasm32"))]
1187 {
1188 let _ = (bucket, in_key, out_key, max_dim, quality);
1189 None
1190 }
1191 }
1192}
1193
1194pub mod redis {
1201 #[allow(unused_imports)]
1202 use super::*;
1203
1204 pub fn get(key: &str) -> Option<String> {
1213 #[cfg(target_arch = "wasm32")]
1214 {
1215 let bytes = key.as_bytes();
1216 let result = unsafe { redis_get(bytes.as_ptr() as i32, bytes.len() as i32) };
1217 if result < 0 {
1218 return None;
1219 }
1220 read_redis_response()
1221 }
1222 #[cfg(not(target_arch = "wasm32"))]
1223 {
1224 let _ = key;
1225 None
1226 }
1227 }
1228
1229 pub fn set(key: &str, value: &str, ttl_secs: i32) -> bool {
1237 #[cfg(target_arch = "wasm32")]
1238 {
1239 let key_bytes = key.as_bytes();
1240 let val_bytes = value.as_bytes();
1241 let result = unsafe {
1242 redis_set(
1243 key_bytes.as_ptr() as i32,
1244 key_bytes.len() as i32,
1245 val_bytes.as_ptr() as i32,
1246 val_bytes.len() as i32,
1247 ttl_secs,
1248 )
1249 };
1250 result == 0
1251 }
1252 #[cfg(not(target_arch = "wasm32"))]
1253 {
1254 let _ = (key, value, ttl_secs);
1255 true
1256 }
1257 }
1258
1259 pub fn del(key: &str) -> bool {
1267 #[cfg(target_arch = "wasm32")]
1268 {
1269 let bytes = key.as_bytes();
1270 let result = unsafe { redis_del(bytes.as_ptr() as i32, bytes.len() as i32) };
1271 result == 0
1272 }
1273 #[cfg(not(target_arch = "wasm32"))]
1274 {
1275 let _ = key;
1276 true
1277 }
1278 }
1279
1280 #[cfg(target_arch = "wasm32")]
1282 fn read_redis_response() -> Option<String> {
1283 let len = unsafe { get_host_response_len() };
1284 if len <= 0 {
1285 return None;
1286 }
1287 let mut buf = vec![0u8; len as usize];
1288 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1289 if read <= 0 {
1290 return None;
1291 }
1292 buf.truncate(read as usize);
1293 String::from_utf8(buf).ok()
1294 }
1295}
1296
1297pub mod util {
1301 #[allow(unused_imports)]
1302 use super::*;
1303
1304 pub fn current_time() -> String {
1319 #[cfg(target_arch = "wasm32")]
1320 {
1321 let result = unsafe { super::current_time() };
1322 if result < 0 {
1323 return String::new();
1324 }
1325 let len = unsafe { get_host_response_len() };
1326 if len <= 0 {
1327 return String::new();
1328 }
1329 let mut buf = vec![0u8; len as usize];
1330 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1331 if read <= 0 {
1332 return String::new();
1333 }
1334 buf.truncate(read as usize);
1335 String::from_utf8(buf).unwrap_or_default()
1336 }
1337
1338 #[cfg(not(target_arch = "wasm32"))]
1339 {
1340 let secs = std::time::SystemTime::now()
1341 .duration_since(std::time::UNIX_EPOCH)
1342 .map(|d| d.as_secs())
1343 .unwrap_or(0);
1344 format!("1970-01-01T00:00:00Z+{}", secs)
1345 }
1346 }
1347
1348 pub fn generate_uuid() -> String {
1349 #[cfg(target_arch = "wasm32")]
1350 {
1351 let result = unsafe { super::generate_uuid() };
1352 if result < 0 {
1353 return String::new();
1354 }
1355 let len = unsafe { get_host_response_len() };
1356 if len <= 0 {
1357 return String::new();
1358 }
1359 let mut buf = vec![0u8; len as usize];
1360 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1361 if read <= 0 {
1362 return String::new();
1363 }
1364 buf.truncate(read as usize);
1365 String::from_utf8(buf).unwrap_or_default()
1366 }
1367
1368 #[cfg(not(target_arch = "wasm32"))]
1369 {
1370 format!(
1371 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
1372 std::time::SystemTime::now()
1373 .duration_since(std::time::UNIX_EPOCH)
1374 .map(|d| d.as_nanos() as u32)
1375 .unwrap_or(0),
1376 std::process::id() as u16,
1377 0u16,
1378 0x8000u16,
1379 0u64,
1380 )
1381 }
1382 }
1383}
1384
1385#[doc(hidden)]
1389pub fn __run_handler<F>(ptr: i32, len: i32, f: F) -> i32
1390where
1391 F: FnOnce(Request) -> Response,
1392{
1393 let request_json = unsafe {
1395 let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
1396 String::from_utf8_lossy(slice).into_owned()
1397 };
1398
1399 let request = Request::from_json(&request_json).unwrap_or_else(|| Request {
1401 method: "GET".to_string(),
1402 handler: String::new(),
1403 headers: HashMap::new(),
1404 body: Value::Null,
1405 raw_body: Vec::new(),
1406 tenant: String::new(),
1407 service: String::new(),
1408 auth: None,
1409 job: None,
1410 });
1411
1412 let response = f(request);
1414 let response_bytes = response.into_data().into_bytes();
1415
1416 let total = 4 + response_bytes.len();
1418 let layout = std::alloc::Layout::from_size_align(total, 1).expect("invalid layout");
1419 let out_ptr = unsafe { std::alloc::alloc(layout) };
1420
1421 unsafe {
1422 let len_bytes = (response_bytes.len() as u32).to_le_bytes();
1423 std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
1424 std::ptr::copy_nonoverlapping(
1425 response_bytes.as_ptr(),
1426 out_ptr.add(4),
1427 response_bytes.len(),
1428 );
1429 }
1430
1431 out_ptr as i32
1432}
1433
1434#[macro_export]
1447macro_rules! init {
1448 () => {
1449 #[no_mangle]
1450 pub extern "C" fn alloc(size: i32) -> i32 {
1451 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
1452 unsafe { std::alloc::alloc(layout) as i32 }
1453 }
1454 };
1455}
1456
1457#[macro_export]
1484macro_rules! handler {
1485 ($name:ident, |$req:ident : Request| $body:expr) => {
1486 #[no_mangle]
1487 pub extern "C" fn $name(ptr: i32, len: i32) -> i32 {
1488 $crate::__run_handler(ptr, len, |$req: $crate::Request| $body)
1489 }
1490 };
1491}
1492
1493pub mod migrate {
1532 use super::{Request, Response};
1533 pub use cufflink_types::SchemaDiff;
1534
1535 pub fn run<F>(req: Request, handler: F) -> Response
1542 where
1543 F: FnOnce(SchemaDiff) -> Result<(), String>,
1544 {
1545 match serde_json::from_value::<SchemaDiff>(req.body().clone()) {
1546 Ok(diff) => match handler(diff) {
1547 Ok(()) => Response::json(&serde_json::json!({"ok": true})),
1548 Err(e) => Response::error(&e),
1549 },
1550 Err(e) => Response::error(&format!(
1551 "on_migrate: failed to parse SchemaDiff payload: {}",
1552 e
1553 )),
1554 }
1555 }
1556}
1557
1558pub mod prelude {
1566 pub use crate::config;
1567 pub use crate::db;
1568 pub use crate::http;
1569 pub use crate::image;
1570 pub use crate::log;
1571 pub use crate::migrate;
1572 pub use crate::nats;
1573 pub use crate::redis;
1574 pub use crate::storage;
1575 pub use crate::util;
1576 pub use crate::Auth;
1577 pub use crate::JobContext;
1578 pub use crate::Request;
1579 pub use crate::Response;
1580 pub use serde_json::{json, Value};
1581}
1582
1583#[cfg(test)]
1586mod tests {
1587 use super::*;
1588 use serde_json::json;
1589
1590 #[test]
1591 fn test_request_parsing() {
1592 let json = serde_json::to_string(&json!({
1593 "method": "POST",
1594 "handler": "checkout",
1595 "headers": {"content-type": "application/json"},
1596 "body": {"item": "widget", "qty": 3},
1597 "tenant": "acme",
1598 "service": "shop"
1599 }))
1600 .unwrap();
1601
1602 let req = Request::from_json(&json).unwrap();
1603 assert_eq!(req.method(), "POST");
1604 assert_eq!(req.handler(), "checkout");
1605 assert_eq!(req.tenant(), "acme");
1606 assert_eq!(req.service(), "shop");
1607 assert_eq!(req.body()["item"], "widget");
1608 assert_eq!(req.body()["qty"], 3);
1609 assert_eq!(req.header("content-type"), Some("application/json"));
1610 }
1611
1612 #[test]
1613 fn test_request_missing_fields() {
1614 let json = r#"{"method": "GET"}"#;
1615 let req = Request::from_json(json).unwrap();
1616 assert_eq!(req.method(), "GET");
1617 assert_eq!(req.handler(), "");
1618 assert_eq!(req.tenant(), "");
1619 assert_eq!(req.body(), &Value::Null);
1620 assert!(req.raw_body().is_empty());
1621 }
1622
1623 #[test]
1624 fn test_request_raw_body_round_trip() {
1625 use base64::{engine::general_purpose, Engine};
1626 let raw = br#"{"type":"message.delivered","data":{"object":{"id":"AC1"}}}"#;
1627 let json = serde_json::to_string(&json!({
1628 "method": "POST",
1629 "handler": "webhook",
1630 "headers": {"content-type": "application/json"},
1631 "body": serde_json::from_slice::<Value>(raw).unwrap(),
1632 "body_raw_b64": general_purpose::STANDARD.encode(raw),
1633 "tenant": "acme",
1634 "service": "shop",
1635 }))
1636 .unwrap();
1637 let req = Request::from_json(&json).unwrap();
1638 assert_eq!(req.raw_body(), raw);
1639 }
1640
1641 #[test]
1642 fn test_request_raw_body_invalid_base64_yields_empty() {
1643 let json = serde_json::to_string(&json!({
1644 "method": "POST",
1645 "handler": "webhook",
1646 "body": Value::Null,
1647 "body_raw_b64": "not%base64!",
1648 "tenant": "acme",
1649 "service": "shop",
1650 }))
1651 .unwrap();
1652 let req = Request::from_json(&json).unwrap();
1653 assert!(req.raw_body().is_empty());
1654 }
1655
1656 #[test]
1657 fn test_request_job_context_parsed_when_present() {
1658 let json = serde_json::to_string(&json!({
1659 "method": "POST",
1660 "handler": "handle_run_ai",
1661 "body": {"id": "item-1"},
1662 "tenant": "acme",
1663 "service": "asset",
1664 "_job": {
1665 "id": "11111111-1111-1111-1111-111111111111",
1666 "attempt": 2,
1667 "max_attempts": 3,
1668 },
1669 }))
1670 .unwrap();
1671 let req = Request::from_json(&json).unwrap();
1672 let job = req.job().expect("job context should be parsed");
1673 assert_eq!(job.id, "11111111-1111-1111-1111-111111111111");
1674 assert_eq!(job.attempt, 2);
1675 assert_eq!(job.max_attempts, 3);
1676 assert!(job.is_retry());
1677 }
1678
1679 #[test]
1680 fn test_request_job_context_absent_on_http_direct() {
1681 let json = serde_json::to_string(&json!({
1682 "method": "GET",
1683 "handler": "list",
1684 "body": Value::Null,
1685 "tenant": "acme",
1686 "service": "asset",
1687 }))
1688 .unwrap();
1689 let req = Request::from_json(&json).unwrap();
1690 assert!(req.job().is_none());
1691 }
1692
1693 #[test]
1694 fn test_request_job_context_first_attempt_is_not_retry() {
1695 let json = serde_json::to_string(&json!({
1696 "method": "POST",
1697 "handler": "handle_run_ai",
1698 "body": Value::Null,
1699 "tenant": "acme",
1700 "service": "asset",
1701 "_job": {
1702 "id": "22222222-2222-2222-2222-222222222222",
1703 "attempt": 1,
1704 "max_attempts": 2,
1705 },
1706 }))
1707 .unwrap();
1708 let req = Request::from_json(&json).unwrap();
1709 let job = req.job().expect("job context should be parsed");
1710 assert_eq!(job.attempt, 1);
1711 assert!(!job.is_retry());
1712 }
1713
1714 #[test]
1715 fn test_request_job_context_rejects_malformed_envelope() {
1716 let json = serde_json::to_string(&json!({
1717 "method": "POST",
1718 "handler": "handle_run_ai",
1719 "body": Value::Null,
1720 "tenant": "acme",
1721 "service": "asset",
1722 "_job": {
1723 "id": "33333333-3333-3333-3333-333333333333",
1724 "max_attempts": 2,
1725 },
1726 }))
1727 .unwrap();
1728 let req = Request::from_json(&json).unwrap();
1729 assert!(
1730 req.job().is_none(),
1731 "missing attempt should yield None, not a partial JobContext"
1732 );
1733 }
1734
1735 #[test]
1736 fn test_response_json() {
1737 let resp = Response::json(&json!({"status": "ok", "count": 42}));
1738 let data = resp.into_data();
1739 let parsed: Value = serde_json::from_str(&data).unwrap();
1740 assert_eq!(parsed["status"], "ok");
1741 assert_eq!(parsed["count"], 42);
1742 }
1743
1744 #[test]
1745 fn test_response_error() {
1746 let resp = Response::error("something went wrong");
1747 let data = resp.into_data();
1748 let parsed: Value = serde_json::from_str(&data).unwrap();
1749 assert_eq!(parsed["__status"], 400);
1751 assert_eq!(parsed["__body"]["error"], "something went wrong");
1752 }
1753
1754 #[test]
1755 fn test_response_not_found() {
1756 let resp = Response::not_found("item not found");
1757 let data = resp.into_data();
1758 let parsed: Value = serde_json::from_str(&data).unwrap();
1759 assert_eq!(parsed["__status"], 404);
1760 assert_eq!(parsed["__body"]["error"], "item not found");
1761 }
1762
1763 #[test]
1764 fn test_response_with_status() {
1765 let resp = Response::json(&serde_json::json!({"ok": true})).with_status(201);
1766 let data = resp.into_data();
1767 let parsed: Value = serde_json::from_str(&data).unwrap();
1768 assert_eq!(parsed["__status"], 201);
1769 assert_eq!(parsed["__body"]["ok"], true);
1770 }
1771
1772 fn migrate_request(diff: serde_json::Value) -> Request {
1773 let payload = serde_json::to_string(&json!({
1774 "method": "POST",
1775 "handler": "handle_on_migrate",
1776 "headers": {},
1777 "body": diff,
1778 "tenant": "default",
1779 "service": "logistics-service",
1780 }))
1781 .unwrap();
1782 Request::from_json(&payload).unwrap()
1783 }
1784
1785 #[test]
1786 fn test_migrate_run_success() {
1787 let req = migrate_request(json!({
1788 "added_columns": [["pickups", "min"]],
1789 "dropped_columns": [["pickups", "midpoint"]],
1790 }));
1791 let resp = migrate::run(req, |diff| {
1792 assert!(diff.added_column("pickups", "min"));
1793 assert!(diff.dropped_column("pickups", "midpoint"));
1794 Ok(())
1795 });
1796 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1797 assert_eq!(parsed["ok"], true);
1798 }
1799
1800 #[test]
1801 fn test_migrate_run_handler_error() {
1802 let req = migrate_request(json!({}));
1803 let resp = migrate::run(req, |_| Err("backfill failed".into()));
1804 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1805 assert_eq!(parsed["__status"], 400);
1806 assert_eq!(parsed["__body"]["error"], "backfill failed");
1807 }
1808
1809 #[test]
1810 fn test_migrate_run_invalid_payload() {
1811 let req = migrate_request(json!("not a diff"));
1813 let resp = migrate::run(req, |_| {
1814 panic!("closure should not be called for invalid payload")
1815 });
1816 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1817 assert_eq!(parsed["__status"], 400);
1818 assert!(parsed["__body"]["error"]
1819 .as_str()
1820 .unwrap()
1821 .contains("on_migrate: failed to parse SchemaDiff payload"));
1822 }
1823
1824 #[test]
1825 fn test_migrate_run_empty_diff() {
1826 let req = migrate_request(json!({}));
1827 let mut called = false;
1828 let resp = migrate::run(req, |diff| {
1829 assert!(diff.is_empty());
1830 called = true;
1831 Ok(())
1832 });
1833 assert!(called);
1834 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1835 assert_eq!(parsed["ok"], true);
1836 }
1837
1838 #[test]
1839 fn test_response_200_no_wrapper() {
1840 let resp = Response::json(&serde_json::json!({"data": "test"}));
1841 let data = resp.into_data();
1842 let parsed: Value = serde_json::from_str(&data).unwrap();
1843 assert_eq!(parsed["data"], "test");
1845 assert!(parsed.get("__status").is_none());
1846 }
1847
1848 #[test]
1849 fn test_response_empty() {
1850 let resp = Response::empty();
1851 let data = resp.into_data();
1852 let parsed: Value = serde_json::from_str(&data).unwrap();
1853 assert_eq!(parsed["ok"], true);
1854 }
1855
1856 #[test]
1857 fn test_response_text() {
1858 let resp = Response::text("hello world");
1859 let data = resp.into_data();
1860 let parsed: Value = serde_json::from_str(&data).unwrap();
1861 assert_eq!(parsed, "hello world");
1862 }
1863
1864 #[test]
1865 fn test_db_query_noop_on_native() {
1866 let rows = db::query("SELECT 1");
1868 assert!(rows.is_empty());
1869 }
1870
1871 #[test]
1872 fn test_db_query_one_noop_on_native() {
1873 let row = db::query_one("SELECT 1");
1874 assert!(row.is_none());
1875 }
1876
1877 #[test]
1878 fn test_db_execute_noop_on_native() {
1879 let affected = db::execute("INSERT INTO x VALUES (1)");
1880 assert_eq!(affected, 0);
1881 }
1882
1883 #[test]
1884 fn test_nats_publish_noop_on_native() {
1885 let ok = nats::publish("test.subject", "payload");
1886 assert!(ok);
1887 }
1888
1889 #[test]
1890 fn test_request_with_auth() {
1891 let json = serde_json::to_string(&json!({
1892 "method": "POST",
1893 "handler": "checkout",
1894 "headers": {},
1895 "body": {},
1896 "tenant": "acme",
1897 "service": "shop",
1898 "auth": {
1899 "sub": "user-123",
1900 "preferred_username": "john",
1901 "name": "John Doe",
1902 "email": "john@example.com",
1903 "realm_roles": ["admin", "manager"],
1904 "claims": {"department": "engineering"}
1905 }
1906 }))
1907 .unwrap();
1908
1909 let req = Request::from_json(&json).unwrap();
1910 let auth = req.auth().unwrap();
1911 assert_eq!(auth.sub, "user-123");
1912 assert_eq!(auth.preferred_username.as_deref(), Some("john"));
1913 assert_eq!(auth.name.as_deref(), Some("John Doe"));
1914 assert_eq!(auth.email.as_deref(), Some("john@example.com"));
1915 assert!(auth.has_role("admin"));
1916 assert!(auth.has_role("manager"));
1917 assert!(!auth.has_role("viewer"));
1918 assert_eq!(
1919 auth.claim("department").and_then(|v| v.as_str()),
1920 Some("engineering")
1921 );
1922 }
1923
1924 #[test]
1925 fn test_request_without_auth() {
1926 let json = r#"{"method": "GET"}"#;
1927 let req = Request::from_json(json).unwrap();
1928 assert!(req.auth().is_none());
1929 }
1930
1931 #[test]
1932 fn test_request_null_auth() {
1933 let json = serde_json::to_string(&json!({
1934 "method": "GET",
1935 "auth": null
1936 }))
1937 .unwrap();
1938 let req = Request::from_json(&json).unwrap();
1939 assert!(req.auth().is_none());
1940 }
1941
1942 #[test]
1943 fn test_require_auth_success() {
1944 let json = serde_json::to_string(&json!({
1945 "method": "GET",
1946 "auth": {"sub": "user-1", "realm_roles": [], "claims": {}}
1947 }))
1948 .unwrap();
1949 let req = Request::from_json(&json).unwrap();
1950 assert!(req.require_auth().is_ok());
1951 assert_eq!(req.require_auth().unwrap().sub, "user-1");
1952 }
1953
1954 #[test]
1955 fn test_require_auth_fails_when_unauthenticated() {
1956 let json = r#"{"method": "GET"}"#;
1957 let req = Request::from_json(json).unwrap();
1958 assert!(req.require_auth().is_err());
1959 }
1960
1961 #[test]
1962 fn test_http_fetch_noop_on_native() {
1963 let resp = http::fetch("GET", "https://example.com", &[], None);
1964 assert!(resp.is_none());
1965 }
1966
1967 #[test]
1968 fn test_http_get_noop_on_native() {
1969 let resp = http::get("https://example.com", &[]);
1970 assert!(resp.is_none());
1971 }
1972
1973 #[test]
1974 fn test_http_post_noop_on_native() {
1975 let resp = http::post("https://example.com", &[], "{}");
1976 assert!(resp.is_none());
1977 }
1978
1979 #[test]
1980 fn test_storage_download_noop_on_native() {
1981 let data = storage::download("my-bucket", "images/photo.jpg");
1982 assert!(data.is_none());
1983 }
1984
1985 #[test]
1986 fn test_image_transform_jpeg_noop_on_native() {
1987 let bytes = image::transform_jpeg("my-bucket", "in.jpg", "out.jpg", 1024, 80);
1988 assert!(bytes.is_none());
1989 }
1990
1991 #[test]
1992 fn test_auth_permissions() {
1993 let json = serde_json::to_string(&json!({
1994 "method": "POST",
1995 "handler": "test",
1996 "headers": {},
1997 "body": {},
1998 "tenant": "acme",
1999 "service": "shop",
2000 "auth": {
2001 "sub": "user-1",
2002 "realm_roles": ["admin"],
2003 "claims": {},
2004 "permissions": ["staff:create", "staff:view", "items:*"],
2005 "role_names": ["admin", "manager"]
2006 }
2007 }))
2008 .unwrap();
2009
2010 let req = Request::from_json(&json).unwrap();
2011 let auth = req.auth().unwrap();
2012
2013 assert!(auth.can("staff", "create"));
2015 assert!(auth.can("staff", "view"));
2016 assert!(!auth.can("staff", "delete"));
2017
2018 assert!(auth.can("items", "create"));
2020 assert!(auth.can("items", "view"));
2021 assert!(auth.can("items", "delete"));
2022
2023 assert!(!auth.can("batches", "view"));
2025
2026 assert!(auth.has_cufflink_role("admin"));
2028 assert!(auth.has_cufflink_role("manager"));
2029 assert!(!auth.has_cufflink_role("viewer"));
2030 }
2031
2032 #[test]
2033 fn test_auth_super_wildcard() {
2034 let auth = Auth {
2035 sub: "user-1".to_string(),
2036 preferred_username: None,
2037 name: None,
2038 email: None,
2039 realm_roles: vec![],
2040 claims: HashMap::new(),
2041 permissions: vec!["*".to_string()],
2042 role_names: vec!["superadmin".to_string()],
2043 is_service_account: false,
2044 };
2045
2046 assert!(auth.can("anything", "everything"));
2047 assert!(auth.can("staff", "create"));
2048 }
2049
2050 #[test]
2051 fn test_auth_empty_permissions() {
2052 let auth = Auth {
2053 sub: "user-1".to_string(),
2054 preferred_username: None,
2055 name: None,
2056 email: None,
2057 realm_roles: vec![],
2058 claims: HashMap::new(),
2059 permissions: vec![],
2060 role_names: vec![],
2061 is_service_account: false,
2062 };
2063
2064 assert!(!auth.can("staff", "create"));
2065 assert!(!auth.has_cufflink_role("admin"));
2066 }
2067
2068 #[test]
2069 fn test_redis_get_noop_on_native() {
2070 let val = redis::get("some-key");
2071 assert!(val.is_none());
2072 }
2073
2074 #[test]
2075 fn test_redis_set_noop_on_native() {
2076 let ok = redis::set("key", "value", 3600);
2077 assert!(ok);
2078 }
2079
2080 #[test]
2081 fn test_redis_del_noop_on_native() {
2082 let ok = redis::del("key");
2083 assert!(ok);
2084 }
2085
2086 #[test]
2087 fn test_http_fetch_response_helpers() {
2088 let resp = http::FetchResponse {
2089 status: 200,
2090 body: r#"{"key": "value"}"#.to_string(),
2091 body_encoding: "utf8".to_string(),
2092 headers: HashMap::new(),
2093 };
2094 assert!(resp.is_success());
2095 assert!(!resp.is_base64());
2096 let json = resp.json().unwrap();
2097 assert_eq!(json["key"], "value");
2098
2099 let err_resp = http::FetchResponse {
2100 status: 404,
2101 body: "not found".to_string(),
2102 body_encoding: "utf8".to_string(),
2103 headers: HashMap::new(),
2104 };
2105 assert!(!err_resp.is_success());
2106
2107 let binary_resp = http::FetchResponse {
2108 status: 200,
2109 body: "aW1hZ2VkYXRh".to_string(),
2110 body_encoding: "base64".to_string(),
2111 headers: HashMap::new(),
2112 };
2113 assert!(binary_resp.is_base64());
2114 }
2115}