use std::path::{Path, PathBuf};
use aion_core::Payload;
use crate::error::HarnessError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HarnessWorkspace {
Fixed(PathBuf),
PerRun(String),
}
impl HarnessWorkspace {
pub fn for_attempt(&self, input: &Payload) -> Result<PathBuf, HarnessError> {
match self {
Self::Fixed(directory) => Ok(directory.clone()),
Self::PerRun(parameter) => Self::read(parameter, input),
}
}
#[must_use]
pub fn fixed(&self) -> Option<&Path> {
match self {
Self::Fixed(directory) => Some(directory.as_path()),
Self::PerRun(_) => None,
}
}
#[must_use]
pub fn per_run(&self) -> Option<&str> {
match self {
Self::PerRun(parameter) => Some(parameter.as_str()),
Self::Fixed(_) => None,
}
}
pub fn prompt_for_attempt(
&self,
input: &Payload,
reserved: &[&str],
) -> Result<String, HarnessError> {
let text = std::str::from_utf8(input.bytes())
.map(str::to_owned)
.map_err(|source| {
HarnessError::protocol(format!("the run input is not valid UTF-8: {source}"))
})?;
match input.content_type() {
aion_core::ContentType::Json => {
match serde_json::from_str::<serde_json::Value>(&text) {
Ok(serde_json::Value::String(inner)) => Ok(inner),
Ok(serde_json::Value::Object(fields)) => self.prompt_field(&fields, reserved),
_ => Ok(text),
}
}
}
}
fn prompt_field(
&self,
fields: &serde_json::Map<String, serde_json::Value>,
reserved: &[&str],
) -> Result<String, HarnessError> {
let directory = self.per_run();
let carried = fields
.iter()
.filter(|(name, _)| {
Some(name.as_str()) != directory && !reserved.contains(&name.as_str())
})
.collect::<Vec<_>>();
let [(name, value)] = carried.as_slice() else {
return Err(HarnessError::configuration(format!(
"an agent is asked one thing, and this job's input carries {carried} \
{besides} to ask it with{named}. The agent is not started rather than \
started on instructions nobody wrote.",
carried = carried.len(),
besides = match (directory, reserved.is_empty()) {
(Some(_), _) | (None, false) =>
"fields besides the parameters this worker's harness reads itself",
(None, true) => "fields",
},
named = list(&carried)
)));
};
let Some(prompt) = value.as_str() else {
return Err(HarnessError::configuration(format!(
"an agent is asked in words, and this job carries its `{name}` as {kind} \
rather than text",
kind = describe(value)
)));
};
Ok(prompt.to_owned())
}
fn read(parameter: &str, input: &Payload) -> Result<PathBuf, HarnessError> {
let value: serde_json::Value = serde_json::from_slice(input.bytes()).map_err(|error| {
HarnessError::configuration(format!(
"this worker takes each agent's working directory from the `{parameter}` \
parameter of the job, and this job's input is not readable as JSON: {error}"
))
})?;
let serde_json::Value::Object(fields) = value else {
return Err(HarnessError::configuration(format!(
"this worker takes each agent's working directory from the `{parameter}` \
parameter of the job, and this job's input is not an object, so it carries no \
parameters at all"
)));
};
let Some(field) = fields.get(parameter) else {
return Err(HarnessError::configuration(format!(
"this worker takes each agent's working directory from the `{parameter}` \
parameter of the job, and this job's input does not carry it. The agent is \
not started rather than started in whichever directory this worker process \
happens to be in."
)));
};
let Some(directory) = field.as_str() else {
return Err(HarnessError::configuration(format!(
"this worker takes each agent's working directory from the `{parameter}` \
parameter of the job, and this job carries `{parameter}` as {kind} rather \
than a path",
kind = describe(field)
)));
};
if directory.trim().is_empty() {
return Err(HarnessError::configuration(format!(
"this worker takes each agent's working directory from the `{parameter}` \
parameter of the job, and this job carries it empty. An empty directory is \
not the current one; it is a job that did not say where to work."
)));
}
Ok(PathBuf::from(directory))
}
}
fn list(fields: &[(&String, &serde_json::Value)]) -> String {
if fields.is_empty() {
return String::new();
}
format!(
" ({})",
fields
.iter()
.map(|(name, _)| name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}
fn describe(value: &serde_json::Value) -> &'static str {
match value {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "a boolean",
serde_json::Value::Number(_) => "a number",
serde_json::Value::String(_) => "a string",
serde_json::Value::Array(_) => "a list",
serde_json::Value::Object(_) => "an object",
}
}
#[cfg(test)]
mod tests;