axioval_engine/
source_metadata.rs1use std::collections::BTreeMap;
12
13use axioval_ir::SourceId;
14use axioval_ir::contract::SourceField;
15
16#[derive(Clone, Debug, Default, Eq, PartialEq)]
18pub struct SourceMetadata {
19 fields: BTreeMap<SourceField, Vec<String>>,
20}
21
22impl SourceMetadata {
23 #[must_use]
25 pub fn new() -> Self {
26 Self::default()
27 }
28
29 #[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 #[must_use]
54 pub fn values(&self, field: SourceField) -> Option<&[String]> {
55 self.fields.get(&field).map(Vec::as_slice)
56 }
57
58 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 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#[derive(Clone, Debug, Default)]
93pub struct SourceMetadataIndex(BTreeMap<SourceId, SourceMetadata>);
94
95impl SourceMetadataIndex {
96 pub fn new(metadata: impl IntoIterator<Item = (SourceId, SourceMetadata)>) -> Self {
100 Self(metadata.into_iter().collect())
101 }
102
103 #[must_use]
105 pub fn of(&self, source: &SourceId) -> Option<&SourceMetadata> {
106 self.0.get(source)
107 }
108
109 #[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}