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!("key '{}' has no value, cannot check contains", pre.key)),
56            Some(val) => {
57                let val_str = match &val {
58                    Value::String(s) => s.clone(),
59                    other => other.to_string(),
60                };
61                let needle = match &pre.value {
62                    Value::String(s) => s.clone(),
63                    other => other.to_string(),
64                };
65                if val_str.contains(&needle) {
66                    None
67                } else {
68                    Some(format!("key '{}' does not contain {:?}", pre.key, pre.value))
69                }
70            }
71        };
72    }
73
74    // For value-comparison operators, we need the current value
75    if state.is_unknown(&pre.key) {
76        return None; // can't disprove, assume ok
77    }
78
79    let current = state.get_value(&pre.key);
80    if current.is_none() {
81        return Some(format!("key '{}' has no value", pre.key));
82    }
83    let current = current.unwrap();
84
85    match op {
86        "eq" => {
87            if current != pre.value {
88                Some(format!("'{}' is {:?}, need {:?}", pre.key, current, pre.value))
89            } else {
90                None
91            }
92        }
93        "neq" => {
94            if current == pre.value {
95                Some(format!("'{}' is {:?}, must not be", pre.key, current))
96            } else {
97                None
98            }
99        }
100        "gt" | "lt" | "gte" | "lte" => {
101            let c = current.as_f64();
102            let e = pre.value.as_f64();
103            match (c, e) {
104                (Some(c), Some(e)) => {
105                    let pass = match op {
106                        "gt" => c > e,
107                        "lt" => c < e,
108                        "gte" => c >= e,
109                        "lte" => c <= e,
110                        _ => unreachable!(),
111                    };
112                    if !pass {
113                        Some(format!("'{}' is {:?}, need {} {:?}", pre.key, current, op, pre.value))
114                    } else {
115                        None
116                    }
117                }
118                _ => Some(format!("cannot compare '{}': {:?} {} {:?}", pre.key, current, op, pre.value)),
119            }
120        }
121        _ => Some(format!("unknown operator '{}'", op)),
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use std::collections::HashMap;
129
130    /// Simple in-memory state for testing.
131    struct TestState {
132        data: HashMap<String, Value>,
133    }
134
135    impl StateView for TestState {
136        fn get_value(&self, key: &str) -> Option<Value> {
137            self.data.get(key).cloned()
138        }
139        fn key_exists(&self, key: &str) -> bool {
140            self.data.contains_key(key)
141        }
142    }
143
144    fn pre(key: &str, op: &str, value: Value) -> Precondition {
145        Precondition {
146            key: key.to_string(),
147            operator: op.to_string(),
148            value,
149            description: String::new(),
150        }
151    }
152
153    #[test]
154    fn exists_passes_when_present() {
155        let state = TestState { data: [("x".into(), Value::from(1))].into() };
156        assert!(check_precondition(&pre("x", "exists", Value::Null), &state).is_none());
157    }
158
159    #[test]
160    fn exists_fails_when_absent() {
161        let state = TestState { data: HashMap::new() };
162        assert!(check_precondition(&pre("x", "exists", Value::Null), &state).is_some());
163    }
164
165    #[test]
166    fn not_exists_passes_when_absent() {
167        let state = TestState { data: HashMap::new() };
168        assert!(check_precondition(&pre("x", "not_exists", Value::Null), &state).is_none());
169    }
170
171    #[test]
172    fn eq_passes() {
173        let state = TestState { data: [("x".into(), Value::from(42))].into() };
174        assert!(check_precondition(&pre("x", "eq", Value::from(42)), &state).is_none());
175    }
176
177    #[test]
178    fn eq_fails() {
179        let state = TestState { data: [("x".into(), Value::from(1))].into() };
180        assert!(check_precondition(&pre("x", "eq", Value::from(2)), &state).is_some());
181    }
182
183    #[test]
184    fn neq_passes() {
185        let state = TestState { data: [("x".into(), Value::from(1))].into() };
186        assert!(check_precondition(&pre("x", "neq", Value::from(2)), &state).is_none());
187    }
188
189    #[test]
190    fn gt_lt_gte_lte() {
191        let state = TestState { data: [("n".into(), Value::from(10))].into() };
192        assert!(check_precondition(&pre("n", "gt", Value::from(5)), &state).is_none());
193        assert!(check_precondition(&pre("n", "gt", Value::from(20)), &state).is_some());
194        assert!(check_precondition(&pre("n", "lt", Value::from(20)), &state).is_none());
195        assert!(check_precondition(&pre("n", "gte", Value::from(10)), &state).is_none());
196        assert!(check_precondition(&pre("n", "lte", Value::from(10)), &state).is_none());
197    }
198
199    #[test]
200    fn contains_passes() {
201        let state = TestState { data: [("msg".into(), Value::from("hello world"))].into() };
202        assert!(check_precondition(&pre("msg", "contains", Value::from("world")), &state).is_none());
203    }
204
205    #[test]
206    fn contains_fails() {
207        let state = TestState { data: [("msg".into(), Value::from("hello"))].into() };
208        assert!(check_precondition(&pre("msg", "contains", Value::from("world")), &state).is_some());
209    }
210}