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_ai::Ai;
23use apiplant_cache::Cache;
24use apiplant_db::Db;
25use apiplant_email::Mailer;
26use apiplant_payments::Payments;
27
28pub type BuiltinHandler = fn(&HostBridge, &str) -> Result<String, String>;
32
33enum Body {
36 Dynamic(BoxedFunction),
38 Builtin(BuiltinHandler),
40}
41
42pub struct LoadedFunction {
44 pub manifest: FunctionManifest,
45 pub config_json: String,
48 body: Body,
49}
50
51impl LoadedFunction {
52 pub fn new(func: BoxedFunction, config_json: String) -> Self {
55 LoadedFunction {
56 manifest: func.manifest(),
57 config_json,
58 body: Body::Dynamic(func),
59 }
60 }
61
62 pub fn builtin(
65 manifest: FunctionManifest,
66 handler: BuiltinHandler,
67 config_json: String,
68 ) -> Self {
69 LoadedFunction {
70 manifest,
71 config_json,
72 body: Body::Builtin(handler),
73 }
74 }
75
76 pub fn invoke(&self, bridge: HostBridge, input: &str) -> Result<String, String> {
79 match &self.body {
80 Body::Builtin(handler) => handler(&bridge, input),
81 Body::Dynamic(func) => {
82 let host = HostApi_TO::from_value(bridge, TD_Opaque);
83 match func.invoke(host, RStr::from_str(input)) {
84 RResult::ROk(s) => Ok(s.into_string()),
85 RResult::RErr(e) => Err(e.into_string()),
86 }
87 }
88 }
89 }
90}
91
92#[derive(Default)]
94pub struct FunctionRegistry {
95 functions: BTreeMap<String, LoadedFunction>,
96}
97
98impl FunctionRegistry {
99 pub fn load(app: &apiplant_core::App) -> Self {
108 let mut registry = FunctionRegistry::default();
109 crate::builtins::register_all(&mut registry, app);
110 for (name, f) in Self::load_dir(&app.functions_dir).functions {
111 if registry.functions.contains_key(&name) {
112 tracing::warn!(function = %name, "app function replaces the built-in of the same name");
113 }
114 registry.functions.insert(name, f);
115 }
116 registry
117 }
118
119 pub fn register_builtin(
121 &mut self,
122 manifest: FunctionManifest,
123 handler: BuiltinHandler,
124 config_json: String,
125 ) {
126 let loaded = LoadedFunction::builtin(manifest, handler, config_json);
127 self.functions
128 .insert(loaded.manifest.name.to_string(), loaded);
129 }
130
131 pub fn load_dir(dir: &Path) -> Self {
134 let mut registry = FunctionRegistry::default();
135 let entries = match std::fs::read_dir(dir) {
136 Ok(e) => e,
137 Err(_) => {
138 tracing::info!(dir = %dir.display(), "no functions/ directory");
139 return registry;
140 }
141 };
142 for entry in entries.flatten() {
143 let path = entry.path();
144 let loadable = matches!(
148 path.extension().and_then(|e| e.to_str()),
149 Some("so") | Some("dylib") | Some("dll") | Some(apiplant_js::EXTENSION)
150 );
151 if !loadable {
152 continue;
153 }
154 match Self::load_library(&path) {
155 Ok(loaded) => {
156 for f in loaded {
157 tracing::info!(
158 function = %f.manifest.name,
159 version = %f.manifest.version,
160 library = %path.display(),
161 "loaded function"
162 );
163 registry.functions.insert(f.manifest.name.to_string(), f);
164 }
165 }
166 Err(e) => {
167 tracing::error!(path = %path.display(), error = %e, "failed to load function")
168 }
169 }
170 }
171 registry
172 }
173
174 fn load_library(path: &Path) -> Result<Vec<LoadedFunction>, String> {
183 let exported = if path.extension().and_then(|e| e.to_str()) == Some(apiplant_js::EXTENSION)
187 {
188 apiplant_js::load(path)?.into()
189 } else {
190 Self::load_native(path)?
191 };
192 Self::wrap(path, exported)
193 }
194
195 fn load_native(path: &Path) -> Result<abi_stable::std_types::RVec<BoxedFunction>, String> {
197 let exported = match Self::open(path) {
198 Ok(module) => module.new_functions()(),
199 Err(rust_abi_error) => match crate::cabi::load(path)? {
200 Some(functions) => functions.into(),
201 None => return Err(rust_abi_error),
204 },
205 };
206 Ok(exported)
207 }
208
209 fn wrap(
212 path: &Path,
213 exported: abi_stable::std_types::RVec<BoxedFunction>,
214 ) -> Result<Vec<LoadedFunction>, String> {
215 if exported.is_empty() {
216 return Err("library exports no functions".to_string());
217 }
218
219 let mut loaded: Vec<LoadedFunction> = Vec::with_capacity(exported.len());
220 for func in exported {
221 let manifest = func.manifest();
222 let name = manifest.name.to_string();
223 if loaded.iter().any(|f| f.manifest.name == manifest.name) {
224 return Err(format!("library exports two functions named `{name}`"));
225 }
226
227 let config_path = path.with_file_name(format!("{name}.toml"));
230 let config_json = std::fs::read_to_string(&config_path)
233 .ok()
234 .and_then(|t| toml::from_str::<toml::Value>(&t).ok())
235 .map(|mut v| {
236 apiplant_core::expand_document(&mut v, &format!("{name}.toml"));
237 v
238 })
239 .and_then(|v| serde_json::to_string(&v).ok())
240 .unwrap_or_else(|| "{}".to_string());
241
242 loaded.push(LoadedFunction {
243 manifest,
244 config_json,
245 body: Body::Dynamic(func),
246 });
247 }
248 Ok(loaded)
249 }
250
251 fn open(path: &Path) -> Result<FunctionMod_Ref, String> {
260 let library = RawLibrary::load_at(path).map_err(|e| e.to_string())?;
261
262 let library: &'static RawLibrary = Box::leak(Box::new(library));
265
266 let header = unsafe { lib_header_from_raw_library(library).map_err(|e| e.to_string())? };
269 header
270 .init_root_module::<FunctionMod_Ref>()
271 .map_err(|e| e.to_string())
272 }
273
274 pub fn register(&mut self, func: BoxedFunction, config_json: String) {
277 let loaded = LoadedFunction::new(func, config_json);
278 self.functions
279 .insert(loaded.manifest.name.to_string(), loaded);
280 }
281
282 pub fn get(&self, name: &str) -> Option<&LoadedFunction> {
283 self.functions.get(name)
284 }
285
286 pub fn iter(&self) -> impl Iterator<Item = &LoadedFunction> {
287 self.functions.values()
288 }
289}
290
291pub struct HostBridge {
298 db: Db,
299 handle: tokio::runtime::Handle,
300 mailer: Option<Mailer>,
303 cache: Option<Cache>,
305 payments: Option<Payments>,
307 ai: Option<Ai>,
309 chunks: Option<tokio::sync::mpsc::UnboundedSender<String>>,
314 config_json: String,
315 principal_id: String,
316 hook_json: String,
318}
319
320impl HostBridge {
321 pub fn new(
322 db: Db,
323 handle: tokio::runtime::Handle,
324 config_json: String,
325 principal_id: String,
326 ) -> Self {
327 HostBridge {
328 db,
329 handle,
330 mailer: None,
331 cache: None,
332 payments: None,
333 ai: None,
334 chunks: None,
335 config_json,
336 principal_id,
337 hook_json: String::new(),
338 }
339 }
340
341 pub fn with_services(
348 mut self,
349 mailer: Option<Mailer>,
350 cache: Option<Cache>,
351 payments: Option<Payments>,
352 ai: Option<Ai>,
353 ) -> Self {
354 self.mailer = mailer;
355 self.cache = cache;
356 self.payments = payments;
357 self.ai = ai;
358 self
359 }
360
361 pub fn streaming(mut self, chunks: tokio::sync::mpsc::UnboundedSender<String>) -> Self {
364 self.chunks = Some(chunks);
365 self
366 }
367
368 async fn relay(
378 &self,
379 ai: &apiplant_ai::Ai,
380 request: &apiplant_ai::ChatRequest,
381 ) -> Result<apiplant_ai::ChatReply, apiplant_ai::AiError> {
382 use futures_util::StreamExt;
383
384 let mut stream = Box::pin(ai.stream(request).await?);
385 let mut text = String::new();
386 let mut done = apiplant_ai::Done::default();
387 while let Some(event) = stream.next().await {
388 match event? {
389 apiplant_ai::Event::Delta(delta) => {
390 text.push_str(&delta);
391 if !self.emit(abi_stable::std_types::RStr::from_str(&delta)) {
395 break;
396 }
397 }
398 apiplant_ai::Event::Reasoning(_) => {}
402 apiplant_ai::Event::Done(end) => {
403 done = end;
404 break;
405 }
406 }
407 }
408 Ok(apiplant_ai::ChatReply {
409 text,
410 reasoning: String::new(),
411 provider: ai.provider().as_str().to_string(),
412 model: request
413 .model
414 .clone()
415 .unwrap_or_else(|| ai.model().to_string()),
416 done,
417 tool_calls: Vec::new(),
418 })
419 }
420
421 pub fn with_hook(mut self, hook_json: String) -> Self {
423 self.hook_json = hook_json;
424 self
425 }
426}
427
428impl HostApi for HostBridge {
429 fn query(&self, request: RStr<'_>) -> RResult<RString, RString> {
430 #[derive(serde::Deserialize)]
431 struct Req {
432 sql: String,
433 #[serde(default)]
434 params: Vec<serde_json::Value>,
435 }
436 let req: Req = match serde_json::from_str(request.as_str()) {
437 Ok(r) => r,
438 Err(e) => return RResult::RErr(format!("invalid query request: {e}").into()),
439 };
440 let result = self
441 .handle
442 .block_on(async { self.db.raw_json(&req.sql, &req.params).await });
443 match result {
444 Ok(v) => RResult::ROk(v.to_string().into()),
445 Err(e) => RResult::RErr(e.to_string().into()),
446 }
447 }
448
449 fn send_email(&self, request: RStr<'_>) -> RResult<RString, RString> {
450 let Some(mailer) = &self.mailer else {
451 return RResult::RErr(
452 "no email provider configured — set [email] provider in main.toml"
453 .to_string()
454 .into(),
455 );
456 };
457 let message: apiplant_email::Message = match serde_json::from_str(request.as_str()) {
458 Ok(m) => m,
459 Err(e) => return RResult::RErr(format!("invalid email: {e}").into()),
460 };
461 match self.handle.block_on(mailer.send(&message)) {
462 Ok(sent) => RResult::ROk(
463 serde_json::to_string(&sent)
464 .unwrap_or_else(|_| "{}".to_string())
465 .into(),
466 ),
467 Err(e) => RResult::RErr(e.to_string().into()),
468 }
469 }
470
471 fn cache(&self, request: RStr<'_>) -> RResult<RString, RString> {
472 let Some(cache) = &self.cache else {
473 return RResult::RErr(
474 "no cache configured — set [cache] url in main.toml"
475 .to_string()
476 .into(),
477 );
478 };
479 match self.handle.block_on(cache.execute(request.as_str())) {
480 Ok(value) => RResult::ROk(value.to_string().into()),
481 Err(e) => RResult::RErr(e.to_string().into()),
482 }
483 }
484
485 fn payments(&self, request: RStr<'_>) -> RResult<RString, RString> {
486 let Some(payments) = &self.payments else {
487 return RResult::RErr(
488 "no payment provider configured — set [payments] provider in main.toml"
489 .to_string()
490 .into(),
491 );
492 };
493 match self.handle.block_on(payments.execute(request.as_str())) {
494 Ok(value) => RResult::ROk(value.to_string().into()),
495 Err(e) => RResult::RErr(e.to_string().into()),
496 }
497 }
498
499 fn ai(&self, request: RStr<'_>) -> RResult<RString, RString> {
500 let Some(ai) = &self.ai else {
501 return RResult::RErr(
502 "no ai provider configured — set [ai] provider in main.toml"
503 .to_string()
504 .into(),
505 );
506 };
507 let raw: serde_json::Value = match serde_json::from_str(request.as_str()) {
508 Ok(r) => r,
509 Err(e) => return RResult::RErr(format!("invalid chat request: {e}").into()),
510 };
511 let forward = raw.get("stream").and_then(serde_json::Value::as_bool) == Some(true);
515 let request: apiplant_ai::ChatRequest = match serde_json::from_value(raw) {
516 Ok(r) => r,
517 Err(e) => return RResult::RErr(format!("invalid chat request: {e}").into()),
518 };
519
520 let result = match (forward, &self.chunks) {
521 (true, Some(_)) => self.handle.block_on(self.relay(ai, &request)),
522 _ => self.handle.block_on(ai.chat(&request)),
523 };
524 match result {
525 Ok(reply) => RResult::ROk(
526 serde_json::to_string(&reply)
527 .unwrap_or_else(|_| "{}".to_string())
528 .into(),
529 ),
530 Err(e) => RResult::RErr(e.to_string().into()),
531 }
532 }
533
534 fn emit(&self, chunk: RStr<'_>) -> bool {
535 match &self.chunks {
536 Some(chunks) => chunks.send(chunk.as_str().to_string()).is_ok(),
539 None => true,
547 }
548 }
549
550 fn log(&self, level: LogLevel, message: RStr<'_>) {
551 let msg = message.as_str();
552 match level {
553 LogLevel::Trace => tracing::trace!(target: "apiplant::function", "{msg}"),
554 LogLevel::Debug => tracing::debug!(target: "apiplant::function", "{msg}"),
555 LogLevel::Info => tracing::info!(target: "apiplant::function", "{msg}"),
556 LogLevel::Warn => tracing::warn!(target: "apiplant::function", "{msg}"),
557 LogLevel::Error => tracing::error!(target: "apiplant::function", "{msg}"),
558 }
559 }
560
561 fn config(&self) -> RString {
562 self.config_json.clone().into()
563 }
564
565 fn principal_id(&self) -> RString {
566 self.principal_id.clone().into()
567 }
568
569 fn hook(&self) -> RString {
570 self.hook_json.clone().into()
571 }
572}