use std::marker::PhantomData;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
pub const ENVELOPE_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum WorkspaceState {
Unloaded = 0,
Loading = 1,
Loaded = 2,
Rebuilding = 3,
Evicted = 4,
Failed = 5,
}
impl WorkspaceState {
#[must_use]
pub const fn as_u8(self) -> u8 {
self as u8
}
#[must_use]
pub const fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Self::Unloaded),
1 => Some(Self::Loading),
2 => Some(Self::Loaded),
3 => Some(Self::Rebuilding),
4 => Some(Self::Evicted),
5 => Some(Self::Failed),
_ => None,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Unloaded => "unloaded",
Self::Loading => "loading",
Self::Loaded => "loaded",
Self::Rebuilding => "rebuilding",
Self::Evicted => "evicted",
Self::Failed => "failed",
}
}
#[must_use]
pub const fn is_serving(self) -> bool {
matches!(self, Self::Loaded | Self::Rebuilding | Self::Failed)
}
}
impl std::fmt::Display for WorkspaceState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DaemonHello {
pub client_version: String,
pub protocol_version: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DaemonHelloResponse {
pub compatible: bool,
pub daemon_version: String,
pub envelope_version: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ShimProtocol {
Lsp,
Mcp,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ShimRegister {
pub protocol: ShimProtocol,
pub pid: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ShimRegisterAck {
pub accepted: bool,
pub daemon_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub envelope_version: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseEnvelope<T> {
pub result: T,
pub meta: ResponseMeta,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ResponseMeta {
pub stale: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_good_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_state: Option<WorkspaceState>,
pub daemon_version: String,
}
impl ResponseMeta {
#[must_use]
pub fn management(daemon_version: &str) -> Self {
Self {
stale: false,
last_good_at: None,
last_error: None,
workspace_state: None,
daemon_version: daemon_version.to_owned(),
}
}
#[must_use]
pub fn loaded(daemon_version: &str) -> Self {
Self {
stale: false,
last_good_at: None,
last_error: None,
workspace_state: Some(WorkspaceState::Loaded),
daemon_version: daemon_version.to_owned(),
}
}
#[must_use]
pub fn fresh_from(state: WorkspaceState, daemon_version: &str) -> Self {
Self {
stale: false,
last_good_at: None,
last_error: None,
workspace_state: Some(state),
daemon_version: daemon_version.to_owned(),
}
}
#[must_use]
pub fn stale_from(
last_good_at: std::time::SystemTime,
last_error: Option<String>,
daemon_version: &str,
) -> Self {
use chrono::{DateTime, SecondsFormat, Utc};
let rfc3339 =
DateTime::<Utc>::from(last_good_at).to_rfc3339_opts(SecondsFormat::Secs, true);
Self {
stale: true,
last_good_at: Some(rfc3339),
last_error,
workspace_state: Some(WorkspaceState::Failed),
daemon_version: daemon_version.to_owned(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct LoadResult {
pub root: std::path::PathBuf,
pub current_bytes: u64,
pub state: WorkspaceState,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RebuildResult {
pub root: std::path::PathBuf,
pub duration_ms: u64,
pub nodes: u64,
pub edges: u64,
pub files_indexed: u64,
pub was_full: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CancelRebuildResult {
pub root: std::path::PathBuf,
pub cancelled: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct JsonRpcVersion;
impl Serialize for JsonRpcVersion {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str("2.0")
}
}
impl<'de> Deserialize<'de> for JsonRpcVersion {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct Vis(PhantomData<JsonRpcVersion>);
impl<'de> de::Visitor<'de> for Vis {
type Value = JsonRpcVersion;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("the string \"2.0\"")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
if v == "2.0" {
Ok(JsonRpcVersion)
} else {
Err(E::invalid_value(de::Unexpected::Str(v), &"\"2.0\""))
}
}
}
d.deserialize_str(Vis(PhantomData))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(untagged)]
pub enum JsonRpcId {
I64(i64),
U64(u64),
Str(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: JsonRpcVersion,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<JsonRpcId>,
pub method: String,
#[serde(default)]
pub params: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
pub jsonrpc: JsonRpcVersion,
pub id: Option<JsonRpcId>,
#[serde(flatten)]
pub payload: JsonRpcPayload,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum JsonRpcPayload {
Success { result: serde_json::Value },
Error { error: JsonRpcError },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
impl JsonRpcResponse {
#[must_use]
pub fn success(id: Option<JsonRpcId>, result: serde_json::Value) -> Self {
Self {
jsonrpc: JsonRpcVersion,
id,
payload: JsonRpcPayload::Success { result },
}
}
#[must_use]
pub fn error(
id: Option<JsonRpcId>,
code: i32,
message: impl Into<String>,
data: Option<serde_json::Value>,
) -> Self {
Self {
jsonrpc: JsonRpcVersion,
id,
payload: JsonRpcPayload::Error {
error: JsonRpcError {
code,
message: message.into(),
data,
},
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jsonrpc_version_roundtrip() {
let wire = serde_json::to_string(&JsonRpcVersion).unwrap();
assert_eq!(wire, r#""2.0""#);
let back: JsonRpcVersion = serde_json::from_str(&wire).unwrap();
assert_eq!(back, JsonRpcVersion);
}
#[test]
fn jsonrpc_version_rejects_wrong_string() {
let err = serde_json::from_str::<JsonRpcVersion>(r#""1.0""#)
.expect_err("must reject non-\"2.0\"");
assert!(err.to_string().contains("\"2.0\""));
}
#[test]
fn jsonrpc_id_untagged_roundtrip() {
let cases: &[(&str, JsonRpcId)] = &[
("0", JsonRpcId::I64(0)),
("-7", JsonRpcId::I64(-7)),
(&i64::MAX.to_string(), JsonRpcId::I64(i64::MAX)),
("\"abc\"", JsonRpcId::Str("abc".into())),
];
for (wire, expected) in cases {
let parsed: JsonRpcId = serde_json::from_str(wire).expect(wire);
assert_eq!(&parsed, expected, "round-trip failed for {wire}");
}
let u: JsonRpcId = serde_json::from_str("9223372036854775808").unwrap();
assert_eq!(u, JsonRpcId::U64(9_223_372_036_854_775_808));
}
#[test]
fn response_id_none_serializes_as_json_null() {
let resp = JsonRpcResponse::error(None, -32700, "Parse error", None);
let wire = serde_json::to_string(&resp).unwrap();
assert!(
wire.contains(r#""id":null"#),
"expected id:null in wire form, got: {wire}"
);
}
#[test]
fn response_id_some_serializes_as_value() {
let resp = JsonRpcResponse::success(Some(JsonRpcId::I64(7)), serde_json::json!({}));
let wire = serde_json::to_string(&resp).unwrap();
assert!(wire.contains(r#""id":7"#));
}
#[test]
fn response_meta_management_has_none_workspace_state() {
let meta = ResponseMeta::management("8.0.6");
let wire = serde_json::to_string(&meta).unwrap();
assert!(!wire.contains("workspace_state"), "wire: {wire}");
assert!(wire.contains(r#""stale":false"#));
assert!(wire.contains(r#""daemon_version":"8.0.6""#));
}
#[test]
fn response_meta_loaded_has_loaded_workspace_state() {
let meta = ResponseMeta::loaded("8.0.6");
let wire = serde_json::to_string(&meta).unwrap();
assert!(
wire.contains(r#""workspace_state":"Loaded""#),
"wire: {wire}"
);
}
#[test]
fn response_meta_fresh_from_emits_state() {
let meta = ResponseMeta::fresh_from(WorkspaceState::Loaded, "8.0.6");
let wire = serde_json::to_string(&meta).unwrap();
assert!(
wire.contains(r#""workspace_state":"Loaded""#),
"wire: {wire}"
);
assert!(wire.contains(r#""stale":false"#), "wire: {wire}");
assert!(!wire.contains("last_good_at"), "wire: {wire}");
assert!(!wire.contains("last_error"), "wire: {wire}");
let meta_rebuild = ResponseMeta::fresh_from(WorkspaceState::Rebuilding, "8.0.6");
let wire_rebuild = serde_json::to_string(&meta_rebuild).unwrap();
assert!(
wire_rebuild.contains(r#""workspace_state":"Rebuilding""#),
"wire: {wire_rebuild}"
);
}
#[test]
fn response_meta_stale_from_rfc3339_and_workspace_state() {
let anchor =
std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_760_000_000);
let meta = ResponseMeta::stale_from(anchor, Some("boom".to_owned()), "8.0.6");
let wire = serde_json::to_string(&meta).unwrap();
assert!(wire.contains(r#""stale":true"#), "wire: {wire}");
assert!(
wire.contains(r#""workspace_state":"Failed""#),
"wire: {wire}"
);
assert!(wire.contains(r#""last_error":"boom""#), "wire: {wire}");
let last_good_marker = r#""last_good_at":""#;
let start = wire
.find(last_good_marker)
.unwrap_or_else(|| panic!("missing last_good_at in wire: {wire}"))
+ last_good_marker.len();
let rest = &wire[start..];
let end = rest
.find('"')
.expect("last_good_at must be a closed string");
let rfc = &rest[..end];
assert!(rfc.ends_with('Z'), "expected UTC-Zulu, got: {rfc}");
assert!(
rfc.contains('T'),
"RFC3339 must carry a 'T' separator: {rfc}"
);
}
#[test]
fn shim_register_ack_accepted_omits_reason_on_wire() {
let ack = ShimRegisterAck {
accepted: true,
daemon_version: "8.0.6".to_owned(),
reason: None,
envelope_version: 1,
};
let wire = serde_json::to_string(&ack).unwrap();
assert!(!wire.contains("reason"), "wire: {wire}");
assert!(wire.contains(r#""accepted":true"#), "wire: {wire}");
assert!(wire.contains(r#""daemon_version":"8.0.6""#), "wire: {wire}");
assert!(wire.contains(r#""envelope_version":1"#), "wire: {wire}");
}
#[test]
fn shim_register_ack_rejected_includes_reason() {
let ack = ShimRegisterAck {
accepted: false,
daemon_version: "8.0.6".to_owned(),
reason: Some("cap".to_owned()),
envelope_version: 1,
};
let wire = serde_json::to_string(&ack).unwrap();
assert!(wire.contains(r#""reason":"cap""#), "wire: {wire}");
assert!(wire.contains(r#""accepted":false"#), "wire: {wire}");
}
#[test]
fn daemon_hello_rejects_unknown_fields() {
let wire = r#"{"client_version":"x","protocol_version":1,"extra":true}"#;
let err = serde_json::from_str::<DaemonHello>(wire)
.expect_err("DaemonHello must reject unknown fields");
let msg = err.to_string();
assert!(
msg.contains("unknown field"),
"expected 'unknown field' in error, got: {msg}"
);
}
#[test]
fn shim_register_rejects_unknown_fields() {
let wire = r#"{"protocol":"lsp","pid":1,"extra":true}"#;
let err = serde_json::from_str::<ShimRegister>(wire)
.expect_err("ShimRegister must reject unknown fields");
let msg = err.to_string();
assert!(
msg.contains("unknown field"),
"expected 'unknown field' in error, got: {msg}"
);
}
}