1use abi_stable::std_types::{RBox, RResult, RStr};
74use apiplant_abi::{HostApi_TO, LogLevel};
75
76pub struct Context<'a, 'h, C> {
83 host: &'a HostApi_TO<'h, RBox<()>>,
84 config: C,
85 principal_id: String,
86 hook: Option<Hook>,
87}
88
89impl<'a, 'h, C> Context<'a, 'h, C> {
90 #[doc(hidden)]
92 pub fn __new(
93 host: &'a HostApi_TO<'h, RBox<()>>,
94 config: C,
95 principal_id: String,
96 hook: Option<Hook>,
97 ) -> Self {
98 Context {
99 host,
100 config,
101 principal_id,
102 hook,
103 }
104 }
105
106 pub fn hook(&self) -> Option<&Hook> {
126 self.hook.as_ref()
127 }
128
129 pub fn config(&self) -> &C {
131 &self.config
132 }
133
134 pub fn principal_id(&self) -> &str {
137 &self.principal_id
138 }
139
140 pub fn query(
142 &self,
143 sql: &str,
144 params: &[serde_json::Value],
145 ) -> Result<Vec<serde_json::Value>, String> {
146 match self.raw(sql, params)? {
147 serde_json::Value::Array(rows) => Ok(rows),
148 other => Err(format!("expected rows, got {other}")),
149 }
150 }
151
152 pub fn query_one(
154 &self,
155 sql: &str,
156 params: &[serde_json::Value],
157 ) -> Result<Option<serde_json::Value>, String> {
158 Ok(self.query(sql, params)?.into_iter().next())
159 }
160
161 pub fn execute(&self, sql: &str, params: &[serde_json::Value]) -> Result<u64, String> {
163 match self.raw(sql, params)? {
164 serde_json::Value::Object(map) => Ok(map
165 .get("rows_affected")
166 .and_then(|v| v.as_u64())
167 .unwrap_or(0)),
168 serde_json::Value::Array(rows) => Ok(rows.len() as u64),
169 _ => Ok(0),
170 }
171 }
172
173 fn raw(&self, sql: &str, params: &[serde_json::Value]) -> Result<serde_json::Value, String> {
174 let request = serde_json::json!({ "sql": sql, "params": params }).to_string();
175 match self.host.query(RStr::from_str(request.as_str())) {
176 RResult::ROk(s) => serde_json::from_str(s.as_str()).map_err(|e| e.to_string()),
177 RResult::RErr(e) => Err(e.into_string()),
178 }
179 }
180
181 pub fn send_email(&self, email: Email) -> Result<Sent, String> {
205 let request = serde_json::to_string(&email).map_err(|e| e.to_string())?;
206 match self.host.send_email(RStr::from_str(&request)) {
207 RResult::ROk(receipt) => serde_json::from_str(receipt.as_str())
208 .map_err(|e| format!("unreadable email receipt: {e}")),
209 RResult::RErr(e) => Err(e.into_string()),
210 }
211 }
212
213 pub fn cache_get(&self, key: &str) -> Result<Option<serde_json::Value>, String> {
232 let reply = self.cache(serde_json::json!({ "op": "get", "key": key }))?;
233 match reply.get("value") {
234 None | Some(serde_json::Value::Null) => Ok(None),
235 Some(value) => Ok(Some(value.clone())),
236 }
237 }
238
239 pub fn cache_get_as<T: serde::de::DeserializeOwned>(
243 &self,
244 key: &str,
245 ) -> Result<Option<T>, String> {
246 Ok(self
247 .cache_get(key)?
248 .and_then(|value| serde_json::from_value(value).ok()))
249 }
250
251 pub fn cache_set<T: serde::Serialize>(
254 &self,
255 key: &str,
256 value: &T,
257 ttl_secs: Option<u64>,
258 ) -> Result<(), String> {
259 let value = serde_json::to_value(value).map_err(|e| e.to_string())?;
260 self.cache(serde_json::json!({
261 "op": "set", "key": key, "value": value, "ttl": ttl_secs
262 }))?;
263 Ok(())
264 }
265
266 pub fn cache_delete(&self, key: &str) -> Result<bool, String> {
268 let reply = self.cache(serde_json::json!({ "op": "delete", "key": key }))?;
269 Ok(reply
270 .get("deleted")
271 .and_then(|v| v.as_bool())
272 .unwrap_or(false))
273 }
274
275 pub fn cache_incr(&self, key: &str, by: i64, ttl_secs: Option<u64>) -> Result<i64, String> {
283 let reply = self.cache(serde_json::json!({
284 "op": "incr", "key": key, "by": by, "ttl": ttl_secs
285 }))?;
286 Ok(reply.get("value").and_then(|v| v.as_i64()).unwrap_or(0))
287 }
288
289 pub fn cache_ttl(&self, key: &str) -> Result<Option<i64>, String> {
291 let reply = self.cache(serde_json::json!({ "op": "ttl", "key": key }))?;
292 Ok(reply.get("ttl").and_then(|v| v.as_i64()))
293 }
294
295 fn cache(&self, request: serde_json::Value) -> Result<serde_json::Value, String> {
297 match self.host.cache(RStr::from_str(&request.to_string())) {
298 RResult::ROk(reply) => serde_json::from_str(reply.as_str())
299 .map_err(|e| format!("unreadable cache reply: {e}")),
300 RResult::RErr(e) => Err(e.into_string()),
301 }
302 }
303
304 pub fn log(&self, level: LogLevel, message: &str) {
306 self.host.log(level, RStr::from_str(message));
307 }
308
309 pub fn info(&self, message: &str) {
311 self.log(LogLevel::Info, message);
312 }
313
314 pub fn warn(&self, message: &str) {
316 self.log(LogLevel::Warn, message);
317 }
318
319 pub fn error(&self, message: &str) {
321 self.log(LogLevel::Error, message);
322 }
323
324 pub fn debug(&self, message: &str) {
326 self.log(LogLevel::Debug, message);
327 }
328}
329
330#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
342pub struct Email {
343 pub to: Vec<String>,
344 #[serde(skip_serializing_if = "Vec::is_empty", default)]
345 pub cc: Vec<String>,
346 #[serde(skip_serializing_if = "Vec::is_empty", default)]
347 pub bcc: Vec<String>,
348 pub subject: String,
349 #[serde(skip_serializing_if = "String::is_empty", default)]
350 pub text: String,
351 #[serde(skip_serializing_if = "String::is_empty", default)]
352 pub html: String,
353 #[serde(skip_serializing_if = "Option::is_none", default)]
354 pub from: Option<String>,
355 #[serde(skip_serializing_if = "Option::is_none", default)]
356 pub reply_to: Option<String>,
357}
358
359impl Email {
360 pub fn to(recipient: impl Into<String>) -> Email {
362 Email {
363 to: vec![recipient.into()],
364 ..Email::default()
365 }
366 }
367
368 pub fn to_all<S: Into<String>>(recipients: impl IntoIterator<Item = S>) -> Email {
370 Email {
371 to: recipients.into_iter().map(Into::into).collect(),
372 ..Email::default()
373 }
374 }
375
376 pub fn cc(mut self, address: impl Into<String>) -> Email {
377 self.cc.push(address.into());
378 self
379 }
380
381 pub fn bcc(mut self, address: impl Into<String>) -> Email {
382 self.bcc.push(address.into());
383 self
384 }
385
386 pub fn subject(mut self, subject: impl Into<String>) -> Email {
387 self.subject = subject.into();
388 self
389 }
390
391 pub fn text(mut self, body: impl Into<String>) -> Email {
395 self.text = body.into();
396 self
397 }
398
399 pub fn html(mut self, body: impl Into<String>) -> Email {
400 self.html = body.into();
401 self
402 }
403
404 pub fn from(mut self, address: impl Into<String>) -> Email {
406 self.from = Some(address.into());
407 self
408 }
409
410 pub fn reply_to(mut self, address: impl Into<String>) -> Email {
411 self.reply_to = Some(address.into());
412 self
413 }
414}
415
416#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
419#[serde(default)]
420pub struct Sent {
421 pub provider: String,
423 pub id: String,
425 pub recipients: usize,
427}
428
429#[derive(Debug, Clone, Default, serde::Deserialize)]
435#[serde(default)]
436pub struct Hook {
437 pub event: String,
439 pub action: String,
441 pub phase: String,
443 pub resource: String,
445 pub url: String,
447 pub method: String,
449 pub query: std::collections::BTreeMap<String, String>,
451 pub authenticated: bool,
453 pub principal_id: Option<String>,
455 pub organization_id: Option<String>,
457 pub role: Option<String>,
459 #[serde(default)]
465 pub roles: Vec<String>,
466 pub record_id: Option<String>,
468 pub data: Option<serde_json::Value>,
470 pub row: Option<serde_json::Value>,
472 pub rows: Option<Vec<serde_json::Value>>,
474}
475
476impl Hook {
477 pub fn parse(json: &str) -> Option<Hook> {
480 if json.trim().is_empty() {
481 return None;
482 }
483 serde_json::from_str(json).ok()
484 }
485
486 pub fn is_before(&self) -> bool {
489 self.phase == "before"
490 }
491
492 pub fn is_after(&self) -> bool {
494 self.phase == "after"
495 }
496
497 pub fn data(&self) -> &serde_json::Value {
499 self.data.as_ref().unwrap_or(&serde_json::Value::Null)
500 }
501
502 pub fn row(&self) -> &serde_json::Value {
504 self.row.as_ref().unwrap_or(&serde_json::Value::Null)
505 }
506
507 pub fn rows(&self) -> &[serde_json::Value] {
509 self.rows.as_deref().unwrap_or(&[])
510 }
511
512 pub fn field(&self, name: &str) -> Option<&serde_json::Value> {
515 let subject = if self.data.is_some() {
516 self.data()
517 } else {
518 self.row()
519 };
520 subject.get(name)
521 }
522}
523
524pub mod reply {
542 use serde_json::{json, Value};
543
544 pub fn proceed() -> Value {
546 json!({})
547 }
548
549 pub fn replace(data: Value) -> Value {
552 json!({ "data": data })
553 }
554
555 pub fn abort(status: u16, message: impl Into<String>) -> Value {
558 json!({ "error": { "status": status, "message": message.into() } })
559 }
560}
561
562#[doc(hidden)]
575pub fn invoke_handler<C, I, O, E, F>(
576 host: &HostApi_TO<'_, RBox<()>>,
577 input: RStr<'_>,
578 handler: F,
579) -> RResult<abi_stable::std_types::RString, abi_stable::std_types::RString>
580where
581 C: serde::de::DeserializeOwned + Default,
582 I: serde::de::DeserializeOwned,
583 O: serde::Serialize,
584 E: core::fmt::Display,
585 F: FnOnce(&Context<'_, '_, C>, I) -> Result<O, E>,
586{
587 use abi_stable::std_types::RString;
588 use std::panic::{catch_unwind, AssertUnwindSafe};
589
590 let outcome = catch_unwind(AssertUnwindSafe(|| {
596 run_handler::<C, I, O, E, F>(host, input, handler)
597 }));
598
599 match outcome {
600 Ok(result) => result,
601 Err(payload) => RResult::RErr(RString::from(format!(
608 "{}{}",
609 apiplant_abi::INTERNAL_ERROR_PREFIX,
610 panic_message(&*payload)
611 ))),
612 }
613}
614
615fn run_handler<C, I, O, E, F>(
618 host: &HostApi_TO<'_, RBox<()>>,
619 input: RStr<'_>,
620 handler: F,
621) -> RResult<abi_stable::std_types::RString, abi_stable::std_types::RString>
622where
623 C: serde::de::DeserializeOwned + Default,
624 I: serde::de::DeserializeOwned,
625 O: serde::Serialize,
626 E: core::fmt::Display,
627 F: FnOnce(&Context<'_, '_, C>, I) -> Result<O, E>,
628{
629 use abi_stable::std_types::RString;
630
631 let config: C = serde_json::from_str(host.config().as_str()).unwrap_or_default();
632 let principal_id = host.principal_id().into_string();
633 let hook = Hook::parse(host.hook().as_str());
634
635 let input: I = match serde_json::from_str(input.as_str()) {
636 Ok(v) => v,
637 Err(e) => return RResult::RErr(RString::from(format!("invalid input: {e}"))),
638 };
639
640 let ctx = Context::__new(host, config, principal_id, hook);
641 match handler(&ctx, input) {
642 Ok(output) => match serde_json::to_string(&output) {
643 Ok(s) => RResult::ROk(RString::from(s)),
644 Err(e) => RResult::RErr(RString::from(format!("failed to serialize output: {e}"))),
645 },
646 Err(e) => RResult::RErr(RString::from(e.to_string())),
647 }
648}
649
650fn panic_message(payload: &(dyn core::any::Any + Send)) -> &str {
654 if let Some(s) = payload.downcast_ref::<&'static str>() {
655 s
656 } else if let Some(s) = payload.downcast_ref::<String>() {
657 s.as_str()
658 } else {
659 "panicked"
660 }
661}
662
663type Signature<C, I, O, E> = fn(C, I) -> Result<O, E>;
672
673#[doc(hidden)]
674pub struct Exported<C, I, O, E, F> {
675 manifest: apiplant_abi::FunctionManifest,
676 handler: F,
677 _signature: core::marker::PhantomData<Signature<C, I, O, E>>,
678}
679
680impl<C, I, O, E, F> Exported<C, I, O, E, F> {
681 pub fn new(manifest: apiplant_abi::FunctionManifest, handler: F) -> Self {
682 Exported {
683 manifest,
684 handler,
685 _signature: core::marker::PhantomData,
686 }
687 }
688}
689
690impl<C, I, O, E, F> apiplant_abi::Function for Exported<C, I, O, E, F>
691where
692 C: serde::de::DeserializeOwned + Default,
693 I: serde::de::DeserializeOwned,
694 O: serde::Serialize,
695 E: core::fmt::Display,
696 F: Fn(&Context<'_, '_, C>, I) -> Result<O, E> + Send + Sync,
697{
698 fn manifest(&self) -> apiplant_abi::FunctionManifest {
699 self.manifest.clone()
700 }
701
702 fn invoke(
703 &self,
704 host: HostApi_TO<'_, RBox<()>>,
705 input: RStr<'_>,
706 ) -> RResult<abi_stable::std_types::RString, abi_stable::std_types::RString> {
707 invoke_handler(&host, input, &self.handler)
708 }
709}
710
711#[doc(hidden)]
715#[cfg(feature = "schema")]
716pub fn input_schema_json<C, I, O, E, F>(_handler: &F) -> String
717where
718 F: Fn(&Context<'_, '_, C>, I) -> Result<O, E>,
719 I: schemars::JsonSchema,
720{
721 serde_json::to_string(&schemars::schema_for!(I)).unwrap_or_default()
722}
723
724#[doc(hidden)]
726#[cfg(feature = "schema")]
727pub fn output_schema_json<C, I, O, E, F>(_handler: &F) -> String
728where
729 F: Fn(&Context<'_, '_, C>, I) -> Result<O, E>,
730 O: schemars::JsonSchema,
731{
732 serde_json::to_string(&schemars::schema_for!(O)).unwrap_or_default()
733}
734
735#[doc(hidden)]
736#[cfg(not(feature = "schema"))]
737pub fn input_schema_json<C, I, O, E, F>(_handler: &F) -> String
738where
739 F: Fn(&Context<'_, '_, C>, I) -> Result<O, E>,
740{
741 String::new()
742}
743
744#[doc(hidden)]
745#[cfg(not(feature = "schema"))]
746pub fn output_schema_json<C, I, O, E, F>(_handler: &F) -> String
747where
748 F: Fn(&Context<'_, '_, C>, I) -> Result<O, E>,
749{
750 String::new()
751}
752
753#[doc(hidden)]
760pub fn derive_visibility(permission: &str) -> (apiplant_abi::Visibility, String) {
761 use apiplant_abi::{FunctionAccess, Visibility};
762 match FunctionAccess::parse(permission) {
763 Some(FunctionAccess::Public) => (Visibility::Public, String::new()),
764 Some(FunctionAccess::Authenticated) | Some(FunctionAccess::Member) => {
765 (Visibility::Authenticated, String::new())
766 }
767 Some(FunctionAccess::Role(role)) => (Visibility::RoleGated, role),
768 Some(FunctionAccess::Private) | None => (Visibility::Private, String::new()),
770 }
771}
772
773#[doc(hidden)]
779#[derive(Default)]
780pub struct AdminBuilder {
781 pub visible: Option<bool>,
782 pub roles: Vec<String>,
783 pub label: Option<String>,
784 pub group: Option<String>,
785 pub description: Option<String>,
786 pub confirm: Option<String>,
787 pub run_label: Option<String>,
788 pub order: Option<i64>,
789}
790
791impl AdminBuilder {
792 pub fn finish(self) -> String {
793 let mut object = serde_json::Map::new();
794 let mut put = |key: &str, value: Option<serde_json::Value>| {
795 if let Some(value) = value {
796 object.insert(key.to_string(), value);
797 }
798 };
799 put("visible", self.visible.map(serde_json::Value::from));
800 put("label", self.label.map(serde_json::Value::from));
801 put("group", self.group.map(serde_json::Value::from));
802 put("description", self.description.map(serde_json::Value::from));
803 put("confirm", self.confirm.map(serde_json::Value::from));
804 put("run_label", self.run_label.map(serde_json::Value::from));
805 put("order", self.order.map(serde_json::Value::from));
806 if !self.roles.is_empty() {
807 object.insert("roles".to_string(), serde_json::Value::from(self.roles));
808 }
809 if object.is_empty() {
810 return String::new();
811 }
812 serde_json::to_string(&object).unwrap_or_default()
813 }
814}
815
816pub mod prelude {
818 pub use crate::{reply, Context, Email, Hook, Sent};
819 pub use apiplant_abi::{HttpMethod, LogLevel, Visibility};
820 #[cfg(feature = "schema")]
822 pub use schemars::JsonSchema;
823}
824
825#[doc(hidden)]
827pub mod __rt {
828 pub use crate::{
829 derive_visibility, input_schema_json, invoke_handler, output_schema_json, AdminBuilder,
830 Context, Exported, Hook,
831 };
832 pub use abi_stable::export_root_module;
833 pub use abi_stable::prefix_type::PrefixTypeTrait;
834 pub use abi_stable::sabi_extern_fn;
835 pub use abi_stable::sabi_trait::TD_Opaque;
836 pub use abi_stable::std_types::{RBox, RResult, RStr, RString, RVec};
837 pub use apiplant_abi::{
838 BoxedFunction, Function, FunctionManifest, FunctionMod, FunctionMod_Ref, Function_TO,
839 HostApi_TO, HttpMethod, Visibility,
840 };
841}
842
843#[macro_export]
908macro_rules! function {
909 ( $($definition:tt)* ) => {
910 $crate::functions! { { $($definition)* } }
911 };
912}
913
914#[macro_export]
957macro_rules! functions {
958 (
959 $(
960 {
961 name: $name:expr,
962 $(version: $version:expr,)?
963 description: $description:expr,
964 method: $method:ident,
965 $(visibility: $visibility:ident,)?
966 $(permission: $permission:expr,)?
967 $(role: $role:expr,)?
968 $(admin: {
969 $(visible: $admin_visible:expr,)?
970 $(roles: $admin_roles:expr,)?
971 $(label: $admin_label:expr,)?
972 $(group: $admin_group:expr,)?
973 $(description: $admin_description:expr,)?
974 $(confirm: $admin_confirm:expr,)?
975 $(run_label: $admin_run_label:expr,)?
976 $(order: $admin_order:expr,)?
977 },)?
978 handler: $handler:path
979 $(,)?
980 }
981 ),+
982 $(,)?
983 ) => {
984 #[doc(hidden)]
985 pub mod __apiplant_generated_functions {
986 use super::*;
987
988 #[$crate::__rt::export_root_module]
989 fn __apiplant_root_module() -> $crate::__rt::FunctionMod_Ref {
990 use $crate::__rt::PrefixTypeTrait as _;
991 $crate::__rt::FunctionMod {
992 new_functions: __apiplant_new_functions,
993 }
994 .leak_into_prefix()
995 }
996
997 #[$crate::__rt::sabi_extern_fn]
998 fn __apiplant_new_functions() -> $crate::__rt::RVec<$crate::__rt::BoxedFunction> {
999 let mut exported = $crate::__rt::RVec::new();
1000 $(
1001 exported.push({
1002 #[allow(unused_mut)]
1003 let mut version =
1004 $crate::__rt::RString::from(::core::env!("CARGO_PKG_VERSION"));
1005 $( version = $crate::__rt::RString::from($version); )?
1006
1007 #[allow(unused_mut)]
1008 let mut role = ::std::string::String::new();
1009 $( role = ::std::string::String::from($role); )?
1010
1011 #[allow(unused_mut)]
1015 let mut permission = ::std::string::String::new();
1016 $( permission = ::std::string::String::from($permission); )?
1017
1018 #[allow(unused_mut)]
1019 let mut declared_visibility:
1020 ::core::option::Option<$crate::__rt::Visibility> =
1021 ::core::option::Option::None;
1022 $(
1023 declared_visibility = ::core::option::Option::Some(
1024 $crate::__rt::Visibility::$visibility,
1025 );
1026 )?
1027
1028 let (visibility, role) = match declared_visibility {
1029 ::core::option::Option::Some(visibility) => {
1030 if permission.is_empty() {
1031 permission = match visibility {
1032 $crate::__rt::Visibility::Public =>
1033 "public".to_string(),
1034 $crate::__rt::Visibility::Authenticated =>
1035 "authenticated".to_string(),
1036 $crate::__rt::Visibility::Private =>
1037 "private".to_string(),
1038 $crate::__rt::Visibility::RoleGated =>
1039 ::std::format!("role:{}", role),
1040 };
1041 }
1042 (visibility, role)
1043 }
1044 ::core::option::Option::None => {
1045 let (visibility, derived_role) =
1046 $crate::__rt::derive_visibility(&permission);
1047 let role = if role.is_empty() { derived_role } else { role };
1048 (visibility, role)
1049 }
1050 };
1051
1052 #[allow(unused_mut)]
1053 let mut admin = $crate::__rt::AdminBuilder::default();
1054 $(
1055 $( admin.visible = ::core::option::Option::Some($admin_visible); )?
1056 $(
1057 admin.roles = $admin_roles
1058 .iter()
1059 .map(|role| ::std::string::ToString::to_string(role))
1060 .collect();
1061 )?
1062 $( admin.label =
1063 ::core::option::Option::Some($admin_label.to_string()); )?
1064 $( admin.group =
1065 ::core::option::Option::Some($admin_group.to_string()); )?
1066 $( admin.description =
1067 ::core::option::Option::Some($admin_description.to_string()); )?
1068 $( admin.confirm =
1069 ::core::option::Option::Some($admin_confirm.to_string()); )?
1070 $( admin.run_label =
1071 ::core::option::Option::Some($admin_run_label.to_string()); )?
1072 $( admin.order = ::core::option::Option::Some($admin_order); )?
1073 )?
1074
1075 let manifest = $crate::__rt::FunctionManifest {
1076 name: $crate::__rt::RString::from($name),
1077 version,
1078 description: $crate::__rt::RString::from($description),
1079 visibility,
1080 role: $crate::__rt::RString::from(role),
1081 method: $crate::__rt::HttpMethod::$method,
1082 permission: $crate::__rt::RString::from(permission),
1083 admin: $crate::__rt::RString::from(admin.finish()),
1084 config_schema: $crate::__rt::RString::new(),
1085 input_schema: $crate::__rt::RString::from(
1086 $crate::__rt::input_schema_json(&$handler),
1087 ),
1088 output_schema: $crate::__rt::RString::from(
1089 $crate::__rt::output_schema_json(&$handler),
1090 ),
1091 };
1092 $crate::__rt::Function_TO::from_value(
1093 $crate::__rt::Exported::new(manifest, $handler),
1094 $crate::__rt::TD_Opaque,
1095 )
1096 });
1097 )+
1098 exported
1099 }
1100 }
1101 };
1102}
1103
1104#[cfg(test)]
1105mod tests {
1106 use super::*;
1107 use abi_stable::sabi_trait::TD_Opaque;
1108 use abi_stable::std_types::{RResult, RStr, RString};
1109 use apiplant_abi::{HostApi, HostApi_TO, LogLevel};
1110 use serde::{Deserialize, Serialize};
1111 use std::sync::Mutex;
1112
1113 struct MockHost {
1114 config_json: String,
1115 principal_id: String,
1116 hook_json: String,
1117 query_result: Result<String, String>,
1118 service_result: Result<String, String>,
1120 requests: Mutex<Vec<String>>,
1121 service_requests: Mutex<Vec<String>>,
1123 logs: Mutex<Vec<(LogLevel, String)>>,
1124 }
1125
1126 impl MockHost {
1127 fn success(config_json: &str, principal_id: &str, response: serde_json::Value) -> Self {
1128 Self {
1129 config_json: config_json.into(),
1130 principal_id: principal_id.into(),
1131 hook_json: String::new(),
1132 query_result: Ok(response.to_string()),
1133 service_result: Ok("{}".to_string()),
1134 requests: Mutex::new(Vec::new()),
1135 service_requests: Mutex::new(Vec::new()),
1136 logs: Mutex::new(Vec::new()),
1137 }
1138 }
1139
1140 fn with_hook(mut self, hook: serde_json::Value) -> Self {
1141 self.hook_json = hook.to_string();
1142 self
1143 }
1144
1145 fn replying(mut self, reply: serde_json::Value) -> Self {
1147 self.service_result = Ok(reply.to_string());
1148 self
1149 }
1150
1151 fn failing(mut self, error: &str) -> Self {
1152 self.service_result = Err(error.to_string());
1153 self
1154 }
1155
1156 fn last_service_request(&self) -> serde_json::Value {
1158 let requests = self.service_requests.lock().unwrap();
1159 serde_json::from_str(requests.last().expect("no service request was made")).unwrap()
1160 }
1161
1162 fn service(&self, request: RStr<'_>) -> RResult<RString, RString> {
1165 self.service_requests
1166 .lock()
1167 .unwrap()
1168 .push(request.as_str().to_string());
1169 match &self.service_result {
1170 Ok(reply) => RResult::ROk(RString::from(reply.as_str())),
1171 Err(error) => RResult::RErr(RString::from(error.as_str())),
1172 }
1173 }
1174 }
1175
1176 impl HostApi for MockHost {
1177 fn query(&self, request: RStr<'_>) -> RResult<RString, RString> {
1178 self.requests
1179 .lock()
1180 .unwrap()
1181 .push(request.as_str().to_string());
1182 match &self.query_result {
1183 Ok(json) => RResult::ROk(RString::from(json.as_str())),
1184 Err(err) => RResult::RErr(RString::from(err.as_str())),
1185 }
1186 }
1187
1188 fn log(&self, level: LogLevel, message: RStr<'_>) {
1189 self.logs
1190 .lock()
1191 .unwrap()
1192 .push((level, message.as_str().to_string()));
1193 }
1194
1195 fn send_email(&self, request: RStr<'_>) -> RResult<RString, RString> {
1196 self.service(request)
1197 }
1198
1199 fn cache(&self, request: RStr<'_>) -> RResult<RString, RString> {
1200 self.service(request)
1201 }
1202
1203 fn config(&self) -> RString {
1204 self.config_json.clone().into()
1205 }
1206
1207 fn principal_id(&self) -> RString {
1208 self.principal_id.clone().into()
1209 }
1210
1211 fn hook(&self) -> RString {
1212 self.hook_json.clone().into()
1213 }
1214 }
1215
1216 struct Shared(std::sync::Arc<MockHost>);
1219
1220 impl HostApi for Shared {
1221 fn query(&self, request: RStr<'_>) -> RResult<RString, RString> {
1222 self.0.query(request)
1223 }
1224
1225 fn log(&self, level: LogLevel, message: RStr<'_>) {
1226 self.0.log(level, message)
1227 }
1228
1229 fn send_email(&self, request: RStr<'_>) -> RResult<RString, RString> {
1230 self.0.send_email(request)
1231 }
1232
1233 fn cache(&self, request: RStr<'_>) -> RResult<RString, RString> {
1234 self.0.cache(request)
1235 }
1236
1237 fn config(&self) -> RString {
1238 self.0.config()
1239 }
1240
1241 fn principal_id(&self) -> RString {
1242 self.0.principal_id()
1243 }
1244
1245 fn hook(&self) -> RString {
1246 self.0.hook()
1247 }
1248 }
1249
1250 fn shared(mock: MockHost) -> (std::sync::Arc<MockHost>, HostApi_TO<'static, RBox<()>>) {
1252 let mock = std::sync::Arc::new(mock);
1253 let host = HostApi_TO::from_value(Shared(mock.clone()), TD_Opaque);
1254 (mock, host)
1255 }
1256
1257 #[derive(Deserialize)]
1258 struct Config {
1259 greeting: String,
1260 }
1261
1262 impl Default for Config {
1263 fn default() -> Self {
1264 Self {
1265 greeting: "Hello".into(),
1266 }
1267 }
1268 }
1269
1270 #[derive(Deserialize)]
1271 struct Input {
1272 name: String,
1273 }
1274
1275 #[derive(Serialize, serde::Deserialize, schemars::JsonSchema)]
1276 struct Output {
1277 message: String,
1278 }
1279
1280 #[test]
1281 fn context_bridges_queries_execution_and_principal_id() {
1282 let host = MockHost::success("{}", "user-123", serde_json::json!([{ "n": 1 }]));
1283 let host = HostApi_TO::from_value(host, TD_Opaque);
1284 let ctx = Context::__new(&host, (), "user-123".into(), None);
1285
1286 let rows = ctx
1287 .query("SELECT count(*) AS n", &[serde_json::json!(true)])
1288 .unwrap();
1289 assert_eq!(rows.len(), 1);
1290 assert_eq!(ctx.principal_id(), "user-123");
1291
1292 let request = &host.config().into_string();
1293 assert_eq!(request, "{}");
1294 }
1295
1296 #[test]
1297 fn context_execute_and_logging_use_host_bridge() {
1298 let host = MockHost::success("{}", "user-123", serde_json::json!({ "rows_affected": 3 }));
1299 let host = HostApi_TO::from_value(host, TD_Opaque);
1300 let ctx = Context::__new(&host, (), "user-123".into(), None);
1301
1302 assert_eq!(ctx.execute("DELETE FROM apiplant_post", &[]).unwrap(), 3);
1303 ctx.warn("careful");
1304 }
1305
1306 #[test]
1307 fn send_email_hands_the_host_the_message_and_reads_the_receipt() {
1308 let host = MockHost::success("{}", "u1", serde_json::json!([]))
1309 .replying(serde_json::json!({ "provider": "ses", "id": "abc", "recipients": 2 }));
1310 let host = HostApi_TO::from_value(host, TD_Opaque);
1311 let ctx = Context::__new(&host, (), "u1".into(), None);
1312
1313 let sent = ctx
1314 .send_email(
1315 Email::to("Ann <ann@example.com>")
1316 .cc("bo@example.com")
1317 .subject("Welcome")
1318 .text("Hello")
1319 .reply_to("help@example.com"),
1320 )
1321 .unwrap();
1322
1323 assert_eq!(sent.provider, "ses");
1324 assert_eq!(sent.id, "abc");
1325 assert_eq!(sent.recipients, 2);
1326 }
1327
1328 #[test]
1331 fn an_email_only_carries_the_fields_it_was_given() {
1332 let (mock, host) = shared(
1333 MockHost::success("{}", "u1", serde_json::json!([]))
1334 .replying(serde_json::json!({ "provider": "smtp", "id": "", "recipients": 1 })),
1335 );
1336 let ctx = Context::__new(&host, (), "u1".into(), None);
1337
1338 ctx.send_email(Email::to("ann@example.com").subject("Hi").text("Hello"))
1339 .unwrap();
1340
1341 let request = mock.last_service_request();
1342 assert_eq!(request["to"][0], "ann@example.com");
1343 assert_eq!(request["subject"], "Hi");
1344 assert_eq!(request["text"], "Hello");
1345 assert!(request.get("html").is_none());
1346 assert!(request.get("cc").is_none());
1347 assert!(request.get("from").is_none());
1348 }
1349
1350 #[test]
1351 fn a_provider_failure_surfaces_as_an_error() {
1352 let host = MockHost::success("{}", "u1", serde_json::json!([]))
1353 .failing("sendgrid rejected the message (401): unauthorized");
1354 let host = HostApi_TO::from_value(host, TD_Opaque);
1355 let ctx = Context::__new(&host, (), "u1".into(), None);
1356
1357 let err = ctx
1358 .send_email(Email::to("ann@example.com").subject("Hi").text("Hello"))
1359 .unwrap_err();
1360 assert!(err.contains("401"), "{err}");
1361 }
1362
1363 #[test]
1364 fn cache_get_distinguishes_a_hit_from_a_miss() {
1365 let hit = MockHost::success("{}", "u1", serde_json::json!([]))
1366 .replying(serde_json::json!({ "hit": true, "value": { "eur": 1.1 } }));
1367 let hit = HostApi_TO::from_value(hit, TD_Opaque);
1368 let ctx = Context::__new(&hit, (), "u1".into(), None);
1369 assert_eq!(
1370 ctx.cache_get("rates").unwrap(),
1371 Some(serde_json::json!({ "eur": 1.1 }))
1372 );
1373
1374 let miss = MockHost::success("{}", "u1", serde_json::json!([]))
1375 .replying(serde_json::json!({ "hit": false, "value": null }));
1376 let miss = HostApi_TO::from_value(miss, TD_Opaque);
1377 let ctx = Context::__new(&miss, (), "u1".into(), None);
1378 assert_eq!(ctx.cache_get("rates").unwrap(), None);
1379 }
1380
1381 #[test]
1384 fn cache_get_as_treats_an_unreadable_value_as_a_miss() {
1385 #[derive(serde::Deserialize)]
1386 struct Rates {
1387 #[allow(dead_code)]
1388 eur: f64,
1389 }
1390
1391 let host = MockHost::success("{}", "u1", serde_json::json!([]))
1392 .replying(serde_json::json!({ "hit": true, "value": { "old_shape": true } }));
1393 let host = HostApi_TO::from_value(host, TD_Opaque);
1394 let ctx = Context::__new(&host, (), "u1".into(), None);
1395
1396 assert!(ctx.cache_get_as::<Rates>("rates").unwrap().is_none());
1397 }
1398
1399 #[test]
1400 fn cache_writes_name_their_operation_key_and_ttl() {
1401 let (mock, host) = shared(
1402 MockHost::success("{}", "u1", serde_json::json!([])).replying(
1403 serde_json::json!({ "ok": true, "deleted": true, "value": 3, "ttl": 42 }),
1404 ),
1405 );
1406 let ctx = Context::__new(&host, (), "u1".into(), None);
1407
1408 ctx.cache_set("rates", &serde_json::json!({ "eur": 1.1 }), Some(900))
1409 .unwrap();
1410 let request = mock.last_service_request();
1411 assert_eq!(request["op"], "set");
1412 assert_eq!(request["key"], "rates");
1413 assert_eq!(request["value"]["eur"], 1.1);
1414 assert_eq!(request["ttl"], 900);
1415
1416 ctx.cache_set("rates", &1, None).unwrap();
1419 assert!(mock.last_service_request()["ttl"].is_null());
1420
1421 assert_eq!(ctx.cache_incr("hits", 1, Some(60)).unwrap(), 3);
1422 assert_eq!(mock.last_service_request()["op"], "incr");
1423
1424 assert!(ctx.cache_delete("rates").unwrap());
1425 assert_eq!(mock.last_service_request()["op"], "delete");
1426
1427 assert_eq!(ctx.cache_ttl("rates").unwrap(), Some(42));
1428 }
1429
1430 #[test]
1431 fn invoke_handler_uses_default_config_when_host_config_is_invalid() {
1432 let host = MockHost::success("{not-json", "u1", serde_json::json!([]));
1433 let host = HostApi_TO::from_value(host, TD_Opaque);
1434
1435 let result = invoke_handler::<Config, Input, Output, String, _>(
1436 &host,
1437 RStr::from_str(r#"{"name":"Ann"}"#),
1438 |ctx, input| {
1439 Ok(Output {
1440 message: format!("{}, {}!", ctx.config().greeting, input.name),
1441 })
1442 },
1443 );
1444
1445 let json = match result {
1446 RResult::ROk(v) => v.into_string(),
1447 RResult::RErr(e) => panic!("unexpected error: {}", e.into_string()),
1448 };
1449 assert!(json.contains("Hello, Ann!"));
1450 }
1451
1452 #[test]
1453 fn invoke_handler_rejects_invalid_input_json() {
1454 let host = MockHost::success("{}", "u1", serde_json::json!([]));
1455 let host = HostApi_TO::from_value(host, TD_Opaque);
1456
1457 let result = invoke_handler::<Config, Input, Output, String, _>(
1458 &host,
1459 RStr::from_str("{"),
1460 |_ctx, _input| {
1461 Ok(Output {
1462 message: "never".into(),
1463 })
1464 },
1465 );
1466
1467 match result {
1468 RResult::ROk(v) => panic!("unexpected success: {}", v.into_string()),
1469 RResult::RErr(e) => assert!(e.into_string().contains("invalid input")),
1470 }
1471 }
1472
1473 #[test]
1477 fn invoke_handler_turns_a_panicking_handler_into_an_internal_error() {
1478 let host = MockHost::success("{}", "u1", serde_json::json!([]));
1479 let host = HostApi_TO::from_value(host, TD_Opaque);
1480
1481 let result = invoke_handler::<Config, Input, Output, String, _>(
1482 &host,
1483 RStr::from_str(r#"{"name":"Ann"}"#),
1484 |_ctx, _input| panic!("handler exploded"),
1485 );
1486
1487 match result {
1488 RResult::ROk(v) => panic!("unexpected success: {}", v.into_string()),
1489 RResult::RErr(e) => {
1490 let msg = e.into_string();
1491 let detail = msg
1492 .strip_prefix(apiplant_abi::INTERNAL_ERROR_PREFIX)
1493 .expect("a panic must be marked internal so the host answers 500, not 400");
1494 assert_eq!(detail, "handler exploded");
1496 }
1497 }
1498 }
1499
1500 #[test]
1502 fn invoke_handler_catches_panics_from_unwrap_and_indexing() {
1503 for (label, handler) in [
1504 (
1505 "unwrap",
1506 Box::new(
1507 |_: &Context<'_, '_, Config>, input: Input| -> Result<Output, String> {
1508 let missing = input.name.strip_prefix("nonexistent-prefix");
1511 Ok(Output {
1512 message: missing.unwrap().to_string(),
1513 })
1514 },
1515 )
1516 as Box<dyn Fn(&Context<'_, '_, Config>, Input) -> Result<Output, String>>,
1517 ),
1518 (
1519 "index",
1520 Box::new(
1521 |_: &Context<'_, '_, Config>, input: Input| -> Result<Output, String> {
1522 let empty: Vec<u8> = Vec::new();
1525 let _ = empty[input.name.len()];
1526 unreachable!()
1527 },
1528 ),
1529 ),
1530 ] {
1531 let host = MockHost::success("{}", "u1", serde_json::json!([]));
1532 let host = HostApi_TO::from_value(host, TD_Opaque);
1533 let result = invoke_handler::<Config, Input, Output, String, _>(
1534 &host,
1535 RStr::from_str(r#"{"name":"Ann"}"#),
1536 handler,
1537 );
1538 match result {
1539 RResult::ROk(_) => panic!("{label}: expected an error"),
1540 RResult::RErr(e) => {
1541 let msg = e.into_string();
1542 assert!(
1543 msg.starts_with(apiplant_abi::INTERNAL_ERROR_PREFIX),
1544 "{label}: not marked internal: {msg}"
1545 );
1546 assert_ne!(
1550 msg,
1551 format!("{}panicked", apiplant_abi::INTERNAL_ERROR_PREFIX),
1552 "{label}: panic message was lost"
1553 );
1554 }
1555 }
1556 }
1557 }
1558
1559 #[test]
1562 fn a_returned_error_is_not_marked_internal() {
1563 let host = MockHost::success("{}", "u1", serde_json::json!([]));
1564 let host = HostApi_TO::from_value(host, TD_Opaque);
1565
1566 let result = invoke_handler::<Config, Input, Output, String, _>(
1567 &host,
1568 RStr::from_str(r#"{"name":"Ann"}"#),
1569 |_ctx, _input| Err("name is taken".to_string()),
1570 );
1571
1572 match result {
1573 RResult::ROk(_) => panic!("expected an error"),
1574 RResult::RErr(e) => assert_eq!(e.into_string(), "name is taken"),
1575 }
1576 }
1577
1578 #[test]
1581 fn a_panic_does_not_cross_the_abi_boundary() {
1582 let manifest = apiplant_abi::FunctionManifest {
1583 name: "boom".into(),
1584 version: "0.0.0".into(),
1585 description: RString::new(),
1586 visibility: apiplant_abi::Visibility::Public,
1587 role: RString::new(),
1588 method: apiplant_abi::HttpMethod::Post,
1589 permission: RString::new(),
1590 admin: RString::new(),
1591 config_schema: RString::new(),
1592 input_schema: RString::new(),
1593 output_schema: RString::new(),
1594 };
1595 let exported = Exported::<Config, Input, Output, String, _>::new(
1596 manifest,
1597 |_ctx: &Context<'_, '_, Config>, _input: Input| -> Result<Output, String> {
1598 panic!("handler exploded")
1599 },
1600 );
1601
1602 let boxed: apiplant_abi::BoxedFunction =
1605 apiplant_abi::Function_TO::from_value(exported, TD_Opaque);
1606 assert_eq!(boxed.manifest().name.as_str(), "boom");
1607
1608 let host = HostApi_TO::from_value(
1609 MockHost::success("{}", "u1", serde_json::json!([])),
1610 TD_Opaque,
1611 );
1612 match boxed.invoke(host, RStr::from_str(r#"{"name":"Ann"}"#)) {
1613 RResult::ROk(v) => panic!("unexpected success: {}", v.into_string()),
1614 RResult::RErr(e) => assert!(e
1615 .into_string()
1616 .starts_with(apiplant_abi::INTERNAL_ERROR_PREFIX)),
1617 }
1618 }
1619
1620 fn hook_context() -> serde_json::Value {
1621 serde_json::json!({
1622 "event": "after_create",
1623 "action": "create",
1624 "phase": "after",
1625 "resource": "post",
1626 "url": "/api/post?draft=true",
1627 "method": "POST",
1628 "query": { "draft": "true" },
1629 "authenticated": true,
1630 "principal_id": "11111111-1111-1111-1111-111111111111",
1631 "organization_id": "22222222-2222-2222-2222-222222222222",
1632 "role": "admin",
1633 "record_id": null,
1634 "data": null,
1635 "row": { "id": "33333333-3333-3333-3333-333333333333", "title": "Hi" },
1636 "rows": null,
1637 })
1638 }
1639
1640 #[test]
1641 fn context_exposes_hook_data_when_invoked_as_a_hook() {
1642 let host = MockHost::success("{}", "u1", serde_json::json!([])).with_hook(hook_context());
1643 let host = HostApi_TO::from_value(host, TD_Opaque);
1644 let hook = Hook::parse(host.hook().as_str());
1645 let ctx = Context::__new(&host, (), "u1".into(), hook);
1646
1647 let hook = ctx.hook().expect("hook context should be present");
1648 assert_eq!(hook.event, "after_create");
1649 assert_eq!(hook.action, "create");
1650 assert!(hook.is_after());
1651 assert!(!hook.is_before());
1652 assert_eq!(hook.resource, "post");
1653 assert_eq!(hook.url, "/api/post?draft=true");
1654 assert_eq!(hook.method, "POST");
1655 assert_eq!(hook.query.get("draft").map(String::as_str), Some("true"));
1656 assert!(hook.authenticated);
1657 assert_eq!(hook.role.as_deref(), Some("admin"));
1658 assert!(hook.roles.is_empty());
1661 assert!(hook.organization_id.is_some());
1662 assert_eq!(hook.record_id, None);
1663 assert_eq!(hook.row()["title"], "Hi");
1664 assert!(hook.data().is_null());
1665 assert!(hook.rows().is_empty());
1666 assert_eq!(hook.field("title").and_then(|v| v.as_str()), Some("Hi"));
1668 }
1669
1670 #[test]
1671 fn hook_is_absent_for_plain_http_invocations() {
1672 let host = MockHost::success("{}", "u1", serde_json::json!([]));
1673 let host = HostApi_TO::from_value(host, TD_Opaque);
1674 let ctx = Context::__new(&host, (), "u1".into(), Hook::parse(host.hook().as_str()));
1675
1676 assert!(ctx.hook().is_none());
1677 assert!(Hook::parse("").is_none());
1678 assert!(Hook::parse(" ").is_none());
1679 assert!(Hook::parse("{not json").is_none());
1680 }
1681
1682 #[test]
1683 fn hook_reads_submitted_data_on_before_events_and_lists_on_after_list() {
1684 let before = Hook::parse(
1685 &serde_json::json!({
1686 "event": "before_create",
1687 "phase": "before",
1688 "data": { "title": "Draft" },
1689 })
1690 .to_string(),
1691 )
1692 .unwrap();
1693 assert!(before.is_before());
1694 assert_eq!(
1695 before.field("title").and_then(|v| v.as_str()),
1696 Some("Draft")
1697 );
1698 assert!(before.row().is_null());
1699
1700 let listed = Hook::parse(
1701 &serde_json::json!({
1702 "event": "after_list",
1703 "phase": "after",
1704 "rows": [{ "id": "a" }, { "id": "b" }],
1705 })
1706 .to_string(),
1707 )
1708 .unwrap();
1709 assert_eq!(listed.rows().len(), 2);
1710 assert_eq!(listed.rows()[1]["id"], "b");
1711 }
1712
1713 #[test]
1714 fn hook_tolerates_missing_and_unknown_fields() {
1715 let sparse = Hook::parse(r#"{"event":"before_delete","surprise":42}"#).unwrap();
1716 assert_eq!(sparse.event, "before_delete");
1717 assert_eq!(sparse.resource, "");
1718 assert!(!sparse.authenticated);
1719 assert!(sparse.principal_id.is_none());
1720 }
1721
1722 #[test]
1723 fn reply_helpers_build_the_host_protocol() {
1724 assert_eq!(reply::proceed(), serde_json::json!({}));
1725 assert_eq!(
1726 reply::replace(serde_json::json!({ "title": "clean" })),
1727 serde_json::json!({ "data": { "title": "clean" } })
1728 );
1729 assert_eq!(
1730 reply::abort(422, "title is required"),
1731 serde_json::json!({ "error": { "status": 422, "message": "title is required" } })
1732 );
1733 }
1734
1735 #[test]
1736 fn invoke_handler_passes_hook_context_through_to_the_handler() {
1737 let host = MockHost::success("{}", "u1", serde_json::json!([])).with_hook(hook_context());
1738 let host = HostApi_TO::from_value(host, TD_Opaque);
1739
1740 let result = invoke_handler::<(), serde_json::Value, serde_json::Value, String, _>(
1741 &host,
1742 RStr::from_str(r#"{"id":"33333333-3333-3333-3333-333333333333","title":"Hi"}"#),
1743 |ctx, input| {
1744 let hook = ctx.hook().ok_or("expected a hook context")?;
1745 assert_eq!(input["title"], "Hi");
1746 Ok(reply::replace(serde_json::json!({
1747 "event": hook.event,
1748 "title": hook.row()["title"],
1749 })))
1750 },
1751 );
1752
1753 let json = match result {
1754 RResult::ROk(v) => v.into_string(),
1755 RResult::RErr(e) => panic!("unexpected error: {}", e.into_string()),
1756 };
1757 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1758 assert_eq!(value["data"]["event"], "after_create");
1759 assert_eq!(value["data"]["title"], "Hi");
1760 }
1761
1762 #[test]
1763 fn exported_functions_carry_their_own_manifest_and_handler() {
1764 use apiplant_abi::{Function, FunctionManifest, HttpMethod, Visibility};
1765
1766 fn manifest(name: &str) -> FunctionManifest {
1767 FunctionManifest {
1768 name: RString::from(name),
1769 version: RString::from("1.0.0"),
1770 description: RString::from("test"),
1771 visibility: Visibility::Private,
1772 role: RString::new(),
1773 method: HttpMethod::Post,
1774 permission: RString::new(),
1775 admin: RString::new(),
1776 config_schema: RString::new(),
1777 input_schema: RString::new(),
1778 output_schema: RString::new(),
1779 }
1780 }
1781
1782 let before: Exported<(), Input, Output, String, _> = Exported::new(
1785 manifest("post_before_create"),
1786 |_ctx: &Context<'_, '_, ()>, input: Input| {
1787 Ok(Output {
1788 message: format!("before {}", input.name),
1789 })
1790 },
1791 );
1792 let after: Exported<(), Vec<i64>, Output, String, _> = Exported::new(
1793 manifest("post_after_list"),
1794 |_ctx: &Context<'_, '_, ()>, rows: Vec<i64>| {
1795 Ok(Output {
1796 message: format!("after {}", rows.len()),
1797 })
1798 },
1799 );
1800
1801 assert_eq!(before.manifest().name.as_str(), "post_before_create");
1802 assert_eq!(after.manifest().name.as_str(), "post_after_list");
1803 assert_eq!(before.manifest().version.as_str(), "1.0.0");
1804
1805 let new_host = || {
1806 HostApi_TO::from_value(
1807 MockHost::success("{}", "u1", serde_json::json!([])),
1808 TD_Opaque,
1809 )
1810 };
1811
1812 let first = match before.invoke(new_host(), RStr::from_str(r#"{"name":"Ann"}"#)) {
1813 RResult::ROk(v) => v.into_string(),
1814 RResult::RErr(e) => panic!("unexpected error: {}", e.into_string()),
1815 };
1816 assert!(first.contains("before Ann"));
1817
1818 let second = match after.invoke(new_host(), RStr::from_str("[1,2,3]")) {
1819 RResult::ROk(v) => v.into_string(),
1820 RResult::RErr(e) => panic!("unexpected error: {}", e.into_string()),
1821 };
1822 assert!(second.contains("after 3"));
1823 }
1824
1825 #[test]
1826 fn exported_functions_are_abi_trait_objects() {
1827 use apiplant_abi::{FunctionManifest, Function_TO, HttpMethod, Visibility};
1828
1829 let exported: Exported<Config, Input, Output, String, _> = Exported::new(
1830 FunctionManifest {
1831 name: RString::from("greet"),
1832 version: RString::from("0.1.0"),
1833 description: RString::from("test"),
1834 visibility: Visibility::Public,
1835 role: RString::new(),
1836 method: HttpMethod::Post,
1837 permission: RString::new(),
1838 admin: RString::new(),
1839 config_schema: RString::new(),
1840 input_schema: RString::new(),
1841 output_schema: RString::new(),
1842 },
1843 |ctx: &Context<'_, '_, Config>, input: Input| {
1844 Ok(Output {
1845 message: format!("{}, {}!", ctx.config().greeting, input.name),
1846 })
1847 },
1848 );
1849
1850 let boxed = Function_TO::from_value(exported, TD_Opaque);
1852 assert_eq!(boxed.manifest().name.as_str(), "greet");
1853
1854 let host = MockHost::success(r#"{"greeting":"Hi"}"#, "u1", serde_json::json!([]));
1855 let host = HostApi_TO::from_value(host, TD_Opaque);
1856 let reply = match boxed.invoke(host, RStr::from_str(r#"{"name":"Ann"}"#)) {
1857 RResult::ROk(v) => v.into_string(),
1858 RResult::RErr(e) => panic!("unexpected error: {}", e.into_string()),
1859 };
1860 assert!(reply.contains("Hi, Ann!"));
1861 }
1862
1863 #[derive(Deserialize, schemars::JsonSchema)]
1864 struct SchemaInput {
1865 name: String,
1866 }
1867
1868 #[derive(Serialize, schemars::JsonSchema)]
1869 struct SchemaOutput {
1870 ok: bool,
1871 }
1872
1873 #[test]
1874 fn schema_generation_is_typed() {
1875 let handler =
1876 |_ctx: &Context<'_, '_, ()>, input: SchemaInput| -> Result<SchemaOutput, String> {
1877 Ok(SchemaOutput {
1878 ok: !input.name.is_empty(),
1879 })
1880 };
1881
1882 let input_schema = input_schema_json::<(), SchemaInput, SchemaOutput, String, _>(&handler);
1883 let output_schema =
1884 output_schema_json::<(), SchemaInput, SchemaOutput, String, _>(&handler);
1885
1886 assert!(input_schema.contains("\"name\""));
1887 assert!(output_schema.contains("\"ok\""));
1888 }
1889}