use std::path::PathBuf;
use std::sync::Arc;
use aion::EngineError;
use aion_proto::WireError;
use aion_toolchain::{CompileRequest, ToolchainError, compile_source};
use serde::{Deserialize, Serialize};
use super::error::AuthoringApiError;
use crate::config::{AUTHORING_GLEAM_PATH_EMPTY, AUTHORING_PROJECT_ROOT_REQUIRED};
use crate::{CallerIdentity, ServerState};
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CompileSourceRequest {
pub source: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompileSourceResponse {
pub workflow_type: String,
pub content_hash: String,
pub deployed_entry_module: String,
pub entry_function: String,
pub freshly_loaded: bool,
pub route_changed: bool,
}
pub async fn compile_and_load(
state: &ServerState,
caller: &CallerIdentity,
transport: &'static str,
request: CompileSourceRequest,
) -> Result<CompileSourceResponse, AuthoringApiError> {
authorize_mutation(state, caller, transport)?;
let (gleam_path, template_root) = authoring_paths(state)?;
let compiled = run_compile(gleam_path, template_root, request.source).await?;
let engine = engine_handle(state)?;
match engine.load_package(compiled.package).await {
Ok(outcome) => {
let workflow_type = outcome.record.workflow_type().to_owned();
let content_hash = outcome.record.version().to_string();
tracing::info!(
operation = "authoring.compile",
subject = caller.subject(),
grant_source = caller.grant_source().label(),
transport,
workflow_type = %workflow_type,
content_hash = %content_hash,
outcome = "loaded",
freshly_loaded = outcome.freshly_loaded,
route_changed = outcome.route_changed,
"authoring compile-and-load applied"
);
Ok(CompileSourceResponse {
workflow_type,
content_hash,
deployed_entry_module: outcome.record.deployed_entry_module().to_owned(),
entry_function: outcome.record.entry_function().to_owned(),
freshly_loaded: outcome.freshly_loaded,
route_changed: outcome.route_changed,
})
}
Err(error) => Err(map_load_failure(caller, transport, error)),
}
}
fn authorize_mutation(
state: &ServerState,
caller: &CallerIdentity,
transport: &'static str,
) -> Result<(), AuthoringApiError> {
let guard = state.deploy_guard();
if let Err(error) = guard.authorize(caller) {
let wire = error.to_wire_error();
tracing::warn!(
operation = "authoring.compile",
subject = caller.subject(),
grant_source = caller.grant_source().label(),
transport,
reason = %wire.message,
"authoring operation denied"
);
return Err(AuthoringApiError::Wire(wire));
}
if state.drain_state().is_draining() {
return Err(AuthoringApiError::Unavailable(WireError::backend(
"server is draining and not accepting authoring submissions",
)));
}
Ok(())
}
fn authoring_paths(state: &ServerState) -> Result<(PathBuf, PathBuf), AuthoringApiError> {
let authoring = &state.runtime_config().authoring;
let Some(gleam_path) = authoring.gleam_path.clone() else {
return Err(AuthoringApiError::Wire(WireError::backend(
AUTHORING_GLEAM_PATH_EMPTY,
)));
};
let Some(project_root) = authoring.project_root.clone() else {
return Err(AuthoringApiError::Wire(WireError::backend(
AUTHORING_PROJECT_ROOT_REQUIRED,
)));
};
Ok((gleam_path, project_root))
}
async fn run_compile(
gleam_path: PathBuf,
template_root: PathBuf,
source: String,
) -> Result<aion_toolchain::CompiledWorkflow, AuthoringApiError> {
let join = tokio::task::spawn_blocking(move || {
compile_source(&CompileRequest {
template_root: &template_root,
gleam_path: &gleam_path,
source: &source,
})
})
.await;
match join {
Ok(Ok(compiled)) => Ok(compiled),
Ok(Err(error)) => Err(map_toolchain_error(error)),
Err(join_error) => Err(AuthoringApiError::Wire(WireError::backend(format!(
"authoring compile task failed to run: {join_error}"
)))),
}
}
fn map_toolchain_error(error: ToolchainError) -> AuthoringApiError {
match error {
ToolchainError::TypeCheck { diagnostics } => AuthoringApiError::TypeError(diagnostics),
ToolchainError::GleamSpawn { .. } | ToolchainError::Io { .. } => {
AuthoringApiError::Wire(
WireError::backend(error.to_string()).with_error_type("Toolchain"),
)
}
ToolchainError::Packaging(_) | ToolchainError::InvalidProject { .. } => {
AuthoringApiError::Wire(
WireError::invalid_input(error.to_string()).with_error_type("Toolchain"),
)
}
}
}
fn map_load_failure(
caller: &CallerIdentity,
transport: &'static str,
error: EngineError,
) -> AuthoringApiError {
let mapped = match error {
EngineError::ShuttingDown => AuthoringApiError::Unavailable(
WireError::backend(error.to_string()).with_error_type("ShuttingDown"),
),
EngineError::Load { .. } => AuthoringApiError::Wire(
WireError::invalid_input(error.to_string()).with_error_type("Load"),
),
EngineError::Package(_) => AuthoringApiError::Wire(
WireError::invalid_input(error.to_string()).with_error_type("Package"),
),
other => AuthoringApiError::Wire(crate::ServerError::from(other).to_wire_error()),
};
tracing::info!(
operation = "authoring.compile",
subject = caller.subject(),
grant_source = caller.grant_source().label(),
transport,
outcome = mapped.outcome(),
"authoring compile-and-load refused at hot-load"
);
mapped
}
fn engine_handle(state: &ServerState) -> Result<Arc<aion::Engine>, AuthoringApiError> {
state
.deploy_guard()
.engine()
.map(Arc::clone)
.map_err(|error| AuthoringApiError::Wire(error.to_wire_error()))
}