use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub(super) const LOCAL_RUNNER_CLASS: &str = "auv.core.local";
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct RunnerProviderConfig {
pub runner_class: String,
pub runtime: RunnerRuntime,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(tag = "type", content = "config", rename_all = "kebab-case")]
pub enum RunnerRuntime {
Executable(ExecutableRunnerRuntime),
RemoteGrpc(RemoteGrpcRunnerRuntime),
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct ExecutableRunnerRuntime {
pub executable: PathBuf,
#[serde(default)]
pub arguments: Vec<String>,
pub working_directory: Option<PathBuf>,
#[serde(default)]
pub environment: BTreeMap<String, String>,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct RemoteGrpcRunnerRuntime {
pub endpoint: String,
}
#[derive(Clone, Debug, Default)]
pub struct FirstPartyRunnerRuntimes {
pub local_driver: Option<RunnerRuntime>,
}
impl RunnerProviderConfig {
pub fn load_json(path: impl AsRef<Path>) -> Result<Self, RunnerProviderConfigError> {
let path = path.as_ref();
let bytes = std::fs::read(path).map_err(|source| RunnerProviderConfigError::FileRead {
path: path.to_path_buf(),
source,
})?;
let mut config: Self = serde_json::from_slice(&bytes).map_err(|source| RunnerProviderConfigError::Json {
path: path.to_path_buf(),
source,
})?;
config.resolve_manifest_relative_executable(path);
Ok(config)
}
fn resolve_manifest_relative_executable(&mut self, manifest_path: &Path) {
let RunnerRuntime::Executable(runtime) = &mut self.runtime else {
return;
};
let directory = manifest_path.parent().unwrap_or_else(|| Path::new(""));
if runtime.executable.is_absolute() || is_bare_executable_name(&runtime.executable) {
} else {
runtime.executable = directory.join(&runtime.executable);
}
if let Some(working_directory) = &mut runtime.working_directory
&& !working_directory.is_absolute()
{
*working_directory = directory.join(&*working_directory);
}
}
}
fn is_bare_executable_name(path: &Path) -> bool {
path.parent().is_none_or(|parent| parent.as_os_str().is_empty())
}
#[derive(Debug, thiserror::Error)]
pub enum RunnerProviderConfigError {
#[error("failed to read Runner provider config {path}: {source}")]
FileRead {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("invalid Runner provider JSON {path}: {source}")]
Json {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("invalid RunnerClass identity: {0}")]
InvalidRunnerClass(String),
#[error("RunnerClass {0} is reserved by the daemon")]
ReservedRunnerClass(String),
#[error("RunnerClass is configured more than once: {0}")]
DuplicateRunnerClass(String),
}
#[derive(Clone, Debug)]
pub(crate) struct RegisteredRunnerProvider {
pub(crate) runner_class: String,
pub(crate) runtime: RunnerRuntime,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct RunnerProviderRegistry {
providers: BTreeMap<String, RegisteredRunnerProvider>,
}
impl RunnerProviderRegistry {
pub(crate) fn build_with_first_party(
local_runtime: Option<RunnerRuntime>,
custom: Vec<RunnerProviderConfig>,
) -> Result<Self, RunnerProviderConfigError> {
let mut registry = Self::default();
if let Some(runtime) = local_runtime {
registry.insert(first_party_provider(LOCAL_RUNNER_CLASS, runtime))?;
}
for config in custom {
registry.insert(register_custom(config)?)?;
}
Ok(registry)
}
fn insert(&mut self, provider: RegisteredRunnerProvider) -> Result<(), RunnerProviderConfigError> {
if self.providers.contains_key(&provider.runner_class) {
return Err(RunnerProviderConfigError::DuplicateRunnerClass(provider.runner_class));
}
self.providers.insert(provider.runner_class.clone(), provider);
Ok(())
}
pub(crate) fn get(&self, runner_class: &str) -> Option<&RegisteredRunnerProvider> {
self.providers.get(runner_class)
}
pub(crate) fn values(&self) -> impl Iterator<Item = &RegisteredRunnerProvider> {
self.providers.values()
}
}
fn first_party_provider(runner_class: &str, runtime: RunnerRuntime) -> RegisteredRunnerProvider {
RegisteredRunnerProvider {
runner_class: runner_class.to_string(),
runtime,
}
}
fn register_custom(config: RunnerProviderConfig) -> Result<RegisteredRunnerProvider, RunnerProviderConfigError> {
validate_class_identity(&config.runner_class)?;
if is_reserved_runner_class(&config.runner_class) {
return Err(RunnerProviderConfigError::ReservedRunnerClass(config.runner_class));
}
Ok(RegisteredRunnerProvider {
runner_class: config.runner_class,
runtime: config.runtime,
})
}
fn validate_class_identity(value: &str) -> Result<(), RunnerProviderConfigError> {
if value.is_empty() {
return Err(RunnerProviderConfigError::InvalidRunnerClass(value.to_string()));
}
Ok(())
}
fn is_reserved_runner_class(value: &str) -> bool {
value == LOCAL_RUNNER_CLASS
}
#[cfg(test)]
#[path = "runner_provider_test.rs"]
mod tests;