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

            
11
use base64::Engine;
12
use base64::engine::general_purpose::STANDARD as BASE64;
13
use scripting::runtime::{alloc_string_ref, read_string_arg};
14
use server::command::{
15
    CmdError, CmdResult,
16
    config::{GetBuildDate, GetConfig, GetVersion, SetConfig},
17
};
18
use wasmtime::{ArrayRef, Caller, Linker, Rooted};
19

            
20
use crate::session::SessionData;
21

            
22
pub const REGISTERED_COMMANDS: &[&str] = &[
23
    "get-config",
24
    "get-version",
25
    "get-build-date",
26
    "set-config",
27
    "select-column",
28
];
29

            
30
2559
pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
31
2559
    linker.func_wrap_async(
32
2559
        "nomi",
33
2559
        "config_get_version",
34
        |mut caller: Caller<'_, SessionData>,
35
         ()|
36
         -> Box<
37
            dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<ArrayRef>>>> + Send,
38
19
        > {
39
19
            Box::new(async move {
40
19
                let bytes = command_string("get-version", GetVersion::new().run().await)?;
41
19
                Ok(Some(alloc_string_ref(&mut caller, bytes.as_bytes())?))
42
19
            })
43
19
        },
44
    )?;
45
2559
    linker.func_wrap_async(
46
2559
        "nomi",
47
2559
        "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
1
        > {
53
1
            Box::new(async move {
54
1
                let bytes = command_string("get-build-date", GetBuildDate::new().run().await)?;
55
1
                Ok(Some(alloc_string_ref(&mut caller, bytes.as_bytes())?))
56
1
            })
57
1
        },
58
    )?;
59
2559
    linker.func_wrap_async(
60
2559
        "nomi",
61
2559
        "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
54
        > {
67
54
            Box::new(async move {
68
54
                let user_id = caller.data().ctx().user_id;
69
54
                let name = read_string_arg(&mut caller, name_arg)?;
70
54
                let formatted = run_get_config(user_id, name).await;
71
54
                Ok(Some(alloc_string_ref(&mut caller, formatted.as_bytes())?))
72
54
            })
73
54
        },
74
    )?;
75
2559
    linker.func_wrap_async(
76
2559
        "nomi",
77
2559
        "config_set_config",
78
        |mut caller: Caller<'_, SessionData>,
79
         (name_arg, value_arg): (Option<Rooted<ArrayRef>>, Option<Rooted<ArrayRef>>)|
80
18
         -> Box<dyn std::future::Future<Output = wasmtime::Result<i32>> + Send> {
81
18
            Box::new(async move {
82
18
                let user_id = caller.data().ctx().user_id;
83
18
                let name = read_string_arg(&mut caller, name_arg)?;
84
18
                let value = read_string_arg(&mut caller, value_arg)?;
85
18
                run_set_config(user_id, name, value).await
86
18
            })
87
18
        },
88
    )?;
89
2559
    Ok(())
90
2559
}
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".
99
20
async fn run_set_config(
100
20
    user_id: uuid::Uuid,
101
20
    name_arg: Option<String>,
102
20
    value_arg: Option<String>,
103
20
) -> wasmtime::Result<i32> {
104
19
    let name = match name_arg {
105
19
        Some(s) if !s.is_empty() => s,
106
        _ => {
107
1
            return Err(wasmtime::Error::msg(
108
1
                "set-config: missing or empty :name arg",
109
1
            ));
110
        }
111
    };
112
19
    let value = match value_arg {
113
18
        Some(s) => s,
114
1
        _ => return Err(wasmtime::Error::msg("set-config: missing :value arg")),
115
    };
116
18
    SetConfig::new()
117
18
        .user_id(user_id)
118
18
        .name(name)
119
18
        .value(value)
120
18
        .run()
121
18
        .await
122
18
        .map(|_| 1)
123
18
        .map_err(|err| wasmtime::Error::msg(format!("set-config: {err}")))
124
20
}
125

            
126
56
async fn run_get_config(user_id: uuid::Uuid, name_arg: Option<String>) -> String {
127
55
    let name = match name_arg {
128
55
        Some(s) if !s.is_empty() => s,
129
2
        _ => return "(:error \"get-config: missing or empty :name arg\")".into(),
130
    };
131
54
    match GetConfig::new().user_id(user_id).name(name).run().await {
132
36
        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
18
        Err(err) => format!("(:error \"get-config: {err}\")"),
141
    }
142
56
}
143

            
144
39
fn quote_string(s: &str) -> String {
145
39
    let mut q = String::with_capacity(s.len() + 2);
146
39
    q.push('"');
147
263
    for ch in s.chars() {
148
263
        match ch {
149
1
            '"' => q.push_str("\\\""),
150
1
            '\\' => q.push_str("\\\\"),
151
261
            other => q.push(other),
152
        }
153
    }
154
39
    q.push('"');
155
39
    q
156
39
}
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.
163
20
fn command_string(
164
20
    name: &str,
165
20
    result: Result<Option<CmdResult>, CmdError>,
166
20
) -> wasmtime::Result<String> {
167
20
    match result {
168
20
        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
20
}
178

            
179
#[cfg(test)]
180
mod 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
1
    async fn run_get_config_no_arg_emits_error() {
189
1
        let out = run_get_config(TEST_USER, None).await;
190
1
        assert!(out.contains("(:error"));
191
1
        assert!(out.contains("missing or empty"));
192
1
    }
193

            
194
    #[tokio::test]
195
1
    async fn run_get_config_empty_arg_emits_error() {
196
1
        let out = run_get_config(TEST_USER, Some(String::new())).await;
197
1
        assert!(out.contains("(:error"));
198
1
        assert!(out.contains("missing or empty"));
199
1
    }
200

            
201
    #[tokio::test]
202
1
    async fn run_set_config_no_name_emits_error() {
203
1
        let err = run_set_config(TEST_USER, None, Some("v".into()))
204
1
            .await
205
1
            .unwrap_err();
206
1
        assert!(err.to_string().contains(":name"));
207
1
    }
208

            
209
    #[tokio::test]
210
1
    async fn run_set_config_no_value_emits_error() {
211
1
        let err = run_set_config(TEST_USER, Some("k".into()), None)
212
1
            .await
213
1
            .unwrap_err();
214
1
        assert!(err.to_string().contains(":value"));
215
1
    }
216

            
217
    #[test]
218
1
    fn quote_string_escapes_quotes_and_backslashes() {
219
1
        assert_eq!(quote_string("plain"), "\"plain\"");
220
1
        assert_eq!(quote_string("a\"b"), "\"a\\\"b\"");
221
1
        assert_eq!(quote_string("c\\d"), "\"c\\\\d\"");
222
1
    }
223
}