Skip to main content

algocline_core/
spec.rs

1use serde::{Deserialize, Serialize};
2
3/// All information needed to start an execution.
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct ExecutionSpec {
6    /// Resolved Lua source code.
7    pub code: String,
8    /// Context passed to Lua as the `ctx` global.
9    pub ctx: serde_json::Value,
10    /// Namespace for alc.state (from ctx._ns, defaults to "default").
11    pub namespace: String,
12}
13
14impl ExecutionSpec {
15    /// Construct from code and ctx. Extracts namespace from ctx._ns
16    /// with "default" as fallback.
17    pub fn new(code: String, ctx: serde_json::Value) -> Self {
18        let namespace = ctx
19            .as_object()
20            .and_then(|o| o.get("_ns"))
21            .and_then(|v| v.as_str())
22            .unwrap_or("default")
23            .to_string();
24        Self {
25            code,
26            ctx,
27            namespace,
28        }
29    }
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35    use serde_json::json;
36
37    #[test]
38    fn namespace_from_ctx() {
39        let spec = ExecutionSpec::new("code".into(), json!({"_ns": "custom"}));
40        assert_eq!(spec.namespace, "custom");
41    }
42
43    #[test]
44    fn namespace_default_when_missing() {
45        let spec = ExecutionSpec::new("code".into(), json!({"key": "value"}));
46        assert_eq!(spec.namespace, "default");
47    }
48
49    #[test]
50    fn namespace_default_when_null_ctx() {
51        let spec = ExecutionSpec::new("code".into(), serde_json::Value::Null);
52        assert_eq!(spec.namespace, "default");
53    }
54
55    #[test]
56    fn namespace_default_when_ns_is_non_string() {
57        let spec = ExecutionSpec::new("code".into(), json!({"_ns": 42}));
58        assert_eq!(spec.namespace, "default");
59    }
60
61    #[test]
62    fn roundtrip_json() {
63        let spec = ExecutionSpec::new("return 1".into(), json!({"_ns": "test", "data": [1,2]}));
64        let json = serde_json::to_value(&spec).unwrap();
65        let restored: ExecutionSpec = serde_json::from_value(json).unwrap();
66        assert_eq!(restored.code, spec.code);
67        assert_eq!(restored.ctx, spec.ctx);
68        assert_eq!(restored.namespace, spec.namespace);
69    }
70}
71
72#[cfg(test)]
73mod proptests {
74    use super::*;
75    use proptest::prelude::*;
76
77    fn arb_json_value() -> impl Strategy<Value = serde_json::Value> {
78        prop_oneof![
79            Just(serde_json::Value::Null),
80            any::<bool>().prop_map(serde_json::Value::Bool),
81            any::<i64>().prop_map(|n| serde_json::json!(n)),
82            "\\PC{0,50}".prop_map(|s| serde_json::Value::String(s)),
83            Just(serde_json::json!([])),
84            Just(serde_json::json!({})),
85            Just(serde_json::json!({"key": "value"})),
86            "\\PC{1,30}".prop_map(|ns| serde_json::json!({"_ns": ns})),
87        ]
88    }
89
90    proptest! {
91        #[test]
92        fn new_never_panics(code in "\\PC{0,200}", ctx in arb_json_value()) {
93            let spec = ExecutionSpec::new(code, ctx);
94            // namespace must always be a non-empty string
95            prop_assert!(!spec.namespace.is_empty());
96        }
97
98        #[test]
99        fn namespace_is_ctx_ns_or_default(ns in "\\PC{1,30}") {
100            let ctx = serde_json::json!({"_ns": ns});
101            let spec = ExecutionSpec::new("code".into(), ctx);
102            prop_assert_eq!(&spec.namespace, &ns);
103        }
104
105        #[test]
106        fn serde_roundtrip(code in "\\PC{0,100}", ctx in arb_json_value()) {
107            let spec = ExecutionSpec::new(code, ctx);
108            let json = serde_json::to_value(&spec).unwrap();
109            let restored: ExecutionSpec = serde_json::from_value(json).unwrap();
110            prop_assert_eq!(&restored.code, &spec.code);
111            prop_assert_eq!(&restored.ctx, &spec.ctx);
112            prop_assert_eq!(&restored.namespace, &spec.namespace);
113        }
114    }
115}