source_map_tauri/
projects.rs1use 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}