use std::{
fmt::Display,
fs::{create_dir_all, read_to_string, File},
path::PathBuf,
rc::Rc,
};
use anyhow::{bail, Result};
use colored::*;
use serde::{Deserialize, Serialize};
use crate::project_info::{
is_valid_python_version, Day, DependabotSchedule, LicenseType, ProjectManager,
Pyo3PythonManager,
};
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Config {
pub creator: Option<String>,
pub creator_email: Option<String>,
pub license: Option<LicenseType>,
pub python_version: Option<String>,
pub min_python_version: Option<String>,
pub project_manager: Option<ProjectManager>,
pub pyo3_python_manager: Option<Pyo3PythonManager>,
pub is_async_project: Option<bool>,
pub is_application: Option<bool>,
pub github_actions_python_test_versions: Option<Vec<String>>,
pub max_line_length: Option<u8>,
pub use_dependabot: Option<bool>,
pub dependabot_schedule: Option<DependabotSchedule>,
pub dependabot_day: Option<Day>,
pub use_continuous_deployment: Option<bool>,
pub use_release_drafter: Option<bool>,
pub use_multi_os_ci: Option<bool>,
pub include_docs: Option<bool>,
#[cfg(feature = "fastapi")]
pub is_fastapi_project: Option<bool>,
#[serde(skip)]
config_dir: Rc<Option<PathBuf>>,
#[serde(skip)]
config_file_path: Rc<Option<PathBuf>>,
}
impl Default for Config {
fn default() -> Self {
Self {
creator: None,
creator_email: None,
license: None,
python_version: None,
min_python_version: None,
project_manager: None,
pyo3_python_manager: None,
is_async_project: None,
is_application: None,
github_actions_python_test_versions: None,
max_line_length: None,
use_dependabot: None,
dependabot_schedule: None,
dependabot_day: None,
use_continuous_deployment: None,
use_release_drafter: None,
use_multi_os_ci: None,
include_docs: None,
config_dir: config_dir(),
config_file_path: config_file_path(),
#[cfg(feature = "fastapi")]
is_fastapi_project: None,
}
}
}
impl Config {
pub fn load_config(&self) -> Self {
if let Some(config_file) = &*self.config_file_path {
if config_file.exists() {
if let Ok(config_str) = read_to_string(config_file) {
if let Ok(config) = serde_json::from_str::<Self>(&config_str) {
return Self {
creator: config.creator,
creator_email: config.creator_email,
license: config.license,
python_version: config.python_version,
min_python_version: config.min_python_version,
project_manager: config.project_manager,
pyo3_python_manager: config.pyo3_python_manager,
is_async_project: config.is_async_project,
is_application: config.is_application,
github_actions_python_test_versions: config
.github_actions_python_test_versions,
max_line_length: config.max_line_length,
use_dependabot: config.use_dependabot,
dependabot_schedule: config.dependabot_schedule,
dependabot_day: config.dependabot_day,
use_continuous_deployment: config.use_continuous_deployment,
use_release_drafter: config.use_release_drafter,
use_multi_os_ci: config.use_multi_os_ci,
include_docs: config.include_docs,
config_dir: self.config_dir.clone(),
config_file_path: self.config_file_path.clone(),
#[cfg(feature = "fastapi")]
is_fastapi_project: config.is_fastapi_project,
};
}
}
}
};
Self::default()
}
pub fn reset() -> Result<()> {
let config = Self::default();
config.save()?;
Ok(())
}
pub fn save(&self) -> Result<()> {
match &*self.config_dir {
Some(c) => {
if !c.exists() {
create_dir_all(c)?;
}
match &*self.config_file_path {
Some(c) => {
let config_file = File::create(c)?;
serde_json::to_writer_pretty(config_file, self)?;
}
None => {
bail!("Error saving config file");
}
}
}
None => {
bail!("Error saving config file");
}
}
Ok(())
}
pub fn save_creator(&self, value: String) -> Result<()> {
self.handle_save_config(|config| &mut config.creator, Some(value))?;
Ok(())
}
pub fn reset_creator(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.creator, None)?;
Ok(())
}
pub fn save_creator_email(&self, value: String) -> Result<()> {
self.handle_save_config(|config| &mut config.creator_email, Some(value))?;
Ok(())
}
pub fn reset_creator_email(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.creator_email, None)?;
Ok(())
}
pub fn save_license(&self, value: LicenseType) -> Result<()> {
self.handle_save_config(|config| &mut config.license, Some(value))?;
Ok(())
}
pub fn reset_license(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.license, None)?;
Ok(())
}
pub fn save_python_version(&self, value: String) -> Result<()> {
self.handle_save_config(|config| &mut config.python_version, Some(value))?;
Ok(())
}
pub fn reset_python_version(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.python_version, None)?;
Ok(())
}
pub fn save_min_python_version(&self, value: String) -> Result<()> {
self.handle_save_config(|config| &mut config.min_python_version, Some(value))?;
Ok(())
}
pub fn reset_min_python_version(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.min_python_version, None)?;
Ok(())
}
pub fn save_project_manager(&self, value: ProjectManager) -> Result<()> {
self.handle_save_config(|config| &mut config.project_manager, Some(value))?;
Ok(())
}
pub fn reset_project_manager(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.project_manager, None)?;
Ok(())
}
pub fn save_pyo3_python_manager(&self, value: Pyo3PythonManager) -> Result<()> {
self.handle_save_config(|config| &mut config.pyo3_python_manager, Some(value))?;
Ok(())
}
pub fn reset_pyo3_python_manager(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.pyo3_python_manager, None)?;
Ok(())
}
pub fn save_is_async_project(&self, value: bool) -> Result<()> {
self.handle_save_config(|config| &mut config.is_async_project, Some(value))?;
Ok(())
}
pub fn reset_is_async_project(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.is_async_project, None)?;
Ok(())
}
pub fn save_is_application(&self, value: bool) -> Result<()> {
self.handle_save_config(|config| &mut config.is_application, Some(value))?;
Ok(())
}
pub fn reset_is_application(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.is_application, None)?;
Ok(())
}
pub fn save_github_actions_python_test_versions(&self, value: String) -> Result<()> {
self.handle_save_github_actions_python_test_versions(Some(value))?;
Ok(())
}
pub fn reset_github_actions_python_test_versions(&self) -> Result<()> {
self.handle_save_github_actions_python_test_versions(None)?;
Ok(())
}
fn handle_save_github_actions_python_test_versions(&self, value: Option<String>) -> Result<()> {
let mut config = self.load_config();
if let Some(v) = value {
let versions = v
.replace(' ', "")
.split(',')
.map(|s| s.to_string())
.collect::<Vec<String>>();
for version in &versions {
if !is_valid_python_version(&version.replace('"', "")) {
bail!(format!("{} is not a valid Python Version", version));
}
}
config.github_actions_python_test_versions = Some(versions);
} else {
config.github_actions_python_test_versions = None;
}
config.save()?;
Ok(())
}
pub fn save_max_line_length(&self, value: u8) -> Result<()> {
self.handle_save_config(|config| &mut config.max_line_length, Some(value))?;
Ok(())
}
pub fn reset_max_line_length(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.max_line_length, None)?;
Ok(())
}
pub fn save_use_dependabot(&self, value: bool) -> Result<()> {
self.handle_save_config(|config| &mut config.use_dependabot, Some(value))?;
Ok(())
}
pub fn reset_use_dependabot(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.use_dependabot, None)?;
Ok(())
}
pub fn save_dependabot_schedule(&self, value: DependabotSchedule) -> Result<()> {
self.handle_save_config(|config| &mut config.dependabot_schedule, Some(value))?;
Ok(())
}
pub fn reset_dependabot_schedule(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.dependabot_schedule, None)?;
Ok(())
}
pub fn save_dependabot_day(&self, value: Day) -> Result<()> {
self.handle_save_config(|config| &mut config.dependabot_day, Some(value))?;
Ok(())
}
pub fn reset_dependabot_day(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.dependabot_day, None)?;
Ok(())
}
pub fn save_use_continuous_deployment(&self, value: bool) -> Result<()> {
self.handle_save_config(|config| &mut config.use_continuous_deployment, Some(value))?;
Ok(())
}
pub fn reset_use_continuous_deployment(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.use_continuous_deployment, None)?;
Ok(())
}
pub fn save_use_release_drafter(&self, value: bool) -> Result<()> {
self.handle_save_config(|config| &mut config.use_release_drafter, Some(value))?;
Ok(())
}
pub fn reset_use_release_drafter(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.use_release_drafter, None)?;
Ok(())
}
pub fn save_use_multi_os_ci(&self, value: bool) -> Result<()> {
self.handle_save_config(|config| &mut config.use_multi_os_ci, Some(value))?;
Ok(())
}
pub fn reset_use_multi_os_ci(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.use_multi_os_ci, None)?;
Ok(())
}
pub fn save_include_docs(&self, value: bool) -> Result<()> {
self.handle_save_config(|config| &mut config.include_docs, Some(value))?;
Ok(())
}
pub fn reset_include_docs(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.include_docs, None)?;
Ok(())
}
fn handle_save_config<F, T>(&self, func: F, value: Option<T>) -> Result<()>
where
F: FnOnce(&mut Self) -> &mut Option<T>,
{
let mut config = self.load_config();
let field = func(&mut config);
*field = value;
config.save()?;
Ok(())
}
#[cfg(feature = "fastapi")]
pub fn save_is_fastapi_project(&self, value: bool) -> Result<()> {
self.handle_save_config(|config| &mut config.is_fastapi_project, Some(value))?;
Ok(())
}
#[cfg(feature = "fastapi")]
pub fn reset_is_fastapi_project(&self) -> Result<()> {
self.handle_save_config(|config| &mut config.is_fastapi_project, None)?;
Ok(())
}
pub fn show(&self) {
let config = self.load_config();
print_config_value("Creator", &config.creator);
print_config_value("Creator Email", &config.creator_email);
print_config_value("License", &config.license);
print_config_value("Python Version", &config.python_version);
print_config_value("Min Python Version", &config.min_python_version);
let is_application_label = "Application or Library";
if let Some(is_application) = config.is_application {
if is_application {
println!("{}: application", is_application_label.blue());
} else {
println!("{}: lib", is_application_label.blue());
}
} else {
println!("{}: null", is_application_label.blue());
}
let gha_python_label = "Python Versions for Github Actions Testing";
if let Some(gha_python) = config.github_actions_python_test_versions {
let gha_python_str = gha_python.join(", ");
println!("{}: {gha_python_str}", gha_python_label.blue());
} else {
println!("{}: null", gha_python_label.blue());
}
print_config_value("Project Manager", &config.project_manager);
print_config_value("PyO3 Python Manager", &config.pyo3_python_manager);
print_config_value("Async Project", &config.is_async_project);
print_config_value("Max Line Length", &config.max_line_length);
print_config_value("Use Dependabot", &config.use_dependabot);
print_config_value("Dependabot Schedule", &config.dependabot_schedule);
print_config_value("Dependabot Day", &config.dependabot_day);
print_config_value(
"Use Continuous Deployment",
&config.use_continuous_deployment,
);
print_config_value("Use Release Drafter", &config.use_release_drafter);
print_config_value("Use Multi OS CI", &config.use_multi_os_ci);
print_config_value("Include Docs", &config.include_docs);
#[cfg(feature = "fastapi")]
print_config_value("FastAPI Project", &config.is_fastapi_project);
#[cfg(feature = "fastapi")]
print_config_value("Database Manager", &config.is_fastapi_project);
}
}
fn config_dir() -> Rc<Option<PathBuf>> {
let config_dir: Option<PathBuf> = dirs::config_dir();
if let Some(mut c) = config_dir {
c.push("python-project-generator");
return Rc::new(Some(c));
}
Rc::new(None)
}
fn config_file_path() -> Rc<Option<PathBuf>> {
if let Some(c) = &config_dir().as_ref() {
let mut c = c.clone();
c.push("config.json");
return Rc::new(Some(c));
};
Rc::new(None)
}
fn print_config_value<T: Display>(label: &str, value: &Option<T>) {
if let Some(v) = value {
println!("{}: {}", label.blue(), v);
} else {
println!("{}: null", label.blue());
}
}
#[cfg(test)]
mod tests {
use super::*;
use tmp_path::tmp_path;
#[tmp_path]
fn mock_config() -> Config {
tmp_path.push("python-project-generator");
let config_dir = tmp_path.clone();
create_dir_all(&config_dir).unwrap();
tmp_path.push("config.json");
let config_file_path = tmp_path;
let config = Config {
config_dir: Some(config_dir).into(),
config_file_path: Some(config_file_path).into(),
..Default::default()
};
config.save().unwrap();
config
}
#[test]
fn test_config_dir() {
let config_dir = config_dir();
assert_ne!(config_dir, Rc::new(None));
let config = config_dir.as_ref().as_ref().unwrap();
let last = config.file_name();
assert_ne!(last, None);
assert_eq!(last.unwrap(), "python-project-generator");
}
#[test]
fn test_config_file_path() {
let config_file_path = config_file_path();
assert_ne!(config_file_path, Rc::new(None));
let mut config = config_file_path.as_ref().as_ref().unwrap().clone();
let last = config.file_name();
assert_ne!(last, None);
assert_eq!(last.unwrap(), "config.json");
config.pop();
let dir = config.file_name();
assert_ne!(dir, None);
assert_eq!(dir.unwrap(), "python-project-generator");
}
#[test]
fn test_save_and_load_config() {
let mut config = mock_config();
config.creator = Some("Some Person".to_string());
config.creator_email = Some("someone@email.com".to_string());
config.save().unwrap();
let result = config.load_config();
assert_eq!(result, config);
}
#[test]
fn test_save_creator() {
let config = mock_config();
let expected = "Some Person".to_string();
config.save_creator(expected.clone()).unwrap();
let result = config.load_config();
assert_eq!(result.creator, Some(expected));
}
#[test]
fn test_reset_creator() {
let config = mock_config();
config.save_creator("Some Person".to_string()).unwrap();
config.reset_creator().unwrap();
let result = config.load_config();
assert!(result.creator.is_none());
}
#[test]
fn test_save_creator_email() {
let config = mock_config();
let expected = "someone@email.com".to_string();
config.save_creator_email(expected.clone()).unwrap();
let result = config.load_config();
assert_eq!(result.creator_email, Some(expected));
}
#[test]
fn test_reset_creator_email() {
let config = mock_config();
config
.save_creator_email("someone@email.com".to_string())
.unwrap();
config.reset_creator_email().unwrap();
let result = config.load_config();
assert_eq!(result.creator_email, None);
}
#[test]
fn test_save_license() {
let config = mock_config();
let expected = LicenseType::Apache2;
config.save_license(expected.clone()).unwrap();
let result = config.load_config();
assert_eq!(result.license, Some(expected));
}
#[test]
fn test_reset_license() {
let config = mock_config();
config.save_license(LicenseType::Apache2).unwrap();
config.reset_license().unwrap();
let result = config.load_config();
assert_eq!(result.license, None);
}
#[test]
fn test_save_python_version() {
let config = mock_config();
let expected = "3.12".to_string();
config.save_python_version(expected.clone()).unwrap();
let result = config.load_config();
assert_eq!(result.python_version, Some(expected));
}
#[test]
fn test_reset_python_version() {
let config = mock_config();
config.save_python_version("3.12".to_string()).unwrap();
config.reset_python_version().unwrap();
let result = config.load_config();
assert_eq!(result.python_version, None);
}
#[test]
fn test_save_min_python_version() {
let config = mock_config();
let expected = "3.12".to_string();
config.save_min_python_version(expected.clone()).unwrap();
let result = config.load_config();
assert_eq!(result.min_python_version, Some(expected));
}
#[test]
fn test_reset_min_python_version() {
let config = mock_config();
config.save_min_python_version("3.12".to_string()).unwrap();
config.reset_min_python_version().unwrap();
let result = config.load_config();
assert_eq!(result.min_python_version, None);
}
#[test]
fn test_save_project_manager() {
let config = mock_config();
let expected = ProjectManager::Maturin;
config.save_project_manager(expected.clone()).unwrap();
let result = config.load_config();
assert_eq!(result.project_manager, Some(expected));
}
#[test]
fn test_reset_project_manager() {
let config = mock_config();
config
.save_project_manager(ProjectManager::Maturin)
.unwrap();
config.reset_project_manager().unwrap();
let result = config.load_config();
assert_eq!(result.project_manager, None);
}
#[test]
fn test_save_pyo3_python_manger() {
let config = mock_config();
let expected = Pyo3PythonManager::Uv;
config.save_pyo3_python_manager(expected.clone()).unwrap();
let result = config.load_config();
assert_eq!(result.pyo3_python_manager, Some(expected));
}
#[test]
fn test_reset_pyo3_project_manager() {
let config = mock_config();
config
.save_pyo3_python_manager(Pyo3PythonManager::Uv)
.unwrap();
config.reset_pyo3_python_manager().unwrap();
let result = config.load_config();
assert_eq!(result.pyo3_python_manager, None);
}
#[test]
fn test_save_is_async_project() {
let config = mock_config();
let expected = true;
config.save_is_async_project(expected).unwrap();
let result = config.load_config();
assert_eq!(result.is_async_project, Some(expected));
}
#[test]
fn test_reset_is_async_project() {
let config = mock_config();
config.save_is_async_project(true).unwrap();
config.reset_is_async_project().unwrap();
let result = config.load_config();
assert_eq!(result.is_async_project, None);
}
#[test]
fn test_save_is_application() {
let config = mock_config();
let expected = false;
config.save_is_application(expected).unwrap();
let result = config.load_config();
assert_eq!(result.is_application, Some(expected));
}
#[test]
fn test_reset_is_application() {
let config = mock_config();
config.save_is_application(false).unwrap();
config.reset_is_application().unwrap();
let result = config.load_config();
assert_eq!(result.is_application, None);
}
#[test]
fn test_save_github_actions_python_test_versions() {
let config = mock_config();
let expected = vec!["3.11".to_string(), "3.12".to_string()];
config
.save_github_actions_python_test_versions("3.11, 3.12".to_string())
.unwrap();
let result = config.load_config();
assert_eq!(result.github_actions_python_test_versions, Some(expected));
}
#[test]
fn test_reset_github_actions_python_test_versions() {
let config = mock_config();
config
.save_github_actions_python_test_versions("3.11, 3.12".to_string())
.unwrap();
config.reset_github_actions_python_test_versions().unwrap();
let result = config.load_config();
assert_eq!(result.github_actions_python_test_versions, None);
}
#[test]
fn test_save_max_line_length() {
let config = mock_config();
let expected = 42;
config.save_max_line_length(expected).unwrap();
let result = config.load_config();
assert_eq!(result.max_line_length, Some(expected));
}
#[test]
fn test_reset_max_line_length() {
let config = mock_config();
config.save_max_line_length(42).unwrap();
config.reset_max_line_length().unwrap();
let result = config.load_config();
assert_eq!(result.max_line_length, None);
}
#[test]
fn test_save_use_dependabot() {
let config = mock_config();
let expected = false;
config.save_use_dependabot(expected).unwrap();
let result = config.load_config();
assert_eq!(result.use_dependabot, Some(expected));
}
#[test]
fn test_reset_use_dependabot() {
let config = mock_config();
config.save_use_dependabot(false).unwrap();
config.reset_use_dependabot().unwrap();
let result = config.load_config();
assert_eq!(result.use_dependabot, None);
}
#[test]
fn test_save_dependabot_schedule() {
let config = mock_config();
let expected = DependabotSchedule::Weekly;
config.save_dependabot_schedule(expected.clone()).unwrap();
let result = config.load_config();
assert_eq!(result.dependabot_schedule, Some(expected));
}
#[test]
fn test_reset_dependabot_schedule() {
let config = mock_config();
config
.save_dependabot_schedule(DependabotSchedule::Weekly)
.unwrap();
config.reset_dependabot_schedule().unwrap();
let result = config.load_config();
assert_eq!(result.dependabot_schedule, None);
}
#[test]
fn test_save_dependabot_day() {
let config = mock_config();
let expected = Day::Monday;
config.save_dependabot_day(expected.clone()).unwrap();
let result = config.load_config();
assert_eq!(result.dependabot_day, Some(expected));
}
#[test]
fn test_reset_dependabot_day() {
let config = mock_config();
config.save_dependabot_day(Day::Tuesday).unwrap();
config.reset_dependabot_day().unwrap();
let result = config.load_config();
assert_eq!(result.dependabot_day, None);
}
#[test]
fn test_save_use_continuous_deployment() {
let config = mock_config();
let expected = false;
config.save_use_continuous_deployment(expected).unwrap();
let result = config.load_config();
assert_eq!(result.use_continuous_deployment, Some(expected));
}
#[test]
fn test_reset_use_continuous_deployment() {
let config = mock_config();
config.save_use_continuous_deployment(false).unwrap();
config.reset_use_continuous_deployment().unwrap();
let result = config.load_config();
assert_eq!(result.use_continuous_deployment, None);
}
#[test]
fn test_save_use_release_drafter() {
let config = mock_config();
let expected = false;
config.save_use_release_drafter(expected).unwrap();
let result = config.load_config();
assert_eq!(result.use_release_drafter, Some(expected));
}
#[test]
fn test_reset_use_release_drafter() {
let config = mock_config();
config.save_use_release_drafter(false).unwrap();
config.reset_use_release_drafter().unwrap();
let result = config.load_config();
assert_eq!(result.use_release_drafter, None);
}
#[test]
fn test_save_use_multi_os_ci() {
let config = mock_config();
let expected = false;
config.save_use_multi_os_ci(expected).unwrap();
let result = config.load_config();
assert_eq!(result.use_multi_os_ci, Some(expected));
}
#[test]
fn test_reset_use_multi_os_ci() {
let config = mock_config();
config.save_use_multi_os_ci(false).unwrap();
config.reset_use_multi_os_ci().unwrap();
let result = config.load_config();
assert_eq!(result.use_multi_os_ci, None);
}
#[test]
fn test_save_include_docs() {
let config = mock_config();
let expected = true;
config.save_include_docs(expected).unwrap();
let result = config.load_config();
assert_eq!(result.include_docs, Some(expected));
}
#[test]
fn test_reset_include_docs() {
let config = mock_config();
config.save_include_docs(true).unwrap();
config.reset_include_docs().unwrap();
let result = config.load_config();
assert_eq!(result.include_docs, None);
}
#[cfg(feature = "fastapi")]
#[test]
fn test_save_is_fastapi_project() {
let config = mock_config();
let expected = true;
config.save_is_fastapi_project(expected).unwrap();
let result = config.load_config();
assert_eq!(result.is_fastapi_project, Some(expected));
}
#[cfg(feature = "fastapi")]
#[test]
fn test_reset_is_fastapi_project() {
let config = mock_config();
config.save_is_fastapi_project(true).unwrap();
config.reset_is_fastapi_project().unwrap();
let result = config.load_config();
assert_eq!(result.is_fastapi_project, None);
}
}