Skip to main content

morphir_core/metadata/
assertion.rs

1//! Authored ownership and descriptive source claims.
2
3use super::{Fact, MetadataError};
4use crate::node_address::NodeUri;
5
6/// Stable identity of the containing document, supplied by its reader.
7///
8/// This is deliberately separate from a [`NodeUri`]: a graph statement may
9/// describe a node in another artifact while its own document retains ownership.
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
11pub struct DocumentId(String);
12
13impl DocumentId {
14    /// Create a document identity from a nonempty reader-provided ID.
15    pub fn new(id: impl Into<String>) -> Result<Self, MetadataError> {
16        let id = id.into();
17        if id.is_empty() {
18            Err(MetadataError::EmptyDocumentId)
19        } else {
20            Ok(Self(id))
21        }
22    }
23
24    /// The reader-provided stable identifier.
25    pub fn as_str(&self) -> &str {
26        &self.0
27    }
28}
29
30/// The semantic container where a document authored a fact.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum Carrier {
33    /// A Type or Value node's `attributes.facts` container.
34    AttributesFacts(NodeUri),
35    /// A specification node's `annotations.facts` container.
36    AnnotationsFacts(NodeUri),
37    /// The containing document's `$meta.@graph` container.
38    DocumentGraph,
39    /// One V3/V4 decorator sidecar entry after upstream target and value validation.
40    Sidecar {
41        /// The entry's addressed target node.
42        target: NodeUri,
43        /// The entry point declaration that supplies its projected predicate.
44        entry_point: NodeUri,
45    },
46}
47
48impl Carrier {
49    fn identity(&self) -> String {
50        match self {
51            Self::AttributesFacts(uri) => serde_json::to_string(&("attributes", uri.to_string())),
52            Self::AnnotationsFacts(uri) => serde_json::to_string(&("annotations", uri.to_string())),
53            Self::DocumentGraph => serde_json::to_string(&("documentGraph",)),
54            Self::Sidecar {
55                target,
56                entry_point,
57            } => serde_json::to_string(&("sidecar", target.to_string(), entry_point.to_string())),
58        }
59        .expect("carrier identities serialize")
60    }
61}
62
63/// An authored assertion's alias-independent identity.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct AssertionKey {
66    owner: DocumentId,
67    carrier: Carrier,
68    fact: Fact,
69}
70
71impl AssertionKey {
72    /// Bind an expanded fact to its owning document and semantic carrier.
73    ///
74    /// Node-local facts must use their enclosing node as subject. A document
75    /// graph can assert a fact about any explicitly addressed node.
76    pub fn new(owner: DocumentId, carrier: Carrier, fact: Fact) -> Result<Self, MetadataError> {
77        match &carrier {
78            Carrier::AttributesFacts(uri) | Carrier::AnnotationsFacts(uri)
79                if uri != fact.subject() =>
80            {
81                Err(MetadataError::CarrierSubjectMismatch)
82            }
83            Carrier::Sidecar { target, .. } if target != fact.subject() => {
84                Err(MetadataError::CarrierSubjectMismatch)
85            }
86            _ => Ok(Self {
87                owner,
88                carrier,
89                fact,
90            }),
91        }
92    }
93
94    /// The document that contains this assertion.
95    pub fn owner(&self) -> &DocumentId {
96        &self.owner
97    }
98
99    /// The authored semantic container.
100    pub fn carrier(&self) -> &Carrier {
101        &self.carrier
102    }
103
104    /// The expanded graph fact.
105    pub fn fact(&self) -> &Fact {
106        &self.fact
107    }
108
109    pub(crate) fn identity(&self) -> String {
110        serde_json::to_string(&(
111            self.owner.as_str(),
112            self.carrier.identity(),
113            self.fact.identity(),
114        ))
115        .expect("assertion identities serialize")
116    }
117}
118
119/// A descriptive source claim; these labels do not prove authorship.
120#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
121pub enum AssertionSource {
122    /// Ordinary provenance from the containing document.
123    Document(DocumentId),
124    /// An optional compiler producer and source reference.
125    Compiler {
126        /// Producer name supplied by the writer.
127        producer: String,
128        /// Optional source location or producer reference.
129        reference: Option<String>,
130    },
131    /// An optional human author reference.
132    Author {
133        /// Writer-supplied reference, not an authenticated identity.
134        reference: String,
135    },
136}
137
138/// One authored assertion and its optional complete source override.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct Assertion {
141    key: AssertionKey,
142    source_override: Option<Vec<AssertionSource>>,
143}
144
145impl Assertion {
146    /// Create an assertion with ordinary document provenance only.
147    pub fn new(key: AssertionKey) -> Self {
148        Self {
149            key,
150            source_override: None,
151        }
152    }
153
154    /// Its document, carrier and expanded fact identity.
155    pub fn key(&self) -> &AssertionKey {
156        &self.key
157    }
158
159    /// Persisted claims other than the document source.
160    pub fn detail(&self) -> Vec<&AssertionSource> {
161        self.source_override
162            .iter()
163            .flatten()
164            .filter(|source| !matches!(source, AssertionSource::Document(_)))
165            .collect()
166    }
167
168    /// The explicit source override, if one was recorded in the document.
169    pub fn source_override(&self) -> Option<&[AssertionSource]> {
170        self.source_override.as_deref()
171    }
172
173    /// Add a compiler or author claim once. Document provenance is derived
174    /// solely from the assertion owner and cannot be supplied as extra detail.
175    pub fn add_detail(&mut self, source: AssertionSource) -> Result<(), MetadataError> {
176        if matches!(source, AssertionSource::Document(_)) {
177            return Err(MetadataError::DocumentProvenanceIsImplicit);
178        }
179        let mut sources = self.sources();
180        sources.push(source);
181        self.set_source_override(sources)?;
182        Ok(())
183    }
184
185    /// Sources visible to a query. The document is implicit only without an override.
186    pub fn sources(&self) -> Vec<AssertionSource> {
187        self.source_override
188            .clone()
189            .unwrap_or_else(|| vec![AssertionSource::Document(self.key.owner.clone())])
190    }
191
192    pub(crate) fn set_source_override(
193        &mut self,
194        mut sources: Vec<AssertionSource>,
195    ) -> Result<(), MetadataError> {
196        if sources.is_empty() {
197            return Err(MetadataError::EmptyAssertionSources);
198        }
199        if sources.iter().any(|source| {
200            matches!(source, AssertionSource::Document(owner) if owner != self.key.owner())
201        }) {
202            return Err(MetadataError::DocumentSourceOwnerMismatch);
203        }
204        sources.sort();
205        sources.dedup();
206        self.source_override = Some(sources);
207        Ok(())
208    }
209
210    pub(crate) fn merge_sources(&mut self, other: &Self) -> Result<(), MetadataError> {
211        if self.source_override.is_some() != other.source_override.is_some() {
212            return Err(MetadataError::SourceKnowledgeConflict(Box::new(
213                self.key.clone(),
214            )));
215        }
216        if let Some(sources) = &other.source_override {
217            let mut merged = self.source_override.clone().unwrap_or_default();
218            merged.extend(sources.iter().cloned());
219            self.set_source_override(merged)?;
220        }
221        Ok(())
222    }
223
224    pub(crate) fn with_key(mut self, key: AssertionKey) -> Self {
225        self.key = key;
226        self
227    }
228}
229
230/// One optional detailed-source table row selected by an expanded assertion key.
231///
232/// Codecs expand aliases before constructing this row; no serialized offset or
233/// compact alias participates in matching.
234///
235/// ```
236/// use morphir_core::metadata::{Assertion, AssertionKey, AssertionSource, Carrier,
237///     DocumentId, Fact, GraphIndex, GraphName, ObjectTerm, SourceRecord};
238/// use morphir_core::node_address::NodeUri;
239/// use serde_json::json;
240///
241/// let subject = NodeUri::parse(
242///     "morphir://ir/pkg/acme/orders?format=4.0.0#/module/api/value/submit-order"
243/// ).unwrap();
244/// let predicate = NodeUri::parse(
245///     "morphir://ir/pkg/acme/metadata?format=4.0.0#/module/lifecycle/value/deprecated"
246/// ).unwrap();
247/// let owner = DocumentId::new("orders/spec.json").unwrap();
248/// let fact = Fact::new(subject, predicate, ObjectTerm::value(json!(true)), GraphName::Default);
249/// let key = AssertionKey::new(owner.clone(), Carrier::DocumentGraph, fact).unwrap();
250/// let source = AssertionSource::Author { reference: "review/42".into() };
251/// let record = SourceRecord::new(key.clone(), vec![source.clone()]).unwrap();
252/// let mut graph = GraphIndex::new();
253/// graph.insert(Assertion::new(key)).unwrap();
254/// graph.apply_source_records(&owner, &[record]).unwrap();
255/// assert_eq!(graph.assertions()[0].sources(), vec![source]);
256/// ```
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct SourceRecord {
259    selector: AssertionKey,
260    sources: Vec<AssertionSource>,
261}
262
263impl SourceRecord {
264    /// Create a row with at least one tagged source.
265    pub fn new(
266        selector: AssertionKey,
267        sources: Vec<AssertionSource>,
268    ) -> Result<Self, MetadataError> {
269        let mut assertion = Assertion::new(selector.clone());
270        assertion.set_source_override(sources)?;
271        Ok(Self {
272            selector,
273            sources: assertion.sources(),
274        })
275    }
276
277    /// The owner, semantic carrier and expanded fact selected by this row.
278    pub fn selector(&self) -> &AssertionKey {
279        &self.selector
280    }
281
282    /// The complete source set, replacing the implicit document default.
283    pub fn sources(&self) -> &[AssertionSource] {
284        &self.sources
285    }
286}