use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct RhaiLimitsConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_operations: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_string_size: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_array_size: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_map_size: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_expression_depth: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_function_expression_depth: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub execution_timeout_ms: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct JsLimitsConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub execution_timeout_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_loop_iterations: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_recursion_depth: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_stack_size: Option<usize>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct RhaiEngineConfig {
#[serde(default)]
pub limits: RhaiLimitsConfig,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct JsEngineConfig {
#[serde(default)]
pub limits: JsLimitsConfig,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguagesConfig {
#[serde(default)]
pub rhai: RhaiEngineConfig,
#[serde(default)]
pub js: JsEngineConfig,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rhai_defaults_to_all_none() {
let cfg = RhaiLimitsConfig::default();
assert_eq!(cfg.max_operations, None);
assert_eq!(cfg.max_string_size, None);
assert_eq!(cfg.max_array_size, None);
assert_eq!(cfg.max_map_size, None);
assert_eq!(cfg.max_expression_depth, None);
assert_eq!(cfg.max_function_expression_depth, None);
assert_eq!(cfg.execution_timeout_ms, None);
}
#[test]
fn rhai_deserialises_full_block() {
let toml = toml::toml! {
max-operations = 500000i64
max-string-size = 10485760i64
max-array-size = 100000i64
max-map-size = 100000i64
max-expression-depth = 10
max-function-expression-depth = 5
execution-timeout-ms = 5000i64
};
let cfg: RhaiLimitsConfig = toml.try_into().expect("deserialize");
assert_eq!(cfg.max_operations, Some(500_000));
assert_eq!(cfg.max_string_size, Some(10_485_760));
assert_eq!(cfg.max_array_size, Some(100_000));
assert_eq!(cfg.max_map_size, Some(100_000));
assert_eq!(cfg.max_expression_depth, Some(10));
assert_eq!(cfg.max_function_expression_depth, Some(5));
assert_eq!(cfg.execution_timeout_ms, Some(5000));
}
#[test]
fn rhai_deserialises_partial_block() {
let toml = toml::toml! {
max-operations = 100000i64
execution-timeout-ms = 3000i64
};
let cfg: RhaiLimitsConfig = toml.try_into().expect("deserialize");
assert_eq!(cfg.max_operations, Some(100_000));
assert_eq!(cfg.execution_timeout_ms, Some(3000));
assert_eq!(cfg.max_string_size, None);
assert_eq!(cfg.max_expression_depth, None);
}
#[test]
fn rhai_rejects_unknown_field() {
let toml = toml::toml! {
max-operations = 100000i64
fuel = 1000i64
};
let result: Result<RhaiLimitsConfig, _> = toml.try_into();
assert!(result.is_err(), "deny_unknown_fields must reject `fuel`");
}
#[test]
fn rhai_serde_round_trip_preserves_set_fields() {
let original = RhaiLimitsConfig {
max_operations: Some(200_000),
max_string_size: Some(5_242_880),
execution_timeout_ms: Some(10_000),
..Default::default()
};
let serialized = toml::to_string(&original).expect("serialize");
let back: RhaiLimitsConfig = toml::from_str(&serialized).expect("deserialize");
assert_eq!(original, back);
}
#[test]
fn rhai_skip_serializing_none_fields() {
let cfg = RhaiLimitsConfig {
max_operations: Some(100_000),
max_string_size: None,
execution_timeout_ms: Some(5000),
..Default::default()
};
let s = toml::to_string(&cfg).expect("serialize");
assert!(s.contains("max-operations"));
assert!(s.contains("execution-timeout-ms"));
assert!(!s.contains("max-string-size"));
assert!(!s.contains("max-expression-depth"));
}
#[test]
fn js_defaults_to_all_none() {
let cfg = JsLimitsConfig::default();
assert_eq!(cfg.execution_timeout_ms, None);
assert_eq!(cfg.max_loop_iterations, None);
assert_eq!(cfg.max_recursion_depth, None);
assert_eq!(cfg.max_stack_size, None);
}
#[test]
fn js_deserialises_full_block() {
let toml = toml::toml! {
execution-timeout-ms = 5000i64
max-loop-iterations = 1000000i64
max-recursion-depth = 64i64
max-stack-size = 1048576i64
};
let cfg: JsLimitsConfig = toml.try_into().expect("deserialize");
assert_eq!(cfg.execution_timeout_ms, Some(5000));
assert_eq!(cfg.max_loop_iterations, Some(1_000_000));
assert_eq!(cfg.max_recursion_depth, Some(64));
assert_eq!(cfg.max_stack_size, Some(1_048_576));
}
#[test]
fn js_deserialises_partial_block() {
let toml = toml::toml! {
execution-timeout-ms = 3000i64
max-recursion-depth = 32i64
};
let cfg: JsLimitsConfig = toml.try_into().expect("deserialize");
assert_eq!(cfg.execution_timeout_ms, Some(3000));
assert_eq!(cfg.max_recursion_depth, Some(32));
assert_eq!(cfg.max_loop_iterations, None);
assert_eq!(cfg.max_stack_size, None);
}
#[test]
fn js_rejects_unknown_field() {
let toml = toml::toml! {
execution-timeout-ms = 5000i64
fuel = 1000i64
};
let result: Result<JsLimitsConfig, _> = toml.try_into();
assert!(result.is_err(), "deny_unknown_fields must reject `fuel`");
}
#[test]
fn js_serde_round_trip_preserves_set_fields() {
let original = JsLimitsConfig {
execution_timeout_ms: Some(10_000),
max_loop_iterations: Some(500_000),
..Default::default()
};
let serialized = toml::to_string(&original).expect("serialize");
let back: JsLimitsConfig = toml::from_str(&serialized).expect("deserialize");
assert_eq!(original, back);
}
#[test]
fn js_skip_serializing_none_fields() {
let cfg = JsLimitsConfig {
execution_timeout_ms: Some(5000),
max_loop_iterations: Some(1_000_000),
..Default::default()
};
let s = toml::to_string(&cfg).expect("serialize");
assert!(s.contains("execution-timeout-ms"));
assert!(s.contains("max-loop-iterations"));
assert!(!s.contains("max-recursion-depth"));
assert!(!s.contains("max-stack-size"));
}
#[test]
fn rhai_engine_config_defaults() {
let cfg = RhaiEngineConfig::default();
assert_eq!(cfg.limits, RhaiLimitsConfig::default());
}
#[test]
fn js_engine_config_defaults() {
let cfg = JsEngineConfig::default();
assert_eq!(cfg.limits, JsLimitsConfig::default());
}
#[test]
fn languages_config_defaults() {
let cfg = LanguagesConfig::default();
assert_eq!(cfg.rhai.limits, RhaiLimitsConfig::default());
assert_eq!(cfg.js.limits, JsLimitsConfig::default());
}
#[test]
fn languages_deserialises_both_engines() {
let toml_str = r#"
[rhai.limits]
max-operations = 500000
execution-timeout-ms = 5000
[js.limits]
execution-timeout-ms = 3000
max-loop-iterations = 1000000
"#;
let cfg: LanguagesConfig = toml::from_str(toml_str).expect("deserialize");
assert_eq!(cfg.rhai.limits.max_operations, Some(500_000));
assert_eq!(cfg.rhai.limits.execution_timeout_ms, Some(5000));
assert_eq!(cfg.js.limits.execution_timeout_ms, Some(3000));
assert_eq!(cfg.js.limits.max_loop_iterations, Some(1_000_000));
}
#[test]
fn languages_serde_round_trip() {
let original = LanguagesConfig {
rhai: RhaiEngineConfig {
limits: RhaiLimitsConfig {
max_operations: Some(100_000),
..Default::default()
},
},
js: JsEngineConfig::default(),
};
let serialized = toml::to_string(&original).expect("serialize");
let back: LanguagesConfig = toml::from_str(&serialized).expect("deserialize");
assert_eq!(original, back);
}
}