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, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ServerState {
Initializing,
Busy,
Ready,
Dead,
Stuck,
}
impl ServerState {
#[must_use]
pub const fn from_u8(value: u8) -> Self {
match value {
0 => Self::Initializing,
1 => Self::Busy,
2 => Self::Ready,
4 => Self::Stuck,
_ => Self::Dead,
}
}
#[must_use]
pub const fn as_u8(self) -> u8 {
match self {
Self::Initializing => 0,
Self::Busy => 1,
Self::Ready => 2,
Self::Dead => 3,
Self::Stuck => 4,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ServerStatus {
pub language: String,
pub state: ServerState,
#[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 test_server_state_conversion() {
assert_eq!(ServerState::from_u8(0), ServerState::Initializing);
assert_eq!(ServerState::from_u8(1), ServerState::Busy);
assert_eq!(ServerState::from_u8(2), ServerState::Ready);
assert_eq!(ServerState::from_u8(3), ServerState::Dead);
assert_eq!(ServerState::from_u8(4), ServerState::Stuck);
assert_eq!(ServerState::from_u8(99), ServerState::Dead);
assert_eq!(ServerState::Initializing.as_u8(), 0);
assert_eq!(ServerState::Busy.as_u8(), 1);
assert_eq!(ServerState::Ready.as_u8(), 2);
assert_eq!(ServerState::Dead.as_u8(), 3);
assert_eq!(ServerState::Stuck.as_u8(), 4);
}
}