dkp_core/procedures/
mod.rs1pub mod schema;
2
3#[cfg(feature = "procedures")]
4pub mod executor;
5
6pub mod scaffold;
7
8pub use schema::{EntryPoint, ProcedureDef, ProcedureSchema};
9
10use std::path::Path;
11
12use crate::{error::DkpError, DkpResult, Pack};
13
14pub fn list(pack: &Pack) -> DkpResult<Vec<ProcedureDef>> {
19 let dir = pack.procedures_dir();
20 if !dir.exists() {
21 return Ok(vec![]);
22 }
23
24 let mut defs = Vec::new();
25
26 for entry in std::fs::read_dir(&dir)? {
27 let entry = entry?;
28 let path = entry.path();
29
30 let name = path.file_name().unwrap_or_default().to_string_lossy();
32 let Some(id) = name.strip_suffix(".schema.json") else {
33 continue;
34 };
35 let id = id.to_string();
36
37 let schema_path = path.clone();
38 let bytes = std::fs::read(&schema_path)?;
39 let schema: ProcedureSchema =
40 serde_json::from_slice(&bytes).map_err(|e| DkpError::AssetParse {
41 asset: format!("procedures/{name}"),
42 source: e,
43 })?;
44
45 let wasm_path = dir.join(format!("{id}.wasm"));
46 let doc_path = dir.join(format!("{id}.md"));
47
48 let entry_point = schema.entry_point.clone();
49
50 defs.push(ProcedureDef {
51 id,
52 wasm_path: if wasm_path.exists() {
53 Some(wasm_path)
54 } else {
55 None
56 },
57 entry_point,
58 doc_path,
59 schema_path,
60 schema,
61 });
62 }
63
64 defs.sort_by(|a, b| a.id.cmp(&b.id));
65 Ok(defs)
66}
67
68pub fn validate_all(pack: &Pack) -> DkpResult<Vec<String>> {
72 let dir = pack.procedures_dir();
73 if !dir.exists() {
74 return Ok(vec![]);
75 }
76
77 let mut errors = Vec::new();
78
79 if pack.manifest.procedure_capabilities.is_none() {
81 errors.push(
82 "manifest.json missing 'procedure_capabilities' (required when machine/procedures/ is non-empty)".to_string(),
83 );
84 }
85
86 let defs = list(pack)?;
87
88 if defs.is_empty() {
89 return Ok(errors);
91 }
92
93 for def in &defs {
94 let id = &def.id;
95
96 if let Err(e) = validate_schema_file(&def.schema_path) {
98 errors.push(format!("procedures/{id}.schema.json: {e}"));
99 }
100
101 if !def.doc_path.exists() {
103 errors.push(format!(
104 "procedures/{id}.md: missing (required alongside .schema.json)"
105 ));
106 } else if std::fs::metadata(&def.doc_path)
107 .map(|m| m.len() == 0)
108 .unwrap_or(false)
109 {
110 errors.push(format!("procedures/{id}.md: empty"));
111 }
112
113 if def.wasm_path.is_none() && def.entry_point.is_none() {
115 errors.push(format!(
116 "procedures/{id}: no .wasm binary and no entry_point in .schema.json; \
117 procedure cannot be executed (see spec ยง9.12)"
118 ));
119 }
120
121 if let Some(ref ep) = def.entry_point {
123 let alt_path = dir.join(&ep.filename);
124 if !alt_path.exists() {
125 errors.push(format!(
126 "procedures/{id}.schema.json entry_point.filename '{}' not found in machine/procedures/",
127 ep.filename
128 ));
129 }
130 }
131 }
132
133 Ok(errors)
134}
135
136fn validate_schema_file(path: &Path) -> Result<(), String> {
137 let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
138 let value: serde_json::Value =
139 serde_json::from_slice(&bytes).map_err(|e| format!("invalid JSON: {e}"))?;
140
141 let obj = value.as_object().ok_or("schema must be a JSON object")?;
143 if !obj.contains_key("input") {
144 return Err("missing required 'input' key".to_string());
145 }
146 if !obj.contains_key("output") {
147 return Err("missing required 'output' key".to_string());
148 }
149
150 for key in ["input", "output"] {
152 if obj[key].as_object().is_none() {
153 return Err(format!("'{key}' must be a JSON object (JSON Schema)"));
154 }
155 }
156
157 Ok(())
158}
159
160pub fn count_with_wasm(pack: &Pack) -> usize {
162 list(pack)
163 .unwrap_or_default()
164 .iter()
165 .filter(|d| d.wasm_path.is_some())
166 .count()
167}
168
169pub fn count_total(pack: &Pack) -> usize {
171 list(pack).unwrap_or_default().len()
172}