use std::collections::BTreeMap;
use std::path::Path;
use aion_package::{
AionDependency, AwlScaffoldError, AwlWorkerScaffold, DocumentRoot, ScaffoldedFile,
WorkerContract, emit_shell_manifest, scaffold_awl_worker,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct ScaffoldRequest {
pub source: String,
pub worker: String,
pub runtime: String,
}
#[derive(Debug, Serialize)]
pub struct ScaffoldResponse {
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub files: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refusal: Option<ScaffoldRefusal>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "code", rename_all = "snake_case")]
pub enum ScaffoldRefusal {
InvalidDocument {
reason: String,
},
UnknownWorker {
reason: String,
},
UnsupportedRuntime {
reason: String,
},
NoServableAction {
reason: String,
},
NodePinnedQueue {
reason: String,
},
Scaffold {
reason: String,
},
}
impl ScaffoldResponse {
fn generated(files: BTreeMap<String, String>) -> Self {
Self {
ok: true,
files: Some(files),
refusal: None,
}
}
fn refused(refusal: ScaffoldRefusal) -> Self {
Self {
ok: false,
files: None,
refusal: Some(refusal),
}
}
}
pub fn scaffold(
request: &ScaffoldRequest,
schema_root: &Path,
) -> Result<ScaffoldResponse, super::documents::DocumentError> {
let document = match aion_awl::parse(&request.source) {
Ok(document) => document,
Err(error) => {
return Ok(ScaffoldResponse::refused(
ScaffoldRefusal::InvalidDocument {
reason: error.to_string(),
},
));
}
};
let requested_path = format!("{}.awl", document.name);
let workspace_absent = matches!(
std::fs::metadata(schema_root),
Err(error) if error.kind() == std::io::ErrorKind::NotFound
);
let (_staging, staged_root) = if workspace_absent {
let staging = tempfile::Builder::new().prefix("aion-schema-").tempdir()?;
let staged_root = staging.path().to_owned();
(staging, staged_root)
} else {
match super::handlers::stage_schema_imports(schema_root, &requested_path, &request.source) {
Ok(staged) => staged,
Err(
error @ (super::documents::DocumentError::InvalidPath(_)
| super::documents::DocumentError::InvalidName(_)),
) => {
return Ok(ScaffoldResponse::refused(
ScaffoldRefusal::InvalidDocument {
reason: error.to_string(),
},
));
}
Err(error) => return Err(error),
}
};
let compiled = match aion_awl::compile(&request.source, &staged_root) {
Ok(compiled) => compiled,
Err(error) => {
return Ok(ScaffoldResponse::refused(
ScaffoldRefusal::InvalidDocument {
reason: error.to_string(),
},
));
}
};
let Some(contract) = compiled
.contract
.workers
.iter()
.find(|worker| worker.task_queue == request.worker)
else {
return Ok(ScaffoldResponse::refused(ScaffoldRefusal::UnknownWorker {
reason: unknown_worker_reason(&request.worker, &compiled.contract.workers),
}));
};
let document_name = format!("{}.awl", compiled.workflow_name);
let generated = match request.runtime.as_str() {
"rust" => {
let crate_name = format!("{}-worker", contract.task_queue.replace('_', "-"));
let document_include = format!("../{document_name}");
let document_directory = DocumentRoot::InCrateTree(".".to_owned());
let dependencies = AionDependency::Version(env!("CARGO_PKG_VERSION").to_owned());
scaffold_awl_worker(&AwlWorkerScaffold {
contract,
crate_name: &crate_name,
document_include: &document_include,
document_directory: &document_directory,
document_name: &document_name,
dependencies: &dependencies,
})
.map(|scaffold| scaffold.files)
}
"shell" => emit_shell_manifest(contract, &document_name),
runtime => {
return Ok(ScaffoldResponse::refused(
ScaffoldRefusal::UnsupportedRuntime {
reason: format!(
"runtime `{runtime}` is not supported; choose `rust` or `shell`"
),
},
));
}
};
Ok(match generated {
Ok(files) => {
ScaffoldResponse::generated(files_with_document(files, &document_name, &request.source))
}
Err(error) => ScaffoldResponse::refused(scaffold_refusal(&error)),
})
}
fn unknown_worker_reason(requested: &str, workers: &[WorkerContract]) -> String {
format!(
"the document declares no `worker {requested}` block; it declares: [{}]",
workers
.iter()
.map(|worker| format!("`{}`", worker.task_queue))
.collect::<Vec<_>>()
.join(", ")
)
}
fn files_with_document(
files: Vec<ScaffoldedFile>,
document_name: &str,
source: &str,
) -> BTreeMap<String, String> {
let mut mapped = files
.into_iter()
.map(|file| (file.relative, file.contents))
.collect::<BTreeMap<_, _>>();
mapped.insert(document_name.to_owned(), source.to_owned());
mapped
}
fn scaffold_refusal(error: &AwlScaffoldError) -> ScaffoldRefusal {
let reason = error.to_string();
match error {
AwlScaffoldError::NoServableAction { .. } => ScaffoldRefusal::NoServableAction { reason },
AwlScaffoldError::NodePinnedQueue { .. } => ScaffoldRefusal::NodePinnedQueue { reason },
_ => ScaffoldRefusal::Scaffold { reason },
}
}