Skip to main content

rpc/natives/
config.rs

1//! Config-domain natives. Wraps `server::command::{GetConfig, GetVersion,
2//! GetBuildDate, SetConfig, SelectColumn}`.
3//!
4//! `get-version` and `get-build-date` are the first server-command bindings
5//! the rpc layer ships. Both return a `CmdResult::String(...)` from a body
6//! that just reads `env!`-baked constants — no DB, no user_id, no real async
7//! work — so they exercise the marshalling shape without yet needing a
8//! tokio runtime or pool. The remaining three commands wait until DB-touching
9//! infrastructure (ScriptCtx::pool, sqlx::test fixtures) lands.
10
11use base64::Engine;
12use base64::engine::general_purpose::STANDARD as BASE64;
13use scripting::runtime::{alloc_string_ref, read_string_arg};
14use server::command::{
15    CmdError, CmdResult,
16    config::{GetBuildDate, GetConfig, GetVersion, SetConfig},
17};
18use wasmtime::{ArrayRef, Caller, Linker, Rooted};
19
20use crate::session::SessionData;
21
22pub const REGISTERED_COMMANDS: &[&str] = &[
23    "get-config",
24    "get-version",
25    "get-build-date",
26    "set-config",
27    "select-column",
28];
29
30pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
31    linker.func_wrap_async(
32        "nomi",
33        "config_get_version",
34        |mut caller: Caller<'_, SessionData>,
35         ()|
36         -> Box<
37            dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<ArrayRef>>>> + Send,
38        > {
39            Box::new(async move {
40                let bytes = command_string("get-version", GetVersion::new().run().await)?;
41                Ok(Some(alloc_string_ref(&mut caller, bytes.as_bytes())?))
42            })
43        },
44    )?;
45    linker.func_wrap_async(
46        "nomi",
47        "config_get_build_date",
48        |mut caller: Caller<'_, SessionData>,
49         ()|
50         -> Box<
51            dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<ArrayRef>>>> + Send,
52        > {
53            Box::new(async move {
54                let bytes = command_string("get-build-date", GetBuildDate::new().run().await)?;
55                Ok(Some(alloc_string_ref(&mut caller, bytes.as_bytes())?))
56            })
57        },
58    )?;
59    linker.func_wrap_async(
60        "nomi",
61        "config_get_config",
62        |mut caller: Caller<'_, SessionData>,
63         (name_arg,): (Option<Rooted<ArrayRef>>,)|
64         -> Box<
65            dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<ArrayRef>>>> + Send,
66        > {
67            Box::new(async move {
68                let user_id = caller.data().ctx().user_id;
69                let name = read_string_arg(&mut caller, name_arg)?;
70                let formatted = run_get_config(user_id, name).await;
71                Ok(Some(alloc_string_ref(&mut caller, formatted.as_bytes())?))
72            })
73        },
74    )?;
75    linker.func_wrap_async(
76        "nomi",
77        "config_set_config",
78        |mut caller: Caller<'_, SessionData>,
79         (name_arg, value_arg): (Option<Rooted<ArrayRef>>, Option<Rooted<ArrayRef>>)|
80         -> Box<dyn std::future::Future<Output = wasmtime::Result<i32>> + Send> {
81            Box::new(async move {
82                let user_id = caller.data().ctx().user_id;
83                let name = read_string_arg(&mut caller, name_arg)?;
84                let value = read_string_arg(&mut caller, value_arg)?;
85                run_set_config(user_id, name, value).await
86            })
87        },
88    )?;
89    Ok(())
90}
91
92/// Args ride the still-extant EvalContext arg-capture queue (FIFO `take_arg`
93/// returns `name` first, then `value`). A6 retires that queue once every
94/// host fn declares typed params in its wasm signature.
95///
96/// Returns 1 on success, 0 on validation/command failure. Failures stream
97/// to the rpc-level error envelope via `wasmtime::Error`; the i32 success
98/// flag encodes "the write happened without erroring".
99async fn run_set_config(
100    user_id: uuid::Uuid,
101    name_arg: Option<String>,
102    value_arg: Option<String>,
103) -> wasmtime::Result<i32> {
104    let name = match name_arg {
105        Some(s) if !s.is_empty() => s,
106        _ => {
107            return Err(wasmtime::Error::msg(
108                "set-config: missing or empty :name arg",
109            ));
110        }
111    };
112    let value = match value_arg {
113        Some(s) => s,
114        _ => return Err(wasmtime::Error::msg("set-config: missing :value arg")),
115    };
116    SetConfig::new()
117        .user_id(user_id)
118        .name(name)
119        .value(value)
120        .run()
121        .await
122        .map(|_| 1)
123        .map_err(|err| wasmtime::Error::msg(format!("set-config: {err}")))
124}
125
126async fn run_get_config(user_id: uuid::Uuid, name_arg: Option<String>) -> String {
127    let name = match name_arg {
128        Some(s) if !s.is_empty() => s,
129        _ => return "(:error \"get-config: missing or empty :name arg\")".into(),
130    };
131    match GetConfig::new().user_id(user_id).name(name).run().await {
132        Ok(Some(CmdResult::String(s))) => format!("(:config-value {})", quote_string(&s)),
133        Ok(Some(CmdResult::Data(bytes))) => {
134            format!("(:config-value #\"{}\")", BASE64.encode(&bytes))
135        }
136        Ok(Some(other)) => {
137            format!("(:error \"get-config: expected String/Data, got {other:?}\")")
138        }
139        Ok(None) => "(:config-value nil)".to_string(),
140        Err(err) => format!("(:error \"get-config: {err}\")"),
141    }
142}
143
144fn quote_string(s: &str) -> String {
145    let mut q = String::with_capacity(s.len() + 2);
146    q.push('"');
147    for ch in s.chars() {
148        match ch {
149            '"' => q.push_str("\\\""),
150            '\\' => q.push_str("\\\\"),
151            other => q.push(other),
152        }
153    }
154    q.push('"');
155    q
156}
157
158/// Unwraps a `CmdResult::String(_)` result and surfaces every other shape
159/// (Data/None/Err) as a `wasmtime::Error`. The rpc envelope layer catches
160/// the trap and renders the `:error` form; callers compose the natives
161/// expecting a typed `StringRef`, so anything else is a hard failure at
162/// the point of use.
163fn command_string(
164    name: &str,
165    result: Result<Option<CmdResult>, CmdError>,
166) -> wasmtime::Result<String> {
167    match result {
168        Ok(Some(CmdResult::String(s))) => Ok(s),
169        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
170            "{name}: expected String result, got {other:?}"
171        ))),
172        Ok(None) => Err(wasmtime::Error::msg(format!(
173            "{name}: command returned no result"
174        ))),
175        Err(err) => Err(wasmtime::Error::msg(format!("{name}: {err}"))),
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    // Arg-validation runs before any DB/user lookup, so a placeholder user id
184    // is fine here.
185    const TEST_USER: uuid::Uuid = uuid::Uuid::nil();
186
187    #[tokio::test]
188    async fn run_get_config_no_arg_emits_error() {
189        let out = run_get_config(TEST_USER, None).await;
190        assert!(out.contains("(:error"));
191        assert!(out.contains("missing or empty"));
192    }
193
194    #[tokio::test]
195    async fn run_get_config_empty_arg_emits_error() {
196        let out = run_get_config(TEST_USER, Some(String::new())).await;
197        assert!(out.contains("(:error"));
198        assert!(out.contains("missing or empty"));
199    }
200
201    #[tokio::test]
202    async fn run_set_config_no_name_emits_error() {
203        let err = run_set_config(TEST_USER, None, Some("v".into()))
204            .await
205            .unwrap_err();
206        assert!(err.to_string().contains(":name"));
207    }
208
209    #[tokio::test]
210    async fn run_set_config_no_value_emits_error() {
211        let err = run_set_config(TEST_USER, Some("k".into()), None)
212            .await
213            .unwrap_err();
214        assert!(err.to_string().contains(":value"));
215    }
216
217    #[test]
218    fn quote_string_escapes_quotes_and_backslashes() {
219        assert_eq!(quote_string("plain"), "\"plain\"");
220        assert_eq!(quote_string("a\"b"), "\"a\\\"b\"");
221        assert_eq!(quote_string("c\\d"), "\"c\\\\d\"");
222    }
223}