From bd926061bfbaf1afb94b5973c2e01ca0afce8079 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Mon, 25 May 2026 11:31:56 +0200 Subject: [PATCH] feat(protocol): JSON-RPC method param/result types --- crates/xy-protocol/src/lib.rs | 1 + crates/xy-protocol/src/rpc.rs | 148 ++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 crates/xy-protocol/src/rpc.rs diff --git a/crates/xy-protocol/src/lib.rs b/crates/xy-protocol/src/lib.rs index 64259ab..bf09517 100644 --- a/crates/xy-protocol/src/lib.rs +++ b/crates/xy-protocol/src/lib.rs @@ -3,6 +3,7 @@ pub mod config; pub mod error; pub mod kdl_parse; +pub mod rpc; pub mod state; pub use config::{RestartConfig, RestartPolicy, ServerConfig, StopConfig}; diff --git a/crates/xy-protocol/src/rpc.rs b/crates/xy-protocol/src/rpc.rs new file mode 100644 index 0000000..6a17a82 --- /dev/null +++ b/crates/xy-protocol/src/rpc.rs @@ -0,0 +1,148 @@ +use crate::ServerState; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServerSummary { + pub name: String, + pub state: ServerState, + #[serde(skip_serializing_if = "Option::is_none")] + pub pid: Option, + pub port: u16, + #[serde(skip_serializing_if = "Option::is_none")] + pub uptime_secs: Option, + pub restart_count: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_exit: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StatusDetail { + #[serde(flatten)] + pub summary: ServerSummary, + pub recent_transitions: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StateTransition { + pub from: ServerState, + pub to: ServerState, + pub at_unix_ms: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum NameOrAll { + All { all: bool }, + Name { name: String }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StatusParams { + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StartResult { + pub started: Vec, + pub already_running: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StopResult { + pub stopped: Vec, + pub not_running: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RestartResult { + pub restarted: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReloadResult { + pub added: Vec, + pub removed: Vec, + pub changed: Vec, + pub unchanged: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogsParams { + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tail: Option, + #[serde(default)] + pub follow: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogsSubscribed { + pub subscription_id: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogsCancelParams { + pub subscription_id: u64, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum LogStream { + Stdout, + Stderr, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogLine { + pub subscription_id: u64, + pub name: String, + pub stream: LogStream, + pub line: String, + pub ts_unix_ms: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogEnd { + pub subscription_id: u64, +} + +pub mod methods { + pub const LIST: &str = "list"; + pub const STATUS: &str = "status"; + pub const START: &str = "start"; + pub const STOP: &str = "stop"; + pub const RESTART: &str = "restart"; + pub const RELOAD: &str = "reload"; + pub const LOGS: &str = "logs"; + pub const LOGS_CANCEL: &str = "logs_cancel"; +} + +pub mod notifications { + pub const LOG: &str = "log"; + pub const LOG_END: &str = "log_end"; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn name_or_all_round_trips_name() { + let v: NameOrAll = serde_json::from_str(r#"{"name":"foo"}"#).unwrap(); + match v { + NameOrAll::Name { name } => assert_eq!(name, "foo"), + _ => panic!("expected Name variant"), + } + } + + #[test] + fn name_or_all_round_trips_all() { + let v: NameOrAll = serde_json::from_str(r#"{"all":true}"#).unwrap(); + match v { + NameOrAll::All { all } => assert!(all), + _ => panic!("expected All variant"), + } + } +}