1use std::sync::{Arc, PoisonError};
17
18use futures::future::BoxFuture;
19use oino_core::extensions::api::ExtensionInstance;
20use oino_core::extensions::host::HostApi;
21use oino_core::extensions::sources::{SourcesContext, resolve};
22use oino_core::services::ExtensionLoader;
23use oino_wasm_host::core_bridge::{CoreLoader, SourcedPackage};
24use oino_wasm_host::{HostConfig, WasmRuntime, WasmRuntimeOptions};
25
26use crate::cli::args::Diagnostic;
27
28pub const EXTENSION_LOAD_FAILURE_HINT: &str = "Hint: Start without extensions using \"oino -ne\".";
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33enum Builtin {
34 #[cfg(feature = "faux")]
36 Faux,
37 #[cfg(feature = "faux")]
39 UiProbe,
40}
41
42impl Builtin {
43 fn parse(id: &str) -> Option<Self> {
44 match id {
45 #[cfg(feature = "faux")]
46 "faux" => Some(Self::Faux),
47 #[cfg(feature = "faux")]
48 "ui-probe" => Some(Self::UiProbe),
49 _ => None,
50 }
51 }
52
53 #[cfg_attr(
54 not(feature = "faux"),
55 allow(unused_variables, reason = "no built-ins without features")
56 )]
57 fn instantiate(self, host: &Arc<dyn HostApi>) -> Arc<dyn ExtensionInstance> {
58 match self {
59 #[cfg(feature = "faux")]
60 Self::Faux => Arc::new(crate::faux::FauxComponent(
61 crate::faux::FauxProvider::from_env().with_host(host),
62 )),
63 #[cfg(feature = "faux")]
64 Self::UiProbe => Arc::new(crate::faux::UiProbe::new(host)),
65 }
66 }
67}
68
69#[derive(Clone, Debug)]
71pub struct LoaderContext {
72 pub agent_dir: std::path::PathBuf,
74 pub cwd: std::path::PathBuf,
76 pub no_extensions: bool,
78 pub exclude_extensions: Vec<String>,
81}
82
83impl LoaderContext {
84 fn sources_context(&self) -> SourcesContext {
85 SourcesContext {
86 agent_dir: self.agent_dir.clone(),
87 cwd: self.cwd.clone(),
88 dist_dirs: Vec::new(),
89 no_extensions: self.no_extensions,
90 excluded: self.exclude_extensions.clone(),
91 }
92 }
93}
94
95pub struct CliLoader {
97 builtins: Vec<Builtin>,
98 wasm: Option<CoreLoader>,
99 diagnostics: Vec<Diagnostic>,
100}
101
102impl std::fmt::Debug for CliLoader {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.debug_struct("CliLoader")
105 .field("builtins", &self.builtins)
106 .field("wasm", &self.wasm)
107 .finish_non_exhaustive()
108 }
109}
110
111fn diagnostic(d: oino_core::extensions::sources::SourceDiagnostic) -> Diagnostic {
112 use oino_core::extensions::sources::SourceSeverity;
113 match d.severity {
114 SourceSeverity::Warning => Diagnostic::warning(d.message),
115 SourceSeverity::Error => Diagnostic::error(d.message),
116 }
117}
118
119fn unknown_builtin(source: &str, id: &str) -> Diagnostic {
121 Diagnostic::error(format!(
122 "Failed to load extension \"{source}\": unknown built-in extension \"{id}\""
123 ))
124}
125
126impl CliLoader {
127 #[must_use]
129 pub fn new(sources: &[String], context: &LoaderContext) -> Self {
130 let mut builtins = Vec::new();
131 let mut explicit = Vec::new();
132 let mut diagnostics = Vec::new();
133 for source in sources {
134 if let Some(id) = source.strip_prefix("builtin:") {
135 match Builtin::parse(id) {
136 Some(builtin) => builtins.push(builtin),
137 None => diagnostics.push(unknown_builtin(source, id)),
138 }
139 } else {
140 explicit.push(source.clone());
141 }
142 }
143 let resolved = resolve(&explicit, &context.sources_context());
144 diagnostics.extend(resolved.diagnostics.into_iter().map(diagnostic));
145 let packages = resolved
146 .packages
147 .into_iter()
148 .map(|p| SourcedPackage {
149 package: p.package,
150 project: p.project,
151 })
152 .collect::<Vec<_>>();
153 let wasm = if packages.is_empty() {
154 None
155 } else {
156 let config = HostConfig::new(context.agent_dir.clone(), context.cwd.clone());
157 match WasmRuntime::new(WasmRuntimeOptions::new(config)) {
160 Ok(runtime) => Some(CoreLoader::new(Arc::new(runtime), packages)),
161 Err(error) => {
162 diagnostics.push(Diagnostic::error(format!(
163 "Failed to start the extension host: {error}"
164 )));
165 None
166 }
167 }
168 };
169 Self {
170 builtins,
171 wasm,
172 diagnostics,
173 }
174 }
175
176 #[must_use]
178 pub fn diagnostics(&self) -> &[Diagnostic] {
179 &self.diagnostics
180 }
181
182 #[must_use]
184 pub fn load_errors(&self) -> Vec<Diagnostic> {
185 self.wasm.as_ref().map_or_else(Vec::new, |wasm| {
186 wasm.errors
187 .lock()
188 .unwrap_or_else(PoisonError::into_inner)
189 .iter()
190 .map(|e| {
191 Diagnostic::warning(format!(
192 "Failed to load extension \"{}\": {}",
193 e.extension, e.message
194 ))
195 })
196 .collect()
197 })
198 }
199}
200
201impl ExtensionLoader for CliLoader {
202 fn load(
203 &self,
204 host: Arc<dyn HostApi>,
205 project_trusted: bool,
206 ) -> BoxFuture<'_, Vec<Arc<dyn ExtensionInstance>>> {
207 self.load_enabled(host, project_trusted, &[])
208 }
209
210 fn load_enabled<'a>(
211 &'a self,
212 host: Arc<dyn HostApi>,
213 project_trusted: bool,
214 disabled: &'a [String],
215 ) -> BoxFuture<'a, Vec<Arc<dyn ExtensionInstance>>> {
216 Box::pin(async move {
217 let mut instances: Vec<Arc<dyn ExtensionInstance>> = self
218 .builtins
219 .iter()
220 .map(|builtin| builtin.instantiate(&host))
221 .collect();
222 if let Some(wasm) = &self.wasm {
223 instances.extend(wasm.load_enabled(host, project_trusted, disabled).await);
224 }
225 instances
226 })
227 }
228}