use crate::file::{FindFileResult, find_file_downward, find_file_upward, to_slash};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
name: String,
version: String,
root: Root,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Root {
working_dir: PathBuf,
}
#[derive(Debug, PartialEq, Eq)]
pub enum InitOutcome {
Initialized,
TemplateSeeded,
}
impl Config {
fn default(dir: &Path) -> Config {
let root_dir = Root {
working_dir: dir.to_path_buf(),
};
Config {
name: "apic".to_string(),
version: "0.1.0".to_string(),
root: root_dir,
}
}
pub fn get_root_dir(&self) -> Result<PathBuf, String> {
let project_root = project_root()?;
let resolved = project_root.join(&self.root.working_dir);
if !resolved.exists() {
let err = format!(
"working directory {} does not exist, try to run `apic config --set-dir <dir>`",
resolved.display()
);
return Err(err);
}
fs::canonicalize(&resolved)
.map_err(|err| format!("Failed to resolve {}: {}", resolved.display(), err))
}
pub fn init(working_dir: Option<&str>) -> Result<InitOutcome, String> {
match find_file_apic_config_file() {
Ok(FindFileResult::Found(_)) => {
let apic_dir = find_apic_dir().ok_or("Already initialized!")?;
return if crate::template::seed_if_missing(&apic_dir)? {
Ok(InitOutcome::TemplateSeeded)
} else {
Err("Already initialized!".to_string())
};
}
Ok(FindFileResult::NotFound) => true,
Err(err) => {
return Err(err);
}
};
let dir = PathBuf::from(".apic");
let pwd = std::env::current_dir()
.map_err(|err| format!("Failed to get current directory: {err}"))?;
let makedir = pwd.join(&dir);
if !makedir.exists() {
match fs::create_dir(&makedir) {
Ok(_) => true,
Err(err) => {
let err = format!("Failed to create {}: {}", &dir.display(), err);
return Err(err);
}
};
}
if let Err(err) = crate::template::seed_if_missing(&makedir) {
eprintln!("Warning: {err}");
}
let working_dir = match working_dir {
Some(dir) => {
let dir = PathBuf::from(dir);
if !dir.exists() {
let err = format!("Directory {} does not exist", dir.display());
return Err(err);
}
relative_to_root(&pwd, &dir)
}
None => PathBuf::from("."),
};
write_config_file(makedir.clone(), &Config::default(&working_dir))?;
Ok(InitOutcome::Initialized)
}
pub fn update_root_dir(&mut self, new_dir: &str) -> Result<(), String> {
let apic_dir = match find_file_apic_dir() {
Ok(FindFileResult::Found(dir)) => dir.first().unwrap().clone(),
Ok(FindFileResult::NotFound) => {
return Err("Not initialized yet".to_string());
}
Err(err) => {
return Err(err);
}
};
let root = apic_dir.parent().unwrap();
let dir = root.join(new_dir);
if !dir.exists() {
let err = format!("Directory {} does not exist", dir.display());
return Err(err);
}
let current = root.join(&self.root.working_dir);
if let (Ok(a), Ok(b)) = (fs::canonicalize(&dir), fs::canonicalize(¤t))
&& a == b
{
let err = format!("Already in {}", dir.display());
return Err(err);
}
self.root.working_dir = relative_to_root(root, Path::new(new_dir));
write_config_file(apic_dir, self)
}
}
fn write_config_file(apic_dir: PathBuf, config: &Config) -> Result<(), String> {
let config_to_str = toml::to_string_pretty(config)
.map_err(|err| format!("Failed to serialize config: {err}"))?;
let path = apic_dir.join("config.toml");
fs::write(&path, config_to_str)
.map_err(|err| format!("Failed to write {}: {}", path.display(), err))?;
Ok(())
}
fn project_root() -> Result<PathBuf, String> {
match find_file_apic_dir()? {
FindFileResult::Found(dir) => {
let apic_dir = dir.first().unwrap();
Ok(apic_dir.parent().unwrap_or(apic_dir).to_path_buf())
}
FindFileResult::NotFound => Err("Not initialized yet, run `apic init` first".to_string()),
}
}
fn relative_to_root(root: &Path, dir: &Path) -> PathBuf {
let rel = if dir.is_absolute() {
dir.strip_prefix(root).unwrap_or(dir)
} else {
dir
};
if rel.as_os_str().is_empty() {
PathBuf::from(".")
} else {
PathBuf::from(to_slash(rel))
}
}
fn find_file_apic_dir() -> Result<FindFileResult, String> {
let pwd = match std::env::current_dir() {
Ok(pwd) => pwd,
Err(err) => {
let err = format!("Failed to get current directory: {}", err);
return Err(err);
}
};
let name = vec![PathBuf::from(".apic")];
Ok(find_file_upward(pwd, &name))
}
pub fn find_apic_dir() -> Option<PathBuf> {
match find_file_apic_dir().ok()? {
FindFileResult::Found(dirs) => dirs.first().cloned(),
FindFileResult::NotFound => None,
}
}
fn find_file_apic_config_file() -> Result<FindFileResult, String> {
let pwd = match find_file_apic_dir()? {
FindFileResult::Found(pwd) => pwd.first().unwrap().clone(),
FindFileResult::NotFound => return Ok(FindFileResult::NotFound),
};
let name = vec![PathBuf::from("config.toml")];
Ok(find_file_downward(pwd, &name))
}
pub fn read_config_file() -> Result<Config, String> {
let config_file = match find_file_apic_config_file()? {
FindFileResult::Found(path) => path.first().unwrap().clone(),
FindFileResult::NotFound => {
return Err("Not initialized yet, run `apic init` first".to_string());
}
};
let content = fs::read_to_string(&config_file)
.map_err(|err| format!("Failed to read {}: {}", config_file.display(), err))?;
let config: Config = toml::from_str(&content)
.map_err(|err| format!("Failed to parse {}: {}", config_file.display(), err))?;
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
fn abs(parts: &[&str]) -> PathBuf {
let mut p = PathBuf::from(if cfg!(windows) { "C:\\" } else { "/" });
for part in parts {
p.push(part);
}
p
}
#[test]
fn relative_input_is_kept_as_is() {
let root = abs(&["home", "u", "project"]);
assert_eq!(
relative_to_root(&root, Path::new("api-contract")),
PathBuf::from("api-contract")
);
}
#[test]
fn absolute_input_under_root_is_made_relative() {
let root = abs(&["home", "u", "project"]);
assert_eq!(
relative_to_root(&root, &abs(&["home", "u", "project", "api-contract"])),
PathBuf::from("api-contract")
);
}
#[test]
fn absolute_input_equal_to_root_collapses_to_dot() {
let root = abs(&["home", "u", "project"]);
assert_eq!(
relative_to_root(&root, &abs(&["home", "u", "project"])),
PathBuf::from(".")
);
}
#[test]
fn absolute_input_outside_root_is_kept_absolute() {
let root = abs(&["home", "u", "project"]);
let outside = abs(&["etc", "contracts"]);
assert_eq!(relative_to_root(&root, &outside), outside);
}
}