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