Lines
88.06 %
Functions
57.89 %
Branches
100 %
//! Config-domain natives. Wraps `server::command::{GetConfig, GetVersion,
//! GetBuildDate, SetConfig, SelectColumn}`.
//!
//! `get-version` and `get-build-date` are the first server-command bindings
//! the rpc layer ships. Both return a `CmdResult::String(...)` from a body
//! that just reads `env!`-baked constants — no DB, no user_id, no real async
//! work — so they exercise the marshalling shape without yet needing a
//! tokio runtime or pool. The remaining three commands wait until DB-touching
//! infrastructure (ScriptCtx::pool, sqlx::test fixtures) lands.
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use scripting::runtime::{alloc_string_ref, read_string_arg};
use server::command::{
CmdError, CmdResult,
config::{GetBuildDate, GetConfig, GetVersion, SetConfig},
};
use wasmtime::{ArrayRef, Caller, Linker, Rooted};
use crate::session::SessionData;
pub const REGISTERED_COMMANDS: &[&str] = &[
"get-config",
"get-version",
"get-build-date",
"set-config",
"select-column",
];
pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
linker.func_wrap_async(
"nomi",
"config_get_version",
|mut caller: Caller<'_, SessionData>,
()|
-> Box<
dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<ArrayRef>>>> + Send,
> {
Box::new(async move {
let bytes = command_string("get-version", GetVersion::new().run().await)?;
Ok(Some(alloc_string_ref(&mut caller, bytes.as_bytes())?))
})
},
)?;
"config_get_build_date",
let bytes = command_string("get-build-date", GetBuildDate::new().run().await)?;
"config_get_config",
(name_arg,): (Option<Rooted<ArrayRef>>,)|
let user_id = caller.data().ctx().user_id;
let name = read_string_arg(&mut caller, name_arg)?;
let formatted = run_get_config(user_id, name).await;
Ok(Some(alloc_string_ref(&mut caller, formatted.as_bytes())?))
"config_set_config",
(name_arg, value_arg): (Option<Rooted<ArrayRef>>, Option<Rooted<ArrayRef>>)|
-> Box<dyn std::future::Future<Output = wasmtime::Result<i32>> + Send> {
let value = read_string_arg(&mut caller, value_arg)?;
run_set_config(user_id, name, value).await
Ok(())
}
/// Args ride the still-extant EvalContext arg-capture queue (FIFO `take_arg`
/// returns `name` first, then `value`). A6 retires that queue once every
/// host fn declares typed params in its wasm signature.
///
/// Returns 1 on success, 0 on validation/command failure. Failures stream
/// to the rpc-level error envelope via `wasmtime::Error`; the i32 success
/// flag encodes "the write happened without erroring".
async fn run_set_config(
user_id: uuid::Uuid,
name_arg: Option<String>,
value_arg: Option<String>,
) -> wasmtime::Result<i32> {
let name = match name_arg {
Some(s) if !s.is_empty() => s,
_ => {
return Err(wasmtime::Error::msg(
"set-config: missing or empty :name arg",
));
let value = match value_arg {
Some(s) => s,
_ => return Err(wasmtime::Error::msg("set-config: missing :value arg")),
SetConfig::new()
.user_id(user_id)
.name(name)
.value(value)
.run()
.await
.map(|_| 1)
.map_err(|err| wasmtime::Error::msg(format!("set-config: {err}")))
async fn run_get_config(user_id: uuid::Uuid, name_arg: Option<String>) -> String {
_ => return "(:error \"get-config: missing or empty :name arg\")".into(),
match GetConfig::new().user_id(user_id).name(name).run().await {
Ok(Some(CmdResult::String(s))) => format!("(:config-value {})", quote_string(&s)),
Ok(Some(CmdResult::Data(bytes))) => {
format!("(:config-value #\"{}\")", BASE64.encode(&bytes))
Ok(Some(other)) => {
format!("(:error \"get-config: expected String/Data, got {other:?}\")")
Ok(None) => "(:config-value nil)".to_string(),
Err(err) => format!("(:error \"get-config: {err}\")"),
fn quote_string(s: &str) -> String {
let mut q = String::with_capacity(s.len() + 2);
q.push('"');
for ch in s.chars() {
match ch {
'"' => q.push_str("\\\""),
'\\' => q.push_str("\\\\"),
other => q.push(other),
q
/// Unwraps a `CmdResult::String(_)` result and surfaces every other shape
/// (Data/None/Err) as a `wasmtime::Error`. The rpc envelope layer catches
/// the trap and renders the `:error` form; callers compose the natives
/// expecting a typed `StringRef`, so anything else is a hard failure at
/// the point of use.
fn command_string(
name: &str,
result: Result<Option<CmdResult>, CmdError>,
) -> wasmtime::Result<String> {
match result {
Ok(Some(CmdResult::String(s))) => Ok(s),
Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
"{name}: expected String result, got {other:?}"
))),
Ok(None) => Err(wasmtime::Error::msg(format!(
"{name}: command returned no result"
Err(err) => Err(wasmtime::Error::msg(format!("{name}: {err}"))),
#[cfg(test)]
mod tests {
use super::*;
// Arg-validation runs before any DB/user lookup, so a placeholder user id
// is fine here.
const TEST_USER: uuid::Uuid = uuid::Uuid::nil();
#[tokio::test]
async fn run_get_config_no_arg_emits_error() {
let out = run_get_config(TEST_USER, None).await;
assert!(out.contains("(:error"));
assert!(out.contains("missing or empty"));
async fn run_get_config_empty_arg_emits_error() {
let out = run_get_config(TEST_USER, Some(String::new())).await;
async fn run_set_config_no_name_emits_error() {
let err = run_set_config(TEST_USER, None, Some("v".into()))
.unwrap_err();
assert!(err.to_string().contains(":name"));
async fn run_set_config_no_value_emits_error() {
let err = run_set_config(TEST_USER, Some("k".into()), None)
assert!(err.to_string().contains(":value"));
#[test]
fn quote_string_escapes_quotes_and_backslashes() {
assert_eq!(quote_string("plain"), "\"plain\"");
assert_eq!(quote_string("a\"b"), "\"a\\\"b\"");
assert_eq!(quote_string("c\\d"), "\"c\\\\d\"");