feat(proto): envelope, commands, events, ids with serde round-trip tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1,18 @@
|
|||||||
// populated in Task 1
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct SurfaceId(pub String);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct WorkspaceId(pub String);
|
||||||
|
|
||||||
|
impl std::fmt::Display for SurfaceId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::fmt::Display for WorkspaceId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,5 @@ pub mod codec;
|
|||||||
pub mod ids;
|
pub mod ids;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
|
|
||||||
// re-exports added in Task 1 when types are defined
|
pub use ids::{SurfaceId, WorkspaceId};
|
||||||
// pub use ids::{SurfaceId, WorkspaceId};
|
pub use message::{Cmd, Envelope, ErrorBody, Evt};
|
||||||
// pub use message::{Cmd, Envelope, ErrorBody, Evt};
|
|
||||||
|
|||||||
@@ -1 +1,115 @@
|
|||||||
// populated in Task 1
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::ids::{SurfaceId, WorkspaceId};
|
||||||
|
|
||||||
|
/// Wire envelope. `kind` is the serde tag.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||||
|
pub enum Envelope {
|
||||||
|
Req {
|
||||||
|
id: u64,
|
||||||
|
cmd: Cmd,
|
||||||
|
},
|
||||||
|
Res {
|
||||||
|
id: u64,
|
||||||
|
ok: bool,
|
||||||
|
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
|
||||||
|
data: serde_json::Value,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
error: Option<ErrorBody>,
|
||||||
|
},
|
||||||
|
Evt(Evt),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct ErrorBody {
|
||||||
|
pub code: String,
|
||||||
|
pub msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Client → daemon commands. The active subset for M0+M1.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "cmd", content = "args", rename_all = "snake_case")]
|
||||||
|
pub enum Cmd {
|
||||||
|
Open { path: String },
|
||||||
|
NewSurface {
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
command: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
args: Vec<String>,
|
||||||
|
cols: u16,
|
||||||
|
rows: u16,
|
||||||
|
},
|
||||||
|
Input {
|
||||||
|
surface_id: SurfaceId,
|
||||||
|
/// base64-encoded keyboard bytes.
|
||||||
|
bytes: String,
|
||||||
|
},
|
||||||
|
Resize { surface_id: SurfaceId, cols: u16, rows: u16 },
|
||||||
|
Attach { surface_id: SurfaceId },
|
||||||
|
Detach { surface_id: SurfaceId },
|
||||||
|
Focus { surface_id: SurfaceId },
|
||||||
|
Close { surface_id: SurfaceId },
|
||||||
|
Status,
|
||||||
|
Shutdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Daemon → subscribers push events. The active subset for M0+M1.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "evt", content = "data", rename_all = "snake_case")]
|
||||||
|
pub enum Evt {
|
||||||
|
Output { surface_id: SurfaceId, bytes: Vec<u8> },
|
||||||
|
Exit { surface_id: SurfaceId, code: i32 },
|
||||||
|
SurfaceCreated { surface_id: SurfaceId, workspace_id: WorkspaceId },
|
||||||
|
SurfaceClosed { surface_id: SurfaceId },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ids::{SurfaceId, WorkspaceId};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn req_round_trips_through_json() {
|
||||||
|
let env = Envelope::Req {
|
||||||
|
id: 42,
|
||||||
|
cmd: Cmd::Focus { surface_id: SurfaceId("s_8f3".into()) },
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&env).unwrap();
|
||||||
|
let back: Envelope = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(env, back);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn res_ok_and_err_serialize_distinctly() {
|
||||||
|
let ok = Envelope::Res { id: 1, ok: true, data: serde_json::json!({"workspace_id":"w_1"}), error: None };
|
||||||
|
let err = Envelope::Res { id: 2, ok: false, data: serde_json::Value::Null,
|
||||||
|
error: Some(ErrorBody { code: "NOT_FOUND".into(), msg: "no surface".into() }) };
|
||||||
|
assert!(serde_json::to_string(&ok).unwrap().contains("\"ok\":true"));
|
||||||
|
assert!(serde_json::to_string(&err).unwrap().contains("NOT_FOUND"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn evt_output_carries_workspace_scoped_surface() {
|
||||||
|
let evt = Envelope::Evt(Evt::Output {
|
||||||
|
surface_id: SurfaceId("s_1".into()),
|
||||||
|
bytes: vec![104, 105],
|
||||||
|
});
|
||||||
|
let json = serde_json::to_string(&evt).unwrap();
|
||||||
|
let back: Envelope = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(evt, back);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_surface_defaults_cmd_to_none() {
|
||||||
|
let json = r#"{"kind":"req","id":7,"cmd":{"cmd":"new_surface","args":{"workspace_id":"w_1","cols":80,"rows":24}}}"#;
|
||||||
|
let env: Envelope = serde_json::from_str(json).unwrap();
|
||||||
|
match env {
|
||||||
|
Envelope::Req { cmd: Cmd::NewSurface { command, args, .. }, .. } => {
|
||||||
|
assert!(command.is_none());
|
||||||
|
assert!(args.is_empty());
|
||||||
|
}
|
||||||
|
_ => panic!("wrong variant"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user