scientific_workflow/
rng_record.rs1use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10use thiserror::Error;
11
12pub const RNG_RECORDS_METADATA_KEY: &str = "rng_records";
14
15#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
22#[serde(deny_unknown_fields)]
23pub struct RngRecord {
24 namespace: String,
25 method: String,
26 version: String,
27 key_encoding: String,
28 key: String,
29 #[serde(default, skip_serializing_if = "Map::is_empty")]
30 parameters: Map<String, Value>,
31}
32
33impl RngRecord {
34 pub fn new(
36 namespace: impl Into<String>,
37 method: impl Into<String>,
38 version: impl Into<String>,
39 key_encoding: impl Into<String>,
40 key: impl Into<String>,
41 ) -> Result<Self, RngRecordError> {
42 let record = Self {
43 namespace: namespace.into(),
44 method: method.into(),
45 version: version.into(),
46 key_encoding: key_encoding.into(),
47 key: key.into(),
48 parameters: Map::new(),
49 };
50 record.validate()?;
51 Ok(record)
52 }
53
54 #[must_use]
56 pub fn with_parameters(mut self, parameters: Map<String, Value>) -> Self {
57 self.parameters = parameters;
58 self
59 }
60
61 pub fn namespace(&self) -> &str {
63 &self.namespace
64 }
65
66 pub fn method(&self) -> &str {
68 &self.method
69 }
70
71 pub fn version(&self) -> &str {
73 &self.version
74 }
75
76 pub fn key_encoding(&self) -> &str {
78 &self.key_encoding
79 }
80
81 pub fn key(&self) -> &str {
83 &self.key
84 }
85
86 pub const fn parameters(&self) -> &Map<String, Value> {
88 &self.parameters
89 }
90
91 pub fn insert_into_metadata(
96 &self,
97 metadata: &mut Map<String, Value>,
98 ) -> Result<(), RngRecordError> {
99 self.validate()?;
100 let records = metadata
101 .entry(RNG_RECORDS_METADATA_KEY.to_owned())
102 .or_insert_with(|| Value::Object(Map::new()))
103 .as_object_mut()
104 .ok_or(RngRecordError::InvalidMetadataShape)?;
105 if records.contains_key(&self.namespace) {
106 return Err(RngRecordError::DuplicateNamespace {
107 namespace: self.namespace.clone(),
108 });
109 }
110 records.insert(
111 self.namespace.clone(),
112 serde_json::to_value(self).expect("RNG records contain only JSON-compatible values"),
113 );
114 Ok(())
115 }
116
117 pub fn from_metadata(
119 metadata: &Map<String, Value>,
120 namespace: &str,
121 ) -> Result<Option<Self>, RngRecordError> {
122 let Some(value) = metadata.get(RNG_RECORDS_METADATA_KEY) else {
123 return Ok(None);
124 };
125 let records = value
126 .as_object()
127 .ok_or(RngRecordError::InvalidMetadataShape)?;
128 let Some(value) = records.get(namespace) else {
129 return Ok(None);
130 };
131 let record: Self = serde_json::from_value(value.clone()).map_err(|source| {
132 RngRecordError::InvalidStoredRecord {
133 namespace: namespace.to_owned(),
134 source,
135 }
136 })?;
137 record.validate()?;
138 if record.namespace != namespace {
139 return Err(RngRecordError::NamespaceMismatch {
140 index: namespace.to_owned(),
141 record: record.namespace,
142 });
143 }
144 Ok(Some(record))
145 }
146
147 fn validate(&self) -> Result<(), RngRecordError> {
148 for (field, value) in [
149 ("namespace", self.namespace.as_str()),
150 ("method", self.method.as_str()),
151 ("version", self.version.as_str()),
152 ("key_encoding", self.key_encoding.as_str()),
153 ("key", self.key.as_str()),
154 ] {
155 if value.trim().is_empty() {
156 return Err(RngRecordError::EmptyField { field });
157 }
158 }
159 Ok(())
160 }
161}
162
163#[derive(Debug, Error)]
165#[non_exhaustive]
166pub enum RngRecordError {
167 #[error("RNG record field `{field}` must not be empty")]
169 EmptyField {
170 field: &'static str,
172 },
173 #[error("RNG namespace `{namespace}` is recorded more than once")]
175 DuplicateNamespace {
176 namespace: String,
178 },
179 #[error("user metadata `{RNG_RECORDS_METADATA_KEY}` entry must be an object")]
181 InvalidMetadataShape,
182 #[error("invalid RNG record for namespace `{namespace}`")]
184 InvalidStoredRecord {
185 namespace: String,
187 #[source]
189 source: serde_json::Error,
190 },
191 #[error("RNG metadata index `{index}` contains record namespace `{record}`")]
193 NamespaceMismatch {
194 index: String,
196 record: String,
198 },
199}