use crate::package::config::PackageManifest;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Severity::Error => write!(f, "error"),
Severity::Warning => write!(f, "warning"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ManifestIssue {
pub severity: Severity,
pub code: String,
pub message: String,
}
impl ManifestIssue {
fn error(code: &str, message: impl Into<String>) -> Self {
Self { severity: Severity::Error, code: code.into(), message: message.into() }
}
fn warning(code: &str, message: impl Into<String>) -> Self {
Self { severity: Severity::Warning, code: code.into(), message: message.into() }
}
}
pub fn validate_manifest(
manifest: &PackageManifest,
entry_exists: Option<bool>,
) -> Vec<ManifestIssue> {
let mut issues = Vec::new();
if manifest.package_info.name.trim().is_empty() {
issues.push(ManifestIssue::error(
"missing-name",
"package has no name; declare [package_info].name or [identity].name",
));
}
if manifest.package_info.version.trim().is_empty() {
issues.push(ManifestIssue::warning(
"missing-version",
"package has no version; declare [package_info].version or [identity].version",
));
}
if !manifest.is_product() {
let runtime = manifest.runtime_kind();
match manifest.resolved_entry() {
None => issues.push(ManifestIssue::error(
"missing-entry",
"no entry resolved; declare [package_info].entry or [package].entry",
)),
Some(entry) => {
let entry_is_wasm = entry.ends_with(".wasm");
if runtime == "wasm" && !entry_is_wasm {
issues.push(ManifestIssue::warning(
"runtime-entry-mismatch",
format!(
"runtime=wasm but entry '{entry}' is not a .wasm file; \
set runtime=service or fix the entry"
),
));
}
if runtime == "service" && entry_is_wasm {
issues.push(ManifestIssue::warning(
"runtime-entry-mismatch",
format!(
"runtime=service but entry '{entry}' is a .wasm file; \
set runtime=wasm or fix the entry"
),
));
}
if let Some(false) = entry_exists {
issues.push(ManifestIssue::error(
"entry-not-found",
format!("entry file '{entry}' does not exist on disk"),
));
}
}
}
}
if !manifest.is_product() && manifest.resolved_provides().is_empty() {
issues.push(ManifestIssue::warning(
"no-provides",
"package declares no capabilities (provides is empty); \
it cannot satisfy any binding",
));
}
issues
}
pub fn has_errors(issues: &[ManifestIssue]) -> bool {
issues.iter().any(|i| i.severity == Severity::Error)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::config::load_manifest;
use std::io::Write;
fn manifest_from(test_name: &str, toml: &str) -> PackageManifest {
let dir = std::env::temp_dir().join(format!("weft-validate-test-{test_name}"));
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::create_dir_all(&dir);
let mut f = std::fs::File::create(dir.join("package.toml")).unwrap();
f.write_all(toml.as_bytes()).unwrap();
load_manifest(&dir).unwrap()
}
#[test]
fn clean_wasm_manifest_has_no_issues() {
let m = manifest_from(
"clean-wasm",
r#"
[package_info]
name = "agent-runtime"
version = "0.1.0"
description = "x"
entry = "package.wasm"
provides = ["agent.runtime"]
"#,
);
let issues = validate_manifest(&m, Some(true));
assert!(!has_errors(&issues), "unexpected issues: {issues:?}");
}
#[test]
fn empty_name_is_error() {
let m = manifest_from(
"empty-name",
r#"
[package]
runtime = "wasm"
entry = "package.wasm"
"#,
);
let issues = validate_manifest(&m, Some(true));
assert!(has_errors(&issues));
assert!(issues.iter().any(|i| i.code == "missing-name"));
}
#[test]
fn wasm_runtime_with_non_wasm_entry_warns() {
let m = manifest_from(
"wasm-nonwasm-entry",
r#"
[package_info]
name = "memory-store"
version = "0.1.0"
description = "x"
entry = "start-memory-runtime.ps1"
provides = ["memory.store"]
[package]
runtime = "wasm"
"#,
);
let issues = validate_manifest(&m, Some(true));
assert!(issues.iter().any(|i| i.code == "runtime-entry-mismatch"));
}
#[test]
fn missing_entry_file_is_error() {
let m = manifest_from(
"stub-missing-entry",
r#"
[package_info]
name = "stub"
version = "0.1.0"
description = "x"
entry = "package.wasm"
provides = ["x.y"]
"#,
);
let issues = validate_manifest(&m, Some(false));
assert!(issues.iter().any(|i| i.code == "entry-not-found"));
}
#[test]
fn product_package_skips_entry_and_provides_checks() {
let m = manifest_from(
"product-pkg",
r#"
schema_version = 2
[identity]
name = "weft-code"
version = "0.1.0"
description = "product"
[package]
kind = "product"
[requires]
capabilities = ["agent.runtime", "weft_code.runtime"]
"#,
);
assert!(m.is_product(), "kind=product should be detected");
let issues = validate_manifest(&m, Some(false));
assert!(
!issues.iter().any(|i| matches!(
i.code.as_str(),
"entry-not-found" | "missing-entry" | "runtime-entry-mismatch" | "no-provides"
)),
"product package should skip entry/provides checks, got: {issues:?}"
);
}
}