use anyhow::Result;
use std::{
fs,
path::{Path, PathBuf},
};
pub(crate) fn sdk_base(root: &Path) -> Result<PathBuf> {
let manifest = root.join(".forklaunch/manifest.toml");
let native = if manifest.is_file() {
let manifest: toml::Value = toml::from_str(&fs::read_to_string(manifest)?)?;
manifest.get("modules_path").and_then(toml::Value::as_str) == Some("src")
} else {
false
};
Ok(PathBuf::from(if native {
"src/client-sdk"
} else {
"sdks"
}))
}
pub(super) fn native(root: &Path) -> Result<bool> {
Ok(sdk_base(root)? == Path::new("src/client-sdk"))
}
pub(super) fn typescript(root: &Path) -> Result<PathBuf> {
Ok(sdk_base(root)?.join("typescript"))
}
pub(super) fn rust(root: &Path) -> Result<PathBuf> {
Ok(sdk_base(root)?.join("rust"))
}
pub(super) fn source(root: &Path, directory: &Path) -> Result<PathBuf> {
Ok(if native(root)? {
directory.to_path_buf()
} else {
directory.join("src")
})
}
pub(super) fn typescript_authored(root: &Path, service: &str) -> Result<bool> {
let manifest: toml::Value = toml::from_str(&fs::read_to_string(root.join(".forklaunch/manifest.toml"))?)?;
Ok(manifest.get("projects").and_then(toml::Value::as_array).and_then(|projects| projects.iter().find(|project| project.get("name").and_then(toml::Value::as_str) == Some(service)))
.and_then(|project| project.get("metadata")).is_some_and(|metadata| metadata.get("sdkLayout").and_then(toml::Value::as_str) == Some("project-local-v1") && metadata.get("language").and_then(toml::Value::as_str) != Some("rust")))
}
pub(super) fn local_sdk(root: &Path, service: &str, language: &str) -> Result<Option<PathBuf>> {
let manifest = root.join(".forklaunch/manifest.toml");
if !manifest.is_file() { return Ok(None); }
let manifest: toml::Value = toml::from_str(&fs::read_to_string(manifest)?)?;
let project = manifest.get("projects").and_then(toml::Value::as_array).and_then(|projects| projects.iter().find(|project| project.get("name").and_then(toml::Value::as_str) == Some(service)));
let Some(metadata) = project.and_then(|project| project.get("metadata")) else { return Ok(None); };
if metadata.get("sdkLayout").and_then(toml::Value::as_str) != Some("project-local-v1") { return Ok(None); }
let authored = metadata.get("language").and_then(toml::Value::as_str) != Some("rust");
let path = metadata.get("cratePath").or_else(|| metadata.get("staticPath")).and_then(toml::Value::as_str).ok_or_else(|| anyhow::anyhow!("Service-local SDK requires project path metadata"))?;
let relative = if authored && language == "typescript" { Path::new(path).join("sdk") } else { Path::new(path).join("sdk").join(language) };
Ok(Some(crate::core::application_output::output_path(root, &relative)?))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unrelated_generation_receipts_do_not_redirect_sdk_output() {
let root = tempfile::tempdir().unwrap();
fs::create_dir_all(root.path().join(".forklaunch")).unwrap();
fs::write(root.path().join(".forklaunch/manifest.toml"), "language = 'rust'\nmodules_path = 'src'\n").unwrap();
fs::write(root.path().join("generation.json"), "application-owned receipt; not compiler configuration").unwrap();
assert_eq!(typescript(root.path()).unwrap(), Path::new("src/client-sdk/typescript"));
}
#[test]
fn source_root_layout_is_independent_of_application_language() {
let root = tempfile::tempdir().unwrap();
fs::create_dir_all(root.path().join(".forklaunch")).unwrap();
for (language, modules, expected) in [
("language = 'typescript'", "src", "src/client-sdk"),
("", "src", "src/client-sdk"),
("language = 'rust'", "src", "src/client-sdk"),
("language = 'mixed'", "src", "src/client-sdk"),
("applicationLanguage = 'mixed'", "src", "src/client-sdk"),
("language = 'mixed'", "crates", "sdks"),
] {
fs::write(
root.path().join(".forklaunch/manifest.toml"),
format!("{language}\nmodules_path = '{modules}'\n"),
)
.unwrap();
assert_eq!(sdk_base(root.path()).unwrap(), Path::new(expected));
}
}
}