use crate::error::{Error, Result};
use serde_json::{Map, Value};
pub const VALID_TYPES: &[&str] = &[
"req",
"res",
"stream_chunk",
"stream_end",
"cancel",
"error",
];
pub type Envelope = Map<String, Value>;
fn require_u64(env: &Envelope, key: &str) -> Result<u64> {
match env.get(key).and_then(|v| v.as_i64()) {
Some(n) if n >= 0 => Ok(n as u64),
_ => Err(Error::InvalidEnvelope(format!(
"envelope.{key} must be a non-negative int"
))),
}
}
pub fn validate(env: &Envelope) -> Result<()> {
require_u64(env, "stream_id")?;
require_u64(env, "seq")?;
let t = env
.get("type")
.and_then(|v| v.as_str())
.ok_or_else(|| Error::InvalidEnvelope("envelope.type missing".into()))?;
if !VALID_TYPES.contains(&t) {
return Err(Error::InvalidEnvelope(format!(
"envelope.type invalid: {t}"
)));
}
if let Some(c) = env.get("credits") {
match c.as_i64() {
Some(n) if n >= 0 => {}
_ => {
return Err(Error::InvalidEnvelope(
"envelope.credits must be a non-negative int".into(),
));
}
}
}
if let Some(m) = env.get("method") {
if !m.is_string() {
return Err(Error::InvalidEnvelope(
"envelope.method must be a string".into(),
));
}
}
if let Some(r) = env.get("reason") {
if !r.is_string() {
return Err(Error::InvalidEnvelope(
"envelope.reason must be a string".into(),
));
}
}
if let Some(err) = env.get("error") {
let m = err
.as_object()
.ok_or_else(|| Error::InvalidEnvelope("envelope.error must be an object".into()))?;
if !m.get("code").map(Value::is_i64).unwrap_or(false)
|| !m.get("message").map(Value::is_string).unwrap_or(false)
{
return Err(Error::InvalidEnvelope(
"envelope.error must be {code:int, message:str}".into(),
));
}
}
Ok(())
}
pub fn encode(env: &Envelope) -> Result<Vec<u8>> {
validate(env)?;
let v: Value = Value::Object(env.clone());
serde_jcs::to_vec(&v).map_err(Error::Json)
}
pub fn decode(bytes: &[u8]) -> Result<Envelope> {
let v: Value = serde_json::from_slice(bytes)?;
let map = match v {
Value::Object(m) => m,
_ => {
return Err(Error::InvalidEnvelope(
"envelope must be a JSON object".into(),
))
}
};
validate(&map)?;
Ok(map)
}