use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::core::types::JsonRpcNotification;
use super::constants::{NOTIFICATIONS_PROGRESS_METHOD, OVERSIZED_TRANSFER_TYPE};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CompletionMode {
#[default]
Render,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "frameType", rename_all = "camelCase")]
pub enum OversizedFrame {
#[serde(rename_all = "camelCase")]
Start {
completion_mode: CompletionMode,
digest: String,
total_bytes: u64,
total_chunks: u64,
},
Accept,
Chunk {
data: String,
},
End,
Abort {
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
}
impl OversizedFrame {
pub fn frame_type(&self) -> &'static str {
match self {
Self::Start { .. } => "start",
Self::Accept => "accept",
Self::Chunk { .. } => "chunk",
Self::End => "end",
Self::Abort { .. } => "abort",
}
}
pub fn to_cvm_value(&self) -> Result<Value, serde_json::Error> {
let mut value = serde_json::to_value(self)?;
if let Value::Object(map) = &mut value {
map.insert(
"type".to_string(),
Value::String(OVERSIZED_TRANSFER_TYPE.to_string()),
);
}
Ok(value)
}
pub fn from_cvm_value(value: &Value) -> Option<Self> {
if value.get("type").and_then(Value::as_str) != Some(OVERSIZED_TRANSFER_TYPE) {
return None;
}
serde_json::from_value(value.clone()).ok()
}
pub fn is_frame_value(value: &Value) -> bool {
value.get("type").and_then(Value::as_str) == Some(OVERSIZED_TRANSFER_TYPE)
&& value.get("frameType").and_then(Value::as_str).is_some()
}
pub fn into_progress_notification(
&self,
progress_token: &str,
progress: u64,
message: Option<&str>,
) -> Result<JsonRpcNotification, serde_json::Error> {
let mut params = Map::new();
params.insert(
"progressToken".to_string(),
Value::String(progress_token.to_string()),
);
params.insert("progress".to_string(), Value::Number(progress.into()));
if let Some(message) = message {
params.insert("message".to_string(), Value::String(message.to_string()));
}
params.insert("cvm".to_string(), self.to_cvm_value()?);
Ok(JsonRpcNotification {
jsonrpc: "2.0".to_string(),
method: NOTIFICATIONS_PROGRESS_METHOD.to_string(),
params: Some(Value::Object(params)),
})
}
}
pub fn progress_token_string(value: &Value) -> Option<String> {
match value {
Value::String(s) => Some(s.clone()),
Value::Number(n) => Some(n.to_string()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn assert_cvm_roundtrip(frame: OversizedFrame) {
let value = frame.to_cvm_value().unwrap();
assert_eq!(value["type"], json!("oversized-transfer"));
assert_eq!(value["frameType"], json!(frame.frame_type()));
assert_eq!(OversizedFrame::from_cvm_value(&value), Some(frame));
}
#[test]
fn start_frame_serializes_with_camel_case_fields() {
let frame = OversizedFrame::Start {
completion_mode: CompletionMode::Render,
digest: "sha256:abcd".to_string(),
total_bytes: 100,
total_chunks: 3,
};
let value = frame.to_cvm_value().unwrap();
assert_eq!(value["type"], json!("oversized-transfer"));
assert_eq!(value["frameType"], json!("start"));
assert_eq!(value["completionMode"], json!("render"));
assert_eq!(value["digest"], json!("sha256:abcd"));
assert_eq!(value["totalBytes"], json!(100));
assert_eq!(value["totalChunks"], json!(3));
assert_cvm_roundtrip(frame);
}
#[test]
fn all_frame_variants_roundtrip() {
assert_cvm_roundtrip(OversizedFrame::Start {
completion_mode: CompletionMode::Render,
digest: "sha256:abcd".to_string(),
total_bytes: 8,
total_chunks: 2,
});
assert_cvm_roundtrip(OversizedFrame::Accept);
assert_cvm_roundtrip(OversizedFrame::Chunk {
data: "payload".to_string(),
});
assert_cvm_roundtrip(OversizedFrame::End);
assert_cvm_roundtrip(OversizedFrame::Abort {
reason: Some("boom".to_string()),
});
assert_cvm_roundtrip(OversizedFrame::Abort { reason: None });
}
#[test]
fn abort_reason_omitted_when_none() {
let value = OversizedFrame::Abort { reason: None }
.to_cvm_value()
.unwrap();
assert!(!value.as_object().unwrap().contains_key("reason"));
}
#[test]
fn from_cvm_value_rejects_wrong_type() {
let value = json!({ "type": "something-else", "frameType": "end" });
assert_eq!(OversizedFrame::from_cvm_value(&value), None);
assert!(!OversizedFrame::is_frame_value(&value));
}
#[test]
fn from_cvm_value_rejects_unknown_frame_type() {
let value = json!({ "type": "oversized-transfer", "frameType": "bogus" });
assert_eq!(OversizedFrame::from_cvm_value(&value), None);
}
#[test]
fn is_frame_value_requires_type_and_frame_type() {
assert!(OversizedFrame::is_frame_value(
&json!({ "type": "oversized-transfer", "frameType": "chunk", "data": "x" })
));
assert!(!OversizedFrame::is_frame_value(
&json!({ "type": "oversized-transfer" })
));
assert!(!OversizedFrame::is_frame_value(
&json!({ "frameType": "chunk" })
));
assert!(!OversizedFrame::is_frame_value(&json!("not an object")));
}
#[test]
fn into_progress_notification_builds_progress_envelope() {
let notification = OversizedFrame::Chunk {
data: "frag".to_string(),
}
.into_progress_notification("tok-1", 7, Some("hi"))
.unwrap();
assert_eq!(notification.method, "notifications/progress");
let params = notification.params.as_ref().unwrap();
assert_eq!(params["progressToken"], json!("tok-1"));
assert_eq!(params["progress"], json!(7));
assert_eq!(params["message"], json!("hi"));
assert_eq!(params["cvm"]["frameType"], json!("chunk"));
assert_eq!(params["cvm"]["data"], json!("frag"));
}
#[test]
fn into_progress_notification_omits_absent_message() {
let notification = OversizedFrame::End
.into_progress_notification("tok-1", 9, None)
.unwrap();
let params = notification.params.as_ref().unwrap();
assert!(!params.as_object().unwrap().contains_key("message"));
}
#[test]
fn progress_token_string_accepts_string_and_number() {
assert_eq!(
progress_token_string(&json!("tok-1")),
Some("tok-1".to_string())
);
assert_eq!(progress_token_string(&json!(7)), Some("7".to_string()));
assert_eq!(progress_token_string(&json!(0)), Some("0".to_string()));
assert_eq!(progress_token_string(&json!(-3)), Some("-3".to_string()));
assert_eq!(progress_token_string(&json!(7.5)), Some("7.5".to_string()));
}
#[test]
fn progress_token_string_rejects_other_types() {
assert_eq!(progress_token_string(&json!(null)), None);
assert_eq!(progress_token_string(&json!(true)), None);
assert_eq!(progress_token_string(&json!({ "t": 1 })), None);
assert_eq!(progress_token_string(&json!([1])), None);
}
}