1use 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
19pub 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 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}