Skip to main content

source_map_php/
projects.rs

1use std::env;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub struct ProjectRecord {
11    pub name: String,
12    pub repo_path: String,
13    pub index_prefix: String,
14    pub framework: String,
15    pub meili_host: String,
16    pub last_run_id: String,
17    pub updated_at: DateTime<Utc>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, Default)]
21pub struct ProjectRegistry {
22    #[serde(default)]
23    pub projects: Vec<ProjectRecord>,
24}
25
26impl ProjectRegistry {
27    pub fn load(path: &Path) -> Result<Self> {
28        if !path.exists() {
29            return Ok(Self::default());
30        }
31        let raw = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
32        serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))
33    }
34
35    pub fn save(&self, path: &Path) -> Result<()> {
36        if let Some(parent) = path.parent() {
37            fs::create_dir_all(parent)
38                .with_context(|| format!("create parent directory for {}", path.display()))?;
39        }
40        fs::write(path, serde_json::to_vec_pretty(self)?)
41            .with_context(|| format!("write {}", path.display()))?;
42        Ok(())
43    }
44
45    pub fn upsert(&mut self, record: ProjectRecord) {
46        if let Some(existing) = self
47            .projects
48            .iter_mut()
49            .find(|item| item.repo_path == record.repo_path || item.name == record.name)
50        {
51            *existing = record;
52        } else {
53            self.projects.push(record);
54        }
55        self.projects.sort_by(|left, right| {
56            left.name
57                .cmp(&right.name)
58                .then(left.repo_path.cmp(&right.repo_path))
59        });
60    }
61
62    pub fn resolve<'a>(&'a self, selector: &str) -> Option<&'a ProjectRecord> {
63        let selector_path = canonicalized(selector);
64        self.projects.iter().find(|record| {
65            record.name == selector
66                || record.repo_path == selector
67                || selector_path
68                    .as_deref()
69                    .is_some_and(|path| path == record.repo_path)
70        })
71    }
72
73    pub fn remove(&mut self, selector: &str) -> Option<ProjectRecord> {
74        let selector_path = canonicalized(selector);
75        let index = self.projects.iter().position(|record| {
76            record.name == selector
77                || record.repo_path == selector
78                || selector_path
79                    .as_deref()
80                    .is_some_and(|path| path == record.repo_path)
81        })?;
82        Some(self.projects.remove(index))
83    }
84}
85
86pub fn default_project_registry_path() -> PathBuf {
87    env::var_os("HOME")
88        .map(PathBuf::from)
89        .unwrap_or_else(|| PathBuf::from("~"))
90        .join(".config/meilisearch/project.json")
91}
92
93fn canonicalized(value: &str) -> Option<String> {
94    fs::canonicalize(value)
95        .ok()
96        .map(|path| path.to_string_lossy().into_owned())
97}
98
99#[cfg(test)]
100mod tests {
101    use tempfile::tempdir;
102
103    use super::{ProjectRecord, ProjectRegistry};
104    use chrono::Utc;
105
106    fn record(name: &str, repo_path: &str) -> ProjectRecord {
107        ProjectRecord {
108            name: name.to_string(),
109            repo_path: repo_path.to_string(),
110            index_prefix: name.to_string(),
111            framework: "hyperf".to_string(),
112            meili_host: "http://127.0.0.1:7700".to_string(),
113            last_run_id: "run".to_string(),
114            updated_at: Utc::now(),
115        }
116    }
117
118    #[test]
119    fn resolves_by_name_or_path() {
120        let dir = tempdir().unwrap();
121        let repo = dir.path().join("staff-api");
122        std::fs::create_dir_all(&repo).unwrap();
123
124        let mut registry = ProjectRegistry::default();
125        registry.upsert(record("staff-api", &repo.to_string_lossy()));
126
127        assert!(registry.resolve("staff-api").is_some());
128        assert!(registry.resolve(&repo.to_string_lossy()).is_some());
129    }
130
131    #[test]
132    fn upsert_replaces_existing_repo_entry() {
133        let mut registry = ProjectRegistry::default();
134        registry.upsert(record("staff-api", "/tmp/staff-api"));
135        let mut updated = record("staff-api-prod", "/tmp/staff-api");
136        updated.index_prefix = "staff-api-prod".to_string();
137        registry.upsert(updated.clone());
138
139        assert_eq!(registry.projects.len(), 1);
140        assert_eq!(registry.projects[0].name, "staff-api-prod");
141        assert_eq!(registry.projects[0].index_prefix, "staff-api-prod");
142    }
143
144    #[test]
145    fn remove_deletes_matching_record() {
146        let mut registry = ProjectRegistry::default();
147        registry.upsert(record("staff-api", "/tmp/staff-api"));
148        registry.upsert(record("front-api", "/tmp/front-api"));
149
150        let removed = registry.remove("staff-api").unwrap();
151
152        assert_eq!(removed.name, "staff-api");
153        assert_eq!(registry.projects.len(), 1);
154        assert_eq!(registry.projects[0].name, "front-api");
155    }
156}