use serde_json::Value;
use std::collections::{HashMap, HashSet};
pub type ClientTag = String;
pub fn tag_request_id(id: &Value, tag: &str) -> Value {
match id {
Value::Number(n) => Value::String(format!("{tag}:{n}")),
Value::String(s) => Value::String(format!("{tag}:{s}")),
other => Value::String(format!("{tag}:{other}")),
}
}
pub fn untag_response_id(id: &Value) -> Option<(String, Value)> {
let s = id.as_str()?;
let colon_pos = s.find(':')?;
let tag = s[..colon_pos].to_string();
let original_raw = &s[colon_pos + 1..];
let original = if let Ok(n) = original_raw.parse::<i64>() {
Value::Number(n.into())
} else if let Ok(n) = original_raw.parse::<u64>() {
Value::Number(n.into())
} else {
Value::String(original_raw.to_string())
};
Some((tag, original))
}
pub struct DocumentState {
files: HashMap<String, (HashSet<String>, i64)>,
}
impl DocumentState {
pub fn new() -> Self {
Self {
files: HashMap::new(),
}
}
pub fn open(&mut self, uri: &str, tag: &str) -> bool {
let entry = self
.files
.entry(uri.to_string())
.or_insert_with(|| (HashSet::new(), 0));
let is_first = entry.0.is_empty();
entry.0.insert(tag.to_string());
is_first
}
pub fn close(&mut self, uri: &str, tag: &str) -> bool {
if let Some(entry) = self.files.get_mut(uri) {
entry.0.remove(tag);
if entry.0.is_empty() {
self.files.remove(uri);
return true;
}
}
false
}
pub fn next_version(&mut self, uri: &str) -> i64 {
let entry = self
.files
.entry(uri.to_string())
.or_insert_with(|| (HashSet::new(), 0));
entry.1 += 1;
entry.1
}
pub fn disconnect(&mut self, tag: &str) -> Vec<String> {
let mut to_close = Vec::new();
let mut to_remove = Vec::new();
for (uri, (clients, _)) in self.files.iter_mut() {
clients.remove(tag);
if clients.is_empty() {
to_close.push(uri.clone());
to_remove.push(uri.clone());
}
}
for uri in &to_remove {
self.files.remove(uri);
}
to_close
}
}
impl Default for DocumentState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn tag_request_id_numeric() {
let id = json!(1);
let tagged = tag_request_id(&id, "a");
assert_eq!(tagged, json!("a:1"));
}
#[test]
fn tag_request_id_string() {
let id = json!("req-5");
let tagged = tag_request_id(&id, "b");
assert_eq!(tagged, json!("b:req-5"));
}
#[test]
fn untag_response_id_numeric() {
let tagged = json!("a:1");
let (tag, original) = untag_response_id(&tagged).unwrap();
assert_eq!(tag, "a");
assert_eq!(original, json!(1));
}
#[test]
fn untag_response_id_string() {
let tagged = json!("b:req-5");
let (tag, original) = untag_response_id(&tagged).unwrap();
assert_eq!(tag, "b");
assert_eq!(original, json!("req-5"));
}
#[test]
fn untag_response_id_no_colon() {
let tagged = json!("no_colon");
assert!(untag_response_id(&tagged).is_none());
}
#[test]
fn untag_response_id_non_string() {
let tagged = json!(42);
assert!(untag_response_id(&tagged).is_none());
}
#[test]
fn untag_response_id_null() {
let tagged = json!(null);
assert!(untag_response_id(&tagged).is_none());
}
#[test]
fn open_first_client_forwards() {
let mut state = DocumentState::new();
assert!(state.open("file:///a.rs", "client-a"));
}
#[test]
fn open_second_client_suppresses() {
let mut state = DocumentState::new();
assert!(state.open("file:///a.rs", "client-a"));
assert!(!state.open("file:///a.rs", "client-b"));
}
#[test]
fn open_same_client_twice_suppresses() {
let mut state = DocumentState::new();
assert!(state.open("file:///a.rs", "client-a"));
assert!(!state.open("file:///a.rs", "client-a"));
}
#[test]
fn close_last_client_forwards() {
let mut state = DocumentState::new();
state.open("file:///a.rs", "client-a");
assert!(state.close("file:///a.rs", "client-a"));
}
#[test]
fn close_non_last_client_suppresses() {
let mut state = DocumentState::new();
state.open("file:///a.rs", "client-a");
state.open("file:///a.rs", "client-b");
assert!(!state.close("file:///a.rs", "client-a"));
}
#[test]
fn close_then_last_close_forwards() {
let mut state = DocumentState::new();
state.open("file:///a.rs", "client-a");
state.open("file:///a.rs", "client-b");
assert!(!state.close("file:///a.rs", "client-a"));
assert!(state.close("file:///a.rs", "client-b"));
}
#[test]
fn close_unknown_uri_returns_false() {
let mut state = DocumentState::new();
assert!(!state.close("file:///unknown.rs", "client-a"));
}
#[test]
fn next_version_monotonically_increasing() {
let mut state = DocumentState::new();
state.open("file:///a.rs", "client-a");
assert_eq!(state.next_version("file:///a.rs"), 1);
assert_eq!(state.next_version("file:///a.rs"), 2);
assert_eq!(state.next_version("file:///a.rs"), 3);
}
#[test]
fn next_version_independent_per_uri() {
let mut state = DocumentState::new();
state.open("file:///a.rs", "client-a");
state.open("file:///b.rs", "client-a");
assert_eq!(state.next_version("file:///a.rs"), 1);
assert_eq!(state.next_version("file:///b.rs"), 1);
assert_eq!(state.next_version("file:///a.rs"), 2);
}
#[test]
fn disconnect_closes_exclusive_files() {
let mut state = DocumentState::new();
state.open("file:///exclusive.rs", "client-a");
state.open("file:///shared.rs", "client-a");
state.open("file:///shared.rs", "client-b");
let closed = state.disconnect("client-a");
assert_eq!(closed, vec!["file:///exclusive.rs"]);
}
#[test]
fn disconnect_keeps_shared_files_open() {
let mut state = DocumentState::new();
state.open("file:///shared.rs", "client-a");
state.open("file:///shared.rs", "client-b");
let closed = state.disconnect("client-a");
assert!(closed.is_empty());
assert!(state.close("file:///shared.rs", "client-b"));
}
#[test]
fn disconnect_unknown_client_is_noop() {
let mut state = DocumentState::new();
state.open("file:///a.rs", "client-a");
let closed = state.disconnect("client-x");
assert!(closed.is_empty());
}
}