use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
pub struct CallMessage {
#[serde(rename = "type")]
pub msg_type: &'static str, pub call_id: String,
pub fn_name: String,
pub fn_type: FnType,
pub args: serde_json::Value,
pub auth: AuthInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub request: Option<RequestInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequestInfo {
pub method: String,
pub path: String,
pub headers: std::collections::HashMap<String, String>,
pub raw_body: String,
}
impl CallMessage {
pub fn new(
call_id: String,
fn_name: String,
fn_type: FnType,
args: serde_json::Value,
auth: AuthInfo,
) -> Self {
Self {
msg_type: "call",
call_id,
fn_name,
fn_type,
args,
auth,
request: None,
}
}
pub fn with_request(mut self, request: RequestInfo) -> Self {
self.request = Some(request);
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DbResultMessage {
#[serde(rename = "type")]
pub msg_type: &'static str, pub call_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub op_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorInfo>,
}
impl DbResultMessage {
pub fn ok(call_id: String, data: serde_json::Value) -> Self {
Self {
msg_type: "result",
call_id,
op_id: None,
data: Some(data),
error: None,
}
}
pub fn ok_with_op(call_id: String, op_id: Option<String>, data: serde_json::Value) -> Self {
Self {
msg_type: "result",
call_id,
op_id,
data: Some(data),
error: None,
}
}
pub fn err(call_id: String, code: &str, message: &str) -> Self {
Self {
msg_type: "result",
call_id,
op_id: None,
data: None,
error: Some(ErrorInfo {
code: code.to_string(),
message: message.to_string(),
}),
}
}
pub fn err_with_op(call_id: String, op_id: Option<String>, code: &str, message: &str) -> Self {
Self {
msg_type: "result",
call_id,
op_id,
data: None,
error: Some(ErrorInfo {
code: code.to_string(),
message: message.to_string(),
}),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum TsMessage {
#[serde(rename = "db")]
Db(DbOpMessage),
#[serde(rename = "stream")]
Stream(StreamChunkMessage),
#[serde(rename = "schedule")]
Schedule(ScheduleMessage),
#[serde(rename = "cancel_schedule")]
CancelSchedule(CancelScheduleMessage),
#[serde(rename = "run_fn")]
RunFn(RunFnMessage),
#[serde(rename = "return")]
Return(ReturnMessage),
#[serde(rename = "error")]
Error(ErrorMessage),
#[serde(rename = "ready")]
Ready(ReadyMessage),
}
#[derive(Debug, Clone, Deserialize)]
pub struct ReadyMessage {
#[serde(default)]
pub functions: Vec<crate::registry::FnDef>,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DbOpMessage {
pub call_id: String,
#[serde(default)]
pub op_id: Option<String>,
pub op: DbOp,
pub entity: String,
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub data: Option<serde_json::Value>,
#[serde(default)]
pub field: Option<String>,
#[serde(default)]
pub value: Option<String>,
#[serde(default)]
pub relation: Option<String>,
#[serde(default)]
pub target_id: Option<String>,
#[serde(default)]
pub after: Option<String>,
#[serde(default)]
pub limit: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DbOp {
Get,
List,
Paginate,
Insert,
Update,
Delete,
Lookup,
Query,
QueryGraph,
Link,
Unlink,
Search,
}
#[derive(Debug, Clone, Deserialize)]
pub struct StreamChunkMessage {
pub call_id: String,
pub data: String,
#[serde(default)]
pub event: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ScheduleMessage {
pub call_id: String,
pub fn_name: String,
pub args: serde_json::Value,
#[serde(default)]
pub delay_ms: Option<u64>,
#[serde(default)]
pub run_at: Option<u64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CancelScheduleMessage {
pub call_id: String,
pub schedule_id: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RunFnMessage {
pub call_id: String,
pub fn_name: String,
pub fn_type: FnType,
pub args: serde_json::Value,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ReturnMessage {
pub call_id: String,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ErrorMessage {
pub call_id: String,
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FnType {
Query,
Mutation,
Action,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
pub is_admin: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tenant_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorInfo {
pub code: String,
pub message: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn call_message_serializes() {
let msg = CallMessage::new(
"c1".into(),
"placeBid".into(),
FnType::Mutation,
serde_json::json!({"lotId": "lot_1", "amount": 100}),
AuthInfo {
user_id: Some("user_1".into()),
is_admin: false,
tenant_id: None,
},
);
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"call\""));
assert!(json.contains("\"fn_type\":\"mutation\""));
}
#[test]
fn ts_message_deserializes_db_op() {
let json = r#"{"type":"db","call_id":"c1","op":"get","entity":"Lot","id":"lot_1"}"#;
let msg: TsMessage = serde_json::from_str(json).unwrap();
match msg {
TsMessage::Db(db) => {
assert_eq!(db.call_id, "c1");
assert_eq!(db.op, DbOp::Get);
assert_eq!(db.entity, "Lot");
assert_eq!(db.id.as_deref(), Some("lot_1"));
}
_ => panic!("expected Db message"),
}
}
#[test]
fn ts_message_deserializes_stream() {
let json = r#"{"type":"stream","call_id":"c1","data":"hello"}"#;
let msg: TsMessage = serde_json::from_str(json).unwrap();
match msg {
TsMessage::Stream(s) => {
assert_eq!(s.data, "hello");
assert!(s.event.is_none());
}
_ => panic!("expected Stream message"),
}
}
#[test]
fn ts_message_deserializes_return() {
let json = r#"{"type":"return","call_id":"c1","value":{"ok":true}}"#;
let msg: TsMessage = serde_json::from_str(json).unwrap();
match msg {
TsMessage::Return(r) => {
assert_eq!(r.value, serde_json::json!({"ok": true}));
}
_ => panic!("expected Return message"),
}
}
#[test]
fn ts_message_deserializes_schedule() {
let json = r#"{"type":"schedule","call_id":"c1","fn_name":"closeLot","args":{"lotId":"x"},"delay_ms":5000}"#;
let msg: TsMessage = serde_json::from_str(json).unwrap();
match msg {
TsMessage::Schedule(s) => {
assert_eq!(s.fn_name, "closeLot");
assert_eq!(s.delay_ms, Some(5000));
}
_ => panic!("expected Schedule message"),
}
}
#[test]
fn db_result_ok() {
let msg = DbResultMessage::ok("c1".into(), serde_json::json!({"id": "x"}));
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"result\""));
assert!(!json.contains("\"error\""));
}
#[test]
fn db_result_err() {
let msg = DbResultMessage::err("c1".into(), "NOT_FOUND", "not found");
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"error\""));
assert!(!json.contains("\"data\""));
}
}