1use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
33#[serde(rename_all = "kebab-case", deny_unknown_fields)]
34pub struct RhaiLimitsConfig {
35 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub max_operations: Option<u64>,
39
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub max_string_size: Option<usize>,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub max_array_size: Option<usize>,
47
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub max_map_size: Option<usize>,
51
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub max_expression_depth: Option<u32>,
55
56 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub max_function_expression_depth: Option<u32>,
60
61 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub execution_timeout_ms: Option<u64>,
66}
67
68#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
84#[serde(rename_all = "kebab-case", deny_unknown_fields)]
85pub struct JsLimitsConfig {
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub execution_timeout_ms: Option<u64>,
89
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub max_loop_iterations: Option<u64>,
93
94 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub max_recursion_depth: Option<usize>,
97
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub max_stack_size: Option<usize>,
101}
102
103#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
109#[serde(rename_all = "kebab-case", deny_unknown_fields)]
110pub struct RhaiEngineConfig {
111 #[serde(default)]
113 pub limits: RhaiLimitsConfig,
114}
115
116#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
118#[serde(rename_all = "kebab-case", deny_unknown_fields)]
119pub struct JsEngineConfig {
120 #[serde(default)]
122 pub limits: JsLimitsConfig,
123}
124
125#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
135#[serde(rename_all = "kebab-case", deny_unknown_fields)]
136pub struct LanguagesConfig {
137 #[serde(default)]
139 pub rhai: RhaiEngineConfig,
140
141 #[serde(default)]
143 pub js: JsEngineConfig,
144}
145
146#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
157 fn rhai_defaults_to_all_none() {
158 let cfg = RhaiLimitsConfig::default();
159 assert_eq!(cfg.max_operations, None);
160 assert_eq!(cfg.max_string_size, None);
161 assert_eq!(cfg.max_array_size, None);
162 assert_eq!(cfg.max_map_size, None);
163 assert_eq!(cfg.max_expression_depth, None);
164 assert_eq!(cfg.max_function_expression_depth, None);
165 assert_eq!(cfg.execution_timeout_ms, None);
166 }
167
168 #[test]
169 fn rhai_deserialises_full_block() {
170 let toml = toml::toml! {
171 max-operations = 500000i64
172 max-string-size = 10485760i64
173 max-array-size = 100000i64
174 max-map-size = 100000i64
175 max-expression-depth = 10
176 max-function-expression-depth = 5
177 execution-timeout-ms = 5000i64
178 };
179 let cfg: RhaiLimitsConfig = toml.try_into().expect("deserialize");
180 assert_eq!(cfg.max_operations, Some(500_000));
181 assert_eq!(cfg.max_string_size, Some(10_485_760));
182 assert_eq!(cfg.max_array_size, Some(100_000));
183 assert_eq!(cfg.max_map_size, Some(100_000));
184 assert_eq!(cfg.max_expression_depth, Some(10));
185 assert_eq!(cfg.max_function_expression_depth, Some(5));
186 assert_eq!(cfg.execution_timeout_ms, Some(5000));
187 }
188
189 #[test]
190 fn rhai_deserialises_partial_block() {
191 let toml = toml::toml! {
192 max-operations = 100000i64
193 execution-timeout-ms = 3000i64
194 };
195 let cfg: RhaiLimitsConfig = toml.try_into().expect("deserialize");
196 assert_eq!(cfg.max_operations, Some(100_000));
197 assert_eq!(cfg.execution_timeout_ms, Some(3000));
198 assert_eq!(cfg.max_string_size, None);
200 assert_eq!(cfg.max_expression_depth, None);
201 }
202
203 #[test]
204 fn rhai_rejects_unknown_field() {
205 let toml = toml::toml! {
206 max-operations = 100000i64
207 fuel = 1000i64
208 };
209 let result: Result<RhaiLimitsConfig, _> = toml.try_into();
210 assert!(result.is_err(), "deny_unknown_fields must reject `fuel`");
211 }
212
213 #[test]
214 fn rhai_serde_round_trip_preserves_set_fields() {
215 let original = RhaiLimitsConfig {
216 max_operations: Some(200_000),
217 max_string_size: Some(5_242_880),
218 execution_timeout_ms: Some(10_000),
219 ..Default::default()
220 };
221 let serialized = toml::to_string(&original).expect("serialize");
222 let back: RhaiLimitsConfig = toml::from_str(&serialized).expect("deserialize");
223 assert_eq!(original, back);
224 }
225
226 #[test]
227 fn rhai_skip_serializing_none_fields() {
228 let cfg = RhaiLimitsConfig {
229 max_operations: Some(100_000),
230 max_string_size: None,
231 execution_timeout_ms: Some(5000),
232 ..Default::default()
233 };
234 let s = toml::to_string(&cfg).expect("serialize");
235 assert!(s.contains("max-operations"));
236 assert!(s.contains("execution-timeout-ms"));
237 assert!(!s.contains("max-string-size"));
238 assert!(!s.contains("max-expression-depth"));
239 }
240
241 #[test]
244 fn js_defaults_to_all_none() {
245 let cfg = JsLimitsConfig::default();
246 assert_eq!(cfg.execution_timeout_ms, None);
247 assert_eq!(cfg.max_loop_iterations, None);
248 assert_eq!(cfg.max_recursion_depth, None);
249 assert_eq!(cfg.max_stack_size, None);
250 }
251
252 #[test]
253 fn js_deserialises_full_block() {
254 let toml = toml::toml! {
255 execution-timeout-ms = 5000i64
256 max-loop-iterations = 1000000i64
257 max-recursion-depth = 64i64
258 max-stack-size = 1048576i64
259 };
260 let cfg: JsLimitsConfig = toml.try_into().expect("deserialize");
261 assert_eq!(cfg.execution_timeout_ms, Some(5000));
262 assert_eq!(cfg.max_loop_iterations, Some(1_000_000));
263 assert_eq!(cfg.max_recursion_depth, Some(64));
264 assert_eq!(cfg.max_stack_size, Some(1_048_576));
265 }
266
267 #[test]
268 fn js_deserialises_partial_block() {
269 let toml = toml::toml! {
270 execution-timeout-ms = 3000i64
271 max-recursion-depth = 32i64
272 };
273 let cfg: JsLimitsConfig = toml.try_into().expect("deserialize");
274 assert_eq!(cfg.execution_timeout_ms, Some(3000));
275 assert_eq!(cfg.max_recursion_depth, Some(32));
276 assert_eq!(cfg.max_loop_iterations, None);
277 assert_eq!(cfg.max_stack_size, None);
278 }
279
280 #[test]
281 fn js_rejects_unknown_field() {
282 let toml = toml::toml! {
283 execution-timeout-ms = 5000i64
284 fuel = 1000i64
285 };
286 let result: Result<JsLimitsConfig, _> = toml.try_into();
287 assert!(result.is_err(), "deny_unknown_fields must reject `fuel`");
288 }
289
290 #[test]
291 fn js_serde_round_trip_preserves_set_fields() {
292 let original = JsLimitsConfig {
293 execution_timeout_ms: Some(10_000),
294 max_loop_iterations: Some(500_000),
295 ..Default::default()
296 };
297 let serialized = toml::to_string(&original).expect("serialize");
298 let back: JsLimitsConfig = toml::from_str(&serialized).expect("deserialize");
299 assert_eq!(original, back);
300 }
301
302 #[test]
303 fn js_skip_serializing_none_fields() {
304 let cfg = JsLimitsConfig {
305 execution_timeout_ms: Some(5000),
306 max_loop_iterations: Some(1_000_000),
307 ..Default::default()
308 };
309 let s = toml::to_string(&cfg).expect("serialize");
310 assert!(s.contains("execution-timeout-ms"));
311 assert!(s.contains("max-loop-iterations"));
312 assert!(!s.contains("max-recursion-depth"));
313 assert!(!s.contains("max-stack-size"));
314 }
315
316 #[test]
319 fn rhai_engine_config_defaults() {
320 let cfg = RhaiEngineConfig::default();
321 assert_eq!(cfg.limits, RhaiLimitsConfig::default());
322 }
323
324 #[test]
325 fn js_engine_config_defaults() {
326 let cfg = JsEngineConfig::default();
327 assert_eq!(cfg.limits, JsLimitsConfig::default());
328 }
329
330 #[test]
331 fn languages_config_defaults() {
332 let cfg = LanguagesConfig::default();
333 assert_eq!(cfg.rhai.limits, RhaiLimitsConfig::default());
334 assert_eq!(cfg.js.limits, JsLimitsConfig::default());
335 }
336
337 #[test]
338 fn languages_deserialises_both_engines() {
339 let toml_str = r#"
340 [rhai.limits]
341 max-operations = 500000
342 execution-timeout-ms = 5000
343
344 [js.limits]
345 execution-timeout-ms = 3000
346 max-loop-iterations = 1000000
347 "#;
348 let cfg: LanguagesConfig = toml::from_str(toml_str).expect("deserialize");
349 assert_eq!(cfg.rhai.limits.max_operations, Some(500_000));
350 assert_eq!(cfg.rhai.limits.execution_timeout_ms, Some(5000));
351 assert_eq!(cfg.js.limits.execution_timeout_ms, Some(3000));
352 assert_eq!(cfg.js.limits.max_loop_iterations, Some(1_000_000));
353 }
354
355 #[test]
356 fn languages_serde_round_trip() {
357 let original = LanguagesConfig {
358 rhai: RhaiEngineConfig {
359 limits: RhaiLimitsConfig {
360 max_operations: Some(100_000),
361 ..Default::default()
362 },
363 },
364 js: JsEngineConfig::default(),
365 };
366 let serialized = toml::to_string(&original).expect("serialize");
367 let back: LanguagesConfig = toml::from_str(&serialized).expect("deserialize");
368 assert_eq!(original, back);
369 }
370}