Skip to main content

seam_server/
derive.rs

1/* src/server/core/rust/src/derive.rs */
2
3//! Execute derive functions via embedded QuickJS runtime.
4//!
5//! Each derive entry declares `sources` (loader keys) and `fn` (JS function source).
6//! This module evaluates the function with the corresponding loader data as arguments
7//! and returns all derive results as a JSON object.
8
9use serde::Deserialize;
10use std::collections::BTreeMap;
11
12#[derive(Deserialize)]
13struct DeriveEntry {
14	sources: Vec<String>,
15	#[serde(rename = "fn")]
16	fn_source: String,
17}
18
19/// Execute all derive definitions against loader data, returning a JSON object
20/// with derive results keyed by derive name.
21///
22/// # Arguments
23/// * `derives_json` - JSON object mapping derive names to `{sources, fn}` entries
24/// * `loader_data_json` - JSON object with loader results keyed by loader name
25///
26/// # Returns
27/// JSON string like `{"repoStats":{"totalStars":142}}`
28pub fn execute_derives(derives_json: &str, loader_data_json: &str) -> Result<String, String> {
29	let derives: BTreeMap<String, DeriveEntry> =
30		serde_json::from_str(derives_json).map_err(|e| format!("invalid derives JSON: {e}"))?;
31
32	if derives.is_empty() {
33		return Ok("{}".to_string());
34	}
35
36	let loader_data: serde_json::Value =
37		serde_json::from_str(loader_data_json).map_err(|e| format!("invalid loader data: {e}"))?;
38
39	let rt = rquickjs::Runtime::new().map_err(|e| format!("QuickJS runtime init: {e}"))?;
40	let ctx = rquickjs::Context::full(&rt).map_err(|e| format!("QuickJS context init: {e}"))?;
41
42	let mut results = serde_json::Map::new();
43
44	for (key, entry) in &derives {
45		let args: Vec<String> = entry
46			.sources
47			.iter()
48			.map(|src| loader_data.get(src).map_or_else(|| "null".to_string(), ToString::to_string))
49			.collect();
50
51		let args_str = args.join(",");
52		// Wrap in parens to handle arrow functions, ?? null for undefined safety
53		let expr = format!("JSON.stringify(({})({}) ?? null)", entry.fn_source, args_str);
54
55		let result_json: String = ctx.with(|ctx| {
56			let js_str = ctx
57				.eval::<rquickjs::String, _>(expr.as_bytes())
58				.map_err(|e| format!("derive \"{key}\" execution failed: {e}"))?;
59			js_str.to_string().map_err(|e| format!("derive \"{key}\" result encoding: {e}"))
60		})?;
61
62		let value: serde_json::Value = serde_json::from_str(&result_json)
63			.map_err(|e| format!("derive \"{key}\" returned invalid JSON: {e}"))?;
64
65		results.insert(key.clone(), value);
66	}
67
68	serde_json::to_string(&serde_json::Value::Object(results))
69		.map_err(|e| format!("derive results serialization: {e}"))
70}
71
72#[cfg(test)]
73mod tests {
74	use super::*;
75
76	#[test]
77	fn array_length() {
78		let derives = serde_json::json!({
79			"count": {
80				"sources": ["repos"],
81				"fn": "(repos) => repos.length"
82			}
83		});
84		let data = serde_json::json!({
85			"repos": [{"name": "a"}, {"name": "b"}, {"name": "c"}]
86		});
87
88		let result = execute_derives(&derives.to_string(), &data.to_string());
89		assert!(result.is_ok(), "execute_derives failed: {:?}", result.err());
90
91		let parsed: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
92		assert_eq!(parsed["count"], 3);
93	}
94
95	#[test]
96	fn reduce_sum() {
97		let derives = serde_json::json!({
98			"repoStats": {
99				"sources": ["user", "repos"],
100				"fn": "(_user, repos) => ({ totalStars: repos.reduce((s, r) => s + r.stars, 0) })"
101			}
102		});
103		let data = serde_json::json!({
104			"user": {"login": "octocat"},
105			"repos": [{"stars": 100}, {"stars": 42}]
106		});
107
108		let result = execute_derives(&derives.to_string(), &data.to_string()).unwrap();
109		let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
110		assert_eq!(parsed["repoStats"]["totalStars"], 142);
111	}
112
113	#[test]
114	fn missing_source_passes_null() {
115		let derives = serde_json::json!({
116			"check": {
117				"sources": ["missing"],
118				"fn": "(x) => x === null"
119			}
120		});
121		let data = serde_json::json!({});
122
123		let result = execute_derives(&derives.to_string(), &data.to_string()).unwrap();
124		let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
125		assert_eq!(parsed["check"], true);
126	}
127
128	#[test]
129	fn multiple_derives() {
130		let derives = serde_json::json!({
131			"total": {
132				"sources": ["nums"],
133				"fn": "(nums) => nums.reduce((a, b) => a + b, 0)"
134			},
135			"count": {
136				"sources": ["nums"],
137				"fn": "(nums) => nums.length"
138			}
139		});
140		let data = serde_json::json!({ "nums": [1, 2, 3, 4, 5] });
141
142		let result = execute_derives(&derives.to_string(), &data.to_string()).unwrap();
143		let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
144		assert_eq!(parsed["total"], 15);
145		assert_eq!(parsed["count"], 5);
146	}
147
148	#[test]
149	fn empty_derives() {
150		let result = execute_derives("{}", r#"{"x": 1}"#).unwrap();
151		assert_eq!(result, "{}");
152	}
153
154	#[test]
155	fn js_error_returns_err() {
156		let derives = serde_json::json!({
157			"bad": {
158				"sources": [],
159				"fn": "() => { throw new Error('boom') }"
160			}
161		});
162
163		let result = execute_derives(&derives.to_string(), "{}");
164		assert!(result.is_err());
165		assert!(result.unwrap_err().contains("bad"));
166	}
167}