aptu_core/repos/
custom.rs1use std::collections::HashMap;
9use std::fs;
10use std::path::PathBuf;
11
12use serde::{Deserialize, Serialize};
13use tracing::{debug, instrument};
14
15use crate::config::config_dir;
16use crate::error::AptuError;
17use crate::repos::CuratedRepo;
18
19#[must_use]
21pub fn repos_file_path() -> PathBuf {
22 config_dir().join("repos.toml")
23}
24
25#[derive(Debug, Serialize, Deserialize, Default)]
27pub struct CustomReposFile {
28 #[serde(default)]
30 pub repos: HashMap<String, CustomRepoEntry>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct CustomRepoEntry {
36 pub owner: String,
38 pub name: String,
40 pub language: String,
42 pub description: String,
44}
45
46impl From<CustomRepoEntry> for CuratedRepo {
47 fn from(entry: CustomRepoEntry) -> Self {
48 CuratedRepo {
49 owner: entry.owner,
50 name: entry.name,
51 language: entry.language,
52 description: entry.description,
53 }
54 }
55}
56
57#[instrument]
65pub fn read_custom_repos() -> crate::Result<Vec<CuratedRepo>> {
66 let path = repos_file_path();
67
68 if !path.exists() {
69 debug!("Custom repos file does not exist: {:?}", path);
70 return Ok(Vec::new());
71 }
72
73 let content = fs::read_to_string(&path).map_err(|e| AptuError::Config {
74 message: format!("Failed to read custom repos file: {e}"),
75 })?;
76
77 let file: CustomReposFile = toml::from_str(&content).map_err(|e| AptuError::Config {
78 message: format!("Failed to parse custom repos TOML: {e}"),
79 })?;
80
81 let repos: Vec<CuratedRepo> = file.repos.into_values().map(CuratedRepo::from).collect();
82
83 debug!("Read {} custom repositories", repos.len());
84 Ok(repos)
85}
86
87#[instrument(skip(repos))]
95pub fn write_custom_repos(repos: &[CuratedRepo]) -> crate::Result<()> {
96 let path = repos_file_path();
97
98 if let Some(parent) = path.parent() {
100 fs::create_dir_all(parent).map_err(|e| AptuError::Config {
101 message: format!("Failed to create config directory: {e}"),
102 })?;
103 }
104
105 let mut file = CustomReposFile::default();
106 for repo in repos {
107 file.repos.insert(
108 repo.full_name(),
109 CustomRepoEntry {
110 owner: repo.owner.clone(),
111 name: repo.name.clone(),
112 language: repo.language.clone(),
113 description: repo.description.clone(),
114 },
115 );
116 }
117
118 let content = toml::to_string_pretty(&file).map_err(|e| AptuError::Config {
119 message: format!("Failed to serialize custom repos: {e}"),
120 })?;
121
122 fs::write(&path, content).map_err(|e| AptuError::Config {
123 message: format!("Failed to write custom repos file: {e}"),
124 })?;
125
126 debug!("Wrote {} custom repositories", repos.len());
127 Ok(())
128}
129
130#[instrument]
148pub async fn validate_and_fetch_metadata(owner: &str, name: &str) -> crate::Result<CuratedRepo> {
149 use octocrab::Octocrab;
150
151 let client = Octocrab::builder().build()?;
152 let repo = client
153 .repos(owner, name)
154 .get()
155 .await
156 .map_err(|e| AptuError::GitHub {
157 message: format!("Failed to fetch repository metadata: {e}"),
158 })?;
159
160 let language = repo
161 .language
162 .map_or_else(|| "Unknown".to_string(), |v| v.to_string());
163 let description = repo.description.map_or_else(String::new, |v| v.clone());
164
165 Ok(CuratedRepo {
166 owner: owner.to_string(),
167 name: name.to_string(),
168 language,
169 description,
170 })
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_custom_repos_toml_roundtrip() {
179 let repos = vec![
180 CuratedRepo {
181 owner: "test".to_string(),
182 name: "repo1".to_string(),
183 language: "Rust".to_string(),
184 description: "Test repo 1".to_string(),
185 },
186 CuratedRepo {
187 owner: "test".to_string(),
188 name: "repo2".to_string(),
189 language: "Python".to_string(),
190 description: "Test repo 2".to_string(),
191 },
192 ];
193
194 let mut file = CustomReposFile::default();
196 for repo in &repos {
197 file.repos.insert(
198 repo.full_name(),
199 CustomRepoEntry {
200 owner: repo.owner.clone(),
201 name: repo.name.clone(),
202 language: repo.language.clone(),
203 description: repo.description.clone(),
204 },
205 );
206 }
207
208 let toml_str = toml::to_string_pretty(&file).expect("should serialize");
209
210 let parsed: CustomReposFile = toml::from_str(&toml_str).expect("should deserialize");
212 let result: Vec<CuratedRepo> = parsed.repos.into_values().map(CuratedRepo::from).collect();
213
214 assert_eq!(result.len(), 2);
215 let full_names: std::collections::HashSet<_> = result
216 .iter()
217 .map(super::super::CuratedRepo::full_name)
218 .collect();
219 assert!(full_names.contains("test/repo1"));
220 assert!(full_names.contains("test/repo2"));
221 }
222}