use std::collections::{BTreeMap, BTreeSet};
use serde_json::{Map, Value as JVal};
pub const OP_TEMPLATES_KEY: &str = "op_templates";
pub const USES_KEY: &str = "uses";
#[derive(Debug, Default)]
pub(crate) struct Instantiated {
pub phases: BTreeMap<String, BTreeSet<String>>,
pub pool: BTreeSet<String>,
pub template_of: BTreeMap<String, String>,
}
pub(crate) fn instantiate(doc: &mut Map<String, JVal>) -> Result<Instantiated, String> {
let templates = match doc.remove(OP_TEMPLATES_KEY) {
None => Map::new(),
Some(JVal::Object(m)) => m,
Some(other) => {
return Err(format!(
"`{OP_TEMPLATES_KEY}:` must be a mapping of template name -> op body, got {other}"
));
}
};
for (name, body) in &templates {
if !body.is_object() {
return Err(format!(
"op template '{name}' must be a mapping (an op body), got {body}"
));
}
if body.get(USES_KEY).is_some() {
return Err(format!(
"op template '{name}' declares `uses:` — a template is a complete \
op body and does not instantiate another"
));
}
}
let mut out = Instantiated::default();
if let Some(ops) = doc.get_mut("ops") {
instantiate_ops(
ops,
&templates,
"top-level ops",
&mut out.pool,
&mut out.template_of,
)?;
}
if let Some(JVal::Object(blocks)) = doc.get_mut("blocks") {
for (block_name, block) in blocks.iter_mut() {
if let Some(ops) = block.get_mut("ops") {
instantiate_ops(
ops,
&templates,
&format!("block '{block_name}'"),
&mut out.pool,
&mut out.template_of,
)?;
}
}
}
if let Some(JVal::Object(phases)) = doc.get_mut("phases") {
for (phase_name, phase) in phases.iter_mut() {
if let Some(ops) = phase.get_mut("ops") {
let mut used = BTreeSet::new();
instantiate_ops(
ops,
&templates,
&format!("phase '{phase_name}'"),
&mut used,
&mut out.template_of,
)?;
if !used.is_empty() {
out.phases.insert(phase_name.clone(), used);
}
}
}
}
Ok(out)
}
fn instantiate_ops(
ops: &mut JVal,
templates: &Map<String, JVal>,
container: &str,
used: &mut BTreeSet<String>,
template_of: &mut BTreeMap<String, String>,
) -> Result<(), String> {
match ops {
JVal::Object(map) => {
for (name, body) in map.iter_mut() {
if let JVal::Object(op) = body
&& op.contains_key(USES_KEY)
{
let template = instantiate_one(op, templates, name, container)?;
template_of.insert(name.clone(), template);
used.insert(name.clone());
}
}
}
JVal::Array(items) => {
for item in items.iter_mut() {
let JVal::Object(entry) = item else { continue };
if entry.contains_key(USES_KEY) {
let name = entry
.get("name")
.and_then(JVal::as_str)
.ok_or_else(|| {
format!("{container}: a list-form op with `uses:` needs a `name:`")
})?
.to_string();
let template = instantiate_one(entry, templates, &name, container)?;
template_of.insert(name.clone(), template);
used.insert(name);
} else if entry.len() == 1 {
let (name, body) = entry.iter_mut().next().expect("one entry");
if let JVal::Object(op) = body
&& op.contains_key(USES_KEY)
{
let template = instantiate_one(op, templates, name, container)?;
template_of.insert(name.clone(), template);
used.insert(name.clone());
}
}
}
}
_ => {}
}
Ok(())
}
fn instantiate_one(
op: &mut Map<String, JVal>,
templates: &Map<String, JVal>,
op_name: &str,
container: &str,
) -> Result<String, String> {
let template_name = match op.remove(USES_KEY) {
Some(JVal::String(s)) => s,
Some(other) => {
return Err(format!(
"{container}: op '{op_name}': `uses:` must name an op template, got {other}"
));
}
None => unreachable!("called only for ops with `uses:`"),
};
let Some(JVal::Object(template)) = templates.get(&template_name) else {
let known: Vec<&str> = templates.keys().map(String::as_str).collect();
return Err(format!(
"{container}: op '{op_name}' uses '{template_name}', but no op template \
has that name (known: [{}]) — declare it under `{OP_TEMPLATES_KEY}:` or \
`extends:` the library that does",
known.join(", ")
));
};
let mut merged = template.clone();
for (key, value) in std::mem::take(op) {
match merged.get_mut(&key) {
None => {
merged.insert(key, value);
}
Some(JVal::Object(base)) if key == "params" || key == "tags" => {
let JVal::Object(over) = value else {
return Err(format!(
"{container}: op '{op_name}': `{key}:` must be a mapping"
));
};
base.extend(over);
}
Some(_) => {
return Err(format!(
"{container}: op '{op_name}' sets `{key}`, which op template \
'{template_name}' already defines — a template's request shape \
is fixed; qualify it through the wires it needs (bindings or \
params), or declare a new template"
));
}
}
}
*op = merged;
Ok(template_name)
}
pub(crate) fn bind_and_check(
inst: &Instantiated,
phases: &mut std::collections::HashMap<String, crate::model::WorkloadPhase>,
pool: &mut [crate::model::ParsedOp],
declared_params: &[String],
doc_bindings: &crate::model::BindingsDef,
scenarios: &std::collections::HashMap<String, Vec<crate::model::ScenarioNode>>,
) -> Result<(), String> {
for op in pool.iter_mut() {
if inst.pool.contains(&op.name) && op.abstract_interface.is_some() {
op.interface_bound = true;
}
}
if inst.pool.is_empty() && inst.phases.is_empty() {
return Ok(());
}
let mut workload_wide: BTreeSet<String> = declared_params.iter().cloned().collect();
workload_wide.extend(binding_names(doc_bindings));
for nodes in scenarios.values() {
scenario_names(nodes, &mut workload_wide);
}
for (phase_name, phase) in phases.iter_mut() {
let mut phase_wide = workload_wide.clone();
phase_wide.extend(binding_names(&phase.bindings));
if let Some(spec) = phase.for_each.as_deref()
&& let Ok(comp) = polydat::iteration::comprehension::spec::parse_inline(spec)
{
phase_wide.extend(comp.coordinate_names());
}
let in_phase = inst.phases.get(phase_name);
for op in phase.ops.iter_mut() {
let from_template =
in_phase.is_some_and(|s| s.contains(&op.name)) || inst.pool.contains(&op.name);
if !from_template {
continue;
}
let Some(iface) = op.abstract_interface.as_ref() else {
continue;
};
let mut provided = phase_wide.clone();
provided.extend(binding_names(&op.bindings));
for (need, typ) in &iface.needs {
if !provided.contains(need) {
let template = inst.template_of.get(&op.name).map_or("?", String::as_str);
return Err(format!(
"op '{phase_name}.{}' uses op template '{template}', which needs \
'{need}' ({typ}) — supply it in the op's `bindings:`, or from \
the phase, workload, or scenario (a binding or a declared param)",
op.name
));
}
}
op.interface_bound = true;
}
}
Ok(())
}
fn binding_names(bindings: &crate::model::BindingsDef) -> Vec<String> {
match bindings {
crate::model::BindingsDef::PolydatSource(s) => crate::inline::binding_wire_names(s),
crate::model::BindingsDef::Map(m) => m.keys().cloned().collect(),
}
}
fn scenario_names(nodes: &[crate::model::ScenarioNode], out: &mut BTreeSet<String>) {
use crate::model::ScenarioNode as N;
for node in nodes {
match node {
N::Phase(_) => {}
N::Comprehension {
comprehension,
children,
..
} => {
out.extend(comprehension.coordinate_names());
scenario_names(children, out);
}
N::DoWhile {
counter, children, ..
}
| N::DoUntil {
counter, children, ..
} => {
out.extend(counter.iter().cloned());
scenario_names(children, out);
}
N::Bindings { source, children } => {
out.extend(crate::inline::binding_wire_names(source));
scenario_names(children, out);
}
N::IncludedScenario { children, .. } => scenario_names(children, out),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn doc(v: JVal) -> Map<String, JVal> {
v.as_object().expect("object").clone()
}
#[test]
fn a_phase_op_becomes_its_template_with_its_own_keys() {
let mut d = doc(json!({
"op_templates": {
"getPet": {
"abstract": {"needs": {"petId": "u64"}},
"method": "GET",
"uri": "{base_url}/pets/{petId}"
}
},
"phases": {"read": {"ops": {"fetch": {
"uses": "getPet",
"bindings": "petId := 7"
}}}}
}));
let out = instantiate(&mut d).expect("instantiates");
assert!(d.get(OP_TEMPLATES_KEY).is_none());
let op = &d["phases"]["read"]["ops"]["fetch"];
assert_eq!(op["method"], "GET");
assert_eq!(op["bindings"], "petId := 7");
assert!(op.get(USES_KEY).is_none());
assert!(out.phases["read"].contains("fetch"));
assert_eq!(out.template_of["fetch"], "getPet");
}
#[test]
fn redefining_a_template_field_is_a_load_error() {
let mut d = doc(json!({
"op_templates": {"getPet": {"method": "GET", "uri": "/pets"}},
"phases": {"read": {"ops": {"fetch": {"uses": "getPet", "method": "POST"}}}}
}));
let err = instantiate(&mut d).unwrap_err();
assert!(err.contains("sets `method`"), "{err}");
}
#[test]
fn params_and_tags_merge_with_the_op_winning() {
let mut d = doc(json!({
"op_templates": {"t": {"stmt": "x", "params": {"a": "1", "b": "2"}, "tags": {"k": "v"}}},
"ops": {"o": {"uses": "t", "params": {"b": "3"}, "tags": {"z": "q"}}}
}));
let out = instantiate(&mut d).expect("instantiates");
let op = &d["ops"]["o"];
assert_eq!(op["params"], json!({"a": "1", "b": "3"}));
assert_eq!(op["tags"], json!({"k": "v", "z": "q"}));
assert!(out.pool.contains("o"));
}
#[test]
fn an_unknown_template_names_the_known_ones() {
let mut d = doc(json!({
"op_templates": {"getPet": {"stmt": "x"}},
"phases": {"p": {"ops": [{"name": "o", "uses": "nope"}]}}
}));
let err = instantiate(&mut d).unwrap_err();
assert!(err.contains("'nope'") && err.contains("getPet"), "{err}");
}
}