use std::path::Path;
use std::sync::OnceLock;
use aion_awl::TypeBody;
use aion_package::{
ActionBodyContract, ContentHash, ExtractionLimits, Package, PackageError, WorkerContract,
};
pub const EMBEDDED_UPDATE_CHECK_DOCUMENT: &str =
include_str!("../../update-check-embed/update-check.awl");
pub const EMBEDDED_UPDATE_CHECK_FILENAME: &str = "update-check.awl";
pub const UPDATE_CHECK_WORKFLOW_TYPE: &str = "update_check";
pub const UPDATE_CHECK_QUEUE: &str = "update_check";
pub const FETCH_ACTION: &str = "fetch_crate_index";
pub const FETCH_COMMAND: &str = "curl -fsS https://index.crates.io/ai/on/aion-cli";
#[derive(Debug, thiserror::Error)]
pub enum EmbeddedUpdateCheckError {
#[error("the embedded update-check document does not parse: {message}")]
Parse {
message: String,
},
#[error(
"the embedded update-check document imports schema `{path}`, but the binary embeds the \
document alone and has no directory to resolve imports against; declare the type \
inline in the document"
)]
SchemaImport {
path: String,
},
#[error("the embedded update-check document does not compile: {message}")]
Compile {
message: String,
},
#[error("the embedded update-check document could not be packaged: {message}")]
Assemble {
message: String,
},
#[error("the embedded update-check package did not validate: {source}")]
Package {
#[from]
source: PackageError,
},
#[error(
"the embedded update-check document compiles to workflow type `{found}`, but the server \
half in crates/aion-server/src/update_check/document.rs names \
`{UPDATE_CHECK_WORKFLOW_TYPE}`, so document and constants have diverged"
)]
WrongWorkflowType {
found: String,
},
#[error("the embedded update-check package carries no readable contract: {message}")]
MissingContract {
message: String,
},
#[error(
"the embedded update-check contract declares no `{FETCH_ACTION}` action on queue \
`{UPDATE_CHECK_QUEUE}`; the server half names both, so document and constants have \
diverged"
)]
MissingAction,
#[error(
"the embedded update-check action `{FETCH_ACTION}` declares body {found:?}, but the \
server half records results only from `{FETCH_COMMAND}`, so document and constants \
have diverged"
)]
WrongBody {
found: Option<String>,
},
}
#[derive(Debug, Clone)]
pub struct EmbeddedUpdateCheck {
source: String,
package: Package,
workflow_type: String,
}
impl EmbeddedUpdateCheck {
pub fn load() -> Result<Self, EmbeddedUpdateCheckError> {
Self::from_source(EMBEDDED_UPDATE_CHECK_DOCUMENT)
}
pub fn from_source(source: &str) -> Result<Self, EmbeddedUpdateCheckError> {
let document =
aion_awl::parse(source).map_err(|error| EmbeddedUpdateCheckError::Parse {
message: error.message,
})?;
for declaration in &document.types {
if let TypeBody::SchemaImport { path, .. } = &declaration.body {
return Err(EmbeddedUpdateCheckError::SchemaImport { path: path.clone() });
}
}
let root = Path::new("<embedded-update-check-has-no-schema-directory>");
let prepared = aion_awl_package::compile_and_assemble_awl(
source,
root,
EMBEDDED_UPDATE_CHECK_FILENAME,
)
.map_err(|error| match error {
aion_awl_package::PrepareAwlError::Compile(compile) => {
EmbeddedUpdateCheckError::Compile {
message: compile.to_string(),
}
}
other => EmbeddedUpdateCheckError::Assemble {
message: other.to_string(),
},
})?;
let package = Package::load_from_bytes(&prepared.archive, ExtractionLimits::unbounded())?;
let workflow_type = package.manifest().entry_module.clone();
if workflow_type != UPDATE_CHECK_WORKFLOW_TYPE {
return Err(EmbeddedUpdateCheckError::WrongWorkflowType {
found: workflow_type,
});
}
let contract =
package
.contract()
.map_err(|error| EmbeddedUpdateCheckError::MissingContract {
message: error.to_string(),
})?;
let declared_body = contract
.workers
.iter()
.filter(|worker: &&WorkerContract| worker.task_queue == UPDATE_CHECK_QUEUE)
.flat_map(|worker| worker.actions.iter())
.find(|action| action.name == FETCH_ACTION)
.ok_or(EmbeddedUpdateCheckError::MissingAction)?
.body
.clone();
match declared_body {
Some(ActionBodyContract::Run { ref command }) if command == FETCH_COMMAND => {}
Some(ActionBodyContract::Run { command }) => {
return Err(EmbeddedUpdateCheckError::WrongBody {
found: Some(command),
});
}
Some(ActionBodyContract::Command { command, .. }) => {
return Err(EmbeddedUpdateCheckError::WrongBody {
found: Some(format!("runs command {}", command.name)),
});
}
None => return Err(EmbeddedUpdateCheckError::WrongBody { found: None }),
}
Ok(Self {
source: source.to_owned(),
package,
workflow_type,
})
}
#[must_use]
pub const fn package(&self) -> &Package {
&self.package
}
#[must_use]
pub fn workflow_type(&self) -> &str {
&self.workflow_type
}
#[must_use]
pub const fn content_hash(&self) -> &ContentHash {
self.package.content_hash()
}
#[must_use]
pub fn source(&self) -> &str {
&self.source
}
}
pub fn embedded_update_check()
-> Result<&'static EmbeddedUpdateCheck, &'static EmbeddedUpdateCheckError> {
static EMBEDDED: OnceLock<Result<EmbeddedUpdateCheck, EmbeddedUpdateCheckError>> =
OnceLock::new();
EMBEDDED.get_or_init(EmbeddedUpdateCheck::load).as_ref()
}
#[cfg(test)]
#[path = "document_tests.rs"]
mod document_tests;