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}