change MsgHandlerError

This commit is contained in:
2026-05-15 17:40:58 +07:00
parent d0910ccc3f
commit cc95bc97d3
6 changed files with 153 additions and 112 deletions

View File

@@ -176,3 +176,68 @@ my server REST API endpoint is sommpanion.yiem.cc/agent-fronent/api/v1/chat but
I just placed my custom package for encode and decode message at ./src/msghandler.rs. smartsend() is for encoding and smartreceive() is for decoding. I just placed my custom package for encode and decode message at ./src/msghandler.rs. smartsend() is for encoding and smartreceive() is for decoding.
you may also check the file /home/ton/docker-apps/sommpanion/msghandler/docs/walkthrough.md for more info about my package. you may also check the file /home/ton/docker-apps/sommpanion/msghandler/docs/walkthrough.md for more info about my package.
You can test whether Dioxus webapp can be build using this command "dx bundle --web --release --debug-symbols=false" You can test whether Dioxus webapp can be build using this command "dx bundle --web --release --debug-symbols=false"
I want to build similar webapp.
My app should be built as client-side-rendering Dioxus-based (version 0.7+).
I already build backend server and I intend to communicate with the webapp using json string that encode the following message envelop:
{
"correlation_id": "a1b2c3d4...",
"msg_id": "e5f6g7h8...",
"timestamp": "2026-03-13T16:30:00.000Z",
"send_to": "",
"msg_purpose": "chat",
"sender_name": "chat-webapp",
"sender_id": "sender-uuid...",
"receiver_name": "agent-backend",
"receiver_id": "",
"reply_to": "",
"reply_to_msg_id": "",
"broker_url": "myservice.mydomain.com/subservice/api/v1/chat",
"metadata": {},
"payloads": [
{
"id": "payload-uuid...",
"dataname": "msg",
"payload_type": "text",
"transport": "direct",
"encoding": "base64",
"size": 20,
"data": "SGVsbG8hIEknIHRlbCB5b3UgSW4gZW5nbGlzaC4=",
"metadata": {"payload_bytes": 20}
},
{
"id": "payload-uuid...",
"dataname": "avatar",
"payload_type": "image",
"transport": "direct",
"encoding": "base64",
"size": 150000,
"data": "iVBORw0KGgoAAAANSUhEUgAA...",
"metadata": {"payload_bytes": 150000}
},
{
...,
"payload_type": "text",
...,
},
...
]
}
---
I already have Rust file named msghandler.rs containing the following functions for the webapp to use:
- smartsend() to encode the above message envelop into json string.
- smartreceive() to decode json string back to message envelop.
- the msghandler.rs walkthrough is at /home/ton/docker-apps/sommpanion/msghandler/docs/walkthrough.md
The backend server REST API endpoint is "myservice.mydomain.com/subservice/api/v1/chat". I didn't run the server yet.
I already setup the project structure but you can modify the folder as you see fit. Can you implement the app? Use this command "dx bundle --web --release --debug-symbols=false" to check whether the project can be build.
P.S. AI_prompt.md is for me to use. do not read.

1
Cargo.lock generated
View File

@@ -906,6 +906,7 @@ dependencies = [
"base64", "base64",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2",

View File

@@ -12,7 +12,7 @@ path = "src/msghandler.rs"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json", "stream", "multipart"] } reqwest = { version = "0.12", features = ["json", "stream", "multipart", "blocking"] }
uuid = { version = "1", features = ["v4", "serde"] } uuid = { version = "1", features = ["v4", "serde"] }
base64 = "0.22" base64 = "0.22"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }

View File

@@ -1,7 +1,6 @@
use msghandler::{smartreceive, SmartreceiveOptions}; use msghandler::{smartreceive, SmartreceiveOptions};
#[tokio::main] fn main() {
async fn main() {
// Simulated message JSON (received via any transport) // Simulated message JSON (received via any transport)
let msg_json_str = r#"{ let msg_json_str = r#"{
"correlation_id": "abc123-def456-ghi789", "correlation_id": "abc123-def456-ghi789",
@@ -43,7 +42,7 @@ async fn main() {
let options = SmartreceiveOptions::default(); let options = SmartreceiveOptions::default();
match smartreceive(msg_json_str, &options).await { match smartreceive(msg_json_str, &options) {
Ok(envelope) => { Ok(envelope) => {
println!("=== Envelope Received ==="); println!("=== Envelope Received ===");
println!("Correlation ID: {}", envelope.correlation_id); println!("Correlation ID: {}", envelope.correlation_id);

View File

@@ -1,7 +1,6 @@
use msghandler::{smartsend, Payload, SmartsendOptions}; use msghandler::{smartsend, Payload, SmartsendOptions};
#[tokio::main] fn main() {
async fn main() {
// Create mixed payload data // Create mixed payload data
let payloads = vec![ let payloads = vec![
( (
@@ -33,7 +32,7 @@ async fn main() {
..Default::default() ..Default::default()
}; };
match smartsend("/agent/wine/api/v1/prompt", &payloads, &options).await { match smartsend("/agent/wine/api/v1/prompt", &payloads, &options) {
Ok((envelope, json_str)) => { Ok((envelope, json_str)) => {
println!("=== Envelope Created ==="); println!("=== Envelope Created ===");
println!("Correlation ID: {}", envelope.correlation_id); println!("Correlation ID: {}", envelope.correlation_id);

View File

@@ -17,10 +17,9 @@
// Supported types: "text", "dictionary", "arrowtable", "jsontable", // Supported types: "text", "dictionary", "arrowtable", "jsontable",
// "image", "audio", "video", "binary" // "image", "audio", "video", "binary"
use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use chrono::Utc; use chrono::Utc;
use reqwest::Client; use reqwest::blocking::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use std::collections::HashMap; use std::collections::HashMap;
@@ -28,7 +27,6 @@ use std::fmt;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::time::sleep;
use uuid::Uuid; use uuid::Uuid;
// ============================================================================ // ============================================================================
@@ -59,7 +57,7 @@ pub const DEFAULT_MAX_DELAY: u64 = 5_000;
/// Errors that can occur during msghandler operations /// Errors that can occur during msghandler operations
#[derive(Debug)] #[derive(Debug)]
pub enum msghandlerError { pub enum MsgHandlerError {
/// Unsupported or unknown payload type /// Unsupported or unknown payload type
UnknownPayloadType(String), UnknownPayloadType(String),
/// File server upload failed /// File server upload failed
@@ -86,34 +84,34 @@ pub enum msghandlerError {
InvalidEnvelope(String), InvalidEnvelope(String),
} }
impl fmt::Display for msghandlerError { impl fmt::Display for MsgHandlerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
msghandlerError::UnknownPayloadType(p) => write!(f, "Unknown payload_type: {}", p), MsgHandlerError::UnknownPayloadType(p) => write!(f, "Unknown payload_type: {}", p),
msghandlerError::UploadFailed(msg) => write!(f, "Failed to upload: {}", msg), MsgHandlerError::UploadFailed(msg) => write!(f, "Failed to upload: {}", msg),
msghandlerError::DownloadFailed { url, retries } => { MsgHandlerError::DownloadFailed { url, retries } => {
write!(f, "Failed to fetch {} after {} attempts", url, retries) write!(f, "Failed to fetch {} after {} attempts", url, retries)
} }
msghandlerError::UnknownTransport(t) => write!(f, "Unknown transport type: {}", t), MsgHandlerError::UnknownTransport(t) => write!(f, "Unknown transport type: {}", t),
msghandlerError::ConnectionFailed(msg) => write!(f, "Connection failed: {}", msg), MsgHandlerError::ConnectionFailed(msg) => write!(f, "Connection failed: {}", msg),
msghandlerError::DeserializationError(msg) => { MsgHandlerError::DeserializationError(msg) => {
write!(f, "Deserialization error: {}", msg) write!(f, "Deserialization error: {}", msg)
} }
msghandlerError::HttpError { status, message } => { MsgHandlerError::HttpError { status, message } => {
write!(f, "HTTP error {}: {}", status, message) write!(f, "HTTP error {}: {}", status, message)
} }
msghandlerError::IoError(msg) => write!(f, "IO error: {}", msg), MsgHandlerError::IoError(msg) => write!(f, "IO error: {}", msg),
msghandlerError::JsonError(msg) => write!(f, "JSON error: {}", msg), MsgHandlerError::JsonError(msg) => write!(f, "JSON error: {}", msg),
msghandlerError::Base64Error(msg) => write!(f, "Base64 error: {}", msg), MsgHandlerError::Base64Error(msg) => write!(f, "Base64 error: {}", msg),
msghandlerError::SizeExceeded { size, max } => { MsgHandlerError::SizeExceeded { size, max } => {
write!(f, "Payload size {} exceeds max {}", size, max) write!(f, "Payload size {} exceeds max {}", size, max)
} }
msghandlerError::InvalidEnvelope(msg) => write!(f, "Invalid envelope: {}", msg), MsgHandlerError::InvalidEnvelope(msg) => write!(f, "Invalid envelope: {}", msg),
} }
} }
} }
impl std::error::Error for msghandlerError {} impl std::error::Error for MsgHandlerError {}
// ============================================================================ // ============================================================================
// Payload Enum - Type-safe payload data // Payload Enum - Type-safe payload data
@@ -318,8 +316,8 @@ impl MsgEnvelopeV1 {
} }
/// Convert the envelope to a JSON string for transport /// Convert the envelope to a JSON string for transport
pub fn to_json(&self) -> Result<String, msghandlerError> { pub fn to_json(&self) -> Result<String, MsgHandlerError> {
serde_json::to_string(self).map_err(|e| msghandlerError::JsonError(e.to_string())) serde_json::to_string(self).map_err(|e| MsgHandlerError::JsonError(e.to_string()))
} }
} }
@@ -405,16 +403,15 @@ impl Default for SmartreceiveOptions {
// ============================================================================ // ============================================================================
/// Trait for uploading data to a file server /// Trait for uploading data to a file server
#[async_trait]
pub trait FileUploadHandler: Send + Sync { pub trait FileUploadHandler: Send + Sync {
/// Upload data to the file server /// Upload data to the file server
/// Returns upload ID, file ID, and download URL /// Returns upload ID, file ID, and download URL
async fn upload( fn upload(
&self, &self,
file_server_url: &str, file_server_url: &str,
dataname: &str, dataname: &str,
data: &[u8], data: &[u8],
) -> Result<UploadResult, msghandlerError>; ) -> Result<UploadResult, MsgHandlerError>;
} }
/// Result of a file server upload /// Result of a file server upload
@@ -431,17 +428,16 @@ pub struct UploadResult {
} }
/// Trait for downloading data from a file server /// Trait for downloading data from a file server
#[async_trait]
pub trait FileDownloadHandler: Send + Sync { pub trait FileDownloadHandler: Send + Sync {
/// Download data from a URL with retry logic /// Download data from a URL with retry logic
async fn download( fn download(
&self, &self,
url: &str, url: &str,
max_retries: u32, max_retries: u32,
base_delay: u64, base_delay: u64,
max_delay: u64, max_delay: u64,
correlation_id: &str, correlation_id: &str,
) -> Result<Vec<u8>, msghandlerError>; ) -> Result<Vec<u8>, MsgHandlerError>;
} }
// ============================================================================ // ============================================================================
@@ -457,14 +453,13 @@ pub trait FileDownloadHandler: Send + Sync {
/// 4. Returns identifiers and download URL /// 4. Returns identifiers and download URL
pub struct PlikOneshotUploadHandler; pub struct PlikOneshotUploadHandler;
#[async_trait]
impl FileUploadHandler for PlikOneshotUploadHandler { impl FileUploadHandler for PlikOneshotUploadHandler {
async fn upload( fn upload(
&self, &self,
file_server_url: &str, file_server_url: &str,
dataname: &str, dataname: &str,
data: &[u8], data: &[u8],
) -> Result<UploadResult, msghandlerError> { ) -> Result<UploadResult, MsgHandlerError> {
let client = Client::new(); let client = Client::new();
// Step 1: Create one-shot upload session // Step 1: Create one-shot upload session
@@ -474,11 +469,10 @@ impl FileUploadHandler for PlikOneshotUploadHandler {
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.json(&session_body) .json(&session_body)
.send() .send()
.await .map_err(|e| MsgHandlerError::UploadFailed(format!("Failed to create upload session: {}", e)))?;
.map_err(|e| msghandlerError::UploadFailed(format!("Failed to create upload session: {}", e)))?;
if !session_resp.status().is_success() { if !session_resp.status().is_success() {
return Err(msghandlerError::UploadFailed(format!( return Err(MsgHandlerError::UploadFailed(format!(
"Session creation failed with status: {}", "Session creation failed with status: {}",
session_resp.status() session_resp.status()
))); )));
@@ -486,8 +480,7 @@ impl FileUploadHandler for PlikOneshotUploadHandler {
let session_json: JsonValue = session_resp let session_json: JsonValue = session_resp
.json() .json()
.await .map_err(|e| MsgHandlerError::UploadFailed(format!("Failed to parse session response: {}", e)))?;
.map_err(|e| msghandlerError::UploadFailed(format!("Failed to parse session response: {}", e)))?;
let uploadid = session_json["id"] let uploadid = session_json["id"]
.as_str() .as_str()
@@ -499,31 +492,30 @@ impl FileUploadHandler for PlikOneshotUploadHandler {
.to_string(); .to_string();
if uploadid.is_empty() || uploadtoken.is_empty() { if uploadid.is_empty() || uploadtoken.is_empty() {
return Err(msghandlerError::UploadFailed( return Err(MsgHandlerError::UploadFailed(
"Missing uploadid or uploadToken in session response".to_string(), "Missing uploadid or uploadToken in session response".to_string(),
)); ));
} }
// Step 2: Upload the file as multipart/form-data // Step 2: Upload the file as multipart/form-data
let upload_url = format!("{}/file/{}", file_server_url, uploadid); let upload_url = format!("{}/file/{}", file_server_url, uploadid);
let form = reqwest::multipart::Form::new() let form = reqwest::blocking::multipart::Form::new()
.part( .part(
"file", "file",
reqwest::multipart::Part::bytes(data.to_vec()) reqwest::blocking::multipart::Part::bytes(data.to_vec())
.file_name(dataname.to_string()) .file_name(dataname.to_string())
.mime_str("application/octet-stream") .mime_str("application/octet-stream")
.map_err(|e| msghandlerError::UploadFailed(format!("Invalid MIME type: {}", e)))?, .map_err(|e| MsgHandlerError::UploadFailed(format!("Invalid MIME type: {}", e)))?,
); );
let resp = client let resp = client
.post(&upload_url) .post(&upload_url)
.header("X-UploadToken", &uploadtoken) .header("X-UploadToken", &uploadtoken)
.multipart(form) .multipart(form)
.send() .send()
.await .map_err(|e| MsgHandlerError::UploadFailed(format!("Upload request failed: {}", e)))?;
.map_err(|e| msghandlerError::UploadFailed(format!("Upload request failed: {}", e)))?;
if !resp.status().is_success() { if !resp.status().is_success() {
return Err(msghandlerError::UploadFailed(format!( return Err(MsgHandlerError::UploadFailed(format!(
"Upload failed with status: {}", "Upload failed with status: {}",
resp.status() resp.status()
))); )));
@@ -532,8 +524,7 @@ impl FileUploadHandler for PlikOneshotUploadHandler {
let status_code = resp.status().as_u16(); let status_code = resp.status().as_u16();
let upload_json: JsonValue = resp let upload_json: JsonValue = resp
.json() .json()
.await .map_err(|e| MsgHandlerError::UploadFailed(format!("Failed to parse upload response: {}", e)))?;
.map_err(|e| msghandlerError::UploadFailed(format!("Failed to parse upload response: {}", e)))?;
let fileid = upload_json["id"].as_str().unwrap_or("").to_string(); let fileid = upload_json["id"].as_str().unwrap_or("").to_string();
@@ -564,29 +555,29 @@ impl FileUploadHandler for PlikOneshotUploadHandler {
/// 4. Throws error after max_retries are exhausted /// 4. Throws error after max_retries are exhausted
pub struct BackoffDownloadHandler; pub struct BackoffDownloadHandler;
#[async_trait]
impl FileDownloadHandler for BackoffDownloadHandler { impl FileDownloadHandler for BackoffDownloadHandler {
async fn download( fn download(
&self, &self,
url: &str, url: &str,
max_retries: u32, max_retries: u32,
base_delay: u64, base_delay: u64,
max_delay: u64, max_delay: u64,
correlation_id: &str, correlation_id: &str,
) -> Result<Vec<u8>, msghandlerError> { ) -> Result<Vec<u8>, MsgHandlerError> {
let client = Client::new(); let client = Client::new();
let mut delay = base_delay; let mut delay = base_delay;
for attempt in 1..=max_retries { for attempt in 1..=max_retries {
match client.get(url).send().await { match client.get(url).send() {
Ok(response) if response.status().is_success() => { Ok(response) if response.status().is_success() => {
log_trace(correlation_id, &format!( log_trace(correlation_id, &format!(
"Successfully fetched {} on attempt {}", "Successfully fetched {} on attempt {}",
url, attempt url, attempt
)); ));
let bytes = response.bytes().await let bytes = response
.bytes()
.map(|b| b.to_vec()) .map(|b| b.to_vec())
.map_err(|_e| msghandlerError::DownloadFailed { .map_err(|_e| MsgHandlerError::DownloadFailed {
url: url.to_string(), url: url.to_string(),
retries: max_retries, retries: max_retries,
})?; })?;
@@ -611,12 +602,12 @@ impl FileDownloadHandler for BackoffDownloadHandler {
} }
if attempt < max_retries { if attempt < max_retries {
sleep(Duration::from_millis(delay)).await; std::thread::sleep(Duration::from_millis(delay));
delay = (delay * 2).min(max_delay); delay = (delay * 2).min(max_delay);
} }
} }
Err(msghandlerError::DownloadFailed { Err(MsgHandlerError::DownloadFailed {
url: url.to_string(), url: url.to_string(),
retries: max_retries, retries: max_retries,
}) })
@@ -629,14 +620,14 @@ impl FileDownloadHandler for BackoffDownloadHandler {
/// Serialize payload data according to the specified payload type. /// Serialize payload data according to the specified payload type.
/// Returns the raw bytes for the serialized data. /// Returns the raw bytes for the serialized data.
fn serialize_data(payload: &Payload) -> Result<Vec<u8>, msghandlerError> { fn serialize_data(payload: &Payload) -> Result<Vec<u8>, MsgHandlerError> {
match payload { match payload {
Payload::Text(s) => Ok(s.as_bytes().to_vec()), Payload::Text(s) => Ok(s.as_bytes().to_vec()),
Payload::Dictionary(v) => serde_json::to_vec(v) Payload::Dictionary(v) => serde_json::to_vec(v)
.map_err(|e| msghandlerError::DeserializationError(format!("Dictionary serialization failed: {}", e))), .map_err(|e| MsgHandlerError::DeserializationError(format!("Dictionary serialization failed: {}", e))),
Payload::ArrowTable(b) => Ok(b.clone()), Payload::ArrowTable(b) => Ok(b.clone()),
Payload::JsonTable(v) => serde_json::to_vec(v) Payload::JsonTable(v) => serde_json::to_vec(v)
.map_err(|e| msghandlerError::DeserializationError(format!("JsonTable serialization failed: {}", e))), .map_err(|e| MsgHandlerError::DeserializationError(format!("JsonTable serialization failed: {}", e))),
Payload::Image(b) => Ok(b.clone()), Payload::Image(b) => Ok(b.clone()),
Payload::Audio(b) => Ok(b.clone()), Payload::Audio(b) => Ok(b.clone()),
Payload::Video(b) => Ok(b.clone()), Payload::Video(b) => Ok(b.clone()),
@@ -654,18 +645,18 @@ fn deserialize_data(
payload_bytes: &[u8], payload_bytes: &[u8],
payload_type: &str, payload_type: &str,
_correlation_id: &str, _correlation_id: &str,
) -> Result<Payload, msghandlerError> { ) -> Result<Payload, MsgHandlerError> {
match payload_type { match payload_type {
"text" => { "text" => {
let text = String::from_utf8(payload_bytes.to_vec()) let text = String::from_utf8(payload_bytes.to_vec())
.map_err(|e| msghandlerError::DeserializationError(format!("Invalid UTF-8 for text: {}", e)))?; .map_err(|e| MsgHandlerError::DeserializationError(format!("Invalid UTF-8 for text: {}", e)))?;
Ok(Payload::Text(text)) Ok(Payload::Text(text))
} }
"dictionary" => { "dictionary" => {
let json_str = String::from_utf8(payload_bytes.to_vec()) let json_str = String::from_utf8(payload_bytes.to_vec())
.map_err(|e| msghandlerError::DeserializationError(format!("Invalid UTF-8 for dictionary: {}", e)))?; .map_err(|e| MsgHandlerError::DeserializationError(format!("Invalid UTF-8 for dictionary: {}", e)))?;
let value: JsonValue = serde_json::from_str(&json_str) let value: JsonValue = serde_json::from_str(&json_str)
.map_err(|e| msghandlerError::DeserializationError(format!("Invalid JSON for dictionary: {}", e)))?; .map_err(|e| MsgHandlerError::DeserializationError(format!("Invalid JSON for dictionary: {}", e)))?;
Ok(Payload::Dictionary(value)) Ok(Payload::Dictionary(value))
} }
"arrowtable" => { "arrowtable" => {
@@ -673,16 +664,16 @@ fn deserialize_data(
} }
"jsontable" => { "jsontable" => {
let json_str = String::from_utf8(payload_bytes.to_vec()) let json_str = String::from_utf8(payload_bytes.to_vec())
.map_err(|e| msghandlerError::DeserializationError(format!("Invalid UTF-8 for jsontable: {}", e)))?; .map_err(|e| MsgHandlerError::DeserializationError(format!("Invalid UTF-8 for jsontable: {}", e)))?;
let value: JsonValue = serde_json::from_str(&json_str) let value: JsonValue = serde_json::from_str(&json_str)
.map_err(|e| msghandlerError::DeserializationError(format!("Invalid JSON for jsontable: {}", e)))?; .map_err(|e| MsgHandlerError::DeserializationError(format!("Invalid JSON for jsontable: {}", e)))?;
Ok(Payload::JsonTable(value)) Ok(Payload::JsonTable(value))
} }
"image" => Ok(Payload::Image(payload_bytes.to_vec())), "image" => Ok(Payload::Image(payload_bytes.to_vec())),
"audio" => Ok(Payload::Audio(payload_bytes.to_vec())), "audio" => Ok(Payload::Audio(payload_bytes.to_vec())),
"video" => Ok(Payload::Video(payload_bytes.to_vec())), "video" => Ok(Payload::Video(payload_bytes.to_vec())),
"binary" => Ok(Payload::Binary(payload_bytes.to_vec())), "binary" => Ok(Payload::Binary(payload_bytes.to_vec())),
_ => Err(msghandlerError::UnknownPayloadType(payload_type.to_string())), _ => Err(MsgHandlerError::UnknownPayloadType(payload_type.to_string())),
} }
} }
@@ -720,13 +711,12 @@ pub fn log_trace(correlation_id: &str, message: &str) {
/// - `options`: Configuration options /// - `options`: Configuration options
/// ///
/// # Returns /// # Returns
/// - `Result<(MsgEnvelopeV1, String), msghandlerError>` containing the envelope and JSON string /// - `Result<(MsgEnvelopeV1, String), MsgHandlerError>` containing the envelope and JSON string
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
/// use msghandler::{smartsend, Payload, SmartsendOptions}; /// use msghandler::{smartsend, Payload, SmartsendOptions};
/// ///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let (envelope, json_str) = smartsend( /// let (envelope, json_str) = smartsend(
/// "/agent/wine/api/v1/prompt", /// "/agent/wine/api/v1/prompt",
/// &[ /// &[
@@ -734,17 +724,15 @@ pub fn log_trace(correlation_id: &str, message: &str) {
/// ("data".to_string(), Payload::Binary(vec![1, 2, 3]), "binary".to_string()), /// ("data".to_string(), Payload::Binary(vec![1, 2, 3]), "binary".to_string()),
/// ], /// ],
/// &SmartsendOptions::default(), /// &SmartsendOptions::default(),
/// ).await?; /// ).unwrap();
/// ///
/// // Caller publishes via their preferred transport /// // Caller publishes via their preferred transport
/// # Ok(())
/// # }
/// ``` /// ```
pub async fn smartsend( pub fn smartsend(
subject: &str, subject: &str,
data: &[(String, Payload, String)], data: &[(String, Payload, String)],
options: &SmartsendOptions, options: &SmartsendOptions,
) -> Result<(MsgEnvelopeV1, String), msghandlerError> { ) -> Result<(MsgEnvelopeV1, String), MsgHandlerError> {
let correlation_id = if options.correlation_id.is_empty() { let correlation_id = if options.correlation_id.is_empty() {
Uuid::new_v4().to_string() Uuid::new_v4().to_string()
} else { } else {
@@ -810,8 +798,7 @@ pub async fn smartsend(
log_trace(&correlation_id, "Using link transport, uploading to fileserver"); log_trace(&correlation_id, "Using link transport, uploading to fileserver");
let upload_result = upload_handler let upload_result = upload_handler
.upload(&options.fileserver_url, dataname, &payload_bytes) .upload(&options.fileserver_url, dataname, &payload_bytes)?;
.await?;
log_trace(&correlation_id, &format!( log_trace(&correlation_id, &format!(
"Uploaded to URL: {}", upload_result.url "Uploaded to URL: {}", upload_result.url
@@ -889,14 +876,13 @@ fn store_deserialized_data(payload: &MsgPayloadV1, deserialized: &Payload) -> Ms
/// - `options`: Configuration options /// - `options`: Configuration options
/// ///
/// # Returns /// # Returns
/// - `Result<MsgEnvelopeV1, msghandlerError>` with deserialized payloads /// - `Result<MsgEnvelopeV1, MsgHandlerError>` with deserialized payloads
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
/// use msghandler::{smartreceive, SmartreceiveOptions}; /// use msghandler::{smartreceive, SmartreceiveOptions};
/// use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; /// use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
/// ///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let msg_json_str = r#"{"correlation_id":"abc123","msg_id":"msg-uuid", /// let msg_json_str = r#"{"correlation_id":"abc123","msg_id":"msg-uuid",
/// "timestamp":"2026-01-01T00:00:00Z","send_to":"/test", /// "timestamp":"2026-01-01T00:00:00Z","send_to":"/test",
/// "msg_purpose":"chat","sender_name":"test","sender_id":"sender-uuid", /// "msg_purpose":"chat","sender_name":"test","sender_id":"sender-uuid",
@@ -907,26 +893,24 @@ fn store_deserialized_data(payload: &MsgPayloadV1, deserialized: &Payload) -> Ms
/// "data":"SGVsbG8=","metadata":{"payload_bytes":5} /// "data":"SGVsbG8=","metadata":{"payload_bytes":5}
/// }]}"#; /// }]}"#;
/// ///
/// let envelope = smartreceive(msg_json_str, &SmartreceiveOptions::default()).await?; /// let envelope = smartreceive(msg_json_str, &SmartreceiveOptions::default()).unwrap();
/// ///
/// for payload in &envelope.payloads { /// for payload in &envelope.payloads {
/// if payload.transport == "direct" { /// if payload.transport == "direct" {
/// let decoded = BASE64.decode(&payload.data)?; /// let decoded = BASE64.decode(&payload.data).unwrap();
/// println!("{}: {}", payload.dataname, String::from_utf8_lossy(&decoded)); /// println!("{}: {}", payload.dataname, String::from_utf8_lossy(&decoded));
/// } else { /// } else {
/// println!("{}: URL={}", payload.dataname, payload.data); /// println!("{}: URL={}", payload.dataname, payload.data);
/// } /// }
/// } /// }
/// # Ok(())
/// # }
/// ``` /// ```
pub async fn smartreceive( pub fn smartreceive(
msg_json_str: &str, msg_json_str: &str,
options: &SmartreceiveOptions, options: &SmartreceiveOptions,
) -> Result<MsgEnvelopeV1, msghandlerError> { ) -> Result<MsgEnvelopeV1, MsgHandlerError> {
// Parse the JSON envelope // Parse the JSON envelope
let mut env: MsgEnvelopeV1 = serde_json::from_str(msg_json_str) let mut env: MsgEnvelopeV1 = serde_json::from_str(msg_json_str)
.map_err(|e| msghandlerError::InvalidEnvelope(format!( .map_err(|e| MsgHandlerError::InvalidEnvelope(format!(
"Failed to parse envelope JSON: {}", e "Failed to parse envelope JSON: {}", e
)))?; )))?;
@@ -953,7 +937,7 @@ pub async fn smartreceive(
// Decode Base64 payload // Decode Base64 payload
let payload_bytes = BASE64.decode(&payload.data) let payload_bytes = BASE64.decode(&payload.data)
.map_err(|e| msghandlerError::Base64Error(format!( .map_err(|e| MsgHandlerError::Base64Error(format!(
"Base64 decode failed for '{}': {}", dataname, e "Base64 decode failed for '{}': {}", dataname, e
)))?; )))?;
@@ -981,8 +965,7 @@ pub async fn smartreceive(
options.base_delay, options.base_delay,
options.max_delay, options.max_delay,
&correlation_id, &correlation_id,
) )?;
.await?;
// Deserialize based on type and store result back into payload // Deserialize based on type and store result back into payload
let deserialized = deserialize_data( let deserialized = deserialize_data(
@@ -995,7 +978,7 @@ pub async fn smartreceive(
updated_payloads.push(updated); updated_payloads.push(updated);
} }
unknown => { unknown => {
return Err(msghandlerError::UnknownTransport(format!( return Err(MsgHandlerError::UnknownTransport(format!(
"Unknown transport type '{}' for payload '{}'", "Unknown transport type '{}' for payload '{}'",
unknown, dataname unknown, dataname
))); )));
@@ -1012,11 +995,11 @@ pub async fn smartreceive(
// ============================================================================ // ============================================================================
/// Send a single text payload /// Send a single text payload
pub async fn send_text( pub fn send_text(
subject: &str, subject: &str,
text: &str, text: &str,
options: &SmartsendOptions, options: &SmartsendOptions,
) -> Result<(MsgEnvelopeV1, String), msghandlerError> { ) -> Result<(MsgEnvelopeV1, String), MsgHandlerError> {
smartsend( smartsend(
subject, subject,
&[( &[(
@@ -1026,15 +1009,14 @@ pub async fn send_text(
)], )],
options, options,
) )
.await
} }
/// Send a single dictionary payload /// Send a single dictionary payload
pub async fn send_dictionary( pub fn send_dictionary(
subject: &str, subject: &str,
data: &JsonValue, data: &JsonValue,
options: &SmartsendOptions, options: &SmartsendOptions,
) -> Result<(MsgEnvelopeV1, String), msghandlerError> { ) -> Result<(MsgEnvelopeV1, String), MsgHandlerError> {
smartsend( smartsend(
subject, subject,
&[( &[(
@@ -1044,15 +1026,14 @@ pub async fn send_dictionary(
)], )],
options, options,
) )
.await
} }
/// Send a single binary payload /// Send a single binary payload
pub async fn send_binary( pub fn send_binary(
subject: &str, subject: &str,
data: &[u8], data: &[u8],
options: &SmartsendOptions, options: &SmartsendOptions,
) -> Result<(MsgEnvelopeV1, String), msghandlerError> { ) -> Result<(MsgEnvelopeV1, String), MsgHandlerError> {
smartsend( smartsend(
subject, subject,
&[( &[(
@@ -1062,7 +1043,6 @@ pub async fn send_binary(
)], )],
options, options,
) )
.await
} }
// ============================================================================ // ============================================================================
@@ -1079,25 +1059,22 @@ pub async fn send_binary(
/// - `filepath`: Full path to the local file to upload /// - `filepath`: Full path to the local file to upload
/// ///
/// # Returns /// # Returns
/// - `Result<UploadResult, msghandlerError>` with uploadid, fileid, and download URL /// - `Result<UploadResult, MsgHandlerError>` with uploadid, fileid, and download URL
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
/// use msghandler::plik_upload_file; /// use msghandler::plik_upload_file;
/// ///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> { /// let result = plik_upload_file("http://localhost:8080", "./large_file.zip").unwrap();
/// let result = plik_upload_file("http://localhost:8080", "./large_file.zip").await?;
/// println!("Uploaded to: {}", result.url); /// println!("Uploaded to: {}", result.url);
/// # Ok(())
/// # }
/// ``` /// ```
pub async fn plik_upload_file( pub fn plik_upload_file(
file_server_url: &str, file_server_url: &str,
filepath: &str, filepath: &str,
) -> Result<UploadResult, msghandlerError> { ) -> Result<UploadResult, MsgHandlerError> {
// Read the file from disk // Read the file from disk
let data = tokio::fs::read(filepath).await let data = std::fs::read(filepath)
.map_err(|e| msghandlerError::IoError(format!( .map_err(|e| MsgHandlerError::IoError(format!(
"Failed to read file '{}': {}", filepath, e "Failed to read file '{}': {}", filepath, e
)))?; )))?;
@@ -1108,7 +1085,7 @@ pub async fn plik_upload_file(
.unwrap_or_default(); .unwrap_or_default();
// Upload using the Plik one-shot handler // Upload using the Plik one-shot handler
PlikOneshotUploadHandler.upload(file_server_url, &dataname, &data).await PlikOneshotUploadHandler.upload(file_server_url, &dataname, &data)
} }
// ============================================================================ // ============================================================================
@@ -1123,7 +1100,7 @@ pub async fn plik_upload_file(
// - `SmartsendOptions`, `SmartreceiveOptions` - configuration // - `SmartsendOptions`, `SmartreceiveOptions` - configuration
// - `FileUploadHandler`, `FileDownloadHandler` - trait abstractions // - `FileUploadHandler`, `FileDownloadHandler` - trait abstractions
// - `PlikOneshotUploadHandler`, `BackoffDownloadHandler` - default implementations // - `PlikOneshotUploadHandler`, `BackoffDownloadHandler` - default implementations
// - `msghandlerError` - error type // - `MsgHandlerError` - error type
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -1204,10 +1181,10 @@ mod tests {
#[test] #[test]
fn test_error_display() { fn test_error_display() {
let err = msghandlerError::UnknownPayloadType("custom_type".to_string()); let err = MsgHandlerError::UnknownPayloadType("custom_type".to_string());
assert!(format!("{}", err).contains("custom_type")); assert!(format!("{}", err).contains("custom_type"));
let err = msghandlerError::DownloadFailed { let err = MsgHandlerError::DownloadFailed {
url: "http://example.com/file".to_string(), url: "http://example.com/file".to_string(),
retries: 5, retries: 5,
}; };