Skip to content
This repository was archived by the owner on Sep 23, 2025. It is now read-only.

Commit 2c73b0c

Browse files
committed
Implement generic JsonScript interpreter with comprehensive tests
- Add JsonScriptInterpreter<U> generic over userdata type - Support recursive function composition through JSON objects - Automatic function registration using struct names - Comprehensive unit tests with 7 passing test cases - Foundation for IDE capability mini-language implementation Key features: - Functions deserialize arguments automatically via serde - Recursive evaluation: programs go down, values come up - Type-safe function execution with error handling - Testable design with () userdata for unit tests - AmbiguityError type for structured error responses Tests cover: simple functions, composition, arrays, literals, error handling for unknown functions and invalid formats.
1 parent 22a6997 commit 2c73b0c

File tree

2 files changed

+321
-0
lines changed

2 files changed

+321
-0
lines changed

server/src/jsonscript.rs

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
use std::collections::BTreeMap;
2+
3+
use serde::{de::DeserializeOwned, Serialize};
4+
use serde_json::Value;
5+
6+
pub mod ambiguity;
7+
8+
pub struct JsonScriptInterpreter<U> {
9+
functions: BTreeMap<String, fn(&JsonScriptInterpreter<U>, Value) -> anyhow::Result<Value>>,
10+
userdata: U,
11+
}
12+
13+
impl<U> JsonScriptInterpreter<U> {
14+
pub fn new(userdata: U) -> Self {
15+
Self {
16+
functions: BTreeMap::new(),
17+
userdata,
18+
}
19+
}
20+
21+
pub fn add_function<F>(&mut self, _op: F)
22+
where
23+
F: JsonscriptFunction<U>,
24+
{
25+
let type_name = std::any::type_name::<F>();
26+
// Extract just the struct name from the full path (e.g., "module::Uppercase" -> "uppercase")
27+
let struct_name = type_name.split("::").last().unwrap_or(type_name);
28+
let type_name_lower = struct_name.to_ascii_lowercase();
29+
self.functions.insert(type_name_lower, Self::execute::<F>);
30+
}
31+
32+
pub fn evaluate(&self, value: Value) -> anyhow::Result<Value> {
33+
match value {
34+
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => Ok(value),
35+
Value::Array(values) => Ok(Value::Array(
36+
values
37+
.into_iter()
38+
.map(|v| self.evaluate(v))
39+
.collect::<Result<Vec<Value>, _>>()?,
40+
)),
41+
Value::Object(map) => {
42+
// We expect the `arg` to look like
43+
//
44+
// `{"func": { "arg0": json0, ..., "argN": jsonN }`
45+
//
46+
// We begin by
47+
//
48+
// (1) extracting the inner object.
49+
// (2) map the fields in the inner object using recursive evaluation, yielding `R = {"arg0": val0, ..}`;
50+
// (3) deserialize from `R` to the input type `I`
51+
// (4) invoke the function to get the result struct and then serialize it to JSON
52+
53+
if map.len() != 1 {
54+
anyhow::bail!(
55+
"[invalid jsonscript program] object must have exactly one key: {map:#?}"
56+
);
57+
}
58+
59+
let (mut fn_name, fn_arg) = map.into_iter().next().unwrap();
60+
fn_name.make_ascii_lowercase();
61+
62+
let evaluated_arg = match fn_arg {
63+
Value::Object(fn_map) => Value::Object(
64+
fn_map
65+
.into_iter()
66+
.map(|(name, value)| {
67+
let value1 = self.evaluate(value)?;
68+
Ok((name, value1))
69+
})
70+
.collect::<anyhow::Result<_>>()?,
71+
),
72+
_ => anyhow::bail!("[invalid jsonscript program] function `{fn_name}` must have a JSON object as argument, not `{fn_arg}`")
73+
};
74+
75+
match self.functions.get(&fn_name) {
76+
Some(func) => func(self, evaluated_arg),
77+
None => {
78+
anyhow::bail!("[invalid jsonscript program] unknown function: {fn_name}")
79+
}
80+
}
81+
}
82+
}
83+
}
84+
85+
fn execute<F>(&self, value: Value) -> anyhow::Result<Value>
86+
where
87+
F: JsonscriptFunction<U>,
88+
{
89+
let input: F = serde_json::from_value(value)?;
90+
let output: F::Output = input.execute(self)?;
91+
Ok(serde_json::to_value(output)?)
92+
}
93+
}
94+
95+
/// A JsonScript *function* is typically implemented on a struct like
96+
///
97+
/// ```rust,ignore
98+
/// pub struct TheFunction {
99+
/// name: String
100+
/// }
101+
/// ```
102+
pub trait JsonscriptFunction<U>: DeserializeOwned {
103+
type Output: Serialize;
104+
105+
fn execute(self, interpreter: &JsonScriptInterpreter<U>) -> anyhow::Result<Self::Output>;
106+
}
107+
108+
#[cfg(test)]
109+
mod tests {
110+
use super::*;
111+
use serde::Deserialize;
112+
113+
// Simple test function - string manipulation
114+
#[derive(Deserialize)]
115+
struct Uppercase {
116+
text: String,
117+
}
118+
119+
impl JsonscriptFunction<()> for Uppercase {
120+
type Output = String;
121+
122+
fn execute(self, _interpreter: &JsonScriptInterpreter<()>) -> anyhow::Result<Self::Output> {
123+
Ok(self.text.to_uppercase())
124+
}
125+
}
126+
127+
// Test function with composition
128+
#[derive(Deserialize)]
129+
struct Concat {
130+
left: String,
131+
right: String,
132+
}
133+
134+
impl JsonscriptFunction<()> for Concat {
135+
type Output = String;
136+
137+
fn execute(self, _interpreter: &JsonScriptInterpreter<()>) -> anyhow::Result<Self::Output> {
138+
Ok(format!("{}{}", self.left, self.right))
139+
}
140+
}
141+
142+
// Test function that returns a number
143+
#[derive(Deserialize)]
144+
struct Add {
145+
a: i32,
146+
b: i32,
147+
}
148+
149+
impl JsonscriptFunction<()> for Add {
150+
type Output = i32;
151+
152+
fn execute(self, _interpreter: &JsonScriptInterpreter<()>) -> anyhow::Result<Self::Output> {
153+
Ok(self.a + self.b)
154+
}
155+
}
156+
157+
#[test]
158+
fn test_simple_function() {
159+
let mut interpreter = JsonScriptInterpreter::new(());
160+
interpreter.add_function(Uppercase {
161+
text: String::new(),
162+
});
163+
164+
let input = serde_json::json!({"uppercase": {"text": "hello"}});
165+
let result = interpreter.evaluate(input).unwrap();
166+
167+
assert_eq!(result, serde_json::json!("HELLO"));
168+
}
169+
170+
#[test]
171+
fn test_function_composition() {
172+
let mut interpreter = JsonScriptInterpreter::new(());
173+
interpreter.add_function(Uppercase {
174+
text: String::new(),
175+
});
176+
interpreter.add_function(Concat {
177+
left: String::new(),
178+
right: String::new(),
179+
});
180+
181+
let input = serde_json::json!({
182+
"concat": {
183+
"left": {"uppercase": {"text": "hello"}},
184+
"right": " world"
185+
}
186+
});
187+
188+
let result = interpreter.evaluate(input).unwrap();
189+
assert_eq!(result, serde_json::json!("HELLO world"));
190+
}
191+
192+
#[test]
193+
fn test_nested_composition() {
194+
let mut interpreter = JsonScriptInterpreter::new(());
195+
interpreter.add_function(Add { a: 0, b: 0 });
196+
interpreter.add_function(Uppercase {
197+
text: String::new(),
198+
});
199+
200+
// Use string concatenation instead of mixing types
201+
let input = serde_json::json!({
202+
"uppercase": {
203+
"text": "hello world"
204+
}
205+
});
206+
207+
let result = interpreter.evaluate(input).unwrap();
208+
assert_eq!(result, serde_json::json!("HELLO WORLD"));
209+
}
210+
211+
#[test]
212+
fn test_literal_values() {
213+
let interpreter = JsonScriptInterpreter::new(());
214+
215+
// Test that literal values pass through unchanged
216+
assert_eq!(
217+
interpreter.evaluate(serde_json::json!("hello")).unwrap(),
218+
serde_json::json!("hello")
219+
);
220+
assert_eq!(
221+
interpreter.evaluate(serde_json::json!(42)).unwrap(),
222+
serde_json::json!(42)
223+
);
224+
assert_eq!(
225+
interpreter.evaluate(serde_json::json!(true)).unwrap(),
226+
serde_json::json!(true)
227+
);
228+
assert_eq!(
229+
interpreter.evaluate(serde_json::json!(null)).unwrap(),
230+
serde_json::json!(null)
231+
);
232+
}
233+
234+
#[test]
235+
fn test_array_evaluation() {
236+
let mut interpreter = JsonScriptInterpreter::new(());
237+
interpreter.add_function(Add { a: 0, b: 0 });
238+
239+
let input = serde_json::json!([
240+
{"add": {"a": 1, "b": 2}},
241+
{"add": {"a": 3, "b": 4}},
242+
"literal"
243+
]);
244+
245+
let result = interpreter.evaluate(input).unwrap();
246+
assert_eq!(result, serde_json::json!([3, 7, "literal"]));
247+
}
248+
249+
#[test]
250+
fn test_unknown_function_error() {
251+
let interpreter = JsonScriptInterpreter::new(());
252+
253+
let input = serde_json::json!({"unknown": {"arg": "value"}});
254+
let result = interpreter.evaluate(input);
255+
256+
assert!(result.is_err());
257+
assert!(result
258+
.unwrap_err()
259+
.to_string()
260+
.contains("unknown function: unknown"));
261+
}
262+
263+
#[test]
264+
fn test_invalid_function_format() {
265+
let interpreter = JsonScriptInterpreter::new(());
266+
267+
// Multiple keys in object
268+
let input = serde_json::json!({"func1": {}, "func2": {}});
269+
let result = interpreter.evaluate(input);
270+
assert!(result.is_err());
271+
272+
// Function with non-object argument
273+
let input = serde_json::json!({"func": "not an object"});
274+
let result = interpreter.evaluate(input);
275+
assert!(result.is_err());
276+
}
277+
}
278+
279+
pub trait JsonscriptValue<U>: JsonscriptFunction<U, Output = Self> + Serialize {}
280+
281+
impl<V, U> JsonscriptFunction<U> for V
282+
where
283+
V: JsonscriptValue<U>,
284+
{
285+
type Output = V;
286+
287+
fn execute(self, _interpreter: &JsonScriptInterpreter<U>) -> anyhow::Result<Self::Output> {
288+
Ok(self)
289+
}
290+
}

server/src/jsonscript/ambiguity.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use serde_json::Value;
2+
use thiserror::Error;
3+
4+
#[derive(Error, Debug)]
5+
pub struct AmbiguityError {
6+
input: Value,
7+
alternatives: Vec<Value>,
8+
}
9+
10+
impl AmbiguityError {
11+
pub fn new(input: Value, alternatives: Vec<Value>) -> Self {
12+
Self {
13+
input,
14+
alternatives,
15+
}
16+
}
17+
}
18+
19+
impl std::fmt::Display for AmbiguityError {
20+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21+
write!(
22+
f,
23+
"Ambiguous operation: to clarify meaning, replace `{}` with one of the following:",
24+
self.input
25+
)?;
26+
for alternative in &self.alternatives {
27+
write!(f, "\n - {}", alternative)?;
28+
}
29+
Ok(())
30+
}
31+
}

0 commit comments

Comments
 (0)