1use std::collections::{BTreeMap, BTreeSet};
37
38use serde_json::{Map, Value as JVal};
39
40pub const OP_TEMPLATES_KEY: &str = "op_templates";
42
43pub const USES_KEY: &str = "uses";
45
46#[derive(Debug, Default)]
49pub(crate) struct Instantiated {
50 pub phases: BTreeMap<String, BTreeSet<String>>,
52 pub pool: BTreeSet<String>,
55 pub template_of: BTreeMap<String, String>,
57}
58
59pub(crate) fn instantiate(doc: &mut Map<String, JVal>) -> Result<Instantiated, String> {
62 let templates = match doc.remove(OP_TEMPLATES_KEY) {
63 None => Map::new(),
64 Some(JVal::Object(m)) => m,
65 Some(other) => {
66 return Err(format!(
67 "`{OP_TEMPLATES_KEY}:` must be a mapping of template name -> op body, got {other}"
68 ));
69 }
70 };
71 for (name, body) in &templates {
72 if !body.is_object() {
73 return Err(format!(
74 "op template '{name}' must be a mapping (an op body), got {body}"
75 ));
76 }
77 if body.get(USES_KEY).is_some() {
78 return Err(format!(
79 "op template '{name}' declares `uses:` — a template is a complete \
80 op body and does not instantiate another"
81 ));
82 }
83 }
84
85 let mut out = Instantiated::default();
86 if let Some(ops) = doc.get_mut("ops") {
87 instantiate_ops(
88 ops,
89 &templates,
90 "top-level ops",
91 &mut out.pool,
92 &mut out.template_of,
93 )?;
94 }
95 if let Some(JVal::Object(blocks)) = doc.get_mut("blocks") {
96 for (block_name, block) in blocks.iter_mut() {
97 if let Some(ops) = block.get_mut("ops") {
98 instantiate_ops(
99 ops,
100 &templates,
101 &format!("block '{block_name}'"),
102 &mut out.pool,
103 &mut out.template_of,
104 )?;
105 }
106 }
107 }
108 if let Some(JVal::Object(phases)) = doc.get_mut("phases") {
109 for (phase_name, phase) in phases.iter_mut() {
110 if let Some(ops) = phase.get_mut("ops") {
111 let mut used = BTreeSet::new();
112 instantiate_ops(
113 ops,
114 &templates,
115 &format!("phase '{phase_name}'"),
116 &mut used,
117 &mut out.template_of,
118 )?;
119 if !used.is_empty() {
120 out.phases.insert(phase_name.clone(), used);
121 }
122 }
123 }
124 }
125 Ok(out)
126}
127
128fn instantiate_ops(
132 ops: &mut JVal,
133 templates: &Map<String, JVal>,
134 container: &str,
135 used: &mut BTreeSet<String>,
136 template_of: &mut BTreeMap<String, String>,
137) -> Result<(), String> {
138 match ops {
139 JVal::Object(map) => {
140 for (name, body) in map.iter_mut() {
141 if let JVal::Object(op) = body
142 && op.contains_key(USES_KEY)
143 {
144 let template = instantiate_one(op, templates, name, container)?;
145 template_of.insert(name.clone(), template);
146 used.insert(name.clone());
147 }
148 }
149 }
150 JVal::Array(items) => {
151 for item in items.iter_mut() {
152 let JVal::Object(entry) = item else { continue };
153 if entry.contains_key(USES_KEY) {
154 let name = entry
155 .get("name")
156 .and_then(JVal::as_str)
157 .ok_or_else(|| {
158 format!("{container}: a list-form op with `uses:` needs a `name:`")
159 })?
160 .to_string();
161 let template = instantiate_one(entry, templates, &name, container)?;
162 template_of.insert(name.clone(), template);
163 used.insert(name);
164 } else if entry.len() == 1 {
165 let (name, body) = entry.iter_mut().next().expect("one entry");
166 if let JVal::Object(op) = body
167 && op.contains_key(USES_KEY)
168 {
169 let template = instantiate_one(op, templates, name, container)?;
170 template_of.insert(name.clone(), template);
171 used.insert(name.clone());
172 }
173 }
174 }
175 }
176 _ => {}
177 }
178 Ok(())
179}
180
181fn instantiate_one(
184 op: &mut Map<String, JVal>,
185 templates: &Map<String, JVal>,
186 op_name: &str,
187 container: &str,
188) -> Result<String, String> {
189 let template_name = match op.remove(USES_KEY) {
190 Some(JVal::String(s)) => s,
191 Some(other) => {
192 return Err(format!(
193 "{container}: op '{op_name}': `uses:` must name an op template, got {other}"
194 ));
195 }
196 None => unreachable!("called only for ops with `uses:`"),
197 };
198 let Some(JVal::Object(template)) = templates.get(&template_name) else {
199 let known: Vec<&str> = templates.keys().map(String::as_str).collect();
200 return Err(format!(
201 "{container}: op '{op_name}' uses '{template_name}', but no op template \
202 has that name (known: [{}]) — declare it under `{OP_TEMPLATES_KEY}:` or \
203 `extends:` the library that does",
204 known.join(", ")
205 ));
206 };
207 let mut merged = template.clone();
208 for (key, value) in std::mem::take(op) {
209 match merged.get_mut(&key) {
210 None => {
211 merged.insert(key, value);
212 }
213 Some(JVal::Object(base)) if key == "params" || key == "tags" => {
215 let JVal::Object(over) = value else {
216 return Err(format!(
217 "{container}: op '{op_name}': `{key}:` must be a mapping"
218 ));
219 };
220 base.extend(over);
221 }
222 Some(_) => {
223 return Err(format!(
224 "{container}: op '{op_name}' sets `{key}`, which op template \
225 '{template_name}' already defines — a template's request shape \
226 is fixed; qualify it through the wires it needs (bindings or \
227 params), or declare a new template"
228 ));
229 }
230 }
231 }
232 *op = merged;
233 Ok(template_name)
234}
235
236pub(crate) fn bind_and_check(
249 inst: &Instantiated,
250 phases: &mut std::collections::HashMap<String, crate::model::WorkloadPhase>,
251 pool: &mut [crate::model::ParsedOp],
252 declared_params: &[String],
253 doc_bindings: &crate::model::BindingsDef,
254 scenarios: &std::collections::HashMap<String, Vec<crate::model::ScenarioNode>>,
255) -> Result<(), String> {
256 for op in pool.iter_mut() {
257 if inst.pool.contains(&op.name) && op.abstract_interface.is_some() {
258 op.interface_bound = true;
259 }
260 }
261 if inst.pool.is_empty() && inst.phases.is_empty() {
262 return Ok(());
263 }
264
265 let mut workload_wide: BTreeSet<String> = declared_params.iter().cloned().collect();
266 workload_wide.extend(binding_names(doc_bindings));
267 for nodes in scenarios.values() {
268 scenario_names(nodes, &mut workload_wide);
269 }
270
271 for (phase_name, phase) in phases.iter_mut() {
272 let mut phase_wide = workload_wide.clone();
273 phase_wide.extend(binding_names(&phase.bindings));
274 if let Some(spec) = phase.for_each.as_deref()
275 && let Ok(comp) = polydat::iteration::comprehension::spec::parse_inline(spec)
276 {
277 phase_wide.extend(comp.coordinate_names());
278 }
279 let in_phase = inst.phases.get(phase_name);
280 for op in phase.ops.iter_mut() {
281 let from_template =
282 in_phase.is_some_and(|s| s.contains(&op.name)) || inst.pool.contains(&op.name);
283 if !from_template {
284 continue;
285 }
286 let Some(iface) = op.abstract_interface.as_ref() else {
287 continue;
288 };
289 let mut provided = phase_wide.clone();
290 provided.extend(binding_names(&op.bindings));
291 for (need, typ) in &iface.needs {
292 if !provided.contains(need) {
293 let template = inst.template_of.get(&op.name).map_or("?", String::as_str);
294 return Err(format!(
295 "op '{phase_name}.{}' uses op template '{template}', which needs \
296 '{need}' ({typ}) — supply it in the op's `bindings:`, or from \
297 the phase, workload, or scenario (a binding or a declared param)",
298 op.name
299 ));
300 }
301 }
302 op.interface_bound = true;
303 }
304 }
305 Ok(())
306}
307
308fn binding_names(bindings: &crate::model::BindingsDef) -> Vec<String> {
310 match bindings {
311 crate::model::BindingsDef::PolydatSource(s) => crate::inline::binding_wire_names(s),
312 crate::model::BindingsDef::Map(m) => m.keys().cloned().collect(),
313 }
314}
315
316fn scenario_names(nodes: &[crate::model::ScenarioNode], out: &mut BTreeSet<String>) {
318 use crate::model::ScenarioNode as N;
319 for node in nodes {
320 match node {
321 N::Phase(_) => {}
322 N::Comprehension {
323 comprehension,
324 children,
325 ..
326 } => {
327 out.extend(comprehension.coordinate_names());
328 scenario_names(children, out);
329 }
330 N::DoWhile {
331 counter, children, ..
332 }
333 | N::DoUntil {
334 counter, children, ..
335 } => {
336 out.extend(counter.iter().cloned());
337 scenario_names(children, out);
338 }
339 N::Bindings { source, children } => {
340 out.extend(crate::inline::binding_wire_names(source));
341 scenario_names(children, out);
342 }
343 N::IncludedScenario { children, .. } => scenario_names(children, out),
344 }
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use serde_json::json;
352
353 fn doc(v: JVal) -> Map<String, JVal> {
354 v.as_object().expect("object").clone()
355 }
356
357 #[test]
358 fn a_phase_op_becomes_its_template_with_its_own_keys() {
359 let mut d = doc(json!({
360 "op_templates": {
361 "getPet": {
362 "abstract": {"needs": {"petId": "u64"}},
363 "method": "GET",
364 "uri": "{base_url}/pets/{petId}"
365 }
366 },
367 "phases": {"read": {"ops": {"fetch": {
368 "uses": "getPet",
369 "bindings": "petId := 7"
370 }}}}
371 }));
372 let out = instantiate(&mut d).expect("instantiates");
373 assert!(d.get(OP_TEMPLATES_KEY).is_none());
374 let op = &d["phases"]["read"]["ops"]["fetch"];
375 assert_eq!(op["method"], "GET");
376 assert_eq!(op["bindings"], "petId := 7");
377 assert!(op.get(USES_KEY).is_none());
378 assert!(out.phases["read"].contains("fetch"));
379 assert_eq!(out.template_of["fetch"], "getPet");
380 }
381
382 #[test]
383 fn redefining_a_template_field_is_a_load_error() {
384 let mut d = doc(json!({
385 "op_templates": {"getPet": {"method": "GET", "uri": "/pets"}},
386 "phases": {"read": {"ops": {"fetch": {"uses": "getPet", "method": "POST"}}}}
387 }));
388 let err = instantiate(&mut d).unwrap_err();
389 assert!(err.contains("sets `method`"), "{err}");
390 }
391
392 #[test]
393 fn params_and_tags_merge_with_the_op_winning() {
394 let mut d = doc(json!({
395 "op_templates": {"t": {"stmt": "x", "params": {"a": "1", "b": "2"}, "tags": {"k": "v"}}},
396 "ops": {"o": {"uses": "t", "params": {"b": "3"}, "tags": {"z": "q"}}}
397 }));
398 let out = instantiate(&mut d).expect("instantiates");
399 let op = &d["ops"]["o"];
400 assert_eq!(op["params"], json!({"a": "1", "b": "3"}));
401 assert_eq!(op["tags"], json!({"k": "v", "z": "q"}));
402 assert!(out.pool.contains("o"));
403 }
404
405 #[test]
406 fn an_unknown_template_names_the_known_ones() {
407 let mut d = doc(json!({
408 "op_templates": {"getPet": {"stmt": "x"}},
409 "phases": {"p": {"ops": [{"name": "o", "uses": "nope"}]}}
410 }));
411 let err = instantiate(&mut d).unwrap_err();
412 assert!(err.contains("'nope'") && err.contains("getPet"), "{err}");
413 }
414}