1use std::error::Error;
2use std::fs::File;
3use std::io::BufWriter;
4use std::{ffi::OsString, fs};
5
6use chrono::{DateTime, Utc};
7use inquire::validator::Validation;
8use serde::{Deserialize, Serialize};
9use strum::IntoEnumIterator;
10
11use crate::categories::Category;
12use crate::cmd::check::OsedaCheckError;
13use crate::github;
14
15pub fn read_config_file<P: AsRef<std::path::Path>>(
16 path: P,
17) -> Result<OsedaConfig, OsedaCheckError> {
18 let config_str = fs::read_to_string(path.as_ref()).map_err(|_| {
19 OsedaCheckError::MissingConfig(format!(
20 "Could not find config file in {}",
21 path.as_ref().display()
22 ))
23 })?;
24
25 let conf: OsedaConfig = serde_json::from_str(&config_str)
26 .map_err(|_| OsedaCheckError::BadConfig("Could not parse oseda config file".to_owned()))?;
27
28 Ok(conf)
29}
30
31pub fn read_and_validate_config() -> Result<OsedaConfig, OsedaCheckError> {
45 let path = std::env::current_dir().map_err(|_| {
46 OsedaCheckError::DirectoryNameMismatch("Could not get path of working directory".to_owned())
47 })?;
48
49 let config_path = path.join("oseda-config.json");
50
51 let conf = read_config_file(config_path)?;
52
53 let is_in_ci = std::env::var("GITHUB_ACTIONS").map_or(false, |v| v == "true");
54 let skip_git = is_in_ci;
55
56 validate_config(&conf, &path, skip_git, || {
57 github::get_config_from_user_git("user.name")
58 })?;
59
60 Ok(conf)
61}
62
63pub fn validate_config(
64 conf: &OsedaConfig,
65 current_dir: &std::path::Path,
66 skip_git: bool,
67 get_git_user: impl Fn() -> Option<String>,
70) -> Result<(), OsedaCheckError> {
71 if !skip_git {
72 let gh_name = get_git_user().ok_or_else(|| {
73 OsedaCheckError::BadGitCredentials(
74 "Could not get git user.name from git config".to_owned(),
75 )
76 })?;
77
78 if gh_name != conf.author {
79 return Err(OsedaCheckError::BadGitCredentials(
80 "Config author does not match git credentials".to_owned(),
81 ));
82 }
83 }
84
85 let cwd = current_dir.file_name().ok_or_else(|| {
86 OsedaCheckError::DirectoryNameMismatch("Could not resolve path name".to_owned())
87 })?;
88
89 if cwd != OsString::from(conf.title.clone()) {
90 return Err(OsedaCheckError::DirectoryNameMismatch(
91 "Config title does not match directory name".to_owned(),
92 ));
93 }
94
95 Ok(())
96}
97
98#[derive(Serialize, Deserialize)]
100pub struct OsedaConfig {
101 pub title: String,
102 pub author: String,
103 pub category: Vec<Category>,
104 pub last_updated: DateTime<Utc>,
106}
107
108pub fn create_conf() -> Result<OsedaConfig, Box<dyn Error>> {
114 let validator = |input: &str| {
118 if input.chars().count() < 2 {
119 Ok(Validation::Invalid(
120 ("Title must be longer than two characters").into(),
121 ))
122 } else {
123 Ok(Validation::Valid)
124 }
125 };
126
127 let mut title = inquire::Text::new("Title: ")
128 .with_validator(validator)
129 .prompt()?;
130
131 title = title.replace(" ", "-");
132
133 let categories = get_categories()?;
134
135 let user_name = github::get_config_from_user_git("user.name")
136 .ok_or("Could not get github username. Please ensure you are signed into github")?;
137
138 Ok(OsedaConfig {
139 title: title.trim().to_owned(),
140 author: user_name,
141 category: categories,
142 last_updated: get_time(),
143 })
144}
145
146fn get_categories() -> Result<Vec<Category>, Box<dyn Error>> {
152 let options: Vec<Category> = Category::iter().collect();
153
154 let selected_categories =
155 inquire::MultiSelect::new("Select categories (type to search):", options.clone())
156 .prompt()?;
157
158 println!("You selected:");
159 for category in selected_categories.iter() {
160 println!("- {:?}", category);
161 }
162
163 Ok(selected_categories)
164}
165
166pub fn update_time(mut conf: OsedaConfig) -> Result<(), Box<dyn Error>> {
176 conf.last_updated = get_time();
177
178 write_config(".", &conf)?;
179 Ok(())
180}
181
182fn get_time() -> DateTime<Utc> {
187 chrono::offset::Utc::now()
188}
189
190pub fn write_config(path: &str, conf: &OsedaConfig) -> Result<(), Box<dyn Error>> {
200 let file = File::create(format!("{}/oseda-config.json", path))?;
201 let writer = BufWriter::new(file);
202
203 serde_json::to_writer_pretty(writer, &conf)?;
204
205 Ok(())
206}
207
208#[cfg(test)]
209mod test {
210 use std::path::Path;
211
212 use chrono::{Date, NaiveDate};
213 use tempfile::tempdir;
214
215 use super::*;
216
217 fn mock_config_json() -> String {
218 r#"
219 {
220 "title": "TestableRust",
221 "author": "JaneDoe",
222 "category": ["ComputerScience"],
223 "last_updated": "2024-07-10T12:34:56Z"
224 }
225 "#
226 .trim()
227 .to_string()
228 }
229
230 #[test]
231 fn test_read_config_file_missing() {
232 let dir = tempdir().unwrap();
233 let config_path = dir.path().join("oseda-config.json");
234
235 let result = read_config_file(&config_path);
236 assert!(matches!(result, Err(OsedaCheckError::MissingConfig(_))));
237 }
238
239 #[test]
240 fn test_validate_config_success() {
241 let conf = OsedaConfig {
242 title: "my-project".to_string(),
243 author: "JaneDoe".to_string(),
244 category: vec![Category::ComputerScience],
245 last_updated: chrono::Utc::now(),
246 };
247
248 let fake_dir = Path::new("/tmp/my-project");
249 let result = validate_config(&conf, fake_dir, false, || Some("JaneDoe".to_string()));
251
252 assert!(result.is_ok());
253 }
254
255 #[test]
256 fn test_validate_config_bad_git_user() {
257 let conf = OsedaConfig {
258 title: "my-project".to_string(),
259 author: "JaneDoe".to_string(),
260 category: vec![Category::ComputerScience],
261 last_updated: chrono::Utc::now(),
262 };
263
264 let fake_dir = Path::new("/tmp/oseda");
265
266 let result = validate_config(&conf, fake_dir, false, || Some("NotJane".to_string()));
267
268 assert!(matches!(result, Err(OsedaCheckError::BadGitCredentials(_))));
269 }
270
271 #[test]
272 fn test_validate_config_bad_dir_name() {
273 let conf = OsedaConfig {
274 title: "correct-name".to_string(),
275 author: "JaneDoe".to_string(),
276 category: vec![Category::ComputerScience],
277 last_updated: chrono::Utc::now(),
278 };
279
280 let fake_dir = Path::new("/tmp/wrong-name");
281
282 let result = validate_config(&conf, fake_dir, false, || Some("JaneDoe".to_string()));
283 assert!(matches!(
284 result,
285 Err(OsedaCheckError::DirectoryNameMismatch(_))
286 ));
287 }
288
289 #[test]
290 fn test_validate_config_skip_git() {
291 let conf = OsedaConfig {
292 title: "oseda".to_string(),
293 author: "JaneDoe".to_string(),
294 category: vec![Category::ComputerScience],
295 last_updated: chrono::Utc::now(),
296 };
297
298 let fake_dir = Path::new("/tmp/oseda");
299
300 let result = validate_config(&conf, fake_dir, true, || None);
301 assert!(result.is_ok());
302 }
303}