use std::ffi::{CStr, CString};
use std::marker::PhantomData;
use std::ptr::NonNull;
use std::slice;
use std::time::Duration;
use crate::error::{Error, Result, last_ffi_error};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Protocol {
Udp,
Quic,
Tcp,
}
impl Protocol {
fn as_raw(self) -> adamo_sys::adamo_protocol_t {
match self {
Protocol::Udp => adamo_sys::ADAMO_PROTOCOL_UDP,
Protocol::Quic => adamo_sys::ADAMO_PROTOCOL_QUIC,
Protocol::Tcp => adamo_sys::ADAMO_PROTOCOL_TCP,
}
}
}
pub struct Session {
raw: NonNull<adamo_sys::adamo_session_t>,
}
unsafe impl Send for Session {}
unsafe impl Sync for Session {}
impl Session {
pub fn open(api_key: &str, protocol: Protocol) -> Result<Self> {
let key = CString::new(api_key)?;
let raw = unsafe { adamo_sys::adamo_open(key.as_ptr(), protocol.as_raw()) };
match NonNull::new(raw) {
Some(raw) => Ok(Session { raw }),
None => Err(last_ffi_error()),
}
}
pub fn org(&self) -> Result<&str> {
let ptr = unsafe { adamo_sys::adamo_session_org(self.raw.as_ptr()) };
if ptr.is_null() {
return Err(last_ffi_error());
}
unsafe { CStr::from_ptr(ptr) }
.to_str()
.map_err(|_| Error::InvalidUtf8)
}
pub fn put(&self, key: &str, payload: &[u8], opts: PublishOptions) -> Result<()> {
let key = CString::new(key)?;
let rc = unsafe {
adamo_sys::adamo_put(
self.raw.as_ptr(),
key.as_ptr(),
payload.as_ptr(),
payload.len(),
opts.priority,
opts.express as i32,
)
};
if rc == 0 { Ok(()) } else { Err(last_ffi_error()) }
}
pub fn publisher(&self, key: &str, opts: PublisherOptions) -> Result<Publisher<'_>> {
let key = CString::new(key)?;
let raw = unsafe {
adamo_sys::adamo_publisher(
self.raw.as_ptr(),
key.as_ptr(),
opts.priority,
opts.express as i32,
opts.reliable as i32,
)
};
match NonNull::new(raw) {
Some(raw) => Ok(Publisher {
raw,
_session: PhantomData,
}),
None => Err(last_ffi_error()),
}
}
pub fn log(&self, name: &str, message: &str, level: &str) -> Result<()> {
let key = format!("{name}/logs");
let truncated: String;
let message_ref = if message.len() > 10_000 {
let cutoff = message
.char_indices()
.take_while(|(i, _)| *i < 10_000)
.last()
.map_or(0, |(i, c)| i + c.len_utf8());
truncated = format!("{}... [truncated]", &message[..cutoff]);
truncated.as_str()
} else {
message
};
let payload = format!(
"{{\"ts_us\":{},\"level\":\"{}\",\"message\":{}}}",
crate::fabric_now_us(),
json_escape(level),
json_quote(message_ref),
);
self.put(
&key,
payload.as_bytes(),
PublishOptions {
priority: 230,
express: true,
},
)
}
pub fn subscribe(&self, key: &str) -> Result<Subscriber<'_>> {
let key = CString::new(key)?;
let raw = unsafe { adamo_sys::adamo_subscribe(self.raw.as_ptr(), key.as_ptr()) };
match NonNull::new(raw) {
Some(raw) => Ok(Subscriber {
raw,
_session: PhantomData,
}),
None => Err(last_ffi_error()),
}
}
}
impl Drop for Session {
fn drop(&mut self) {
unsafe { adamo_sys::adamo_session_free(self.raw.as_ptr()) };
}
}
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
use std::fmt::Write;
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out
}
fn json_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
out.push_str(&json_escape(s));
out.push('"');
out
}
#[derive(Debug, Clone, Copy)]
pub struct PublishOptions {
pub priority: u8,
pub express: bool,
}
impl Default for PublishOptions {
fn default() -> Self {
Self {
priority: 128,
express: false,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PublisherOptions {
pub priority: u8,
pub express: bool,
pub reliable: bool,
}
impl Default for PublisherOptions {
fn default() -> Self {
Self {
priority: 128,
express: false,
reliable: true,
}
}
}
pub struct Publisher<'a> {
raw: NonNull<adamo_sys::adamo_publisher_t>,
_session: PhantomData<&'a Session>,
}
unsafe impl Send for Publisher<'_> {}
unsafe impl Sync for Publisher<'_> {}
impl Publisher<'_> {
pub fn put(&self, payload: &[u8]) -> Result<()> {
let rc = unsafe {
adamo_sys::adamo_publisher_put(self.raw.as_ptr(), payload.as_ptr(), payload.len())
};
if rc == 0 { Ok(()) } else { Err(last_ffi_error()) }
}
}
impl Drop for Publisher<'_> {
fn drop(&mut self) {
unsafe { adamo_sys::adamo_publisher_free(self.raw.as_ptr()) };
}
}
pub struct Subscriber<'a> {
raw: NonNull<adamo_sys::adamo_subscriber_t>,
_session: PhantomData<&'a Session>,
}
unsafe impl Send for Subscriber<'_> {}
impl Subscriber<'_> {
pub fn recv(&self, timeout: Option<Duration>) -> Result<Sample> {
let ms = timeout.map_or(0u64, |d| d.as_millis().min(u64::MAX as u128) as u64);
let raw = unsafe { adamo_sys::adamo_sub_recv(self.raw.as_ptr(), ms) };
Sample::from_raw(raw).ok_or_else(|| {
match last_ffi_error() {
Error::Ffi(m) if m == "(no error message)" => Error::Timeout,
e => e,
}
})
}
pub fn try_recv(&self) -> Result<Option<Sample>> {
let raw = unsafe { adamo_sys::adamo_sub_try_recv(self.raw.as_ptr()) };
if let Some(sample) = Sample::from_raw(raw) {
return Ok(Some(sample));
}
match last_ffi_error() {
Error::Ffi(m) if m == "(no error message)" => Ok(None),
e => Err(e),
}
}
}
impl Drop for Subscriber<'_> {
fn drop(&mut self) {
unsafe { adamo_sys::adamo_sub_free(self.raw.as_ptr()) };
}
}
#[derive(Debug, Clone)]
pub struct Sample {
pub key: String,
pub payload: Vec<u8>,
pub is_delete: bool,
}
impl Sample {
fn from_raw(raw: *mut adamo_sys::adamo_sample_t) -> Option<Self> {
if raw.is_null() {
return None;
}
let sample = unsafe {
let s = &*raw;
let key = if s.key.is_null() {
String::new()
} else {
CStr::from_ptr(s.key).to_string_lossy().into_owned()
};
let payload = if s.payload.is_null() || s.payload_len == 0 {
Vec::new()
} else {
slice::from_raw_parts(s.payload, s.payload_len).to_vec()
};
let is_delete = s.is_delete != 0;
adamo_sys::adamo_sample_free(raw);
Sample {
key,
payload,
is_delete,
}
};
Some(sample)
}
}