use super::validate::validate;
use crate::core::config::{Language, NewAlefConfig, ResolvedCrateConfig};
use crate::core::ir::ApiSurface;
use std::path::Path;
use tempfile::TempDir;
const CRATE_NAME: &str = "my-lib";
const CO_LOCATED_CLASS_DIR: &str = "crates/my-lib-php/src";
fn toml_path(path: &Path) -> String {
let escaped = path.to_string_lossy().replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
fn php_config(root: &Path, extra: &str) -> ResolvedCrateConfig {
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"my-lib\"\nversion = \"1.2.3\"\n",
)
.expect("write Cargo.toml");
let cfg: NewAlefConfig = toml::from_str(&format!(
r#"
[workspace]
languages = ["php"]
[[crates]]
name = "{CRATE_NAME}"
sources = ["src/lib.rs"]
version_from = {version_from}
[crates.scaffold]
repository = "https://github.com/acme/my-lib"
description = "My library"
license = "MIT"
authors = ["Alice"]
{extra}
"#,
version_from = toml_path(&root.join("Cargo.toml")),
))
.expect("valid toml");
let mut config = cfg.resolve().expect("resolve ok").remove(0);
config.workspace_root = Some(root.to_path_buf());
config
}
fn write_scaffolded_composer_files(root: &Path, config: &ResolvedCrateConfig) -> Vec<String> {
let files =
crate::scaffold::scaffold(&ApiSurface::default(), config, &[Language::Php]).expect("php scaffold must succeed");
let mut written = Vec::new();
for file in files
.iter()
.filter(|f| f.path.file_name().is_some_and(|name| name == "composer.json"))
{
let target = root.join(&file.path);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).expect("create manifest parent");
}
std::fs::write(&target, &file.content).expect("write manifest");
written.push(file.path.to_string_lossy().into_owned());
}
written.sort();
written
}
#[test]
fn scaffolded_co_located_layout_passes_validation() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let config = php_config(root, "[crates.output]\nphp = \"crates/my-lib-php/src/\"\n");
std::fs::create_dir_all(root.join(CO_LOCATED_CLASS_DIR)).unwrap();
let manifests = write_scaffolded_composer_files(root, &config);
assert_eq!(
manifests,
vec!["composer.json".to_string()],
"the co-located scaffold emits only the root manifest"
);
let issues = validate(&config, &[Language::Php]).unwrap();
assert_eq!(
issues,
Vec::<String>::new(),
"everything the php scaffolder wrote must satisfy the validator; got: {issues:?}"
);
}
#[test]
fn default_co_located_layout_does_not_require_a_package_local_manifest() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let config = php_config(root, "");
std::fs::create_dir_all(root.join(CO_LOCATED_CLASS_DIR)).unwrap();
write_scaffolded_composer_files(root, &config);
let issues = validate(&config, &[Language::Php]).unwrap();
assert!(
!issues
.iter()
.any(|issue| issue.contains("missing") && issue.contains("composer.json")),
"no composer.json may be reported missing when the scaffolder wrote them all; got: {issues:?}"
);
assert_eq!(
issues,
Vec::<String>::new(),
"default co-located layout must validate clean; got: {issues:?}"
);
}
#[test]
fn co_located_layout_reports_a_missing_root_manifest() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let config = php_config(root, "");
std::fs::create_dir_all(root.join(CO_LOCATED_CLASS_DIR)).unwrap();
let issues = validate(&config, &[Language::Php]).unwrap();
assert!(
issues.iter().any(|issue| issue == "php: missing root composer.json"),
"a repository with no composer.json anywhere must be reported; got: {issues:?}"
);
}
#[test]
fn co_located_layout_reports_a_root_psr4_mismatch() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let config = php_config(root, "");
std::fs::create_dir_all(root.join(CO_LOCATED_CLASS_DIR)).unwrap();
std::fs::write(
root.join("composer.json"),
"{\n \"name\": \"acme/my-lib\",\n \"autoload\": {\"psr-4\": {\"Acme\\\\MyLib\\\\\": \"packages/php/src/\"}}\n}\n",
)
.unwrap();
let issues = validate(&config, &[Language::Php]).unwrap();
assert!(
issues
.iter()
.any(|issue| issue == "php: root composer.json PSR-4 path must be crates/my-lib-php/src/"),
"the root manifest must be checked against the resolved class directory; got: {issues:?}"
);
}
#[test]
fn nested_class_directory_still_validates_a_present_package_manifest() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let config = php_config(
root,
"[crates.output]\nphp = \"packages/php\"\n[crates.php.stubs]\noutput = \"packages/php/generated\"\n",
);
std::fs::create_dir_all(root.join("packages/php/generated")).unwrap();
let root_manifest = "{\n \"name\": \"acme/my-lib\",\n \"autoload\": {\"psr-4\": {\"Acme\\\\MyLib\\\\\": \"packages/php/generated/\"}}\n}\n";
let package_manifest =
"{\n \"name\": \"acme/my-lib\",\n \"autoload\": {\"psr-4\": {\"Acme\\\\MyLib\\\\\": \"src/\"}}\n}\n";
std::fs::write(root.join("composer.json"), root_manifest).unwrap();
std::fs::write(root.join("packages/php/composer.json"), package_manifest).unwrap();
let issues = validate(&config, &[Language::Php]).unwrap();
assert!(
issues
.iter()
.any(|issue| issue == "php: packages/php/composer.json PSR-4 path must be generated/"),
"a present package-local manifest must still be checked; got: {issues:?}"
);
}