Skip to main content

boxferry_model/
provenance.rs

1//! Source provenance attached to neutral-model values.
2
3use crate::ModelError;
4
5/// Caller-selected identity for one imported source.
6#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
7pub struct SourceId(String);
8
9impl SourceId {
10    /// Creates a non-empty source identity.
11    ///
12    /// # Errors
13    ///
14    /// Returns [`ModelError::EmptyValue`] for an empty identity and
15    /// [`ModelError::ContainsNul`] for text containing a NUL byte.
16    pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
17        let value = value.into();
18        validate_text("source identity", &value)?;
19        Ok(Self(value))
20    }
21
22    /// Returns the authored identity.
23    #[must_use]
24    pub fn as_str(&self) -> &str {
25        &self.0
26    }
27}
28
29/// Half-open byte range in one imported source.
30#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
31pub struct SourceSpan {
32    start: usize,
33    end: usize,
34}
35
36impl SourceSpan {
37    /// Creates a byte range whose end is not before its start.
38    ///
39    /// # Errors
40    ///
41    /// Returns [`ModelError::ReversedSpan`] when `end < start`.
42    pub const fn new(start: usize, end: usize) -> Result<Self, ModelError> {
43        if end < start {
44            return Err(ModelError::ReversedSpan { start, end });
45        }
46        Ok(Self { start, end })
47    }
48
49    /// Returns the inclusive start offset.
50    #[must_use]
51    pub const fn start(self) -> usize {
52        self.start
53    }
54
55    /// Returns the exclusive end offset.
56    #[must_use]
57    pub const fn end(self) -> usize {
58        self.end
59    }
60}
61
62/// Location from which a neutral-model value was derived.
63#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct Provenance {
65    kind: ProvenanceKind,
66    source: SourceId,
67    span: Option<SourceSpan>,
68}
69
70/// How a neutral-model value entered a conversion plan.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72#[non_exhaustive]
73pub enum ProvenanceKind {
74    /// Authored source document or native definition.
75    SourceDocument,
76    /// Effective state read from a running container environment.
77    RuntimeObservation,
78    /// Explicit caller or user override.
79    UserOverride,
80    /// Default introduced by a named implementation profile.
81    ImplementationDefault,
82    /// Value selected by an explicit conversion decision.
83    ConversionDecision,
84}
85
86impl Provenance {
87    /// Creates provenance for an entire source.
88    #[must_use]
89    pub const fn source(source: SourceId) -> Self {
90        Self {
91            kind: ProvenanceKind::SourceDocument,
92            source,
93            span: None,
94        }
95    }
96
97    /// Creates provenance for one byte range in a source.
98    #[must_use]
99    pub const fn spanned(source: SourceId, span: SourceSpan) -> Self {
100        Self {
101            kind: ProvenanceKind::SourceDocument,
102            source,
103            span: Some(span),
104        }
105    }
106
107    /// Creates provenance for effective state observed from a runtime resource.
108    #[must_use]
109    pub const fn runtime_observation(source: SourceId) -> Self {
110        Self {
111            kind: ProvenanceKind::RuntimeObservation,
112            source,
113            span: None,
114        }
115    }
116
117    /// Creates provenance for an explicit caller or user override.
118    #[must_use]
119    pub const fn user_override(source: SourceId) -> Self {
120        Self {
121            kind: ProvenanceKind::UserOverride,
122            source,
123            span: None,
124        }
125    }
126
127    /// Creates provenance for a default introduced by a named implementation profile.
128    #[must_use]
129    pub const fn implementation_default(source: SourceId) -> Self {
130        Self {
131            kind: ProvenanceKind::ImplementationDefault,
132            source,
133            span: None,
134        }
135    }
136
137    /// Creates provenance for a value chosen by an explicit conversion decision.
138    #[must_use]
139    pub const fn conversion_decision(source: SourceId) -> Self {
140        Self {
141            kind: ProvenanceKind::ConversionDecision,
142            source,
143            span: None,
144        }
145    }
146
147    /// Returns how this origin entered the conversion plan.
148    #[must_use]
149    pub const fn kind(&self) -> ProvenanceKind {
150        self.kind
151    }
152
153    /// Returns the source identity.
154    #[must_use]
155    pub const fn source_id(&self) -> &SourceId {
156        &self.source
157    }
158
159    /// Returns the optional byte range.
160    #[must_use]
161    pub const fn span(&self) -> Option<SourceSpan> {
162        self.span
163    }
164}
165
166/// A value and every source location that contributed to it.
167#[derive(Clone, Debug, Eq, PartialEq)]
168pub struct Sourced<T> {
169    value: T,
170    origins: Vec<Provenance>,
171}
172
173impl<T> Sourced<T> {
174    /// Creates a value without source provenance, such as a generated default.
175    #[must_use]
176    pub const fn generated(value: T) -> Self {
177        Self {
178            value,
179            origins: Vec::new(),
180        }
181    }
182
183    /// Creates a value with one source origin.
184    #[must_use]
185    pub fn from_source(value: T, origin: Provenance) -> Self {
186        Self {
187            value,
188            origins: vec![origin],
189        }
190    }
191
192    /// Adds another contributing origin in discovery order.
193    pub fn add_origin(&mut self, origin: Provenance) {
194        self.origins.push(origin);
195    }
196
197    /// Returns the value.
198    #[must_use]
199    pub const fn value(&self) -> &T {
200        &self.value
201    }
202
203    /// Returns all origins in discovery order.
204    #[must_use]
205    pub fn origins(&self) -> &[Provenance] {
206        &self.origins
207    }
208
209    /// Decomposes the sourced value.
210    #[must_use]
211    pub fn into_parts(self) -> (T, Vec<Provenance>) {
212        (self.value, self.origins)
213    }
214}
215
216fn validate_text(kind: &'static str, value: &str) -> Result<(), ModelError> {
217    if value.is_empty() {
218        return Err(ModelError::EmptyValue(kind));
219    }
220    if value.contains('\0') {
221        return Err(ModelError::ContainsNul(kind));
222    }
223    Ok(())
224}
225
226#[cfg(test)]
227mod tests {
228    use super::{Provenance, ProvenanceKind, SourceId, SourceSpan, Sourced};
229    use crate::ModelError;
230
231    #[test]
232    fn retains_multiple_origins_in_discovery_order() -> Result<(), String> {
233        let first_source = SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
234        let second_source = SourceId::new("compose.override.yaml").map_err(|error| error.to_string())?;
235        let first_span = SourceSpan::new(10, 20).map_err(|error| error.to_string())?;
236        let second_span = SourceSpan::new(30, 40).map_err(|error| error.to_string())?;
237        let mut value = Sourced::from_source("image", Provenance::spanned(first_source, first_span));
238        value.add_origin(Provenance::spanned(second_source, second_span));
239
240        assert_eq!(value.origins()[0].span(), Some(first_span));
241        assert_eq!(value.origins()[1].span(), Some(second_span));
242        Ok(())
243    }
244
245    #[test]
246    fn rejects_reversed_source_spans() {
247        assert!(matches!(
248            SourceSpan::new(20, 10),
249            Err(ModelError::ReversedSpan { start: 20, end: 10 })
250        ));
251    }
252
253    #[test]
254    fn distinguishes_runtime_observations_from_authored_sources_and_decisions() -> Result<(), String> {
255        let runtime = SourceId::new("runtime:container/web").map_err(|error| error.to_string())?;
256        let decision = SourceId::new("decision:working-directory").map_err(|error| error.to_string())?;
257        let runtime = Provenance::runtime_observation(runtime);
258        let decision = Provenance::conversion_decision(decision);
259
260        assert_eq!(runtime.kind(), ProvenanceKind::RuntimeObservation);
261        assert_eq!(runtime.span(), None);
262        assert_eq!(decision.kind(), ProvenanceKind::ConversionDecision);
263        Ok(())
264    }
265}