use std::collections::BTreeMap;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use super::error::{WorkspaceError, WorkspaceResult};
use super::serde_time;
pub const WORKSPACE_REGISTRY_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceRegistry {
pub metadata: WorkspaceMetadata,
#[serde(default)]
pub repositories: Vec<WorkspaceRepository>,
}
impl WorkspaceRegistry {
#[must_use]
pub fn new(workspace_name: Option<String>) -> Self {
Self {
metadata: WorkspaceMetadata::new(workspace_name),
repositories: Vec::new(),
}
}
pub fn load(path: &Path) -> WorkspaceResult<Self> {
let file = File::open(path).map_err(|err| WorkspaceError::io(path, err))?;
let mut registry: WorkspaceRegistry =
serde_json::from_reader(file).map_err(WorkspaceError::Serialization)?;
if registry.metadata.version != WORKSPACE_REGISTRY_VERSION {
return Err(WorkspaceError::UnsupportedVersion {
found: registry.metadata.version,
expected: WORKSPACE_REGISTRY_VERSION,
});
}
registry.sort_repositories();
Ok(registry)
}
pub fn save(&mut self, path: &Path) -> WorkspaceResult<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|err| WorkspaceError::io(parent, err))?;
}
self.sort_repositories();
self.metadata.touch_updated();
let file = File::create(path).map_err(|err| WorkspaceError::io(path, err))?;
serde_json::to_writer_pretty(file, self).map_err(WorkspaceError::Serialization)
}
pub fn upsert_repo(&mut self, repo: WorkspaceRepository) -> WorkspaceResult<()> {
let id = repo.id.clone();
if let Some(existing) = self
.repositories
.iter_mut()
.find(|existing| existing.id == id)
{
*existing = repo;
} else {
self.repositories.push(repo);
}
self.metadata.touch_updated();
Ok(())
}
pub fn remove_repo(&mut self, repo_id: &WorkspaceRepoId) -> bool {
let len_before = self.repositories.len();
self.repositories.retain(|repo| repo.id != *repo_id);
let removed = self.repositories.len() != len_before;
if removed {
self.metadata.touch_updated();
}
removed
}
fn sort_repositories(&mut self) {
self.repositories.sort_by(|a, b| a.id.cmp(&b.id));
}
#[must_use]
pub fn as_map(&self) -> BTreeMap<&WorkspaceRepoId, &WorkspaceRepository> {
self.repositories
.iter()
.map(|repo| (&repo.id, repo))
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceMetadata {
pub version: u32,
pub workspace_name: Option<String>,
#[serde(default)]
pub default_discovery_mode: Option<String>,
#[serde(with = "serde_time")]
pub created_at: SystemTime,
#[serde(with = "serde_time")]
pub updated_at: SystemTime,
}
impl WorkspaceMetadata {
fn new(workspace_name: Option<String>) -> Self {
let now = SystemTime::now();
Self {
version: WORKSPACE_REGISTRY_VERSION,
workspace_name,
default_discovery_mode: None,
created_at: now,
updated_at: now,
}
}
fn touch_updated(&mut self) {
self.updated_at = SystemTime::now();
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct WorkspaceRepoId(String);
impl WorkspaceRepoId {
pub fn new(relative: impl AsRef<Path>) -> WorkspaceRepoId {
let path = relative.as_ref();
let normalized = if path.components().count() == 0 {
".".to_string()
} else {
path.components()
.map(|component| component.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/")
};
WorkspaceRepoId(normalized)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for WorkspaceRepoId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceRepository {
pub id: WorkspaceRepoId,
pub name: String,
pub root: PathBuf,
pub index_path: PathBuf,
#[serde(with = "serde_time::option")]
pub last_indexed_at: Option<SystemTime>,
pub symbol_count: Option<u64>,
pub primary_language: Option<String>,
}
impl WorkspaceRepository {
#[must_use]
pub fn new(
id: WorkspaceRepoId,
name: String,
root: PathBuf,
index_path: PathBuf,
last_indexed_at: Option<SystemTime>,
) -> Self {
Self {
id,
name,
root,
index_path,
last_indexed_at,
symbol_count: None,
primary_language: None,
}
}
}