use crate::ModelError;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SourceId(String);
impl SourceId {
pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
let value = value.into();
validate_text("source identity", &value)?;
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct SourceSpan {
start: usize,
end: usize,
}
impl SourceSpan {
pub const fn new(start: usize, end: usize) -> Result<Self, ModelError> {
if end < start {
return Err(ModelError::ReversedSpan { start, end });
}
Ok(Self { start, end })
}
#[must_use]
pub const fn start(self) -> usize {
self.start
}
#[must_use]
pub const fn end(self) -> usize {
self.end
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Provenance {
kind: ProvenanceKind,
source: SourceId,
span: Option<SourceSpan>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ProvenanceKind {
SourceDocument,
RuntimeObservation,
UserOverride,
ImplementationDefault,
ConversionDecision,
}
impl Provenance {
#[must_use]
pub const fn source(source: SourceId) -> Self {
Self {
kind: ProvenanceKind::SourceDocument,
source,
span: None,
}
}
#[must_use]
pub const fn spanned(source: SourceId, span: SourceSpan) -> Self {
Self {
kind: ProvenanceKind::SourceDocument,
source,
span: Some(span),
}
}
#[must_use]
pub const fn runtime_observation(source: SourceId) -> Self {
Self {
kind: ProvenanceKind::RuntimeObservation,
source,
span: None,
}
}
#[must_use]
pub const fn user_override(source: SourceId) -> Self {
Self {
kind: ProvenanceKind::UserOverride,
source,
span: None,
}
}
#[must_use]
pub const fn implementation_default(source: SourceId) -> Self {
Self {
kind: ProvenanceKind::ImplementationDefault,
source,
span: None,
}
}
#[must_use]
pub const fn conversion_decision(source: SourceId) -> Self {
Self {
kind: ProvenanceKind::ConversionDecision,
source,
span: None,
}
}
#[must_use]
pub const fn kind(&self) -> ProvenanceKind {
self.kind
}
#[must_use]
pub const fn source_id(&self) -> &SourceId {
&self.source
}
#[must_use]
pub const fn span(&self) -> Option<SourceSpan> {
self.span
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Sourced<T> {
value: T,
origins: Vec<Provenance>,
}
impl<T> Sourced<T> {
#[must_use]
pub const fn generated(value: T) -> Self {
Self {
value,
origins: Vec::new(),
}
}
#[must_use]
pub fn from_source(value: T, origin: Provenance) -> Self {
Self {
value,
origins: vec![origin],
}
}
pub fn add_origin(&mut self, origin: Provenance) {
self.origins.push(origin);
}
#[must_use]
pub const fn value(&self) -> &T {
&self.value
}
#[must_use]
pub fn origins(&self) -> &[Provenance] {
&self.origins
}
#[must_use]
pub fn into_parts(self) -> (T, Vec<Provenance>) {
(self.value, self.origins)
}
}
fn validate_text(kind: &'static str, value: &str) -> Result<(), ModelError> {
if value.is_empty() {
return Err(ModelError::EmptyValue(kind));
}
if value.contains('\0') {
return Err(ModelError::ContainsNul(kind));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{Provenance, ProvenanceKind, SourceId, SourceSpan, Sourced};
use crate::ModelError;
#[test]
fn retains_multiple_origins_in_discovery_order() -> Result<(), String> {
let first_source = SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
let second_source = SourceId::new("compose.override.yaml").map_err(|error| error.to_string())?;
let first_span = SourceSpan::new(10, 20).map_err(|error| error.to_string())?;
let second_span = SourceSpan::new(30, 40).map_err(|error| error.to_string())?;
let mut value = Sourced::from_source("image", Provenance::spanned(first_source, first_span));
value.add_origin(Provenance::spanned(second_source, second_span));
assert_eq!(value.origins()[0].span(), Some(first_span));
assert_eq!(value.origins()[1].span(), Some(second_span));
Ok(())
}
#[test]
fn rejects_reversed_source_spans() {
assert!(matches!(
SourceSpan::new(20, 10),
Err(ModelError::ReversedSpan { start: 20, end: 10 })
));
}
#[test]
fn distinguishes_runtime_observations_from_authored_sources_and_decisions() -> Result<(), String> {
let runtime = SourceId::new("runtime:container/web").map_err(|error| error.to_string())?;
let decision = SourceId::new("decision:working-directory").map_err(|error| error.to_string())?;
let runtime = Provenance::runtime_observation(runtime);
let decision = Provenance::conversion_decision(decision);
assert_eq!(runtime.kind(), ProvenanceKind::RuntimeObservation);
assert_eq!(runtime.span(), None);
assert_eq!(decision.kind(), ProvenanceKind::ConversionDecision);
Ok(())
}
}