Skip to main content

car_ir/
precondition.rs

1//! Shared precondition checking logic.
2//!
3//! Both static verification (car-verify) and runtime validation (car-validator)
4//! need to check preconditions against state. This module provides a `StateView`
5//! trait and a generic `check_precondition` function that both can reuse.
6
7use crate::Precondition;
8use serde_json::Value;
9
10/// Abstraction over state access for precondition checking.
11///
12/// Implemented by both `car_verify::StaticState` (symbolic) and
13/// `car_state::StateStore` (runtime).
14pub trait StateView {
15    /// Get a value by key. Returns `None` if the key doesn't exist.
16    fn get_value(&self, key: &str) -> Option<Value>;
17
18    /// Check if a key exists in state.
19    fn key_exists(&self, key: &str) -> bool;
20
21    /// Whether a key's value is unknown (symbolic analysis only).
22    /// Returns `false` by default — runtime state is always known.
23    fn is_unknown(&self, _key: &str) -> bool {
24        false
25    }
26}
27
28/// Check a single precondition against a state view.
29///
30/// Returns `Some(error_message)` if the precondition fails, `None` if it passes.
31/// Supports operators: `exists`, `not_exists`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `contains`.
32pub fn check_precondition(pre: &Precondition, state: &dyn StateView) -> Option<String> {
33    let op = pre.operator.as_str();
34
35    if op == "exists" {
36        return if !state.key_exists(&pre.key) && !state.is_unknown(&pre.key) {
37            Some(format!("key '{}' does not exist", pre.key))
38        } else {
39            None
40        };
41    }
42
43    if op == "not_exists" {
44        return if state.key_exists(&pre.key) || state.is_unknown(&pre.key) {
45            Some(format!("key '{}' exists", pre.key))
46        } else {
47            None
48        };
49    }
50
51    if op == "contains" {
52        let current = state.get_value(&pre.key);
53        return match current {
54            None if state.is_unknown(&pre.key) => None,
55            None => Some(format!(
56                "key '{}' has no value, cannot check contains",
57                pre.key
58            )),
59            Some(val) => {
60                let val_str = match &val {
61                    Value::String(s) => s.clone(),
62                    other => other.to_string(),
63                };
64                let needle = match &pre.value {
65                    Value::String(s) => s.clone(),
66                    other => other.to_string(),
67                };
68                if val_str.contains(&needle) {
69                    None
70                } else {
71                    Some(format!(
72                        "key '{}' does not contain {:?}",
73                        pre.key, pre.value
74                    ))
75                }
76            }
77        };
78    }
79
80    // For value-comparison operators, we need the current value
81    if state.is_unknown(&pre.key) {
82        return None; // can't disprove, assume ok
83    }
84
85    let current = state.get_value(&pre.key);
86    if current.is_none() {
87        return Some(format!("key '{}' has no value", pre.key));
88    }
89    let current = current.unwrap();
90
91    match op {
92        "eq" => {
93            if current != pre.value {
94                Some(format!(
95                    "'{}' is {:?}, need {:?}",
96                    pre.key, current, pre.value
97                ))
98            } else {
99                None
100            }
101        }
102        "neq" => {
103            if current == pre.value {
104                Some(format!("'{}' is {:?}, must not be", pre.key, current))
105            } else {
106                None
107            }
108        }
109        "gt" | "lt" | "gte" | "lte" => {
110            let c = current.as_f64();
111            let e = pre.value.as_f64();
112            match (c, e) {
113                (Some(c), Some(e)) => {
114                    let pass = match op {
115                        "gt" => c > e,
116                        "lt" => c < e,
117                        "gte" => c >= e,
118                        "lte" => c <= e,
119                        _ => unreachable!(),
120                    };
121                    if !pass {
122                        Some(format!(
123                            "'{}' is {:?}, need {} {:?}",
124                            pre.key, current, op, pre.value
125                        ))
126                    } else {
127                        None
128                    }
129                }
130                _ => Some(format!(
131                    "cannot compare '{}': {:?} {} {:?}",
132                    pre.key, current, op, pre.value
133                )),
134            }
135        }
136        _ => Some(format!("unknown operator '{}'", op)),
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use std::collections::HashMap;
144
145    /// Simple in-memory state for testing.
146    struct TestState {
147        data: HashMap<String, Value>,
148    }
149
150    impl StateView for TestState {
151        fn get_value(&self, key: &str) -> Option<Value> {
152            self.data.get(key).cloned()
153        }
154        fn key_exists(&self, key: &str) -> bool {
155            self.data.contains_key(key)
156        }
157    }
158
159    fn pre(key: &str, op: &str, value: Value) -> Precondition {
160        Precondition {
161            key: key.to_string(),
162            operator: op.to_string(),
163            value,
164            description: String::new(),
165        }
166    }
167
168    #[test]
169    fn exists_passes_when_present() {
170        let state = TestState {
171            data: [("x".into(), Value::from(1))].into(),
172        };
173        assert!(check_precondition(&pre("x", "exists", Value::Null), &state).is_none());
174    }
175
176    #[test]
177    fn exists_fails_when_absent() {
178        let state = TestState {
179            data: HashMap::new(),
180        };
181        assert!(check_precondition(&pre("x", "exists", Value::Null), &state).is_some());
182    }
183
184    #[test]
185    fn not_exists_passes_when_absent() {
186        let state = TestState {
187            data: HashMap::new(),
188        };
189        assert!(check_precondition(&pre("x", "not_exists", Value::Null), &state).is_none());
190    }
191
192    #[test]
193    fn eq_passes() {
194        let state = TestState {
195            data: [("x".into(), Value::from(42))].into(),
196        };
197        assert!(check_precondition(&pre("x", "eq", Value::from(42)), &state).is_none());
198    }
199
200    #[test]
201    fn eq_fails() {
202        let state = TestState {
203            data: [("x".into(), Value::from(1))].into(),
204        };
205        assert!(check_precondition(&pre("x", "eq", Value::from(2)), &state).is_some());
206    }
207
208    #[test]
209    fn neq_passes() {
210        let state = TestState {
211            data: [("x".into(), Value::from(1))].into(),
212        };
213        assert!(check_precondition(&pre("x", "neq", Value::from(2)), &state).is_none());
214    }
215
216    #[test]
217    fn gt_lt_gte_lte() {
218        let state = TestState {
219            data: [("n".into(), Value::from(10))].into(),
220        };
221        assert!(check_precondition(&pre("n", "gt", Value::from(5)), &state).is_none());
222        assert!(check_precondition(&pre("n", "gt", Value::from(20)), &state).is_some());
223        assert!(check_precondition(&pre("n", "lt", Value::from(20)), &state).is_none());
224        assert!(check_precondition(&pre("n", "gte", Value::from(10)), &state).is_none());
225        assert!(check_precondition(&pre("n", "lte", Value::from(10)), &state).is_none());
226    }
227
228    #[test]
229    fn contains_passes() {
230        let state = TestState {
231            data: [("msg".into(), Value::from("hello world"))].into(),
232        };
233        assert!(
234            check_precondition(&pre("msg", "contains", Value::from("world")), &state).is_none()
235        );
236    }
237
238    #[test]
239    fn contains_fails() {
240        let state = TestState {
241            data: [("msg".into(), Value::from("hello"))].into(),
242        };
243        assert!(
244            check_precondition(&pre("msg", "contains", Value::from("world")), &state).is_some()
245        );
246    }
247}