Skip to main content

openquiver_runtime/
value.rs

1//! Values in flight.
2
3use openquiver_acp::SessionHandle;
4use openquiver_graph::Value;
5use serde::{Deserialize, Serialize};
6
7/// A value on its way from one pin to another.
8///
9/// Distinct from [`openquiver_graph::Value`], which is only ever a literal written
10/// in a file. This one also carries the handles a run produces, and every one of
11/// them has to survive being written to the journal and read back — which is why
12/// a session appears here as its handle and never as the object.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub enum RunValue {
15    Text(String),
16    Number(#[serde(serialize_with = "finite")] f64),
17    Bool(bool),
18    Array(Vec<RunValue>),
19    /// A connected agent, by the name it has in the project manifest.
20    Agent(String),
21    Session(SessionHandle),
22    /// The *name* of a secret, never its value.
23    ///
24    /// Resolution happens at the point of use, so a secret never reaches the
25    /// journal — and the journal is a file a colleague can be handed.
26    Secret(String),
27}
28
29/// Refuses a number JSON cannot hold.
30///
31/// `serde_json` turns NaN and infinity into `null` without complaining, which
32/// would write a `node.exited` that cannot be read back: a run recorded as
33/// having succeeded, and permanently unresumable. Nothing in this milestone
34/// produces one — the parser refuses non-finite literals — which is exactly when
35/// to close the hole, before something does.
36fn finite<S: serde::Serializer>(n: &f64, serializer: S) -> Result<S::Ok, S::Error> {
37    if !n.is_finite() {
38        return Err(serde::ser::Error::custom(format!(
39            "{n} is not a number a journal can hold"
40        )));
41    }
42    serializer.serialize_f64(*n)
43}
44
45impl From<&Value> for RunValue {
46    fn from(value: &Value) -> Self {
47        match value {
48            Value::Text(text) => RunValue::Text(text.clone()),
49            Value::Number(n) => RunValue::Number(*n),
50            Value::Bool(b) => RunValue::Bool(*b),
51            Value::Array(items) => RunValue::Array(items.iter().map(RunValue::from).collect()),
52            Value::Secret(name) => RunValue::Secret(name.clone()),
53        }
54    }
55}
56
57impl RunValue {
58    /// How this value reads on a `Text` pin.
59    ///
60    /// These are the conversions `openquiver_compile::accepts` lets through into
61    /// `Text`, for every type this milestone can put on a wire. A value the
62    /// compiler allowed and the runtime cannot render is a graph that passes
63    /// `check` and dies mid-run, so the two have to be read together whenever
64    /// either changes.
65    ///
66    /// `accepts` also passes `Wildcard`, which has no rendering because it has
67    /// no resolved type yet. Nothing produces one — wildcard resolution is not
68    /// built — and when it is, resolution has to happen before this point.
69    pub fn as_text(&self) -> Option<String> {
70        match self {
71            RunValue::Text(text) => Some(text.clone()),
72            // `to_string` on `f64` prints 3, not 3.0 — the same rendering the
73            // formatter gives a literal, so a number reads the same in the file
74            // and in the run.
75            RunValue::Number(n) => Some(n.to_string()),
76            RunValue::Bool(b) => Some(b.to_string()),
77            _ => None,
78        }
79    }
80
81    pub fn as_bool(&self) -> Option<bool> {
82        match self {
83            RunValue::Bool(b) => Some(*b),
84            _ => None,
85        }
86    }
87
88    pub fn as_agent(&self) -> Option<&str> {
89        match self {
90            RunValue::Agent(name) => Some(name),
91            _ => None,
92        }
93    }
94
95    pub fn as_session(&self) -> Option<&SessionHandle> {
96        match self {
97            RunValue::Session(handle) => Some(handle),
98            _ => None,
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::RunValue;
106
107    #[test]
108    fn a_number_json_cannot_hold_is_refused_rather_than_written_as_null() {
109        for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
110            let written = serde_json::to_value(RunValue::Number(bad));
111            assert!(
112                written.is_err(),
113                "{bad} was written as {written:?}, which reads back as nothing"
114            );
115        }
116    }
117
118    #[test]
119    fn an_ordinary_number_still_round_trips() {
120        let written = serde_json::to_value(RunValue::Number(3.5)).expect("write");
121        let read: RunValue = serde_json::from_value(written).expect("read");
122        assert_eq!(read, RunValue::Number(3.5));
123    }
124}