use serde::Serialize;
use serde_json::Value;
use std::collections::HashMap;
use std::time::Instant;
use tracing::warn;
pub type ProgressToken = String;
#[derive(Debug, Clone)]
pub struct ProgressState {
pub title: String,
pub message: Option<String>,
pub percentage: Option<u32>,
pub started: Instant,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServerLifecycle {
Initializing,
Probing,
Healthy,
Busy(u32),
Failed,
Dead,
}
impl ServerLifecycle {
#[must_use]
pub const fn is_terminal(&self) -> bool {
matches!(self, Self::Failed | Self::Dead)
}
#[must_use]
pub const fn display_state(&self) -> &str {
match self {
Self::Initializing | Self::Probing => "initializing",
Self::Healthy => "ready",
Self::Busy(_) => "busy",
Self::Failed | Self::Dead => "dead",
}
}
}
impl Serialize for ServerLifecycle {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.display_state())
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ServerStatus {
pub language: String,
pub state: ServerLifecycle,
#[serde(skip_serializing_if = "Option::is_none")]
pub progress_title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub progress_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub progress_percentage: Option<u32>,
pub uptime_secs: u64,
}
#[derive(Debug, Default)]
pub struct ProgressTracker {
active_progress: HashMap<ProgressToken, ProgressState>,
last_broadcast_title: Option<String>,
last_broadcast_percentage: Option<u32>,
}
impl ProgressTracker {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, token: &str, value: &Value) {
match value.get("kind").and_then(Value::as_str) {
Some("begin") => {
self.active_progress.insert(
token.to_string(),
ProgressState {
title: value
.get("title")
.and_then(Value::as_str)
.unwrap_or("")
.to_string(),
message: value
.get("message")
.and_then(Value::as_str)
.map(str::to_string),
percentage: value
.get("percentage")
.and_then(Value::as_u64)
.and_then(|n| u32::try_from(n).ok()),
started: Instant::now(),
},
);
}
Some("report") => {
if let Some(state) = self.active_progress.get_mut(token) {
if let Some(msg) = value.get("message").and_then(Value::as_str) {
state.message = Some(msg.to_string());
}
if let Some(pct) = value
.get("percentage")
.and_then(Value::as_u64)
.and_then(|n| u32::try_from(n).ok())
{
state.percentage = Some(pct);
}
}
}
Some("end") => {
self.active_progress.remove(token);
}
other => {
warn!("Unknown progress kind: {:?}", other);
}
}
}
#[must_use]
pub fn is_busy(&self) -> bool {
!self.active_progress.is_empty()
}
#[must_use]
pub fn primary_progress(&self) -> Option<&ProgressState> {
self.active_progress
.values()
.min_by_key(|p| p.percentage.unwrap_or(0))
}
pub fn broadcast_changed(&mut self) -> bool {
let (title, pct) = self
.primary_progress()
.map_or((None, None), |p| (Some(p.title.clone()), p.percentage));
if title == self.last_broadcast_title && pct == self.last_broadcast_percentage {
return false;
}
self.last_broadcast_title = title;
self.last_broadcast_percentage = pct;
true
}
pub fn clear(&mut self) {
self.active_progress.clear();
}
}
#[cfg(test)]
#[allow(
clippy::expect_used,
reason = "tests use expect for readable assertions"
)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_progress_begin_end() {
let mut tracker = ProgressTracker::new();
assert!(!tracker.is_busy());
let begin = json!({"kind": "begin", "title": "Indexing", "message": "src/main.rs", "percentage": 0});
tracker.update("indexing", &begin);
assert!(tracker.is_busy());
let primary = tracker.primary_progress().expect("active progress");
assert_eq!(primary.title, "Indexing");
assert_eq!(primary.message, Some("src/main.rs".to_string()));
assert_eq!(primary.percentage, Some(0));
let end = json!({"kind": "end"});
tracker.update("indexing", &end);
assert!(!tracker.is_busy());
}
#[test]
fn test_progress_report() {
let mut tracker = ProgressTracker::new();
let begin = json!({"kind": "begin", "title": "Indexing", "percentage": 0});
tracker.update("indexing", &begin);
let report = json!({"kind": "report", "message": "50% done", "percentage": 50});
tracker.update("indexing", &report);
let primary = tracker.primary_progress().expect("active progress");
assert_eq!(primary.percentage, Some(50));
assert_eq!(primary.message, Some("50% done".to_string()));
}
#[test]
fn test_multiple_progress_tokens() {
let mut tracker = ProgressTracker::new();
let begin1 = json!({"kind": "begin", "title": "Indexing", "percentage": 50});
let begin2 = json!({"kind": "begin", "title": "Analyzing", "percentage": 10});
tracker.update("indexing", &begin1);
tracker.update("analyzing", &begin2);
assert!(tracker.is_busy());
let primary = tracker.primary_progress().expect("active progress");
assert_eq!(primary.title, "Analyzing");
assert_eq!(primary.percentage, Some(10));
let end1 = json!({"kind": "end"});
tracker.update("indexing", &end1);
assert!(tracker.is_busy());
let primary = tracker.primary_progress().expect("active progress");
assert_eq!(primary.title, "Analyzing");
}
#[test]
fn lifecycle_display_state() {
assert_eq!(
ServerLifecycle::Initializing.display_state(),
"initializing"
);
assert_eq!(ServerLifecycle::Probing.display_state(), "initializing");
assert_eq!(ServerLifecycle::Healthy.display_state(), "ready");
assert_eq!(ServerLifecycle::Busy(1).display_state(), "busy");
assert_eq!(ServerLifecycle::Busy(3).display_state(), "busy");
assert_eq!(ServerLifecycle::Failed.display_state(), "dead");
assert_eq!(ServerLifecycle::Dead.display_state(), "dead");
}
#[test]
fn lifecycle_is_terminal() {
assert!(!ServerLifecycle::Initializing.is_terminal());
assert!(!ServerLifecycle::Probing.is_terminal());
assert!(!ServerLifecycle::Healthy.is_terminal());
assert!(!ServerLifecycle::Busy(1).is_terminal());
assert!(ServerLifecycle::Failed.is_terminal());
assert!(ServerLifecycle::Dead.is_terminal());
}
#[test]
fn lifecycle_serializes_to_display_state() {
let json = serde_json::to_string(&ServerLifecycle::Healthy).expect("serialize");
assert_eq!(json, "\"ready\"");
let json = serde_json::to_string(&ServerLifecycle::Busy(2)).expect("serialize");
assert_eq!(json, "\"busy\"");
let json = serde_json::to_string(&ServerLifecycle::Dead).expect("serialize");
assert_eq!(json, "\"dead\"");
}
}