use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::service::ServiceStatus;
use crate::build_info::Provenance;
pub const DAEMON_SERVICE: &str = "daemon";
pub const MAX_LINE_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonEnvelope {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service: Option<String>,
pub op: String,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub payload: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origin_invocation_id: Option<String>,
}
impl DaemonEnvelope {
pub fn service(name: impl Into<String>, op: impl Into<String>, payload: Value) -> Self {
Self {
service: Some(name.into()),
op: op.into(),
payload,
origin_invocation_id: None,
}
}
pub fn builtin(op: impl Into<String>) -> Self {
Self {
service: None,
op: op.into(),
payload: Value::Null,
origin_invocation_id: None,
}
}
#[must_use]
pub fn with_origin(mut self, invocation_id: impl Into<String>) -> Self {
self.origin_invocation_id = Some(invocation_id.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonReply {
pub ok: bool,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub payload: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl DaemonReply {
pub fn ok(payload: Value) -> Self {
Self {
ok: true,
payload,
error: None,
}
}
pub fn err(message: impl Into<String>) -> Self {
Self {
ok: false,
payload: Value::Null,
error: Some(message.into()),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StatusReport {
pub services: Vec<ServiceStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub github_rate_limit: Option<crate::github_rate_limit::RateLimitSnapshot>,
#[serde(flatten)]
pub provenance: Provenance,
}
impl StatusReport {
#[must_use]
pub fn current(services: Vec<ServiceStatus>) -> Self {
Self {
services,
version: Some(crate::VERSION.to_string()),
provenance: crate::build_info::provenance(),
..Self::default()
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn envelope_omits_origin_when_absent() {
let line = serde_json::to_string(&DaemonEnvelope::service(
"snowflake",
"query",
serde_json::json!({ "sql": "SELECT 1" }),
))
.unwrap();
assert!(!line.contains("origin_invocation_id"), "{line}");
}
#[test]
fn envelope_round_trips_origin() {
let env = DaemonEnvelope::service("snowflake", "query", Value::Null).with_origin("cli-42");
let line = serde_json::to_string(&env).unwrap();
assert!(line.contains("origin_invocation_id"), "{line}");
let back: DaemonEnvelope = serde_json::from_str(&line).unwrap();
assert_eq!(back.origin_invocation_id.as_deref(), Some("cli-42"));
}
#[test]
fn envelope_from_older_client_defaults_origin_to_none() {
let back: DaemonEnvelope =
serde_json::from_str(r#"{"service":"snowflake","op":"query"}"#).unwrap();
assert!(back.origin_invocation_id.is_none());
}
#[test]
fn status_report_from_older_daemon_defaults_version_to_none() {
let back: StatusReport = serde_json::from_str(r#"{"services":[]}"#).unwrap();
assert!(back.version.is_none());
}
#[test]
fn status_report_round_trips_and_omits_absent_version() {
let line = serde_json::to_string(&StatusReport {
services: vec![],
version: None,
..StatusReport::default()
})
.unwrap();
assert_eq!(line, r#"{"services":[]}"#, "{line}");
let line = serde_json::to_string(&StatusReport {
services: vec![],
version: Some("1.2.3".to_string()),
..StatusReport::default()
})
.unwrap();
assert!(line.contains("\"version\":\"1.2.3\""), "{line}");
let back: StatusReport = serde_json::from_str(&line).unwrap();
assert_eq!(back.version.as_deref(), Some("1.2.3"));
}
#[test]
fn status_report_omits_and_round_trips_github_rate_limit() {
use crate::github_rate_limit::{RateLimitResource, RateLimitSnapshot};
let line = serde_json::to_string(&StatusReport {
services: vec![],
version: Some("1.2.3".to_string()),
github_rate_limit: None,
..StatusReport::default()
})
.unwrap();
assert!(!line.contains("github_rate_limit"), "{line}");
let snap = RateLimitSnapshot {
graphql: Some(RateLimitResource {
used: 4100,
limit: 5000,
remaining: 900,
percent: 82.0,
reset: 1_700_000_000,
}),
core: None,
search: None,
};
let line = serde_json::to_string(&StatusReport {
services: vec![],
version: None,
github_rate_limit: Some(snap),
..StatusReport::default()
})
.unwrap();
assert!(line.contains("github_rate_limit"), "{line}");
let back: StatusReport = serde_json::from_str(&line).unwrap();
assert_eq!(back.github_rate_limit, Some(snap));
}
#[test]
fn status_report_flattens_provenance_as_siblings_and_round_trips() {
let report = StatusReport {
services: vec![],
version: Some("1.2.3".to_string()),
provenance: Provenance {
commit: Some("a6d304fd".to_string()),
dirty: Some(true),
..Provenance::default()
},
..StatusReport::default()
};
let line = serde_json::to_string(&report).unwrap();
assert!(line.contains("\"commit\":\"a6d304fd\""), "{line}");
assert!(line.contains("\"dirty\":true"), "{line}");
let back: StatusReport = serde_json::from_str(&line).unwrap();
assert_eq!(back.provenance.commit.as_deref(), Some("a6d304fd"));
assert_eq!(back.provenance.dirty, Some(true));
assert!(back.provenance.commit_long.is_none());
}
}