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() {
115 if def.entry_point.is_none() {
116 errors.push(format!(
117 "procedures/{id}: no .wasm binary and no entry_point in .schema.json; \
118 procedure cannot be executed (see spec ยง9.12)"
119 ));
120 } else {
121 errors.push(format!(
122 "procedures/{id}: non-WASM procedures are not permitted; \
123 provide a {id}.wasm binary (entry_point is not accepted)"
124 ));
125 }
126 }
127
128 if let Some(ref ep) = def.entry_point {
130 let alt_path = dir.join(&ep.filename);
131 if !alt_path.exists() {
132 errors.push(format!(
133 "procedures/{id}.schema.json entry_point.filename '{}' not found in machine/procedures/",
134 ep.filename
135 ));
136 }
137 }
138 }
139
140 Ok(errors)
141}
142
143fn validate_schema_file(path: &Path) -> Result<(), String> {
144 let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
145 let value: serde_json::Value =
146 serde_json::from_slice(&bytes).map_err(|e| format!("invalid JSON: {e}"))?;
147
148 let obj = value.as_object().ok_or("schema must be a JSON object")?;
150 if !obj.contains_key("input") {
151 return Err("missing required 'input' key".to_string());
152 }
153 if !obj.contains_key("output") {
154 return Err("missing required 'output' key".to_string());
155 }
156
157 for key in ["input", "output"] {
159 if obj[key].as_object().is_none() {
160 return Err(format!("'{key}' must be a JSON object (JSON Schema)"));
161 }
162 }
163
164 Ok(())
165}
166
167pub fn count_with_wasm(pack: &Pack) -> usize {
169 list(pack)
170 .unwrap_or_default()
171 .iter()
172 .filter(|d| d.wasm_path.is_some())
173 .count()
174}
175
176pub fn count_total(pack: &Pack) -> usize {
178 list(pack).unwrap_or_default().len()
179}