Skip to main content

axioval_engine/
source_metadata.rs

1//! Facts about whole sources: the file a host read a source from, the
2//! applications that wrote it, the schema it declares and the project it
3//! describes.
4//!
5//! Metadata is not an object fact and not part of a snapshot's identity:
6//! services bound to a snapshot stay valid whatever a host learns about the
7//! file. Each field is either unread (`None`), or read completely, holding
8//! every distinct value the source states, possibly none. An unread field is
9//! unknown, never "no value": a `source` selector over it is not evaluated.
10
11use std::collections::BTreeMap;
12
13use axioval_ir::SourceId;
14use axioval_ir::contract::SourceField;
15
16/// What is known about one source.
17#[derive(Clone, Debug, Default, Eq, PartialEq)]
18pub struct SourceMetadata {
19    fields: BTreeMap<SourceField, Vec<String>>,
20}
21
22impl SourceMetadata {
23    /// Metadata with no field read.
24    #[must_use]
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// States `field` as read completely, holding `values`.
30    ///
31    /// Values are kept sorted and distinct; blank ones state nothing and are
32    /// dropped, so a field of only blank values is read and empty. Stating a
33    /// field again replaces it.
34    #[must_use]
35    pub fn with<I, V>(mut self, field: SourceField, values: I) -> Self
36    where
37        I: IntoIterator<Item = V>,
38        V: Into<String>,
39    {
40        let mut values: Vec<String> = values
41            .into_iter()
42            .map(Into::into)
43            .filter(|value| !value.trim().is_empty())
44            .collect();
45        values.sort();
46        values.dedup();
47        self.fields.insert(field, values);
48        self
49    }
50
51    /// The values of `field`, sorted and distinct; `None` when it was never
52    /// read.
53    #[must_use]
54    pub fn values(&self, field: SourceField) -> Option<&[String]> {
55        self.fields.get(&field).map(Vec::as_slice)
56    }
57
58    /// Every read field with its values.
59    pub fn fields(&self) -> impl Iterator<Item = (SourceField, &[String])> {
60        self.fields
61            .iter()
62            .map(|(field, values)| (*field, values.as_slice()))
63    }
64
65    /// Both statements together.
66    ///
67    /// # Errors
68    ///
69    /// The first field both state with different values: two readers of
70    /// one source disagree, and neither is taken.
71    pub fn merged(mut self, other: Self) -> Result<Self, SourceField> {
72        for (field, values) in other.fields {
73            match self.fields.get(&field) {
74                Some(held) if *held != values => return Err(field),
75                Some(_) => {}
76                None => {
77                    self.fields.insert(field, values);
78                }
79            }
80        }
81        Ok(self)
82    }
83}
84
85/// The metadata of every source of a run.
86///
87/// Only the engine constructs this, from the evidence session, and registers
88/// it for the duration of one run, replacing any host-registered copy, as it
89/// does [`crate::SourceDisciplines`]. A source's schema comes from its
90/// snapshot unless the session states it. A source without an entry has no
91/// field read.
92#[derive(Clone, Debug, Default)]
93pub struct SourceMetadataIndex(BTreeMap<SourceId, SourceMetadata>);
94
95impl SourceMetadataIndex {
96    /// Indexes `metadata` by source.
97    ///
98    /// Capability tests construct it; a run always uses the runtime's own.
99    pub fn new(metadata: impl IntoIterator<Item = (SourceId, SourceMetadata)>) -> Self {
100        Self(metadata.into_iter().collect())
101    }
102
103    /// Everything known about `source`, if anything.
104    #[must_use]
105    pub fn of(&self, source: &SourceId) -> Option<&SourceMetadata> {
106        self.0.get(source)
107    }
108
109    /// The values of `field` for `source`; `None` when never read.
110    #[must_use]
111    pub fn values(&self, source: &SourceId, field: SourceField) -> Option<&[String]> {
112        self.of(source).and_then(|metadata| metadata.values(field))
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn values_are_sorted_distinct_and_never_blank() {
122        let metadata = SourceMetadata::new().with(SourceField::Application, ["b", " ", "a", "b"]);
123        assert_eq!(
124            metadata.values(SourceField::Application),
125            Some(&["a".to_owned(), "b".to_owned()][..])
126        );
127        assert_eq!(metadata.values(SourceField::Project), None);
128        let empty = SourceMetadata::new().with(SourceField::Project, [""]);
129        assert_eq!(empty.values(SourceField::Project), Some(&[][..]));
130    }
131
132    #[test]
133    fn merging_refuses_a_field_stated_differently() {
134        let adapter = SourceMetadata::new().with(SourceField::Application, ["Tool"]);
135        let host = SourceMetadata::new().with(SourceField::FileName, ["a.ifc"]);
136        let merged = adapter.clone().merged(host).unwrap();
137        assert_eq!(merged.values(SourceField::FileName).unwrap().len(), 1);
138        assert_eq!(
139            merged.clone().merged(adapter.clone()).unwrap(),
140            merged,
141            "an identical statement agrees"
142        );
143        let other = SourceMetadata::new().with(SourceField::Application, ["Other"]);
144        assert_eq!(adapter.merged(other), Err(SourceField::Application));
145    }
146}