Skip to main content

uzumibi_cloudflare_ext/
lib.rs

1#![allow(static_mut_refs)]
2extern crate mrubyedge;
3extern crate uzumibi_gem;
4
5use std::rc::Rc;
6
7#[cfg(feature = "queue")]
8use mrubyedge::yamrb::value::RSym;
9#[expect(unused_imports)]
10use mrubyedge::yamrb::{
11    helpers::{mrb_define_class_cmethod, mrb_define_cmethod, mrb_funcall},
12    prelude::hash::{mrb_hash_new, mrb_hash_set_index},
13    value::{RObject, RValue},
14    vm::VM,
15};
16
17/// Special return value indicating that the request should be passed through to static assets.
18pub const PASS_ASSETS: u64 = 0xFEFFFFFF;
19
20// ---- Cloudflare-specific extern C declarations ----
21
22unsafe extern "C" {
23    unsafe fn debug_console_log(ptr: *const u8, len: usize);
24}
25
26#[cfg(feature = "queue")]
27unsafe extern "C" {
28    unsafe fn uzumibi_cf_message_ack(message_id_ptr: *const u8, message_id_size: usize) -> i32;
29    unsafe fn uzumibi_cf_message_retry(
30        message_id_ptr: *const u8,
31        message_id_size: usize,
32        delay_seconds: i32,
33    ) -> i32;
34}
35
36#[cfg(feature = "enable-external")]
37unsafe extern "C" {
38    unsafe fn uzumibi_cf_fetch(
39        url_ptr: *const u8,
40        url_size: usize,
41        method_ptr: *const u8,
42        method_size: usize,
43        body_ptr: *const u8,
44        body_size: usize,
45        headers_ptr: *const u8,
46        headers_size: usize,
47        result_ptr: *mut u8,
48        result_max_size: usize,
49    ) -> i32;
50    unsafe fn uzumibi_cf_durable_object_get(
51        key_ptr: *const u8,
52        key_size: usize,
53        result_ptr: *mut u8,
54        result_max_size: usize,
55    ) -> i32;
56    unsafe fn uzumibi_cf_durable_object_set(
57        key_ptr: *const u8,
58        key_size: usize,
59        value_ptr: *const u8,
60        value_size: usize,
61    ) -> i32;
62    unsafe fn uzumibi_cf_queue_send(
63        queue_name_ptr: *const u8,
64        queue_name_size: usize,
65        message_ptr: *const u8,
66        message_size: usize,
67    ) -> i32;
68    unsafe fn uzumibi_cf_secret_get(
69        key_ptr: *const u8,
70        key_size: usize,
71        result_ptr: *mut u8,
72        result_max_size: usize,
73    ) -> i32;
74}
75
76// ---- Debug console ----
77
78pub fn debug_console_log_internal(message: &str) {
79    unsafe {
80        debug_console_log(message.as_ptr(), message.len());
81    }
82}
83
84// ---- External API wrappers (only when enable-external feature is active) ----
85
86/// Packed response format (same as Uzumibi::Response#to_shared_memory):
87///   u16 LE status_code
88///   u16 LE headers_count
89///   (u16 LE key_size, key bytes, u16 LE value_size, value bytes) * headers_count
90///   u32 LE body_size
91///   body bytes
92#[cfg(feature = "enable-external")]
93fn cf_fetch(url: &str, method: &str, body: &str, headers: &[u8]) -> Result<Vec<u8>, String> {
94    const BUFFER_SIZE: usize = 65536;
95    let mut buffer = vec![0u8; BUFFER_SIZE];
96
97    unsafe {
98        let result = uzumibi_cf_fetch(
99            url.as_ptr(),
100            url.len(),
101            method.as_ptr(),
102            method.len(),
103            body.as_ptr(),
104            body.len(),
105            headers.as_ptr(),
106            headers.len(),
107            buffer.as_mut_ptr(),
108            BUFFER_SIZE,
109        );
110        match result {
111            len if len >= 0 => {
112                let len = len as usize;
113                Ok(buffer[..len].to_vec())
114            }
115            _ => Err(format!("Fetch failed with return code: {}", result)),
116        }
117    }
118}
119
120#[cfg(feature = "enable-external")]
121fn cf_durable_object_get(key: &str) -> Result<Option<String>, String> {
122    const BUFFER_SIZE: usize = 65536;
123    let mut buffer = vec![0u8; BUFFER_SIZE];
124
125    unsafe {
126        let result = uzumibi_cf_durable_object_get(
127            key.as_ptr(),
128            key.len(),
129            buffer.as_mut_ptr(),
130            BUFFER_SIZE,
131        );
132        match result {
133            -1 => Ok(None),
134            len if len >= 0 => {
135                let len = len as usize;
136                let value = String::from_utf8(buffer[..len].to_vec())
137                    .map_err(|e| format!("Failed to decode UTF-8: {}", e))?;
138                Ok(Some(value))
139            }
140            _ => Err(format!(
141                "Unexpected return value from durable_object_get: {}",
142                result
143            )),
144        }
145    }
146}
147
148#[cfg(feature = "enable-external")]
149fn cf_durable_object_set(key: &str, value: &str) -> Result<(), String> {
150    unsafe {
151        let result =
152            uzumibi_cf_durable_object_set(key.as_ptr(), key.len(), value.as_ptr(), value.len());
153        match result {
154            0 => Ok(()),
155            _ => Err(format!("Failed to set value: return code {}", result)),
156        }
157    }
158}
159
160#[cfg(feature = "enable-external")]
161fn cf_secret_get(key: &str) -> Result<Option<String>, String> {
162    const BUFFER_SIZE: usize = 8192;
163    let mut buffer = vec![0u8; BUFFER_SIZE];
164
165    unsafe {
166        let result =
167            uzumibi_cf_secret_get(key.as_ptr(), key.len(), buffer.as_mut_ptr(), BUFFER_SIZE);
168        match result {
169            -1 => Ok(None),
170            len if len >= 0 => {
171                let len = len as usize;
172                let value = String::from_utf8(buffer[..len].to_vec())
173                    .map_err(|e| format!("Failed to decode UTF-8: {}", e))?;
174                Ok(Some(value))
175            }
176            _ => Err(format!(
177                "Unexpected return value from secret_get: {}",
178                result
179            )),
180        }
181    }
182}
183
184#[cfg(feature = "enable-external")]
185fn cf_queue_send(queue_name: &str, message: &str) -> Result<(), String> {
186    unsafe {
187        let result = uzumibi_cf_queue_send(
188            queue_name.as_ptr(),
189            queue_name.len(),
190            message.as_ptr(),
191            message.len(),
192        );
193        match result {
194            0 => Ok(()),
195            _ => Err(format!(
196                "Failed to send queue message: return code {}",
197                result
198            )),
199        }
200    }
201}
202
203// ---- mruby gem method implementations ----
204
205fn uzumibi_kernel_debug_console_log(
206    vm: &mut VM,
207    args: &[Rc<RObject>],
208) -> Result<Rc<RObject>, mrubyedge::Error> {
209    let msg_obj = &args[0];
210    let msg = mrb_funcall(vm, msg_obj.clone().into(), "to_s", &[])?;
211    let msg: String = msg.as_ref().try_into()?;
212    unsafe {
213        debug_console_log(msg.as_ptr(), msg.len());
214    }
215    Ok(RObject::nil().to_refcount_assigned())
216}
217
218/// Fetch.fetch(url, method="GET", body="", headers={}) -> Uzumibi::Response
219#[cfg(feature = "enable-external")]
220fn uzumibi_fetch_class_fetch(
221    vm: &mut VM,
222    args: &[Rc<RObject>],
223) -> Result<Rc<RObject>, mrubyedge::Error> {
224    let url_obj = &args[0];
225    let url = mrb_funcall(vm, url_obj.clone().into(), "to_s", &[])?;
226    let url: String = url.as_ref().try_into()?;
227
228    let method = if args.len() > 1 {
229        let m = mrb_funcall(vm, args[1].clone().into(), "to_s", &[])?;
230        let m: String = m.as_ref().try_into()?;
231        m
232    } else {
233        "GET".to_string()
234    };
235
236    let body = if args.len() > 2 {
237        let b = mrb_funcall(vm, args[2].clone().into(), "to_s", &[])?;
238        let b: String = b.as_ref().try_into()?;
239        b
240    } else {
241        String::new()
242    };
243
244    // Pack request headers from Hash (4th argument)
245    let packed_headers = if args.len() > 3 {
246        pack_headers_from_hash(vm, &args[3])?
247    } else {
248        vec![0u8; 2] // u16 LE count = 0
249    };
250
251    let packed = cf_fetch(&url, &method, &body, &packed_headers)
252        .map_err(|e| mrubyedge::Error::RuntimeError(format!("Fetch failed: {}", e)))?;
253
254    // Unpack the packed response into Uzumibi::Response
255    unpack_response_to_robject(vm, &packed)
256}
257
258/// Pack a mruby Hash into binary format for request headers:
259///   u16 LE headers_count
260///   (u16 LE key_size, key bytes, u16 LE value_size, value bytes) * count
261#[cfg(feature = "enable-external")]
262fn pack_headers_from_hash(
263    vm: &mut VM,
264    hash_obj: &Rc<RObject>,
265) -> Result<Vec<u8>, mrubyedge::Error> {
266    match &hash_obj.as_ref().value {
267        RValue::Hash(h) => {
268            let hash = h.borrow();
269            let mut buf = Vec::new();
270            let count = hash.len() as u16;
271            buf.extend_from_slice(&count.to_le_bytes());
272            for (_, (key_obj, value_obj)) in hash.iter() {
273                let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?;
274                let key: String = key.as_ref().try_into()?;
275                let value = mrb_funcall(vm, value_obj.clone().into(), "to_s", &[])?;
276                let value: String = value.as_ref().try_into()?;
277                buf.extend_from_slice(&(key.len() as u16).to_le_bytes());
278                buf.extend_from_slice(key.as_bytes());
279                buf.extend_from_slice(&(value.len() as u16).to_le_bytes());
280                buf.extend_from_slice(value.as_bytes());
281            }
282            Ok(buf)
283        }
284        RValue::Nil => {
285            Ok(vec![0u8; 2]) // u16 LE count = 0
286        }
287        _ => Err(mrubyedge::Error::RuntimeError(
288            "headers argument must be a Hash".to_string(),
289        )),
290    }
291}
292
293/// Unpack packed binary response into Uzumibi::Response mruby object
294#[cfg(feature = "enable-external")]
295fn unpack_response_to_robject(vm: &mut VM, buf: &[u8]) -> Result<Rc<RObject>, mrubyedge::Error> {
296    let mut offset = 0;
297
298    // Status code (u16 LE)
299    let status_code = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
300    offset += 2;
301
302    // Headers count (u16 LE)
303    let headers_count = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize;
304    offset += 2;
305
306    // Parse headers
307    let headers_hash = mrb_hash_new(vm, &[])?;
308    for _ in 0..headers_count {
309        let key_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize;
310        offset += 2;
311        let key = String::from_utf8_lossy(&buf[offset..offset + key_size]).to_string();
312        offset += key_size;
313
314        let value_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize;
315        offset += 2;
316        let value = String::from_utf8_lossy(&buf[offset..offset + value_size]).to_string();
317        offset += value_size;
318
319        mrb_hash_set_index(
320            headers_hash.clone(),
321            RObject::string(key).to_refcount_assigned(),
322            RObject::string(value).to_refcount_assigned(),
323        )?;
324    }
325
326    // Body size (u32 LE)
327    let body_size = u32::from_le_bytes([
328        buf[offset],
329        buf[offset + 1],
330        buf[offset + 2],
331        buf[offset + 3],
332    ]) as usize;
333    offset += 4;
334
335    // Body
336    let body = String::from_utf8_lossy(&buf[offset..offset + body_size]).to_string();
337
338    // Create Uzumibi::Response instance
339    let uzumibi = vm
340        .get_const_by_name("Uzumibi")
341        .ok_or_else(|| mrubyedge::Error::RuntimeError("Uzumibi module not found".to_string()))?;
342    let uzumibi_module = match &uzumibi.as_ref().value {
343        RValue::Module(m) => m.clone(),
344        _ => {
345            return Err(mrubyedge::Error::RuntimeError(
346                "Uzumibi must be a module".to_string(),
347            ));
348        }
349    };
350    let response_class = uzumibi_module
351        .get_const_by_name("Response")
352        .ok_or_else(|| {
353            mrubyedge::Error::RuntimeError("Uzumibi::Response class not found".to_string())
354        })?;
355    let response = mrb_funcall(vm, Some(response_class), "new", &[])?;
356
357    response.set_ivar(
358        "@status_code",
359        RObject::integer(status_code as i64).to_refcount_assigned(),
360    );
361    response.set_ivar("@headers", headers_hash);
362    response.set_ivar("@body", RObject::string(body).to_refcount_assigned());
363
364    Ok(response)
365}
366
367/// KV.get(key)
368#[cfg(feature = "enable-external")]
369fn uzumibi_kv_class_get(
370    vm: &mut VM,
371    args: &[Rc<RObject>],
372) -> Result<Rc<RObject>, mrubyedge::Error> {
373    let key_obj = &args[0];
374    let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?;
375    let key: String = key.as_ref().try_into()?;
376
377    match cf_durable_object_get(&key) {
378        Ok(Some(value)) => Ok(RObject::string(value).to_refcount_assigned()),
379        Ok(None) => Ok(RObject::nil().to_refcount_assigned()),
380        Err(e) => Err(mrubyedge::Error::RuntimeError(format!(
381            "Failed to access storage value: {}",
382            e
383        ))),
384    }
385}
386
387/// KV.set(key, value)
388#[cfg(feature = "enable-external")]
389fn uzumibi_kv_class_set(
390    vm: &mut VM,
391    args: &[Rc<RObject>],
392) -> Result<Rc<RObject>, mrubyedge::Error> {
393    let key_obj = &args[0];
394    let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?;
395    let key: String = key.as_ref().try_into()?;
396
397    let value_obj = &args[1];
398    let value = mrb_funcall(vm, value_obj.clone().into(), "to_s", &[])?;
399    let value: String = value.as_ref().try_into()?;
400
401    cf_durable_object_set(&key, &value).map_err(|e| {
402        mrubyedge::Error::RuntimeError(format!("Failed to set storage value: {}", e))
403    })?;
404
405    Ok(RObject::boolean(true).to_refcount_assigned())
406}
407
408/// Secret.get(key) -> String | nil
409#[cfg(feature = "enable-external")]
410fn uzumibi_secret_class_get(
411    vm: &mut VM,
412    args: &[Rc<RObject>],
413) -> Result<Rc<RObject>, mrubyedge::Error> {
414    let key_obj = &args[0];
415    let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?;
416    let key: String = key.as_ref().try_into()?;
417
418    match cf_secret_get(&key) {
419        Ok(Some(value)) => Ok(RObject::string(value).to_refcount_assigned()),
420        Ok(None) => Ok(RObject::nil().to_refcount_assigned()),
421        Err(e) => Err(mrubyedge::Error::RuntimeError(format!(
422            "Failed to get secret: {}",
423            e
424        ))),
425    }
426}
427
428/// Queue.send(queue_name, message)
429#[cfg(feature = "enable-external")]
430fn uzumibi_queue_class_send(
431    vm: &mut VM,
432    args: &[Rc<RObject>],
433) -> Result<Rc<RObject>, mrubyedge::Error> {
434    let queue_name_obj = &args[0];
435    let queue_name = mrb_funcall(vm, queue_name_obj.clone().into(), "to_s", &[])?;
436    let queue_name: String = queue_name.as_ref().try_into()?;
437
438    let message_obj = &args[1];
439    let message = mrb_funcall(vm, message_obj.clone().into(), "to_s", &[])?;
440    let message: String = message.as_ref().try_into()?;
441
442    cf_queue_send(&queue_name, &message).map_err(|e| {
443        mrubyedge::Error::RuntimeError(format!("Failed to send queue message: {}", e))
444    })?;
445
446    Ok(RObject::boolean(true).to_refcount_assigned())
447}
448
449// ---- Queue consumer support (only when queue feature is active) ----
450
451/// Message.ack! -> delegates to JS
452#[cfg(feature = "queue")]
453fn uzumibi_message_ack(
454    vm: &mut VM,
455    _args: &[Rc<RObject>],
456) -> Result<Rc<RObject>, mrubyedge::Error> {
457    let self_obj = vm.getself()?;
458    let id_obj = self_obj.get_ivar("@id");
459    if matches!(id_obj.as_ref().value, RValue::Nil) {
460        return Err(mrubyedge::Error::RuntimeError(
461            "Message object does not have @id".to_string(),
462        ));
463    }
464    let id = mrb_funcall(vm, id_obj.into(), "to_s", &[])?;
465    let id: String = id.as_ref().try_into()?;
466
467    unsafe {
468        let result = uzumibi_cf_message_ack(id.as_ptr(), id.len());
469        if result != 0 {
470            return Err(mrubyedge::Error::RuntimeError(format!(
471                "Failed to ack message: return code {}",
472                result
473            )));
474        }
475    }
476    Ok(RObject::boolean(true).to_refcount_assigned())
477}
478
479/// Message.retry(delay_seconds: N) -> delegates to JS
480#[cfg(feature = "queue")]
481fn uzumibi_message_retry(
482    vm: &mut VM,
483    _args: &[Rc<RObject>],
484) -> Result<Rc<RObject>, mrubyedge::Error> {
485    let self_obj = vm.getself()?;
486    let id_obj = self_obj.get_ivar("@id");
487    if matches!(id_obj.as_ref().value, RValue::Nil) {
488        return Err(mrubyedge::Error::RuntimeError(
489            "Message object does not have @id".to_string(),
490        ));
491    }
492    let id = mrb_funcall(vm, id_obj.into(), "to_s", &[])?;
493    let id: String = id.as_ref().try_into()?;
494
495    let delay_seconds: i32 = match vm.get_kwargs() {
496        Some(kwargs) => match kwargs.get("delay_seconds") {
497            Some(val) => {
498                let v: i64 = val.as_ref().try_into()?;
499                v as i32
500            }
501            None => 0,
502        },
503        None => 0,
504    };
505
506    unsafe {
507        let result = uzumibi_cf_message_retry(id.as_ptr(), id.len(), delay_seconds);
508        if result != 0 {
509            return Err(mrubyedge::Error::RuntimeError(format!(
510                "Failed to retry message: return code {}",
511                result
512            )));
513        }
514    }
515    Ok(RObject::boolean(true).to_refcount_assigned())
516}
517
518/// Message.nack! -> retry with delay_seconds=0
519#[cfg(feature = "queue")]
520fn uzumibi_message_nack(
521    vm: &mut VM,
522    _args: &[Rc<RObject>],
523) -> Result<Rc<RObject>, mrubyedge::Error> {
524    let self_obj = vm.getself()?;
525    let id_obj = self_obj.get_ivar("@id");
526    if matches!(id_obj.as_ref().value, RValue::Nil) {
527        return Err(mrubyedge::Error::RuntimeError(
528            "Message object does not have @id".to_string(),
529        ));
530    }
531    let id = mrb_funcall(vm, id_obj.into(), "to_s", &[])?;
532    let id: String = id.as_ref().try_into()?;
533
534    unsafe {
535        let result = uzumibi_cf_message_retry(id.as_ptr(), id.len(), 0);
536        if result != 0 {
537            return Err(mrubyedge::Error::RuntimeError(format!(
538                "Failed to nack message: return code {}",
539                result
540            )));
541        }
542    }
543    Ok(RObject::boolean(true).to_refcount_assigned())
544}
545
546/// Consumer.on_receive(message) - abstract method, must be overridden
547#[cfg(feature = "queue")]
548fn uzumibi_consumer_on_receive(
549    _vm: &mut VM,
550    _args: &[Rc<RObject>],
551) -> Result<Rc<RObject>, mrubyedge::Error> {
552    Err(mrubyedge::Error::RuntimeError(
553        "on_receive must be implemented by subclass of Uzumibi::Consumer".to_string(),
554    ))
555}
556
557// ---- Cloudflare Access ----
558
559#[cfg(feature = "enable-external")]
560static mut ACCESS_TEAM: Option<String> = None;
561
562/// Extract body string from packed response buffer
563#[cfg(feature = "enable-external")]
564fn unpack_response_body(buf: &[u8]) -> Result<String, mrubyedge::Error> {
565    let mut offset = 0;
566    // Skip status code (u16)
567    offset += 2;
568    // Headers count (u16)
569    let headers_count = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize;
570    offset += 2;
571    // Skip headers
572    for _ in 0..headers_count {
573        let key_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize;
574        offset += 2 + key_size;
575        let value_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize;
576        offset += 2 + value_size;
577    }
578    // Body size (u32)
579    let body_size = u32::from_le_bytes([
580        buf[offset],
581        buf[offset + 1],
582        buf[offset + 2],
583        buf[offset + 3],
584    ]) as usize;
585    offset += 4;
586    // Body
587    Ok(String::from_utf8_lossy(&buf[offset..offset + body_size]).to_string())
588}
589
590/// Access.team=(name)
591#[cfg(feature = "enable-external")]
592fn uzumibi_access_set_team(
593    vm: &mut VM,
594    args: &[Rc<RObject>],
595) -> Result<Rc<RObject>, mrubyedge::Error> {
596    let team = mrb_funcall(vm, args[0].clone().into(), "to_s", &[])?;
597    let team: String = team.as_ref().try_into()?;
598    unsafe {
599        ACCESS_TEAM = Some(team);
600    }
601    Ok(args[0].clone())
602}
603
604/// Access.get_identity(cf_authorization_token) -> AccessIdentity
605#[cfg(feature = "enable-external")]
606fn uzumibi_access_get_identity(
607    vm: &mut VM,
608    args: &[Rc<RObject>],
609) -> Result<Rc<RObject>, mrubyedge::Error> {
610    let token = mrb_funcall(vm, args[0].clone().into(), "to_s", &[])?;
611    let token: String = token.as_ref().try_into()?;
612
613    let team = unsafe {
614        ACCESS_TEAM.as_ref().ok_or_else(|| {
615            mrubyedge::Error::RuntimeError("Uzumibi::Access.team is not set".to_string())
616        })?
617    };
618
619    let url = format!(
620        "https://{}.cloudflareaccess.com/cdn-cgi/access/get-identity",
621        team
622    );
623
624    // Pack Cookie header
625    let cookie_value = format!("CF_Authorization={}", token);
626    let mut headers_buf = Vec::new();
627    let count: u16 = 1;
628    headers_buf.extend_from_slice(&count.to_le_bytes());
629    let key = b"cookie";
630    headers_buf.extend_from_slice(&(key.len() as u16).to_le_bytes());
631    headers_buf.extend_from_slice(key);
632    let val = cookie_value.as_bytes();
633    headers_buf.extend_from_slice(&(val.len() as u16).to_le_bytes());
634    headers_buf.extend_from_slice(val);
635
636    let packed = cf_fetch(&url, "GET", "", &headers_buf)
637        .map_err(|e| mrubyedge::Error::RuntimeError(format!("Access fetch failed: {}", e)))?;
638
639    let body = unpack_response_body(&packed)?;
640
641    // Parse JSON body using mrubyedge-serde-json
642    let body_robj = RObject::string(body).to_refcount_assigned();
643    let json_value = mrubyedge_serde_json::mrb_json_class_load(vm, &[body_robj])?;
644
645    // Create AccessIdentity and set fields from parsed JSON hash
646    let uzumibi = vm
647        .get_const_by_name("Uzumibi")
648        .ok_or_else(|| mrubyedge::Error::RuntimeError("Uzumibi module not found".to_string()))?;
649    let uzumibi_module = match &uzumibi.as_ref().value {
650        RValue::Module(m) => m.clone(),
651        _ => {
652            return Err(mrubyedge::Error::RuntimeError(
653                "Uzumibi must be a module".to_string(),
654            ));
655        }
656    };
657    let identity_class = uzumibi_module
658        .get_const_by_name("AccessIdentity")
659        .ok_or_else(|| {
660            mrubyedge::Error::RuntimeError("Uzumibi::AccessIdentity class not found".to_string())
661        })?;
662    let identity = mrb_funcall(vm, Some(identity_class), "new", &[])?;
663
664    // Extract known fields from JSON hash
665    let field_mappings = [("user_uuid", "@user_uuid"), ("email", "@email")];
666    for (json_key, ivar_key) in &field_mappings {
667        let val = mrb_funcall(
668            vm,
669            Some(json_value.clone()),
670            "[]",
671            &[RObject::string(json_key.to_string()).to_refcount_assigned()],
672        )?;
673        identity.set_ivar(ivar_key, val);
674    }
675
676    // Store raw data hash
677    identity.set_ivar("@raw_data", json_value);
678
679    Ok(identity)
680}
681
682// ---- Assets pass-through ----
683
684fn uzumibi_fetch_assets(
685    _vm: &mut VM,
686    _args: &[Rc<RObject>],
687) -> Result<Rc<RObject>, mrubyedge::Error> {
688    Err(mrubyedge::Error::TaggedError(
689        "UzumibiPassAssets",
690        "pass assets to platform".to_string(),
691    ))
692}
693
694// ---- VM initialization ----
695
696/// Initialize Cloudflare-specific mruby classes and methods on the given VM.
697/// This should be called after `uzumibi_gem::init::init_uzumibi(&mut vm)`.
698pub fn init_cloudflare_ext(vm: &mut VM) {
699    // Define UzumibiPassAssets exception class
700    let runtime_error = vm.get_class_by_name("RuntimeError");
701    vm.define_class("UzumibiPassAssets", Some(runtime_error), None);
702
703    // Kernel-level methods
704    let object = vm.object_class.clone();
705    mrb_define_cmethod(
706        vm,
707        object.clone(),
708        "debug_console",
709        Box::new(uzumibi_kernel_debug_console_log),
710    );
711    mrb_define_cmethod(vm, object, "fetch_assets", Box::new(uzumibi_fetch_assets));
712
713    #[cfg(feature = "enable-external")]
714    {
715        let uzumibi_module = vm.get_module_by_name("Uzumibi");
716
717        // Uzumibi::Fetch.fetch(url, method="GET", body="")
718        let fetch_class = vm.define_class("Fetch", None, Some(uzumibi_module.clone()));
719        mrb_define_class_cmethod(
720            vm,
721            fetch_class,
722            "fetch",
723            Box::new(uzumibi_fetch_class_fetch),
724        );
725
726        // Uzumibi::KV.get(key) / Uzumibi::KV.set(key, value)
727        let kv_class = vm.define_class("KV", None, Some(uzumibi_module.clone()));
728        mrb_define_class_cmethod(vm, kv_class.clone(), "get", Box::new(uzumibi_kv_class_get));
729        mrb_define_class_cmethod(vm, kv_class, "set", Box::new(uzumibi_kv_class_set));
730
731        // Uzumibi::Secret.get(key)
732        let secret_class = vm.define_class("Secret", None, Some(uzumibi_module.clone()));
733        mrb_define_class_cmethod(vm, secret_class, "get", Box::new(uzumibi_secret_class_get));
734
735        // Uzumibi::Queue.send(queue_name, message)
736        let queue_class = vm.define_class("Queue", None, Some(uzumibi_module.clone()));
737        mrb_define_class_cmethod(vm, queue_class, "send", Box::new(uzumibi_queue_class_send));
738
739        // Uzumibi::Access.team= / Uzumibi::Access.get_identity(token)
740        let access_class = vm.define_class("Access", None, Some(uzumibi_module.clone()));
741        mrb_define_class_cmethod(
742            vm,
743            access_class.clone(),
744            "team=",
745            Box::new(uzumibi_access_set_team),
746        );
747        mrb_define_class_cmethod(
748            vm,
749            access_class,
750            "get_identity",
751            Box::new(uzumibi_access_get_identity),
752        );
753
754        // Uzumibi::AccessIdentity with attr_accessor for common fields
755        let identity_class = vm.define_class("AccessIdentity", None, Some(uzumibi_module));
756        let identity_class_obj = RObject::class(identity_class, vm);
757        for attr in ["user_uuid", "email", "raw_data"] {
758            mrb_funcall(
759                vm,
760                Some(identity_class_obj.clone()),
761                "attr_accessor",
762                &[
763                    RObject::symbol(mrubyedge::yamrb::value::RSym::new(attr.to_string()))
764                        .to_refcount_assigned(),
765                ],
766            )
767            .expect("attr_accessor failed");
768        }
769    }
770
771    #[cfg(feature = "queue")]
772    {
773        let uzumibi_module = vm.get_module_by_name("Uzumibi");
774
775        // Uzumibi::Consumer (base class for user-defined consumers)
776        let consumer_class = vm.define_class("Consumer", None, Some(uzumibi_module.clone()));
777        mrb_define_cmethod(
778            vm,
779            consumer_class,
780            "on_receive",
781            Box::new(uzumibi_consumer_on_receive),
782        );
783
784        // Uzumibi::Message with ack! and retry methods
785        let message_class = vm.define_class("Message", None, Some(uzumibi_module));
786        let message_class_obj = RObject::class(message_class.clone(), vm);
787        for attr in ["id", "timestamp", "body", "attempts"] {
788            mrb_funcall(
789                vm,
790                Some(message_class_obj.clone()),
791                "attr_accessor",
792                &[RObject::symbol(RSym::new(attr.to_string())).to_refcount_assigned()],
793            )
794            .expect("attr_accessor failed");
795        }
796        mrb_define_cmethod(
797            vm,
798            message_class.clone(),
799            "ack!",
800            Box::new(uzumibi_message_ack),
801        );
802        mrb_define_cmethod(
803            vm,
804            message_class.clone(),
805            "nack!",
806            Box::new(uzumibi_message_nack),
807        );
808        mrb_define_cmethod(vm, message_class, "retry", Box::new(uzumibi_message_retry));
809    }
810}
811
812/// Unpack a queue message from a binary buffer and call `$CONSUMER.on_receive(message)`.
813///
814/// Message binary format:
815///   u16 LE id_size, id bytes,
816///   u16 LE timestamp_size, timestamp bytes,
817///   u32 LE body_size, body bytes,
818///   u32 LE attempts
819#[cfg(feature = "queue")]
820pub fn dispatch_queue_message(vm: &mut VM, buf: &[u8]) -> Result<(), mrubyedge::Error> {
821    let mut offset = 0;
822
823    // id (u16 LE size + bytes)
824    let id_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize;
825    offset += 2;
826    let id = String::from_utf8_lossy(&buf[offset..offset + id_size]).to_string();
827    offset += id_size;
828
829    // timestamp (u16 LE size + bytes)
830    let ts_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize;
831    offset += 2;
832    let timestamp = String::from_utf8_lossy(&buf[offset..offset + ts_size]).to_string();
833    offset += ts_size;
834
835    // body (u32 LE size + bytes)
836    let body_size = u32::from_le_bytes([
837        buf[offset],
838        buf[offset + 1],
839        buf[offset + 2],
840        buf[offset + 3],
841    ]) as usize;
842    offset += 4;
843    let body = String::from_utf8_lossy(&buf[offset..offset + body_size]).to_string();
844    offset += body_size;
845
846    // attempts (u32 LE)
847    let attempts = u32::from_le_bytes([
848        buf[offset],
849        buf[offset + 1],
850        buf[offset + 2],
851        buf[offset + 3],
852    ]) as i64;
853
854    // Create Uzumibi::Message instance
855    let uzumibi = vm
856        .get_const_by_name("Uzumibi")
857        .ok_or_else(|| mrubyedge::Error::RuntimeError("Uzumibi module not found".to_string()))?;
858    let uzumibi_module = match &uzumibi.as_ref().value {
859        RValue::Module(m) => m.clone(),
860        _ => {
861            return Err(mrubyedge::Error::RuntimeError(
862                "Uzumibi must be a module".to_string(),
863            ));
864        }
865    };
866    let message_class = uzumibi_module.get_const_by_name("Message").ok_or_else(|| {
867        mrubyedge::Error::RuntimeError("Uzumibi::Message class not found".to_string())
868    })?;
869    let message = mrb_funcall(vm, Some(message_class), "new", &[])?;
870
871    message.set_ivar("@id", RObject::string(id).to_refcount_assigned());
872    message.set_ivar(
873        "@timestamp",
874        RObject::string(timestamp).to_refcount_assigned(),
875    );
876    message.set_ivar("@body", RObject::string(body).to_refcount_assigned());
877    message.set_ivar(
878        "@attempts",
879        RObject::integer(attempts).to_refcount_assigned(),
880    );
881
882    // Call $CONSUMER.on_receive(message)
883    let consumer = vm
884        .globals
885        .get("$CONSUMER")
886        .ok_or_else(|| mrubyedge::Error::RuntimeError("$CONSUMER is not defined".to_string()))?;
887    mrb_funcall(vm, consumer.clone().into(), "on_receive", &[message])?;
888
889    Ok(())
890}