Skip to main content

vertep_core/
null.rs

1//! The `null` datamodel (W3C SCXML B.1) and the registry helpers that make it always available.
2
3use alloc::boxed::Box;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use crate::compile::CompiledChart;
8use crate::datamodel::{
9    Datamodel, DatamodelRegistry, Environment, EvalError, Event, SessionInfo, StateQuery, Value,
10};
11use crate::ir::Chart;
12use crate::session::{Outcome, StartError, StartInput, start};
13
14/// The name of the `null` datamodel.
15pub const NULL_DATAMODEL: &str = "null";
16
17/// The `null` datamodel: no data, and only `In('id')` conditions. Every other condition, value
18/// expression, assignment, script and `foreach` is an evaluation error. Inline content (of
19/// `<content>`) is not an expression and is passed through as a string.
20// @tg: implements:: [[openspec:datamodel#Null Datamodel]]
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub struct NullDatamodel;
23
24impl NullDatamodel {
25    /// A new `null` datamodel.
26    pub fn new() -> Self {
27        NullDatamodel
28    }
29}
30
31/// Parse `In('id')` or `In("id")` (surrounding whitespace allowed) and return `id`.
32// @tg: implements:: [[openspec:datamodel#In Predicate]]
33pub fn parse_in_predicate(expr: &str) -> Option<&str> {
34    let e = expr.trim();
35    let inner = e.strip_prefix("In")?.trim_start();
36    let inner = inner.strip_prefix('(')?.strip_suffix(')')?.trim();
37    let quote = inner.chars().next()?;
38    if quote != '\'' && quote != '"' {
39        return None;
40    }
41    let id = inner.strip_prefix(quote)?.strip_suffix(quote)?;
42    if id.is_empty() || id.contains(quote) {
43        return None;
44    }
45    Some(id)
46}
47
48fn unsupported(what: &str) -> EvalError {
49    EvalError::new(alloc::format!("the null datamodel does not support {what}"))
50}
51
52impl Datamodel for NullDatamodel {
53    fn name(&self) -> &str {
54        NULL_DATAMODEL
55    }
56
57    fn init_session(&mut self, _info: &SessionInfo) -> Result<(), EvalError> {
58        Ok(())
59    }
60
61    fn set_environment(&mut self, _env: &Environment) {}
62
63    fn declare(&mut self, _id: &str) -> Result<(), EvalError> {
64        Err(unsupported("data"))
65    }
66
67    fn set_event(&mut self, _event: &Event) {}
68
69    fn eval_cond(&mut self, expr: &str, states: &dyn StateQuery) -> Result<bool, EvalError> {
70        match parse_in_predicate(expr) {
71            Some(id) => Ok(states.in_state(id)),
72            None => Err(unsupported("conditions other than In()")),
73        }
74    }
75
76    fn eval_value(&mut self, _expr: &str, _states: &dyn StateQuery) -> Result<Value, EvalError> {
77        Err(unsupported("value expressions"))
78    }
79
80    fn eval_content(&mut self, content: &str) -> Result<Value, EvalError> {
81        Ok(Value::String(String::from(content)))
82    }
83
84    fn assign_expr(
85        &mut self,
86        _location: &str,
87        _expr: &str,
88        _states: &dyn StateQuery,
89    ) -> Result<(), EvalError> {
90        Err(unsupported("assign"))
91    }
92
93    fn assign_value(&mut self, _location: &str, _value: Value) -> Result<(), EvalError> {
94        Err(unsupported("assign"))
95    }
96
97    fn bind_var(&mut self, _name: &str, _value: Value) -> Result<(), EvalError> {
98        Err(unsupported("variables"))
99    }
100
101    fn is_location(&mut self, _location: &str) -> bool {
102        false
103    }
104
105    fn exec_script(&mut self, _script: &str, _states: &dyn StateQuery) -> Result<(), EvalError> {
106        Err(unsupported("script"))
107    }
108
109    fn foreach_items(
110        &mut self,
111        _array: &str,
112        _states: &dyn StateQuery,
113    ) -> Result<Vec<Value>, EvalError> {
114        Err(unsupported("foreach"))
115    }
116
117    /// The `null` datamodel has no state: its snapshot is the empty value (undefined).
118    // @tg: implements:: [[openspec:datamodel#Datamodel Snapshot]] {confidence: partial, note: "null datamodel only"}
119    fn snapshot(&self) -> Value {
120        Value::Undefined
121    }
122
123    fn restore(&mut self, snapshot: &Value) -> Result<(), EvalError> {
124        match snapshot {
125            Value::Undefined => Ok(()),
126            _ => Err(EvalError::new("a null datamodel snapshot is undefined")),
127        }
128    }
129}
130
131/// A registry with only the `null` datamodel.
132#[derive(Clone, Copy, Debug, Default)]
133pub struct NullRegistry;
134
135impl DatamodelRegistry for NullRegistry {
136    fn create(&self, name: &str) -> Option<Box<dyn Datamodel>> {
137        if name == NULL_DATAMODEL || name.is_empty() {
138            Some(Box::new(NullDatamodel))
139        } else {
140            None
141        }
142    }
143}
144
145/// Wraps a host registry so that `null` is always available.
146#[derive(Clone, Debug, Default)]
147pub struct WithNull<R>(pub R);
148
149impl<R: DatamodelRegistry> DatamodelRegistry for WithNull<R> {
150    fn create(&self, name: &str) -> Option<Box<dyn Datamodel>> {
151        self.0.create(name).or_else(|| NullRegistry.create(name))
152    }
153
154    fn create_for(&self, chart: &Chart) -> Option<Result<Box<dyn Datamodel>, Vec<String>>> {
155        self.0
156            .create_for(chart)
157            .or_else(|| NullRegistry.create_for(chart))
158    }
159
160    fn check(&self, chart: &Chart) -> Result<(), Vec<String>> {
161        self.0.check(chart)
162    }
163}
164
165/// Create the datamodel a chart names (`null` when it names none) from the host's registry, or
166/// fail with [`StartError::UnsupportedDatamodel`]. `null` is available even when the registry
167/// does not list it.
168// @tg: implements:: [[openspec:datamodel#Datamodel Registry]]
169pub fn create_datamodel(
170    registry: &dyn DatamodelRegistry,
171    name: &str,
172) -> Result<Box<dyn Datamodel>, StartError> {
173    let name = if name.is_empty() {
174        NULL_DATAMODEL
175    } else {
176        name
177    };
178    registry
179        .create(name)
180        .or_else(|| NullRegistry.create(name))
181        .ok_or_else(|| StartError::UnsupportedDatamodel {
182            name: String::from(name),
183        })
184}
185
186/// Create the datamodel for `chart` from the host's registry through
187/// [`DatamodelRegistry::create_for`], so a datamodel that checks charts statically gets the
188/// whole chart: [`StartError::ChartRefused`] when it refuses the chart, and otherwise as
189/// [`create_datamodel`] (`null` when the chart names none, `null` always available,
190/// [`StartError::UnsupportedDatamodel`] for an unregistered name).
191// @tg: implements:: [[openspec:datamodel#Datamodel Registry]]
192pub fn create_datamodel_for(
193    registry: &dyn DatamodelRegistry,
194    chart: &Chart,
195) -> Result<Box<dyn Datamodel>, StartError> {
196    if chart.datamodel.is_empty() {
197        return create_datamodel(registry, &chart.datamodel);
198    }
199    match registry.create_for(chart) {
200        Some(Ok(dm)) => Ok(dm),
201        Some(Err(problems)) => Err(StartError::ChartRefused {
202            datamodel: chart.datamodel.clone(),
203            problems,
204        }),
205        None => create_datamodel(registry, &chart.datamodel),
206    }
207}
208
209/// Select the chart's datamodel from the registry and start a session with it. Returns the
210/// outcome and the datamodel instance the session must keep using.
211pub fn start_with_registry(
212    chart: &CompiledChart,
213    registry: &dyn DatamodelRegistry,
214    input: StartInput,
215) -> Result<(Outcome, Box<dyn Datamodel>), StartError> {
216    let mut dm = create_datamodel_for(registry, chart.chart())?;
217    let outcome = start(chart, input, dm.as_mut())?;
218    Ok((outcome, dm))
219}