Skip to main content

triblespace_core/trible/
fragment.rs

1use std::ops::{Add, AddAssign, Deref};
2
3use crate::blob::{BlobEncoding, MemoryBlobStore, IntoBlob};
4use crate::id::Id;
5use crate::id::RawId;
6use crate::patch::Entry;
7use crate::patch::PATCH;
8use crate::inline::encodings::hash::Handle;
9use crate::inline::Inline;
10
11use super::Trible;
12use super::TribleSet;
13
14/// A rooted (or multi-root) fragment of a knowledge graph.
15///
16/// A fragment is a [`TribleSet`] plus a (possibly empty) set of "exported" entity
17/// ids that act as entry points into the contained facts, plus the
18/// [`MemoryBlobStore`] holding any bytes the contained facts reference
19/// by handle. Exports are not privileged in the graph model itself;
20/// they are simply the ids the producer wants to hand back to the
21/// caller as the fragment's interface.
22///
23/// The embedded blob store is what makes a Fragment *self-contained*:
24/// handles in the facts (e.g. `metadata::name: <Inline<Handle</// LongString>>>`) reference bytes that the fragment carries with
25/// itself. An empty `MemoryBlobStore` is structurally a single
26/// PATCH-root pointer — fragments without blobs pay essentially
27/// zero overhead.
28#[derive(Debug, Clone, Default, PartialEq, Eq)]
29pub struct Fragment {
30    exports: PATCH<16>,
31    facts: TribleSet,
32    blobs: MemoryBlobStore,
33}
34
35impl Fragment {
36    /// Creates an empty fragment with no exports and no facts.
37    pub fn empty() -> Self {
38        Self::default()
39    }
40
41    /// Creates a fragment that exports a single root id, with the
42    /// given facts and an empty blob store.
43    pub fn rooted(root: Id, facts: TribleSet) -> Self {
44        let mut exports = PATCH::<16>::new();
45        let raw: RawId = root.into();
46        exports.insert(&Entry::new(&raw));
47        Self {
48            exports,
49            facts,
50            blobs: MemoryBlobStore::new(),
51        }
52    }
53
54    /// Creates a fragment with the given exported ids and an empty blob store.
55    ///
56    /// Export ids are canonicalized as a set (duplicates are ignored). Empty
57    /// exports are allowed.
58    pub fn new<I>(exports: I, facts: TribleSet) -> Self
59    where
60        I: IntoIterator<Item = Id>,
61    {
62        let mut export_set = PATCH::<16>::new();
63        for id in exports {
64            let raw: RawId = id.into();
65            export_set.insert(&Entry::new(&raw));
66        }
67        Self {
68            exports: export_set,
69            facts,
70            blobs: MemoryBlobStore::new(),
71        }
72    }
73
74    /// Creates a fragment with no exports, holding the given facts and
75    /// blob store. Useful when re-wrapping the tail of a destructured
76    /// fragment (e.g. inside `Spread::spread`) where the exports have
77    /// already been consumed.
78    pub fn from_facts_and_blobs(facts: TribleSet, blobs: MemoryBlobStore) -> Self {
79        Self {
80            exports: PATCH::<16>::new(),
81            facts,
82            blobs,
83        }
84    }
85
86    /// Creates a fragment that exports a single root id, with the given
87    /// facts and blob store. The macro-generated `entity!{}` expansion
88    /// uses this to wrap its accumulated state — facts come from per-
89    /// attribute inserts, blobs come from any `field*: spread_source`
90    /// extras the spread sources carried with them.
91    pub fn rooted_with_blobs(
92        root: Id,
93        facts: TribleSet,
94        blobs: MemoryBlobStore,
95    ) -> Self {
96        let mut exports = PATCH::<16>::new();
97        let raw: RawId = root.into();
98        exports.insert(&Entry::new(&raw));
99        Self {
100            exports,
101            facts,
102            blobs,
103        }
104    }
105
106    /// Insert a blob into the fragment's local blob store and return the
107    /// content-addressed handle that references it.
108    ///
109    /// Use this when you want a Fragment to be self-contained — every
110    /// handle in its facts has its bytes available without consulting
111    /// an external blob store. Idempotent under content addressing:
112    /// putting the same bytes twice returns the same handle and
113    /// doesn't grow the store.
114    pub fn put<S, T>(&mut self, item: T) -> Inline<Handle<S>>
115    where
116        S: BlobEncoding,
117        T: IntoBlob<S>,
118    {
119        self.blobs.insert(item.to_blob())
120    }
121
122    /// Returns the exported ids for this fragment, in deterministic (lexicographic) order.
123    pub fn exports(&self) -> impl Iterator<Item = Id> + '_ {
124        self.exports
125            .iter_ordered()
126            .map(|raw| Id::new(*raw).expect("export ids are non-nil"))
127    }
128
129    /// Returns the single exported id if this fragment is rooted.
130    pub fn root(&self) -> Option<Id> {
131        if self.exports.len() == 1 {
132            let raw = self
133                .exports
134                .iter_ordered()
135                .next()
136                .expect("len() == 1 implies a first element exists");
137            Some(Id::new(*raw).expect("export ids are non-nil"))
138        } else {
139            None
140        }
141    }
142
143    pub fn facts(&self) -> &TribleSet {
144        &self.facts
145    }
146
147    /// Borrow the fragment's local blob store.
148    pub fn blobs(&self) -> &MemoryBlobStore {
149        &self.blobs
150    }
151
152    pub fn into_facts(self) -> TribleSet {
153        self.facts
154    }
155
156    /// Consume the fragment, yielding its facts and blob store. The
157    /// exports are dropped — most callers want facts/blobs together
158    /// without the rooted-id concern.
159    pub fn into_facts_and_blobs(self) -> (TribleSet, MemoryBlobStore) {
160        (self.facts, self.blobs)
161    }
162
163    pub fn into_parts(self) -> (PATCH<16>, TribleSet, MemoryBlobStore) {
164        (self.exports, self.facts, self.blobs)
165    }
166
167}
168
169impl Deref for Fragment {
170    type Target = TribleSet;
171
172    fn deref(&self) -> &Self::Target {
173        &self.facts
174    }
175}
176
177impl<'a> IntoIterator for &'a Fragment {
178    type Item = &'a Trible;
179    type IntoIter = super::tribleset::TribleSetIterator<'a>;
180
181    fn into_iter(self) -> Self::IntoIter {
182        self.facts.iter()
183    }
184}
185
186impl AddAssign for Fragment {
187    fn add_assign(&mut self, rhs: Self) {
188        self.facts += rhs.facts;
189        self.exports.union(rhs.exports);
190        self.blobs.union(rhs.blobs);
191    }
192}
193
194impl AddAssign<TribleSet> for Fragment {
195    /// Facts-only merge — does not touch exports or blobs.
196    fn add_assign(&mut self, rhs: TribleSet) {
197        self.facts += rhs;
198    }
199}
200
201impl Add for Fragment {
202    type Output = Self;
203
204    fn add(mut self, rhs: Self) -> Self::Output {
205        self += rhs;
206        self
207    }
208}
209
210impl Add<TribleSet> for Fragment {
211    type Output = Self;
212
213    fn add(mut self, rhs: TribleSet) -> Self::Output {
214        self += rhs;
215        self
216    }
217}
218
219impl AddAssign<Fragment> for TribleSet {
220    fn add_assign(&mut self, rhs: Fragment) {
221        self.union(rhs.facts);
222    }
223}
224
225impl Add<Fragment> for TribleSet {
226    type Output = Self;
227
228    fn add(mut self, rhs: Fragment) -> Self::Output {
229        self += rhs;
230        self
231    }
232}
233
234/// Lossless promotion: a `TribleSet` becomes a Fragment with no
235/// exported root and an empty blob store. The reverse direction is
236/// intentionally not implemented — going from `Fragment` to
237/// `TribleSet` discards the embedded blob store and exports, so it
238/// has to be explicit (`Fragment::into_facts`).
239impl From<TribleSet> for Fragment {
240    fn from(facts: TribleSet) -> Self {
241        Self::from_facts_and_blobs(facts, MemoryBlobStore::new())
242    }
243}
244
245impl From<Fragment> for TribleSet {
246    fn from(value: Fragment) -> Self {
247        value.facts
248    }
249}