weft_core/package/
validate.rs1use crate::package::config::PackageManifest;
14use std::fmt;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Severity {
19 Error,
21 Warning,
23}
24
25impl fmt::Display for Severity {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 Severity::Error => write!(f, "error"),
29 Severity::Warning => write!(f, "warning"),
30 }
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ManifestIssue {
37 pub severity: Severity,
38 pub code: String,
40 pub message: String,
42}
43
44impl ManifestIssue {
45 fn error(code: &str, message: impl Into<String>) -> Self {
46 Self { severity: Severity::Error, code: code.into(), message: message.into() }
47 }
48 fn warning(code: &str, message: impl Into<String>) -> Self {
49 Self { severity: Severity::Warning, code: code.into(), message: message.into() }
50 }
51}
52
53pub fn validate_manifest(
57 manifest: &PackageManifest,
58 entry_exists: Option<bool>,
59) -> Vec<ManifestIssue> {
60 let mut issues = Vec::new();
61
62 if manifest.package_info.name.trim().is_empty() {
64 issues.push(ManifestIssue::error(
65 "missing-name",
66 "package has no name; declare [package_info].name or [identity].name",
67 ));
68 }
69 if manifest.package_info.version.trim().is_empty() {
70 issues.push(ManifestIssue::warning(
71 "missing-version",
72 "package has no version; declare [package_info].version or [identity].version",
73 ));
74 }
75
76 if !manifest.is_product() {
80 let runtime = manifest.runtime_kind();
81 match manifest.resolved_entry() {
82 None => issues.push(ManifestIssue::error(
83 "missing-entry",
84 "no entry resolved; declare [package_info].entry or [package].entry",
85 )),
86 Some(entry) => {
87 let entry_is_wasm = entry.ends_with(".wasm");
89 if runtime == "wasm" && !entry_is_wasm {
90 issues.push(ManifestIssue::warning(
91 "runtime-entry-mismatch",
92 format!(
93 "runtime=wasm but entry '{entry}' is not a .wasm file; \
94 set runtime=service or fix the entry"
95 ),
96 ));
97 }
98 if runtime == "service" && entry_is_wasm {
99 issues.push(ManifestIssue::warning(
100 "runtime-entry-mismatch",
101 format!(
102 "runtime=service but entry '{entry}' is a .wasm file; \
103 set runtime=wasm or fix the entry"
104 ),
105 ));
106 }
107 if let Some(false) = entry_exists {
108 issues.push(ManifestIssue::error(
109 "entry-not-found",
110 format!("entry file '{entry}' does not exist on disk"),
111 ));
112 }
113 }
114 }
115 }
116
117 if !manifest.is_product() && manifest.resolved_provides().is_empty() {
120 issues.push(ManifestIssue::warning(
121 "no-provides",
122 "package declares no capabilities (provides is empty); \
123 it cannot satisfy any binding",
124 ));
125 }
126
127 issues
128}
129
130pub fn has_errors(issues: &[ManifestIssue]) -> bool {
132 issues.iter().any(|i| i.severity == Severity::Error)
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::package::config::load_manifest;
139 use std::io::Write;
140
141 fn manifest_from(test_name: &str, toml: &str) -> PackageManifest {
142 let dir = std::env::temp_dir().join(format!("weft-validate-test-{test_name}"));
144 let _ = std::fs::remove_dir_all(&dir);
145 let _ = std::fs::create_dir_all(&dir);
146 let mut f = std::fs::File::create(dir.join("package.toml")).unwrap();
147 f.write_all(toml.as_bytes()).unwrap();
148 load_manifest(&dir).unwrap()
149 }
150
151 #[test]
152 fn clean_wasm_manifest_has_no_issues() {
153 let m = manifest_from(
154 "clean-wasm",
155 r#"
156[package_info]
157name = "agent-runtime"
158version = "0.1.0"
159description = "x"
160entry = "package.wasm"
161provides = ["agent.runtime"]
162"#,
163 );
164 let issues = validate_manifest(&m, Some(true));
165 assert!(!has_errors(&issues), "unexpected issues: {issues:?}");
166 }
167
168 #[test]
169 fn empty_name_is_error() {
170 let m = manifest_from(
172 "empty-name",
173 r#"
174[package]
175runtime = "wasm"
176entry = "package.wasm"
177"#,
178 );
179 let issues = validate_manifest(&m, Some(true));
180 assert!(has_errors(&issues));
181 assert!(issues.iter().any(|i| i.code == "missing-name"));
182 }
183
184 #[test]
185 fn wasm_runtime_with_non_wasm_entry_warns() {
186 let m = manifest_from(
188 "wasm-nonwasm-entry",
189 r#"
190[package_info]
191name = "memory-store"
192version = "0.1.0"
193description = "x"
194entry = "start-memory-runtime.ps1"
195provides = ["memory.store"]
196
197[package]
198runtime = "wasm"
199"#,
200 );
201 let issues = validate_manifest(&m, Some(true));
202 assert!(issues.iter().any(|i| i.code == "runtime-entry-mismatch"));
203 }
204
205 #[test]
206 fn missing_entry_file_is_error() {
207 let m = manifest_from(
208 "stub-missing-entry",
209 r#"
210[package_info]
211name = "stub"
212version = "0.1.0"
213description = "x"
214entry = "package.wasm"
215provides = ["x.y"]
216"#,
217 );
218 let issues = validate_manifest(&m, Some(false));
219 assert!(issues.iter().any(|i| i.code == "entry-not-found"));
220 }
221
222 #[test]
223 fn product_package_skips_entry_and_provides_checks() {
224 let m = manifest_from(
227 "product-pkg",
228 r#"
229schema_version = 2
230
231[identity]
232name = "weft-code"
233version = "0.1.0"
234description = "product"
235
236[package]
237kind = "product"
238
239[requires]
240capabilities = ["agent.runtime", "weft_code.runtime"]
241"#,
242 );
243 assert!(m.is_product(), "kind=product should be detected");
244 let issues = validate_manifest(&m, Some(false));
246 assert!(
247 !issues.iter().any(|i| matches!(
248 i.code.as_str(),
249 "entry-not-found" | "missing-entry" | "runtime-entry-mismatch" | "no-provides"
250 )),
251 "product package should skip entry/provides checks, got: {issues:?}"
252 );
253 }
254}