Skip to main content

source_map_tauri/
projects.rs

1use std::{
2    env, fs,
3    path::{Path, PathBuf},
4};
5
6use anyhow::{Context, Result};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct ProjectRecord {
12    pub name: String,
13    pub repo_path: String,
14    pub index_uid: String,
15    pub meili_host: String,
16    pub updated_at: DateTime<Utc>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, Default)]
20pub struct ProjectRegistry {
21    #[serde(default)]
22    pub projects: Vec<ProjectRecord>,
23}
24
25impl ProjectRegistry {
26    pub fn load(path: &Path) -> Result<Self> {
27        if !path.exists() {
28            return Ok(Self::default());
29        }
30        let raw = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
31        serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))
32    }
33
34    pub fn save(&self, path: &Path) -> Result<()> {
35        if let Some(parent) = path.parent() {
36            fs::create_dir_all(parent)
37                .with_context(|| format!("create parent directory for {}", path.display()))?;
38        }
39        fs::write(path, serde_json::to_vec_pretty(self)?)
40            .with_context(|| format!("write {}", path.display()))?;
41        Ok(())
42    }
43
44    pub fn upsert(&mut self, record: ProjectRecord) {
45        if let Some(existing) = self
46            .projects
47            .iter_mut()
48            .find(|item| item.repo_path == record.repo_path || item.name == record.name)
49        {
50            *existing = record;
51        } else {
52            self.projects.push(record);
53        }
54        self.projects.sort_by(|left, right| {
55            left.name
56                .cmp(&right.name)
57                .then(left.repo_path.cmp(&right.repo_path))
58        });
59    }
60}
61
62pub fn default_project_registry_path() -> PathBuf {
63    env::var_os("HOME")
64        .map(PathBuf::from)
65        .unwrap_or_else(|| PathBuf::from("~"))
66        .join(".config/meilisearch/project.json")
67}
68
69pub fn upsert_project_registry(record: ProjectRecord) -> Result<()> {
70    let path = default_project_registry_path();
71    let mut registry = ProjectRegistry::load(&path)?;
72    registry.upsert(record);
73    registry.save(&path)
74}
75
76#[cfg(test)]
77mod tests {
78    use tempfile::tempdir;
79
80    use super::{ProjectRecord, ProjectRegistry};
81    use chrono::Utc;
82
83    fn record(name: &str, repo_path: &str) -> ProjectRecord {
84        ProjectRecord {
85            name: name.to_owned(),
86            repo_path: repo_path.to_owned(),
87            index_uid: format!("{name}_index"),
88            meili_host: "http://127.0.0.1:7700/".to_owned(),
89            updated_at: Utc::now(),
90        }
91    }
92
93    #[test]
94    fn upsert_replaces_existing_repo_entry() {
95        let mut registry = ProjectRegistry::default();
96        registry.upsert(record("tool", "/tmp/tool"));
97        let mut updated = record("tool-desktop", "/tmp/tool");
98        updated.index_uid = "tool_desktop_index".to_owned();
99        registry.upsert(updated.clone());
100
101        assert_eq!(registry.projects.len(), 1);
102        assert_eq!(registry.projects[0].name, "tool-desktop");
103        assert_eq!(registry.projects[0].index_uid, "tool_desktop_index");
104    }
105
106    #[test]
107    fn load_and_save_round_trip() {
108        let temp = tempdir().unwrap();
109        let path = temp.path().join("project.json");
110        let mut registry = ProjectRegistry::default();
111        registry.upsert(record("tool", "/tmp/tool"));
112        registry.save(&path).unwrap();
113        let loaded = ProjectRegistry::load(&path).unwrap();
114        assert_eq!(loaded.projects.len(), 1);
115        assert_eq!(loaded.projects[0].name, "tool");
116    }
117}