1use super::digest::ContentDigest;
12use super::policy::{AnalysisPolicy, Fingerprint, PolicyField, ProfileId};
13use std::collections::BTreeMap;
14use std::fmt;
15use std::path::{Path, PathBuf};
16
17#[derive(Clone, PartialEq, Eq, Debug, Default)]
19#[non_exhaustive]
20pub enum SourceRef {
21 #[default]
23 None,
24 Path(PathBuf),
26 Url(Box<str>),
28 Memory,
30}
31
32impl SourceRef {
33 #[must_use]
35 pub fn path(path: impl Into<PathBuf>) -> Self {
36 Self::Path(path.into())
37 }
38
39 #[must_use]
41 pub fn as_path(&self) -> Option<&Path> {
42 match self {
43 Self::Path(path) => Some(path),
44 _ => None,
45 }
46 }
47}
48
49impl fmt::Display for SourceRef {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Self::None => f.write_str("(none)"),
53 Self::Path(path) => write!(f, "{}", path.display()),
54 Self::Url(url) => f.write_str(url),
55 Self::Memory => f.write_str("(memory)"),
56 }
57 }
58}
59
60#[derive(Clone, PartialEq, Eq, Debug)]
62pub struct DictionaryVersion(Box<str>);
63
64impl DictionaryVersion {
65 #[must_use]
67 pub fn new(version: impl Into<Box<str>>) -> Self {
68 Self(version.into())
69 }
70
71 #[must_use]
73 pub fn as_str(&self) -> &str {
74 &self.0
75 }
76}
77
78#[derive(Clone, PartialEq, Eq, Debug)]
80pub struct AlgorithmId {
81 name: Box<str>,
82 version: Box<str>,
83}
84
85impl AlgorithmId {
86 #[must_use]
88 pub fn new(name: impl Into<Box<str>>, version: impl Into<Box<str>>) -> Self {
89 Self {
90 name: name.into(),
91 version: version.into(),
92 }
93 }
94
95 #[must_use]
97 pub fn name(&self) -> &str {
98 &self.name
99 }
100
101 #[must_use]
103 pub fn version(&self) -> &str {
104 &self.version
105 }
106}
107
108#[derive(Clone, PartialEq, Eq, Debug)]
110pub enum ParameterValue {
111 Boolean(bool),
113 Integer(i64),
115 Float(u64),
117 Text(Box<str>),
119}
120
121impl ParameterValue {
122 #[must_use]
124 pub fn finite_float(value: f64) -> Option<Self> {
125 value.is_finite().then_some(Self::Float(value.to_bits()))
126 }
127
128 pub(crate) fn serialised(&self) -> String {
129 match self {
130 Self::Boolean(value) => format!("bool:{value}"),
131 Self::Integer(value) => format!("int:{value}"),
132 Self::Float(bits) => format!("float:{bits:016x}"),
133 Self::Text(value) => format!("text:{value}"),
134 }
135 }
136}
137
138const POLICY_READS: &str = "policy_reads";
140const ESTIMAND: &str = "estimand";
141
142pub type AnalysisParameters = BTreeMap<Box<str>, ParameterValue>;
144
145#[derive(Clone, Debug)]
159pub struct Provenance {
160 pub molframe_version: &'static str,
162 pub input_source: SourceRef,
164 pub input_digest: Option<ContentDigest>,
166 pub policy: AnalysisPolicy,
168 pub policy_fingerprint: Fingerprint,
170 pub profile: Option<ProfileId>,
172 pub algorithm: Option<AlgorithmId>,
174 pub parameters: AnalysisParameters,
176 pub schema_version: Option<DictionaryVersion>,
178 pub component_version: Option<DictionaryVersion>,
180 pub timestamp: Option<Box<str>>,
184}
185
186impl Provenance {
187 #[must_use]
189 pub fn new(policy: &AnalysisPolicy) -> Self {
190 Self {
191 molframe_version: env!("CARGO_PKG_VERSION"),
192 input_source: SourceRef::None,
193 input_digest: None,
194 policy: policy.clone(),
195 policy_fingerprint: policy.fingerprint(),
196 profile: policy.profile(),
197 algorithm: None,
198 parameters: BTreeMap::new(),
199 schema_version: None,
200 component_version: None,
201 timestamp: None,
202 }
203 }
204
205 #[must_use]
212 pub fn with_policy_reads(self, fields: &[PolicyField]) -> Self {
213 let mut names: Vec<&str> = fields.iter().map(|field| field.name()).collect();
214 names.sort_unstable();
215 names.dedup();
216 self.with_parameter(POLICY_READS, ParameterValue::Text(names.join(",").into()))
217 }
218
219 #[must_use]
221 pub fn policy_reads(&self) -> Option<Vec<PolicyField>> {
222 match self.parameters.get(POLICY_READS)? {
223 ParameterValue::Text(names) => Some(
224 names
225 .split(',')
226 .filter_map(PolicyField::from_name)
227 .collect(),
228 ),
229 _ => None,
230 }
231 }
232
233 #[must_use]
238 pub fn with_estimand(self, estimand: &str) -> Self {
239 self.with_parameter(ESTIMAND, ParameterValue::Text(estimand.into()))
240 }
241
242 #[must_use]
244 pub fn estimand(&self) -> Option<&str> {
245 match self.parameters.get(ESTIMAND)? {
246 ParameterValue::Text(text) => Some(text),
247 _ => None,
248 }
249 }
250
251 #[must_use]
253 pub fn with_source(mut self, source: SourceRef) -> Self {
254 self.input_source = source;
255 self
256 }
257
258 #[must_use]
260 pub const fn with_input_digest(mut self, digest: ContentDigest) -> Self {
261 self.input_digest = Some(digest);
262 self
263 }
264
265 #[must_use]
267 pub fn with_timestamp(mut self, timestamp: impl Into<Box<str>>) -> Self {
268 self.timestamp = Some(timestamp.into());
269 self
270 }
271
272 #[must_use]
274 pub fn with_algorithm(mut self, algorithm: AlgorithmId) -> Self {
275 self.algorithm = Some(algorithm);
276 self
277 }
278
279 #[must_use]
281 pub fn with_parameter(mut self, name: impl Into<Box<str>>, value: ParameterValue) -> Self {
282 self.parameters.insert(name.into(), value);
283 self
284 }
285
286 #[must_use]
292 pub fn fingerprint(&self) -> Fingerprint {
293 let mut bytes = Vec::with_capacity(64);
294 bytes.extend_from_slice(self.molframe_version.as_bytes());
295 bytes.extend_from_slice(&self.policy_fingerprint.get().to_le_bytes());
296 if let Some(input) = self.input_digest {
297 bytes.extend_from_slice(input.as_bytes());
298 }
299 if let Some(version) = &self.schema_version {
300 bytes.extend_from_slice(version.as_str().as_bytes());
301 }
302 if let Some(version) = &self.component_version {
303 bytes.extend_from_slice(version.as_str().as_bytes());
304 }
305 if let Some(algorithm) = &self.algorithm {
306 fingerprint_field(&mut bytes, b"algorithm.name", algorithm.name());
307 fingerprint_field(&mut bytes, b"algorithm.version", algorithm.version());
308 }
309 for (name, value) in &self.parameters {
310 fingerprint_field(&mut bytes, name.as_bytes(), &value.serialised());
311 }
312 Fingerprint::of(&bytes)
313 }
314}
315
316fn fingerprint_field(bytes: &mut Vec<u8>, name: &[u8], value: &str) {
317 bytes.extend_from_slice(&(name.len() as u64).to_le_bytes());
318 bytes.extend_from_slice(name);
319 bytes.extend_from_slice(&(value.len() as u64).to_le_bytes());
320 bytes.extend_from_slice(value.as_bytes());
321}
322
323#[cfg(test)]
324#[path = "provenance_tests.rs"]
325mod tests;