use std::sync::{Arc, PoisonError};
use futures::future::BoxFuture;
use oino_core::extensions::api::ExtensionInstance;
use oino_core::extensions::host::HostApi;
use oino_core::extensions::sources::{SourcesContext, resolve};
use oino_core::services::ExtensionLoader;
use oino_wasm_host::core_bridge::{CoreLoader, SourcedPackage};
use oino_wasm_host::{HostConfig, WasmRuntime, WasmRuntimeOptions};
use crate::cli::args::Diagnostic;
pub const EXTENSION_LOAD_FAILURE_HINT: &str = "Hint: Start without extensions using \"oino -ne\".";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Builtin {
#[cfg(feature = "faux")]
Faux,
#[cfg(feature = "faux")]
UiProbe,
}
impl Builtin {
fn parse(id: &str) -> Option<Self> {
match id {
#[cfg(feature = "faux")]
"faux" => Some(Self::Faux),
#[cfg(feature = "faux")]
"ui-probe" => Some(Self::UiProbe),
_ => None,
}
}
#[cfg_attr(
not(feature = "faux"),
allow(unused_variables, reason = "no built-ins without features")
)]
fn instantiate(self, host: &Arc<dyn HostApi>) -> Arc<dyn ExtensionInstance> {
match self {
#[cfg(feature = "faux")]
Self::Faux => Arc::new(crate::faux::FauxComponent(
crate::faux::FauxProvider::from_env().with_host(host),
)),
#[cfg(feature = "faux")]
Self::UiProbe => Arc::new(crate::faux::UiProbe::new(host)),
}
}
}
#[derive(Clone, Debug)]
pub struct LoaderContext {
pub agent_dir: std::path::PathBuf,
pub cwd: std::path::PathBuf,
pub no_extensions: bool,
pub exclude_extensions: Vec<String>,
}
impl LoaderContext {
fn sources_context(&self) -> SourcesContext {
SourcesContext {
agent_dir: self.agent_dir.clone(),
cwd: self.cwd.clone(),
dist_dirs: Vec::new(),
no_extensions: self.no_extensions,
excluded: self.exclude_extensions.clone(),
}
}
}
pub struct CliLoader {
builtins: Vec<Builtin>,
wasm: Option<CoreLoader>,
diagnostics: Vec<Diagnostic>,
}
impl std::fmt::Debug for CliLoader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CliLoader")
.field("builtins", &self.builtins)
.field("wasm", &self.wasm)
.finish_non_exhaustive()
}
}
fn diagnostic(d: oino_core::extensions::sources::SourceDiagnostic) -> Diagnostic {
use oino_core::extensions::sources::SourceSeverity;
match d.severity {
SourceSeverity::Warning => Diagnostic::warning(d.message),
SourceSeverity::Error => Diagnostic::error(d.message),
}
}
fn unknown_builtin(source: &str, id: &str) -> Diagnostic {
Diagnostic::error(format!(
"Failed to load extension \"{source}\": unknown built-in extension \"{id}\""
))
}
impl CliLoader {
#[must_use]
pub fn new(sources: &[String], context: &LoaderContext) -> Self {
let mut builtins = Vec::new();
let mut explicit = Vec::new();
let mut diagnostics = Vec::new();
for source in sources {
if let Some(id) = source.strip_prefix("builtin:") {
match Builtin::parse(id) {
Some(builtin) => builtins.push(builtin),
None => diagnostics.push(unknown_builtin(source, id)),
}
} else {
explicit.push(source.clone());
}
}
let resolved = resolve(&explicit, &context.sources_context());
diagnostics.extend(resolved.diagnostics.into_iter().map(diagnostic));
let packages = resolved
.packages
.into_iter()
.map(|p| SourcedPackage {
package: p.package,
project: p.project,
})
.collect::<Vec<_>>();
let wasm = if packages.is_empty() {
None
} else {
let config = HostConfig::new(context.agent_dir.clone(), context.cwd.clone());
match WasmRuntime::new(WasmRuntimeOptions::new(config)) {
Ok(runtime) => Some(CoreLoader::new(Arc::new(runtime), packages)),
Err(error) => {
diagnostics.push(Diagnostic::error(format!(
"Failed to start the extension host: {error}"
)));
None
}
}
};
Self {
builtins,
wasm,
diagnostics,
}
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
#[must_use]
pub fn load_errors(&self) -> Vec<Diagnostic> {
self.wasm.as_ref().map_or_else(Vec::new, |wasm| {
wasm.errors
.lock()
.unwrap_or_else(PoisonError::into_inner)
.iter()
.map(|e| {
Diagnostic::warning(format!(
"Failed to load extension \"{}\": {}",
e.extension, e.message
))
})
.collect()
})
}
}
impl ExtensionLoader for CliLoader {
fn load(
&self,
host: Arc<dyn HostApi>,
project_trusted: bool,
) -> BoxFuture<'_, Vec<Arc<dyn ExtensionInstance>>> {
self.load_enabled(host, project_trusted, &[])
}
fn load_enabled<'a>(
&'a self,
host: Arc<dyn HostApi>,
project_trusted: bool,
disabled: &'a [String],
) -> BoxFuture<'a, Vec<Arc<dyn ExtensionInstance>>> {
Box::pin(async move {
let mut instances: Vec<Arc<dyn ExtensionInstance>> = self
.builtins
.iter()
.map(|builtin| builtin.instantiate(&host))
.collect();
if let Some(wasm) = &self.wasm {
instances.extend(wasm.load_enabled(host, project_trusted, disabled).await);
}
instances
})
}
}