use anyhow::Context;
use std::path::{Path, PathBuf};
pub fn find_project_root(start: &Path) -> Option<PathBuf> {
let mut dir = start;
loop {
let gobby_dir = dir.join(".gobby");
if gobby_dir.join("project.json").exists() || gobby_dir.join("gcode.json").exists() {
return Some(dir.to_path_buf());
}
match dir.parent() {
Some(parent) => dir = parent,
None => return None,
}
}
}
pub fn read_project_id(project_root: &Path) -> anyhow::Result<String> {
let project_json = project_root.join(".gobby").join("project.json");
if project_json.exists() {
match read_project_id_from(&project_json) {
Ok(project_id) => return Ok(project_id),
Err(project_error) => {
let gcode_json = project_root.join(".gobby").join("gcode.json");
if !gcode_json.exists() {
return Err(project_error);
}
return read_project_id_from(&gcode_json).with_context(|| {
format!(
"failed to read project id from {} and fallback {}",
project_json.display(),
gcode_json.display()
)
});
}
}
}
let gcode_json = project_root.join(".gobby").join("gcode.json");
read_project_id_from(&gcode_json)
}
fn read_project_id_from(path: &Path) -> anyhow::Result<String> {
let contents = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {}", path.display()))?;
let json: serde_json::Value = serde_json::from_str(&contents)
.with_context(|| format!("failed to parse {}", path.display()))?;
json.get("id")
.and_then(|v| v.as_str())
.map(String::from)
.with_context(|| format!("'id' field not found in {}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn read_project_id_is_non_destructive() {
let tmp = tempfile::tempdir().expect("tempdir");
let gobby_dir = tmp.path().join(".gobby");
fs::create_dir(&gobby_dir).expect("create .gobby");
let project_json = gobby_dir.join("project.json");
let contents = r#"{
"id": "project-id",
"name": "example"
}
"#;
fs::write(&project_json, contents).expect("write project json");
let project_id = read_project_id(tmp.path()).expect("read project id");
assert_eq!(project_id, "project-id");
assert_eq!(
fs::read_to_string(&project_json).expect("read project json"),
contents
);
}
#[test]
fn read_project_id_falls_back_to_gcode_json_root_marker() {
let tmp = tempfile::tempdir().expect("tempdir");
let nested = tmp.path().join("src").join("bin");
let gobby_dir = tmp.path().join(".gobby");
fs::create_dir(&gobby_dir).expect("create .gobby");
fs::create_dir_all(&nested).expect("create nested");
fs::write(
gobby_dir.join("gcode.json"),
r#"{
"id": "standalone-code-index",
"name": "example"
}
"#,
)
.expect("write gcode json");
assert_eq!(find_project_root(&nested).as_deref(), Some(tmp.path()));
assert_eq!(
read_project_id(tmp.path()).expect("read gcode project id"),
"standalone-code-index"
);
}
#[test]
fn missing_project_id_error_mentions_id_key() {
let tmp = tempfile::tempdir().expect("tempdir");
let gobby_dir = tmp.path().join(".gobby");
fs::create_dir(&gobby_dir).expect("create .gobby");
fs::write(gobby_dir.join("project.json"), r#"{"name":"example"}"#)
.expect("write project json");
let error = read_project_id(tmp.path()).expect_err("project id is missing");
assert!(error.to_string().contains("'id' field not found"));
}
#[test]
fn read_project_id_falls_back_to_gcode_json_when_project_json_is_bad() {
let tmp = tempfile::tempdir().expect("tempdir");
let gobby_dir = tmp.path().join(".gobby");
fs::create_dir(&gobby_dir).expect("create .gobby");
fs::write(gobby_dir.join("project.json"), r#"{"name":"example"}"#)
.expect("write project json");
fs::write(
gobby_dir.join("gcode.json"),
r#"{"id":"standalone-fallback"}"#,
)
.expect("write gcode json");
assert_eq!(
read_project_id(tmp.path()).expect("read fallback project id"),
"standalone-fallback"
);
}
#[test]
fn read_project_id_falls_back_to_gcode_json_when_project_json_is_malformed() {
let tmp = tempfile::tempdir().expect("tempdir");
let gobby_dir = tmp.path().join(".gobby");
fs::create_dir(&gobby_dir).expect("create .gobby");
fs::write(gobby_dir.join("project.json"), r#"{"id":"broken""#)
.expect("write malformed project json");
fs::write(
gobby_dir.join("gcode.json"),
r#"{"id":"standalone-fallback"}"#,
)
.expect("write gcode json");
assert_eq!(
read_project_id(tmp.path()).expect("read fallback project id"),
"standalone-fallback"
);
}
}