use serde::{Deserialize, Serialize};
pub const ALPN: &[u8] = b"alink/1";
pub const MAX_FRAME: usize = 16 * 1024 * 1024;
pub const MAX_ATTACHMENTS: usize = 8 * 1024 * 1024;
pub const ENVELOPE_VERSION: u8 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Frame {
Deliver { envelope: Envelope },
Join {
invite_id: String,
secret: String,
card: PeerCard,
addr: Option<iroh::EndpointAddr>,
},
GetCard,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Reply {
Ack {
id: String,
},
Rejected {
reason: String,
},
Joined {
card: PeerCard,
},
Card {
card: PeerCard,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
pub v: u8,
pub id: String,
pub thread_id: String,
pub reply_to: Option<String>,
pub created_at: u64,
#[serde(flatten)]
pub payload: Payload,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Payload {
Message {
text: String,
#[serde(default)]
attachments: Vec<Attachment>,
},
Request {
handler: String,
prompt: String,
#[serde(default)]
attachments: Vec<Attachment>,
},
Status {
request_id: String,
state: RequestState,
note: Option<String>,
},
Response {
request_id: String,
state: RequestState,
text: String,
#[serde(default)]
attachments: Vec<Attachment>,
},
Cancel { request_id: String },
}
impl Payload {
pub fn kind(&self) -> &'static str {
match self {
Payload::Message { .. } => "message",
Payload::Request { .. } => "request",
Payload::Status { .. } => "status",
Payload::Response { .. } => "response",
Payload::Cancel { .. } => "cancel",
}
}
pub fn text(&self) -> Option<&str> {
match self {
Payload::Message { text, .. } | Payload::Response { text, .. } => Some(text),
Payload::Request { prompt, .. } => Some(prompt),
Payload::Status { note, .. } => note.as_deref(),
Payload::Cancel { .. } => None,
}
}
pub fn attachments(&self) -> &[Attachment] {
match self {
Payload::Message { attachments, .. }
| Payload::Request { attachments, .. }
| Payload::Response { attachments, .. } => attachments,
_ => &[],
}
}
pub fn request_id(&self) -> Option<&str> {
match self {
Payload::Status { request_id, .. }
| Payload::Response { request_id, .. }
| Payload::Cancel { request_id } => Some(request_id),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RequestState {
Submitted,
Working,
Completed,
Failed,
Canceled,
Rejected,
}
impl RequestState {
pub fn as_str(self) -> &'static str {
match self {
RequestState::Submitted => "submitted",
RequestState::Working => "working",
RequestState::Completed => "completed",
RequestState::Failed => "failed",
RequestState::Canceled => "canceled",
RequestState::Rejected => "rejected",
}
}
pub fn parse(text: &str) -> Option<Self> {
Some(match text {
"submitted" => RequestState::Submitted,
"working" => RequestState::Working,
"completed" => RequestState::Completed,
"failed" => RequestState::Failed,
"canceled" => RequestState::Canceled,
"rejected" => RequestState::Rejected,
_ => return None,
})
}
pub fn is_terminal(self) -> bool {
!matches!(self, RequestState::Submitted | RequestState::Working)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attachment {
pub name: String,
pub media_type: String,
#[serde(with = "base64_bytes")]
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerCard {
pub v: u8,
pub id: String,
pub name: String,
#[serde(default)]
pub handlers: Vec<HandlerInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandlerInfo {
pub name: String,
pub description: Option<String>,
}
mod base64_bytes {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&data_encoding::BASE64.encode(bytes))
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let text = String::deserialize(d)?;
data_encoding::BASE64
.decode(text.as_bytes())
.map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn envelope_round_trips_with_flattened_payload() {
let envelope = Envelope {
v: ENVELOPE_VERSION,
id: "req_1".into(),
thread_id: "thr_1".into(),
reply_to: None,
created_at: 1,
payload: Payload::Request {
handler: "review".into(),
prompt: "look".into(),
attachments: vec![Attachment {
name: "a.patch".into(),
media_type: "text/x-diff".into(),
data: b"diff".to_vec(),
}],
},
};
let json = serde_json::to_value(&envelope).unwrap();
assert_eq!(json["kind"], "request");
assert_eq!(json["attachments"][0]["data"], "ZGlmZg==");
let back: Envelope = serde_json::from_value(json).unwrap();
assert_eq!(back.payload.attachments()[0].data, b"diff");
}
}