oseda_cli/
config.rs

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
15/// Reads and validates an oseda-config.json file in the working directory
16///
17/// This checks a few things:
18/// - the file exists and parses correctly
19/// - the git `user.name` matches the config author (unless --skip-git is passed)
20/// - the config `title` matches the name of the working directory
21///
22/// # Arguments
23/// * `skip_git` - skips the git author validation, primarily used for CI, not by the end user hopefully lol
24///
25/// # Returns
26/// * `Ok(OsedaConfig)` if the file is valid and all checks pass
27/// * `Err(OsedaCheckError)` if any check fails
28pub fn read_and_validate_config() -> Result<OsedaConfig, OsedaCheckError> {
29    let config_str = fs::read_to_string("oseda-config.json").map_err(|_| {
30        OsedaCheckError::MissingConfig(format!(
31            "Could not find config file in {}",
32            std::env::current_dir().unwrap().to_str().unwrap()
33        ))
34    })?;
35
36    let conf: OsedaConfig = serde_json::from_str(&config_str)
37        .map_err(|_| OsedaCheckError::BadConfig("Could not parse oseda config file".to_owned()))?;
38
39    //https://stackoverflow.com/questions/73973332/check-if-were-in-a-github-action-travis-ci-circle-ci-etc-testing-environme
40    let is_in_ci = std::env::var("GITHUB_ACTIONS").map_or(false, |v| v == "true");
41    let skip_git = is_in_ci;
42
43    if !skip_git {
44        println!("Running git checks");
45        let gh_name = github::get_config("user.name").ok_or_else(|| {
46            OsedaCheckError::BadGitCredentials(
47                "Could not get git user.name from git config".to_owned(),
48            )
49        })?;
50
51        if gh_name != conf.author {
52            return Err(OsedaCheckError::BadGitCredentials(
53                "Config author does not match git credentials".to_owned(),
54            ));
55        }
56    }
57
58    let path = std::env::current_dir().map_err(|_| {
59        OsedaCheckError::DirectoryNameMismatch("Could not get path of working directory".to_owned())
60    })?;
61
62    let cwd = path.file_name().ok_or_else(|| {
63        OsedaCheckError::DirectoryNameMismatch("Could not resolve path name".to_owned())
64    })?;
65
66    if cwd != OsString::from(conf.title.clone()) {
67        return Err(OsedaCheckError::DirectoryNameMismatch(
68            "Config title does not match directory name".to_owned(),
69        ));
70    }
71
72    Ok(conf)
73}
74
75/// Structure for an oseda-config.json
76#[derive(Serialize, Deserialize)]
77pub struct OsedaConfig {
78    pub title: String,
79    pub author: String,
80    pub category: Vec<Category>,
81    // effectively mutable. Will get updated on each deployment
82    pub last_updated: DateTime<Utc>,
83}
84
85/// Prompts the user for everything needed to generate a new OsedaConfig
86///
87/// # Returns
88/// * `Ok(OsedaConfig)` containing validated project config options
89/// * `Err` if a required input conf is invalid
90pub fn create_conf() -> Result<OsedaConfig, Box<dyn Error>> {
91    // let mut title = String::new();
92    // std::io::stdin().read_line(&mut title)?;
93
94    let validator = |input: &str| {
95        if input.chars().count() < 2 {
96            Ok(Validation::Invalid(
97                ("Title must be longer than two characters").into(),
98            ))
99        } else {
100            Ok(Validation::Valid)
101        }
102    };
103
104    let mut title = inquire::Text::new("Title: ")
105        .with_validator(validator)
106        .prompt()?;
107
108    title = title.replace(" ", "-");
109
110    let categories = get_categories()?;
111
112    let user_name = github::get_config("user.name")
113        .ok_or("Could not get github username. Please ensure you are signed into github")?;
114
115    Ok(OsedaConfig {
116        title: title.trim().to_owned(),
117        author: user_name,
118        category: categories,
119        last_updated: get_time(),
120    })
121}
122
123/// Prompts user for categories associated with their Oseda project
124///
125/// # Returns
126/// * `Ok(Vec<Category>)` with selected categories
127/// * `Err` if the prompting went wrong somewhere
128fn get_categories() -> Result<Vec<Category>, Box<dyn Error>> {
129    let options: Vec<Category> = Category::iter().collect();
130
131    let selected_categories =
132        inquire::MultiSelect::new("Select categories", options.clone()).prompt()?;
133
134    println!("You selected:");
135    for category in selected_categories.iter() {
136        println!("- {:?}", category);
137    }
138
139    Ok(selected_categories)
140}
141
142/// Updates the configs last-updated
143/// Currently this is used on creation only, TODO fix this
144///
145/// # Arguments
146/// * `conf` - a previously loaded or generated OsedaConfig
147///
148/// # Returns
149/// * `Ok(())` if the file is successfully updated
150/// * `Err` if file writing fails
151pub fn update_time(mut conf: OsedaConfig) -> Result<(), Box<dyn Error>> {
152    conf.last_updated = get_time();
153
154    write_config(".", &conf)?;
155    Ok(())
156}
157
158/// Gets the current system time in UTC
159///
160/// # Returns
161/// * a `DateTime<Utc>` representing the current time
162fn get_time() -> DateTime<Utc> {
163    chrono::offset::Utc::now()
164}
165
166/// Write an OsedaConfig to the provided directory
167///
168/// # Arguments
169/// * `path` - the directory path to write into
170/// * `conf` - the `OsedaConfig` instance to serialize via serde
171///
172/// # Returns
173/// * `Ok(())` if the file is written successfully
174/// * `Err` if file creation or serialization fails
175pub fn write_config(path: &str, conf: &OsedaConfig) -> Result<(), Box<dyn Error>> {
176    let file = File::create(format!("{}/oseda-config.json", path))?;
177    let writer = BufWriter::new(file);
178
179    serde_json::to_writer_pretty(writer, &conf)?;
180
181    Ok(())
182}