Skip to main content

weavatrix_edit/
provenance.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2
3/// Evidence tier attached to an exact source edit.
4#[derive(Clone, Debug, Eq, Hash, PartialEq)]
5pub struct Provenance(ProvenanceValue);
6
7#[derive(Clone, Debug, Eq, Hash, PartialEq)]
8enum ProvenanceValue {
9    ExactLsp,
10    Resolved,
11    Extracted,
12    LexicalExact,
13    // The extra indirection keeps every applicable value allocation-free and
14    // the enum smaller than `String`; unknown wire values are rejected later.
15    #[allow(clippy::box_collection)]
16    Other(Box<String>),
17}
18
19impl Provenance {
20    pub const EXACT_LSP: &'static str = "EXACT_LSP";
21    pub const RESOLVED: &'static str = "RESOLVED";
22    pub const EXTRACTED: &'static str = "EXTRACTED";
23    pub const LEXICAL_EXACT: &'static str = "LEXICAL_EXACT";
24
25    #[must_use]
26    pub fn new(value: impl AsRef<str>) -> Self {
27        let value = value.as_ref();
28        Self(
29            known_value(value)
30                .unwrap_or_else(|| ProvenanceValue::Other(Box::new(value.to_owned()))),
31        )
32    }
33
34    fn from_owned(value: String) -> Self {
35        Self(known_value(&value).unwrap_or_else(|| ProvenanceValue::Other(Box::new(value))))
36    }
37
38    #[must_use]
39    pub fn as_str(&self) -> &str {
40        match &self.0 {
41            ProvenanceValue::ExactLsp => Self::EXACT_LSP,
42            ProvenanceValue::Resolved => Self::RESOLVED,
43            ProvenanceValue::Extracted => Self::EXTRACTED,
44            ProvenanceValue::LexicalExact => Self::LEXICAL_EXACT,
45            ProvenanceValue::Other(value) => value,
46        }
47    }
48
49    #[must_use]
50    pub const fn is_applicable(&self) -> bool {
51        !matches!(self.0, ProvenanceValue::Other(_))
52    }
53}
54
55fn known_value(value: &str) -> Option<ProvenanceValue> {
56    match value {
57        Provenance::EXACT_LSP => Some(ProvenanceValue::ExactLsp),
58        Provenance::RESOLVED => Some(ProvenanceValue::Resolved),
59        Provenance::EXTRACTED => Some(ProvenanceValue::Extracted),
60        Provenance::LEXICAL_EXACT => Some(ProvenanceValue::LexicalExact),
61        _ => None,
62    }
63}
64
65impl Serialize for Provenance {
66    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
67    where
68        S: Serializer,
69    {
70        serializer.serialize_str(self.as_str())
71    }
72}
73
74impl<'de> Deserialize<'de> for Provenance {
75    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
76    where
77        D: Deserializer<'de>,
78    {
79        String::deserialize(deserializer).map(Self::from_owned)
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::Provenance;
86
87    #[test]
88    fn known_values_are_compact_and_unknown_values_roundtrip() {
89        assert!(core::mem::size_of::<Provenance>() < core::mem::size_of::<String>());
90        assert!(Provenance::new(Provenance::EXACT_LSP).is_applicable());
91
92        let unknown = Provenance::new("FUTURE_TIER");
93        assert!(!unknown.is_applicable());
94        let encoded = blazingly_json::to_string(&unknown).unwrap();
95        assert_eq!(encoded, r#""FUTURE_TIER""#);
96        assert_eq!(
97            blazingly_json::from_str::<Provenance>(&encoded).unwrap(),
98            unknown
99        );
100    }
101}