use crate::{api, log, read_string};
use serde::{Serialize, de::DeserializeOwned};
pub fn mod_id() -> &'static str {
let a = api();
if a.mod_id.is_null() {
return "";
}
unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(a.mod_id, a.mod_id_len)) }
}
fn last_text() -> String {
read_string(|b, c| (api().last_text)(b, c))
}
fn json<T: Serialize>(v: &T) -> Result<String, String> {
serde_json::to_string(v).map_err(|e| e.to_string())
}
pub trait Event: Serialize + DeserializeOwned {
const TOPIC: &'static str;
}
fn publish_raw(topic: &str, json: &str, retained: bool) -> Result<(), String> {
let id = mod_id();
let ok = (api().bus_publish)(id.as_ptr(), id.len(), topic.as_ptr(), topic.len(), json.as_ptr(), json.len(), retained as u8);
if ok != 0 { Ok(()) } else { Err(last_text()) }
}
pub fn publish<E: Event>(event: &E) -> Result<(), String> {
publish_raw(E::TOPIC, &json(event)?, false)
}
pub fn publish_retained<E: Event>(event: &E) -> Result<(), String> {
publish_raw(E::TOPIC, &json(event)?, true)
}
pub fn publish_json(topic: &str, json: &str) -> Result<(), String> {
publish_raw(topic, json, false)
}
pub fn publish_retained_json(topic: &str, json: &str) -> Result<(), String> {
publish_raw(topic, json, true)
}
type Callback = Box<dyn Fn(&str) + Send + Sync>;
pub struct Subscription {
id: u64,
callback: *mut Callback,
}
unsafe impl Send for Subscription {}
impl Subscription {
pub fn forever(self) {
std::mem::forget(self);
}
}
impl Drop for Subscription {
fn drop(&mut self) {
(api().bus_unsubscribe)(self.id);
drop(unsafe { Box::from_raw(self.callback) });
}
}
extern "C" fn deliver(user: usize, json: *const u8, len: usize) {
let f = unsafe { &*(user as *const Callback) };
let text = unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(json, len)) };
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(text))).is_err() {
log(&format!("{}: an event callback panicked", mod_id()));
}
}
pub fn subscribe_json(topic: &str, f: impl Fn(&str) + Send + Sync + 'static) -> Result<Subscription, String> {
let callback: *mut Callback = Box::into_raw(Box::new(Box::new(f)));
let id = mod_id();
let sub = (api().bus_subscribe)(id.as_ptr(), id.len(), topic.as_ptr(), topic.len(), deliver, callback as usize);
if sub == 0 {
drop(unsafe { Box::from_raw(callback) });
return Err(last_text());
}
Ok(Subscription { id: sub, callback })
}
pub fn subscribe<E: Event + 'static>(f: impl Fn(E) + Send + Sync + 'static) -> Result<Subscription, String> {
subscribe_json(E::TOPIC, move |text| match serde_json::from_str::<E>(text) {
Ok(e) => f(e),
Err(err) => log(&format!("{}: {} event doesn't fit its type: {err}", mod_id(), E::TOPIC)),
})
}
pub trait Service {
const NAME: &'static str;
type Args: Serialize + DeserializeOwned;
type Out: Serialize + DeserializeOwned;
}
type Provider = Box<dyn Fn(&str) -> Result<String, String> + Send + Sync>;
extern "C" fn serve(user: usize, args: *const u8, len: usize) -> u8 {
let f = unsafe { &*(user as *const Provider) };
let text = unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(args, len)) };
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(text))).unwrap_or_else(|_| Err("the service panicked".into()));
let (ok, reply) = match result {
Ok(json) => (1, json),
Err(e) => (0, e),
};
(api().service_reply)(reply.as_ptr(), reply.len());
ok
}
pub fn provide_json(name: &str, f: impl Fn(&str) -> Result<String, String> + Send + Sync + 'static) -> Result<(), String> {
let provider: *mut Provider = Box::into_raw(Box::new(Box::new(f)));
let id = mod_id();
if (api().service_provide)(id.as_ptr(), id.len(), name.as_ptr(), name.len(), serve, provider as usize) != 0 {
Ok(()) } else {
drop(unsafe { Box::from_raw(provider) });
Err(last_text())
}
}
pub fn provide<S: Service>(f: impl Fn(S::Args) -> Result<S::Out, String> + Send + Sync + 'static) -> Result<(), String> {
provide_json(S::NAME, move |text| {
let args = serde_json::from_str::<S::Args>(text).map_err(|e| format!("bad arguments: {e}"))?;
json(&f(args)?)
})
}
pub fn call_json(name: &str, args_json: &str) -> Result<String, String> {
let id = mod_id();
let ok = (api().service_call)(id.as_ptr(), id.len(), name.as_ptr(), name.len(), args_json.as_ptr(), args_json.len());
let text = last_text();
if ok != 0 { Ok(text) } else { Err(text) }
}
pub fn call<S: Service>(args: &S::Args) -> Result<S::Out, String> {
let out = call_json(S::NAME, &json(args)?)?;
serde_json::from_str(&out).map_err(|e| format!("{} returned something that doesn't fit: {e}", S::NAME))
}