1use std::collections::BTreeMap;
14use std::path::Path;
15
16use abi_stable::library::{lib_header_from_raw_library, RawLibrary};
17use abi_stable::sabi_trait::TD_Opaque;
18use abi_stable::std_types::{RResult, RStr, RString};
19use apiplant_abi::{
20 BoxedFunction, FunctionManifest, FunctionMod_Ref, HostApi, HostApi_TO, LogLevel,
21};
22use apiplant_cache::Cache;
23use apiplant_db::Db;
24use apiplant_email::Mailer;
25
26pub type BuiltinHandler = fn(&HostBridge, &str) -> Result<String, String>;
30
31enum Body {
34 Dynamic(BoxedFunction),
36 Builtin(BuiltinHandler),
38}
39
40pub struct LoadedFunction {
42 pub manifest: FunctionManifest,
43 pub config_json: String,
46 body: Body,
47}
48
49impl LoadedFunction {
50 pub fn new(func: BoxedFunction, config_json: String) -> Self {
53 LoadedFunction {
54 manifest: func.manifest(),
55 config_json,
56 body: Body::Dynamic(func),
57 }
58 }
59
60 pub fn builtin(
63 manifest: FunctionManifest,
64 handler: BuiltinHandler,
65 config_json: String,
66 ) -> Self {
67 LoadedFunction {
68 manifest,
69 config_json,
70 body: Body::Builtin(handler),
71 }
72 }
73
74 pub fn invoke(&self, bridge: HostBridge, input: &str) -> Result<String, String> {
77 match &self.body {
78 Body::Builtin(handler) => handler(&bridge, input),
79 Body::Dynamic(func) => {
80 let host = HostApi_TO::from_value(bridge, TD_Opaque);
81 match func.invoke(host, RStr::from_str(input)) {
82 RResult::ROk(s) => Ok(s.into_string()),
83 RResult::RErr(e) => Err(e.into_string()),
84 }
85 }
86 }
87 }
88}
89
90#[derive(Default)]
92pub struct FunctionRegistry {
93 functions: BTreeMap<String, LoadedFunction>,
94}
95
96impl FunctionRegistry {
97 pub fn load(app: &apiplant_core::App) -> Self {
106 let mut registry = FunctionRegistry::default();
107 crate::builtins::register_all(&mut registry, app);
108 for (name, f) in Self::load_dir(&app.functions_dir).functions {
109 if registry.functions.contains_key(&name) {
110 tracing::warn!(function = %name, "app function replaces the built-in of the same name");
111 }
112 registry.functions.insert(name, f);
113 }
114 registry
115 }
116
117 pub fn register_builtin(
119 &mut self,
120 manifest: FunctionManifest,
121 handler: BuiltinHandler,
122 config_json: String,
123 ) {
124 let loaded = LoadedFunction::builtin(manifest, handler, config_json);
125 self.functions
126 .insert(loaded.manifest.name.to_string(), loaded);
127 }
128
129 pub fn load_dir(dir: &Path) -> Self {
132 let mut registry = FunctionRegistry::default();
133 let entries = match std::fs::read_dir(dir) {
134 Ok(e) => e,
135 Err(_) => {
136 tracing::info!(dir = %dir.display(), "no functions/ directory");
137 return registry;
138 }
139 };
140 for entry in entries.flatten() {
141 let path = entry.path();
142 let loadable = matches!(
146 path.extension().and_then(|e| e.to_str()),
147 Some("so") | Some("dylib") | Some("dll") | Some(apiplant_js::EXTENSION)
148 );
149 if !loadable {
150 continue;
151 }
152 match Self::load_library(&path) {
153 Ok(loaded) => {
154 for f in loaded {
155 tracing::info!(
156 function = %f.manifest.name,
157 version = %f.manifest.version,
158 library = %path.display(),
159 "loaded function"
160 );
161 registry.functions.insert(f.manifest.name.to_string(), f);
162 }
163 }
164 Err(e) => {
165 tracing::error!(path = %path.display(), error = %e, "failed to load function")
166 }
167 }
168 }
169 registry
170 }
171
172 fn load_library(path: &Path) -> Result<Vec<LoadedFunction>, String> {
181 let exported = if path.extension().and_then(|e| e.to_str()) == Some(apiplant_js::EXTENSION)
185 {
186 apiplant_js::load(path)?.into()
187 } else {
188 Self::load_native(path)?
189 };
190 Self::wrap(path, exported)
191 }
192
193 fn load_native(path: &Path) -> Result<abi_stable::std_types::RVec<BoxedFunction>, String> {
195 let exported = match Self::open(path) {
196 Ok(module) => module.new_functions()(),
197 Err(rust_abi_error) => match crate::cabi::load(path)? {
198 Some(functions) => functions.into(),
199 None => return Err(rust_abi_error),
202 },
203 };
204 Ok(exported)
205 }
206
207 fn wrap(
210 path: &Path,
211 exported: abi_stable::std_types::RVec<BoxedFunction>,
212 ) -> Result<Vec<LoadedFunction>, String> {
213 if exported.is_empty() {
214 return Err("library exports no functions".to_string());
215 }
216
217 let mut loaded: Vec<LoadedFunction> = Vec::with_capacity(exported.len());
218 for func in exported {
219 let manifest = func.manifest();
220 let name = manifest.name.to_string();
221 if loaded.iter().any(|f| f.manifest.name == manifest.name) {
222 return Err(format!("library exports two functions named `{name}`"));
223 }
224
225 let config_path = path.with_file_name(format!("{name}.toml"));
228 let config_json = std::fs::read_to_string(&config_path)
231 .ok()
232 .and_then(|t| toml::from_str::<toml::Value>(&t).ok())
233 .map(|mut v| {
234 apiplant_core::expand_document(&mut v, &format!("{name}.toml"));
235 v
236 })
237 .and_then(|v| serde_json::to_string(&v).ok())
238 .unwrap_or_else(|| "{}".to_string());
239
240 loaded.push(LoadedFunction {
241 manifest,
242 config_json,
243 body: Body::Dynamic(func),
244 });
245 }
246 Ok(loaded)
247 }
248
249 fn open(path: &Path) -> Result<FunctionMod_Ref, String> {
258 let library = RawLibrary::load_at(path).map_err(|e| e.to_string())?;
259
260 let library: &'static RawLibrary = Box::leak(Box::new(library));
263
264 let header = unsafe { lib_header_from_raw_library(library).map_err(|e| e.to_string())? };
267 header
268 .init_root_module::<FunctionMod_Ref>()
269 .map_err(|e| e.to_string())
270 }
271
272 pub fn register(&mut self, func: BoxedFunction, config_json: String) {
275 let loaded = LoadedFunction::new(func, config_json);
276 self.functions
277 .insert(loaded.manifest.name.to_string(), loaded);
278 }
279
280 pub fn get(&self, name: &str) -> Option<&LoadedFunction> {
281 self.functions.get(name)
282 }
283
284 pub fn iter(&self) -> impl Iterator<Item = &LoadedFunction> {
285 self.functions.values()
286 }
287}
288
289pub struct HostBridge {
296 db: Db,
297 handle: tokio::runtime::Handle,
298 mailer: Option<Mailer>,
301 cache: Option<Cache>,
303 config_json: String,
304 principal_id: String,
305 hook_json: String,
307}
308
309impl HostBridge {
310 pub fn new(
311 db: Db,
312 handle: tokio::runtime::Handle,
313 config_json: String,
314 principal_id: String,
315 ) -> Self {
316 HostBridge {
317 db,
318 handle,
319 mailer: None,
320 cache: None,
321 config_json,
322 principal_id,
323 hook_json: String::new(),
324 }
325 }
326
327 pub fn with_services(mut self, mailer: Option<Mailer>, cache: Option<Cache>) -> Self {
333 self.mailer = mailer;
334 self.cache = cache;
335 self
336 }
337
338 pub fn with_hook(mut self, hook_json: String) -> Self {
340 self.hook_json = hook_json;
341 self
342 }
343}
344
345impl HostApi for HostBridge {
346 fn query(&self, request: RStr<'_>) -> RResult<RString, RString> {
347 #[derive(serde::Deserialize)]
348 struct Req {
349 sql: String,
350 #[serde(default)]
351 params: Vec<serde_json::Value>,
352 }
353 let req: Req = match serde_json::from_str(request.as_str()) {
354 Ok(r) => r,
355 Err(e) => return RResult::RErr(format!("invalid query request: {e}").into()),
356 };
357 let result = self
358 .handle
359 .block_on(async { self.db.raw_json(&req.sql, &req.params).await });
360 match result {
361 Ok(v) => RResult::ROk(v.to_string().into()),
362 Err(e) => RResult::RErr(e.to_string().into()),
363 }
364 }
365
366 fn send_email(&self, request: RStr<'_>) -> RResult<RString, RString> {
367 let Some(mailer) = &self.mailer else {
368 return RResult::RErr(
369 "no email provider configured — set [email] provider in main.toml"
370 .to_string()
371 .into(),
372 );
373 };
374 let message: apiplant_email::Message = match serde_json::from_str(request.as_str()) {
375 Ok(m) => m,
376 Err(e) => return RResult::RErr(format!("invalid email: {e}").into()),
377 };
378 match self.handle.block_on(mailer.send(&message)) {
379 Ok(sent) => RResult::ROk(
380 serde_json::to_string(&sent)
381 .unwrap_or_else(|_| "{}".to_string())
382 .into(),
383 ),
384 Err(e) => RResult::RErr(e.to_string().into()),
385 }
386 }
387
388 fn cache(&self, request: RStr<'_>) -> RResult<RString, RString> {
389 let Some(cache) = &self.cache else {
390 return RResult::RErr(
391 "no cache configured — set [cache] url in main.toml"
392 .to_string()
393 .into(),
394 );
395 };
396 match self.handle.block_on(cache.execute(request.as_str())) {
397 Ok(value) => RResult::ROk(value.to_string().into()),
398 Err(e) => RResult::RErr(e.to_string().into()),
399 }
400 }
401
402 fn log(&self, level: LogLevel, message: RStr<'_>) {
403 let msg = message.as_str();
404 match level {
405 LogLevel::Trace => tracing::trace!(target: "apiplant::function", "{msg}"),
406 LogLevel::Debug => tracing::debug!(target: "apiplant::function", "{msg}"),
407 LogLevel::Info => tracing::info!(target: "apiplant::function", "{msg}"),
408 LogLevel::Warn => tracing::warn!(target: "apiplant::function", "{msg}"),
409 LogLevel::Error => tracing::error!(target: "apiplant::function", "{msg}"),
410 }
411 }
412
413 fn config(&self) -> RString {
414 self.config_json.clone().into()
415 }
416
417 fn principal_id(&self) -> RString {
418 self.principal_id.clone().into()
419 }
420
421 fn hook(&self) -> RString {
422 self.hook_json.clone().into()
423 }
424}