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 http_fetch(
47 method_ptr: i32,
48 method_len: i32,
49 url_ptr: i32,
50 url_len: i32,
51 headers_ptr: i32,
52 headers_len: i32,
53 body_ptr: i32,
54 body_len: i32,
55 ) -> i32;
56 fn get_config(key_ptr: i32, key_len: i32) -> i32;
57}
58
59#[derive(Debug, Clone)]
80pub struct Auth {
81 pub sub: String,
83 pub preferred_username: Option<String>,
85 pub name: Option<String>,
87 pub email: Option<String>,
89 pub realm_roles: Vec<String>,
91 pub claims: HashMap<String, Value>,
93}
94
95impl Auth {
96 pub fn has_role(&self, role: &str) -> bool {
98 self.realm_roles.iter().any(|r| r == role)
99 }
100
101 pub fn claim(&self, key: &str) -> Option<&Value> {
103 self.claims.get(key)
104 }
105}
106
107#[derive(Debug, Clone)]
114pub struct Request {
115 method: String,
116 handler: String,
117 headers: HashMap<String, String>,
118 body: Value,
119 tenant: String,
120 service: String,
121 auth: Option<Auth>,
122}
123
124impl Request {
125 pub fn from_json(json: &str) -> Option<Self> {
127 let v: Value = serde_json::from_str(json).ok()?;
128 let headers = v["headers"]
129 .as_object()
130 .map(|m| {
131 m.iter()
132 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
133 .collect()
134 })
135 .unwrap_or_default();
136
137 let auth = v["auth"].as_object().map(|auth_obj| {
138 let a = Value::Object(auth_obj.clone());
139 Auth {
140 sub: a["sub"].as_str().unwrap_or("").to_string(),
141 preferred_username: a["preferred_username"].as_str().map(|s| s.to_string()),
142 name: a["name"].as_str().map(|s| s.to_string()),
143 email: a["email"].as_str().map(|s| s.to_string()),
144 realm_roles: a["realm_roles"]
145 .as_array()
146 .map(|arr| {
147 arr.iter()
148 .filter_map(|v| v.as_str().map(|s| s.to_string()))
149 .collect()
150 })
151 .unwrap_or_default(),
152 claims: a["claims"]
153 .as_object()
154 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
155 .unwrap_or_default(),
156 }
157 });
158
159 Some(Self {
160 method: v["method"].as_str().unwrap_or("GET").to_string(),
161 handler: v["handler"].as_str().unwrap_or("").to_string(),
162 headers,
163 body: v["body"].clone(),
164 tenant: v["tenant"].as_str().unwrap_or("").to_string(),
165 service: v["service"].as_str().unwrap_or("").to_string(),
166 auth,
167 })
168 }
169
170 pub fn method(&self) -> &str {
172 &self.method
173 }
174
175 pub fn handler(&self) -> &str {
177 &self.handler
178 }
179
180 pub fn headers(&self) -> &HashMap<String, String> {
182 &self.headers
183 }
184
185 pub fn header(&self, name: &str) -> Option<&str> {
187 self.headers.get(name).map(|s| s.as_str())
188 }
189
190 pub fn body(&self) -> &Value {
192 &self.body
193 }
194
195 pub fn tenant(&self) -> &str {
197 &self.tenant
198 }
199
200 pub fn service(&self) -> &str {
202 &self.service
203 }
204
205 pub fn auth(&self) -> Option<&Auth> {
209 self.auth.as_ref()
210 }
211
212 pub fn require_auth(&self) -> Result<&Auth, Response> {
224 self.auth.as_ref().ok_or_else(|| {
225 Response::json(&serde_json::json!({
226 "error": "Authentication required",
227 "status": 401
228 }))
229 })
230 }
231}
232
233#[derive(Debug, Clone)]
237pub struct Response {
238 data: String,
239}
240
241impl Response {
242 pub fn json(value: &Value) -> Self {
244 Self {
245 data: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
246 }
247 }
248
249 pub fn text(s: &str) -> Self {
251 Self::json(&Value::String(s.to_string()))
252 }
253
254 pub fn error(message: &str) -> Self {
256 Self::json(&serde_json::json!({"error": message}))
257 }
258
259 pub fn empty() -> Self {
261 Self::json(&serde_json::json!({"ok": true}))
262 }
263
264 pub fn into_data(self) -> String {
266 self.data
267 }
268}
269
270pub mod db {
277 use super::*;
278
279 pub fn query(sql: &str) -> Vec<Value> {
290 #[cfg(target_arch = "wasm32")]
291 {
292 let bytes = sql.as_bytes();
293 let result = unsafe { db_query(bytes.as_ptr() as i32, bytes.len() as i32) };
294 if result < 0 {
295 return vec![];
296 }
297 read_host_response()
298 }
299 #[cfg(not(target_arch = "wasm32"))]
300 {
301 let _ = sql;
302 vec![]
303 }
304 }
305
306 pub fn query_one(sql: &str) -> Option<Value> {
314 query(sql).into_iter().next()
315 }
316
317 pub fn execute(sql: &str) -> i32 {
326 #[cfg(target_arch = "wasm32")]
327 {
328 let bytes = sql.as_bytes();
329 unsafe { db_execute(bytes.as_ptr() as i32, bytes.len() as i32) }
330 }
331 #[cfg(not(target_arch = "wasm32"))]
332 {
333 let _ = sql;
334 0
335 }
336 }
337
338 #[cfg(target_arch = "wasm32")]
340 fn read_host_response() -> Vec<Value> {
341 let len = unsafe { get_host_response_len() };
342 if len <= 0 {
343 return vec![];
344 }
345 let mut buf = vec![0u8; len as usize];
346 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
347 if read <= 0 {
348 return vec![];
349 }
350 buf.truncate(read as usize);
351 let json_str = String::from_utf8_lossy(&buf);
352 serde_json::from_str(&json_str).unwrap_or_default()
353 }
354}
355
356pub mod nats {
363 #[allow(unused_imports)]
364 use super::*;
365
366 pub fn publish(subject: &str, payload: &str) -> bool {
377 #[cfg(target_arch = "wasm32")]
378 {
379 let subj_bytes = subject.as_bytes();
380 let payload_bytes = payload.as_bytes();
381 let result = unsafe {
382 nats_publish(
383 subj_bytes.as_ptr() as i32,
384 subj_bytes.len() as i32,
385 payload_bytes.as_ptr() as i32,
386 payload_bytes.len() as i32,
387 )
388 };
389 result == 0
390 }
391 #[cfg(not(target_arch = "wasm32"))]
392 {
393 let _ = (subject, payload);
394 true
395 }
396 }
397}
398
399pub mod log {
405 #[allow(unused_imports)]
406 use super::*;
407
408 pub fn error(msg: &str) {
410 write(0, msg);
411 }
412
413 pub fn warn(msg: &str) {
415 write(1, msg);
416 }
417
418 pub fn info(msg: &str) {
420 write(2, msg);
421 }
422
423 pub fn debug(msg: &str) {
425 write(3, msg);
426 }
427
428 fn write(level: i32, msg: &str) {
429 #[cfg(target_arch = "wasm32")]
430 {
431 let bytes = msg.as_bytes();
432 unsafe {
433 super::cufflink_log(level, bytes.as_ptr() as i32, bytes.len() as i32);
434 }
435 }
436 #[cfg(not(target_arch = "wasm32"))]
437 {
438 let _ = (level, msg);
439 }
440 }
441}
442
443pub mod http {
450 #[allow(unused_imports)]
451 use super::*;
452
453 #[derive(Debug, Clone)]
455 pub struct FetchResponse {
456 pub status: i32,
458 pub body: String,
460 pub body_encoding: String,
462 pub headers: HashMap<String, String>,
464 }
465
466 impl FetchResponse {
467 pub fn json(&self) -> Option<Value> {
469 serde_json::from_str(&self.body).ok()
470 }
471
472 pub fn is_success(&self) -> bool {
474 (200..300).contains(&self.status)
475 }
476
477 pub fn is_base64(&self) -> bool {
479 self.body_encoding == "base64"
480 }
481 }
482
483 pub fn fetch(
494 method: &str,
495 url: &str,
496 headers: &[(&str, &str)],
497 body: Option<&str>,
498 ) -> Option<FetchResponse> {
499 #[cfg(target_arch = "wasm32")]
500 {
501 let method_bytes = method.as_bytes();
502 let url_bytes = url.as_bytes();
503 let headers_map: HashMap<&str, &str> = headers.iter().copied().collect();
504 let headers_json = serde_json::to_string(&headers_map).unwrap_or_default();
505 let headers_bytes = headers_json.as_bytes();
506 let body_bytes = body.unwrap_or("").as_bytes();
507 let body_len = body.map(|b| b.len()).unwrap_or(0);
508
509 let result = unsafe {
510 http_fetch(
511 method_bytes.as_ptr() as i32,
512 method_bytes.len() as i32,
513 url_bytes.as_ptr() as i32,
514 url_bytes.len() as i32,
515 headers_bytes.as_ptr() as i32,
516 headers_bytes.len() as i32,
517 body_bytes.as_ptr() as i32,
518 body_len as i32,
519 )
520 };
521
522 if result < 0 {
523 return None;
524 }
525
526 read_fetch_response()
527 }
528 #[cfg(not(target_arch = "wasm32"))]
529 {
530 let _ = (method, url, headers, body);
531 None
532 }
533 }
534
535 pub fn get(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
537 fetch("GET", url, headers, None)
538 }
539
540 pub fn post(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
542 fetch("POST", url, headers, Some(body))
543 }
544
545 pub fn put(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
547 fetch("PUT", url, headers, Some(body))
548 }
549
550 pub fn delete(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
552 fetch("DELETE", url, headers, None)
553 }
554
555 #[cfg(target_arch = "wasm32")]
557 fn read_fetch_response() -> Option<FetchResponse> {
558 let len = unsafe { get_host_response_len() };
559 if len <= 0 {
560 return None;
561 }
562 let mut buf = vec![0u8; len as usize];
563 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
564 if read <= 0 {
565 return None;
566 }
567 buf.truncate(read as usize);
568 let json_str = String::from_utf8_lossy(&buf);
569 let v: Value = serde_json::from_str(&json_str).ok()?;
570 Some(FetchResponse {
571 status: v["status"].as_i64().unwrap_or(0) as i32,
572 body: v["body"].as_str().unwrap_or("").to_string(),
573 body_encoding: v["body_encoding"].as_str().unwrap_or("utf8").to_string(),
574 headers: v["headers"]
575 .as_object()
576 .map(|m| {
577 m.iter()
578 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
579 .collect()
580 })
581 .unwrap_or_default(),
582 })
583 }
584}
585
586pub mod config {
594 #[allow(unused_imports)]
595 use super::*;
596
597 pub fn get(key: &str) -> Option<String> {
606 #[cfg(target_arch = "wasm32")]
607 {
608 let bytes = key.as_bytes();
609 let result = unsafe { get_config(bytes.as_ptr() as i32, bytes.len() as i32) };
610 if result < 0 {
611 return None;
612 }
613 read_config_response()
614 }
615 #[cfg(not(target_arch = "wasm32"))]
616 {
617 let _ = key;
618 None
619 }
620 }
621
622 #[cfg(target_arch = "wasm32")]
624 fn read_config_response() -> Option<String> {
625 let len = unsafe { get_host_response_len() };
626 if len <= 0 {
627 return None;
628 }
629 let mut buf = vec![0u8; len as usize];
630 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
631 if read <= 0 {
632 return None;
633 }
634 buf.truncate(read as usize);
635 String::from_utf8(buf).ok()
636 }
637}
638
639#[doc(hidden)]
643pub fn __run_handler<F>(ptr: i32, len: i32, f: F) -> i32
644where
645 F: FnOnce(Request) -> Response,
646{
647 let request_json = unsafe {
649 let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
650 String::from_utf8_lossy(slice).into_owned()
651 };
652
653 let request = Request::from_json(&request_json).unwrap_or_else(|| Request {
655 method: "GET".to_string(),
656 handler: String::new(),
657 headers: HashMap::new(),
658 body: Value::Null,
659 tenant: String::new(),
660 service: String::new(),
661 auth: None,
662 });
663
664 let response = f(request);
666 let response_bytes = response.into_data().into_bytes();
667
668 let total = 4 + response_bytes.len();
670 let layout = std::alloc::Layout::from_size_align(total, 1).expect("invalid layout");
671 let out_ptr = unsafe { std::alloc::alloc(layout) };
672
673 unsafe {
674 let len_bytes = (response_bytes.len() as u32).to_le_bytes();
675 std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
676 std::ptr::copy_nonoverlapping(
677 response_bytes.as_ptr(),
678 out_ptr.add(4),
679 response_bytes.len(),
680 );
681 }
682
683 out_ptr as i32
684}
685
686#[macro_export]
699macro_rules! init {
700 () => {
701 #[no_mangle]
702 pub extern "C" fn alloc(size: i32) -> i32 {
703 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
704 unsafe { std::alloc::alloc(layout) as i32 }
705 }
706 };
707}
708
709#[macro_export]
736macro_rules! handler {
737 ($name:ident, |$req:ident : Request| $body:expr) => {
738 #[no_mangle]
739 pub extern "C" fn $name(ptr: i32, len: i32) -> i32 {
740 $crate::__run_handler(ptr, len, |$req: $crate::Request| $body)
741 }
742 };
743}
744
745pub mod prelude {
753 pub use crate::config;
754 pub use crate::db;
755 pub use crate::http;
756 pub use crate::log;
757 pub use crate::nats;
758 pub use crate::Auth;
759 pub use crate::Request;
760 pub use crate::Response;
761 pub use serde_json::{json, Value};
762}
763
764#[cfg(test)]
767mod tests {
768 use super::*;
769 use serde_json::json;
770
771 #[test]
772 fn test_request_parsing() {
773 let json = serde_json::to_string(&json!({
774 "method": "POST",
775 "handler": "checkout",
776 "headers": {"content-type": "application/json"},
777 "body": {"item": "widget", "qty": 3},
778 "tenant": "acme",
779 "service": "shop"
780 }))
781 .unwrap();
782
783 let req = Request::from_json(&json).unwrap();
784 assert_eq!(req.method(), "POST");
785 assert_eq!(req.handler(), "checkout");
786 assert_eq!(req.tenant(), "acme");
787 assert_eq!(req.service(), "shop");
788 assert_eq!(req.body()["item"], "widget");
789 assert_eq!(req.body()["qty"], 3);
790 assert_eq!(req.header("content-type"), Some("application/json"));
791 }
792
793 #[test]
794 fn test_request_missing_fields() {
795 let json = r#"{"method": "GET"}"#;
796 let req = Request::from_json(json).unwrap();
797 assert_eq!(req.method(), "GET");
798 assert_eq!(req.handler(), "");
799 assert_eq!(req.tenant(), "");
800 assert_eq!(req.body(), &Value::Null);
801 }
802
803 #[test]
804 fn test_response_json() {
805 let resp = Response::json(&json!({"status": "ok", "count": 42}));
806 let data = resp.into_data();
807 let parsed: Value = serde_json::from_str(&data).unwrap();
808 assert_eq!(parsed["status"], "ok");
809 assert_eq!(parsed["count"], 42);
810 }
811
812 #[test]
813 fn test_response_error() {
814 let resp = Response::error("something went wrong");
815 let data = resp.into_data();
816 let parsed: Value = serde_json::from_str(&data).unwrap();
817 assert_eq!(parsed["error"], "something went wrong");
818 }
819
820 #[test]
821 fn test_response_empty() {
822 let resp = Response::empty();
823 let data = resp.into_data();
824 let parsed: Value = serde_json::from_str(&data).unwrap();
825 assert_eq!(parsed["ok"], true);
826 }
827
828 #[test]
829 fn test_response_text() {
830 let resp = Response::text("hello world");
831 let data = resp.into_data();
832 let parsed: Value = serde_json::from_str(&data).unwrap();
833 assert_eq!(parsed, "hello world");
834 }
835
836 #[test]
837 fn test_db_query_noop_on_native() {
838 let rows = db::query("SELECT 1");
840 assert!(rows.is_empty());
841 }
842
843 #[test]
844 fn test_db_query_one_noop_on_native() {
845 let row = db::query_one("SELECT 1");
846 assert!(row.is_none());
847 }
848
849 #[test]
850 fn test_db_execute_noop_on_native() {
851 let affected = db::execute("INSERT INTO x VALUES (1)");
852 assert_eq!(affected, 0);
853 }
854
855 #[test]
856 fn test_nats_publish_noop_on_native() {
857 let ok = nats::publish("test.subject", "payload");
858 assert!(ok);
859 }
860
861 #[test]
862 fn test_request_with_auth() {
863 let json = serde_json::to_string(&json!({
864 "method": "POST",
865 "handler": "checkout",
866 "headers": {},
867 "body": {},
868 "tenant": "acme",
869 "service": "shop",
870 "auth": {
871 "sub": "user-123",
872 "preferred_username": "john",
873 "name": "John Doe",
874 "email": "john@example.com",
875 "realm_roles": ["admin", "manager"],
876 "claims": {"department": "engineering"}
877 }
878 }))
879 .unwrap();
880
881 let req = Request::from_json(&json).unwrap();
882 let auth = req.auth().unwrap();
883 assert_eq!(auth.sub, "user-123");
884 assert_eq!(auth.preferred_username.as_deref(), Some("john"));
885 assert_eq!(auth.name.as_deref(), Some("John Doe"));
886 assert_eq!(auth.email.as_deref(), Some("john@example.com"));
887 assert!(auth.has_role("admin"));
888 assert!(auth.has_role("manager"));
889 assert!(!auth.has_role("viewer"));
890 assert_eq!(
891 auth.claim("department").and_then(|v| v.as_str()),
892 Some("engineering")
893 );
894 }
895
896 #[test]
897 fn test_request_without_auth() {
898 let json = r#"{"method": "GET"}"#;
899 let req = Request::from_json(json).unwrap();
900 assert!(req.auth().is_none());
901 }
902
903 #[test]
904 fn test_request_null_auth() {
905 let json = serde_json::to_string(&json!({
906 "method": "GET",
907 "auth": null
908 }))
909 .unwrap();
910 let req = Request::from_json(&json).unwrap();
911 assert!(req.auth().is_none());
912 }
913
914 #[test]
915 fn test_require_auth_success() {
916 let json = serde_json::to_string(&json!({
917 "method": "GET",
918 "auth": {"sub": "user-1", "realm_roles": [], "claims": {}}
919 }))
920 .unwrap();
921 let req = Request::from_json(&json).unwrap();
922 assert!(req.require_auth().is_ok());
923 assert_eq!(req.require_auth().unwrap().sub, "user-1");
924 }
925
926 #[test]
927 fn test_require_auth_fails_when_unauthenticated() {
928 let json = r#"{"method": "GET"}"#;
929 let req = Request::from_json(json).unwrap();
930 assert!(req.require_auth().is_err());
931 }
932
933 #[test]
934 fn test_http_fetch_noop_on_native() {
935 let resp = http::fetch("GET", "https://example.com", &[], None);
936 assert!(resp.is_none());
937 }
938
939 #[test]
940 fn test_http_get_noop_on_native() {
941 let resp = http::get("https://example.com", &[]);
942 assert!(resp.is_none());
943 }
944
945 #[test]
946 fn test_http_post_noop_on_native() {
947 let resp = http::post("https://example.com", &[], "{}");
948 assert!(resp.is_none());
949 }
950
951 #[test]
952 fn test_http_fetch_response_helpers() {
953 let resp = http::FetchResponse {
954 status: 200,
955 body: r#"{"key": "value"}"#.to_string(),
956 body_encoding: "utf8".to_string(),
957 headers: HashMap::new(),
958 };
959 assert!(resp.is_success());
960 assert!(!resp.is_base64());
961 let json = resp.json().unwrap();
962 assert_eq!(json["key"], "value");
963
964 let err_resp = http::FetchResponse {
965 status: 404,
966 body: "not found".to_string(),
967 body_encoding: "utf8".to_string(),
968 headers: HashMap::new(),
969 };
970 assert!(!err_resp.is_success());
971
972 let binary_resp = http::FetchResponse {
973 status: 200,
974 body: "aW1hZ2VkYXRh".to_string(),
975 body_encoding: "base64".to_string(),
976 headers: HashMap::new(),
977 };
978 assert!(binary_resp.is_base64());
979 }
980}