use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use aion_package::{ActivityDescriptor, PackageContract, WorkerContract};
use anyhow::{Context, Result, bail};
#[derive(Clone, Copy, Debug)]
pub(crate) struct ServingSource {
pub(crate) subject: &'static str,
pub(crate) serves: &'static str,
pub(crate) omits: &'static str,
}
pub(crate) fn compile_contract(document: &Path) -> Result<PackageContract> {
let source = std::fs::read_to_string(document)
.with_context(|| format!("failed to read AWL document {}", document.display()))?;
let compiled =
aion_awl::compile(&source, crate::awl::document_root(document)).map_err(|error| {
anyhow::anyhow!(
"{} does not compile, so no action surface can be derived from it:\n{}",
document.display(),
crate::awl::compile_diagnostics(document, &error).join("\n")
)
})?;
Ok(compiled.contract)
}
pub(crate) fn select_worker<'contract>(
document: &Path,
contract: &'contract PackageContract,
requested: Option<&str>,
requested_by: &str,
) -> Result<&'contract WorkerContract> {
let declared = contract
.workers
.iter()
.map(|candidate| candidate.task_queue.as_str())
.collect::<Vec<_>>();
match requested {
Some(queue) => contract
.workers
.iter()
.find(|candidate| candidate.task_queue == queue)
.ok_or_else(|| {
anyhow::anyhow!(
"{requested_by} serves queue `{queue}` but {} declares no such worker \
(it declares: {})",
document.display(),
if declared.is_empty() {
"none".to_owned()
} else {
declared.join(", ")
}
)
}),
None => match contract.workers.as_slice() {
[only] => Ok(only),
[] => bail!(
"{} declares no worker block, so there is no task queue to serve; \
a queue is the name of a `worker` block in the document",
document.display()
),
_ => bail!(
"{} declares {} worker blocks, so which queue to serve is ambiguous; \
select one with {requested_by}: {}",
document.display(),
declared.len(),
declared.join(", ")
),
},
}
}
pub(crate) fn serviceable_action_names(worker: &WorkerContract) -> BTreeSet<String> {
worker
.actions
.iter()
.filter(|action| action.worker_owed())
.map(|action| action.name.clone())
.collect()
}
pub(crate) fn reconcile(
document: &Path,
worker: &WorkerContract,
served: &BTreeSet<String>,
source: ServingSource,
) -> Result<BTreeMap<String, ActivityDescriptor>> {
let queue = worker.task_queue.as_str();
let ServingSource {
subject,
serves,
omits,
} = source;
let mut declared = worker
.actions
.iter()
.filter(|action| action.worker_owed())
.map(|action| {
(
action.name.clone(),
ActivityDescriptor {
name: action.name.clone(),
input_schema: action.input_schema.clone(),
output_schema: action.output_schema.clone(),
},
)
})
.collect::<BTreeMap<_, _>>();
let mut descriptors = BTreeMap::new();
for action in served {
let Some(descriptor) = declared.remove(action) else {
let carries_body = worker
.actions
.iter()
.any(|candidate| &candidate.name == action && !candidate.worker_owed());
if carries_body {
bail!(
"{subject} {serves} action `{action}`, but `{}` declares a body for it: \
the server runs a declared body itself and no worker serves it",
document.display()
);
}
bail!(
"{subject} {serves} action `{action}`, which queue `{queue}` in {} does not declare",
document.display()
);
};
descriptors.insert(action.clone(), descriptor);
}
if !declared.is_empty() {
let unserved = declared.keys().cloned().collect::<Vec<_>>().join(", ");
bail!(
"queue `{queue}` in {} declares {} that the {subject} {omits}; \
a worker must serve its whole queue or none of it, because a partial \
advertisement is admitted and then dispatched an action it never declared",
document.display(),
if unserved.contains(", ") {
format!("actions {unserved}")
} else {
format!("action `{unserved}`")
}
);
}
Ok(descriptors)
}