#![allow(non_camel_case_types, non_local_definitions)]
pub mod c;
#[cfg(feature = "manifest-json")]
mod manifest_json;
#[cfg(feature = "manifest-json")]
pub use manifest_json::manifest_from_json;
use abi_stable::{
declare_root_module_statics,
library::RootModule,
package_version_strings, sabi_trait,
std_types::{RResult, RStr, RString},
StableAbi,
};
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, StableAbi)]
pub enum Visibility {
Public,
Authenticated,
RoleGated,
Private,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, StableAbi)]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, StableAbi)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
#[repr(C)]
#[derive(Debug, Clone, StableAbi)]
pub struct FunctionManifest {
pub name: RString,
pub version: RString,
pub description: RString,
pub visibility: Visibility,
pub role: RString,
pub method: HttpMethod,
pub permission: RString,
pub admin: RString,
pub config_schema: RString,
pub input_schema: RString,
pub output_schema: RString,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FunctionAccess {
Public,
Authenticated,
Member,
Role(String),
Private,
}
impl FunctionAccess {
pub fn parse(value: &str) -> Option<FunctionAccess> {
match value.trim() {
"public" => Some(FunctionAccess::Public),
"authenticated" => Some(FunctionAccess::Authenticated),
"member" => Some(FunctionAccess::Member),
"private" => Some(FunctionAccess::Private),
other => other
.strip_prefix("role:")
.filter(|role| !role.is_empty())
.map(|role| FunctionAccess::Role(role.to_string())),
}
}
pub fn as_string(&self) -> String {
match self {
FunctionAccess::Public => "public".to_string(),
FunctionAccess::Authenticated => "authenticated".to_string(),
FunctionAccess::Member => "member".to_string(),
FunctionAccess::Role(role) => format!("role:{role}"),
FunctionAccess::Private => "private".to_string(),
}
}
pub fn is_public(&self) -> bool {
matches!(self, FunctionAccess::Public)
}
}
impl FunctionManifest {
pub fn access(&self) -> FunctionAccess {
if !self.permission.is_empty() {
return FunctionAccess::parse(self.permission.as_str())
.unwrap_or(FunctionAccess::Private);
}
match self.visibility {
Visibility::Public => FunctionAccess::Public,
Visibility::Authenticated => FunctionAccess::Authenticated,
Visibility::Private => FunctionAccess::Private,
Visibility::RoleGated => FunctionAccess::Role(self.role.to_string()),
}
}
}
#[sabi_trait]
pub trait HostApi: Send + Sync {
fn query(&self, request: RStr<'_>) -> RResult<RString, RString>;
fn log(&self, level: LogLevel, message: RStr<'_>);
fn send_email(&self, request: RStr<'_>) -> RResult<RString, RString>;
fn cache(&self, request: RStr<'_>) -> RResult<RString, RString>;
fn config(&self) -> RString;
fn principal_id(&self) -> RString;
fn hook(&self) -> RString;
}
pub const INTERNAL_ERROR_PREFIX: &str = "\x01apiplant-internal:";
#[sabi_trait]
pub trait Function: Send + Sync {
fn manifest(&self) -> FunctionManifest;
fn invoke(
&self,
host: HostApi_TO<'_, abi_stable::std_types::RBox<()>>,
input: RStr<'_>,
) -> RResult<RString, RString>;
}
#[repr(C)]
#[derive(StableAbi)]
#[sabi(kind(Prefix(prefix_ref = FunctionMod_Ref)))]
#[sabi(missing_field(panic))]
pub struct FunctionMod {
#[sabi(last_prefix_field)]
pub new_functions: extern "C" fn() -> abi_stable::std_types::RVec<BoxedFunction>,
}
impl RootModule for FunctionMod_Ref {
declare_root_module_statics! {FunctionMod_Ref}
const BASE_NAME: &'static str = "apiplant_function";
const NAME: &'static str = "apiplant_function";
const VERSION_STRINGS: abi_stable::sabi_types::VersionStrings = package_version_strings!();
}
pub type BoxedFunction = Function_TO<'static, abi_stable::std_types::RBox<()>>;
#[cfg(test)]
mod tests {
use super::*;
fn manifest(visibility: Visibility, role: &str, permission: &str) -> FunctionManifest {
FunctionManifest {
name: "act".into(),
version: "0.0.0".into(),
description: RString::new(),
visibility,
role: role.into(),
method: HttpMethod::Post,
permission: permission.into(),
admin: RString::new(),
config_schema: RString::new(),
input_schema: RString::new(),
output_schema: RString::new(),
}
}
#[test]
fn access_round_trips_through_its_string_form() {
for value in [
FunctionAccess::Public,
FunctionAccess::Authenticated,
FunctionAccess::Member,
FunctionAccess::Role("buyer".into()),
FunctionAccess::Private,
] {
assert_eq!(FunctionAccess::parse(&value.as_string()), Some(value));
}
assert_eq!(
FunctionAccess::parse(" member "),
Some(FunctionAccess::Member)
);
assert_eq!(FunctionAccess::parse("role:"), None);
assert_eq!(FunctionAccess::parse("owner"), None);
assert_eq!(FunctionAccess::parse("wat"), None);
}
#[test]
fn permission_wins_over_visibility_and_a_typo_closes_the_door() {
assert_eq!(
manifest(Visibility::Public, "", "member").access(),
FunctionAccess::Member
);
assert_eq!(
manifest(Visibility::Private, "", "role:admin").access(),
FunctionAccess::Role("admin".into())
);
assert_eq!(
manifest(Visibility::Public, "", "membre").access(),
FunctionAccess::Private
);
}
#[test]
fn a_manifest_without_permission_keeps_the_access_visibility_gave_it() {
assert_eq!(
manifest(Visibility::Public, "", "").access(),
FunctionAccess::Public
);
assert_eq!(
manifest(Visibility::Authenticated, "", "").access(),
FunctionAccess::Authenticated
);
assert_eq!(
manifest(Visibility::RoleGated, "buyer", "").access(),
FunctionAccess::Role("buyer".into())
);
assert_eq!(
manifest(Visibility::Private, "", "").access(),
FunctionAccess::Private
);
}
}