triblespace_core/collection/encoding.rs
1//! Canonical encodings and mappings for typed collections.
2//!
3//! Durable collection records carry representation-neutral content hashes.
4//! This module restores their physical meaning at the API boundary without
5//! inventing a second runtime planner:
6//!
7//! - a [`CollectionEncoding`] owns the canonical bytes, validation, and join
8//! within one collection;
9//! - a [`CollectionDerivation`] lets one target encoding own its canonical,
10//! parameterized, join-preserving conversion from one source encoding;
11//! - a [`DeriveMapping`] is an explicit mapping whose images are derived into
12//! a collection of their own, and a [`MapMapping`] (or the target-owned
13//! [`CollectionAttachment`]) indexes another collection's nodes instead,
14//! into an attached collection;
15//! - [`Collection`] binds an encoding to one exact, content-addressed
16//! descriptor.
17//!
18//! Logical interpretation is deliberately separate in
19//! [`TryFromCover`](crate::collection::TryFromCover). Maintained-index views
20//! retain their mmap-backed shards and check only the framing, bounds and
21//! alignment needed to access them safely. Attachment does not re-prove
22//! canonical contents, serialize a logical union, or construct an accelerator.
23//! Query preparation may derive scratch data the encoding does not persist;
24//! that work belongs to the requested query, not to attaching the index.
25
26use std::error::Error;
27use std::fmt;
28use std::marker::PhantomData;
29
30use crate::blob::encodings::simplearchive::SimpleArchive;
31use crate::blob::{Blob, BlobEncoding};
32use crate::metadata::{self, MetaDescribe};
33use crate::prelude::{exists, pattern};
34use crate::repo::{BlobStoreGet, BlobStoreMeta, StoreRead};
35use crate::trible::Fragment;
36
37use super::{
38 collection_representation, CollectionData, CollectionHandle, KIND_COLLECTION_DESCRIPTOR,
39};
40
41/// Failure of one exact canonical collection operation.
42///
43/// `Capacity` is reserved for deterministic geometry limits of the chosen
44/// encoding. It must not describe transient allocation, I/O, or accelerator
45/// failures. Malformed or noncanonical bytes are always `Fatal`.
46#[derive(Clone, Debug, Eq, PartialEq)]
47pub enum CollectionOperationError {
48 /// The operation or supplied bytes are invalid and another cover cannot
49 /// repair it.
50 Fatal(String),
51 /// This exact encoding cannot hold the result, but a finer physical cover
52 /// may still represent the same logical value.
53 Capacity(String),
54 /// The canonical result names an immutable dependency which is not present
55 /// in the current snapshot. Storage may materialize or fetch it and retry
56 /// the otherwise pure operation.
57 MissingDependency(CollectionData),
58}
59
60impl fmt::Display for CollectionOperationError {
61 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 match self {
63 Self::Fatal(reason) | Self::Capacity(reason) => formatter.write_str(reason),
64 Self::MissingDependency(member) => write!(
65 formatter,
66 "collection operation requires resident blob {}",
67 hex::encode_upper(member.raw),
68 ),
69 }
70 }
71}
72
73impl Error for CollectionOperationError {}
74
75/// One canonical physical shape carried by a blob encoding.
76///
77/// Collection members are always ordinary typed [`Blob`] values. An encoding
78/// validates its own bytes and owns one canonical intra-shape join. Derived
79/// collections are ordinary lattices connected to their source by a
80/// join-preserving [`CollectionDerivation`], not a weaker kind of collection.
81/// The logical join remains total at the [`Cover`](super::Cover) level: when
82/// one physical member cannot represent the result, `Capacity` retains a finer
83/// cover of members with the same value.
84///
85/// This is intentionally stronger than [`BlobEncoding`]: not every blob format
86/// is a collection member, while every `CollectionEncoding` has an exact
87/// validation boundary.
88pub trait CollectionEncoding: BlobEncoding + MetaDescribe + Sized + 'static {
89 /// Validate encoding-specific context carried by one descriptor.
90 ///
91 /// Most encodings need no context. An encoding whose canonical bytes are
92 /// parameterized (for example a path summary over one automaton) validates
93 /// only the shape information it needs here; the source-to-target mapping
94 /// still owns the parameterized conversion itself.
95 fn validate_descriptor(_descriptor: &Fragment) -> Result<(), CollectionOperationError> {
96 Ok(())
97 }
98
99 /// Explicitly validate one member independently of its provenance.
100 ///
101 /// The root bytes have already passed the blob store's content-address
102 /// boundary. A Merkle encoding may inspect children through `reader`; a
103 /// monolithic encoding normally ignores it. Warm collection resolution
104 /// does not invoke this hook; it is available to producers, untrusted
105 /// ingress, and offline audits. It is not part of ordinary typed-view
106 /// construction, nor a requirement to check locally produced output again.
107 fn validate_member<R>(
108 descriptor: &Fragment,
109 member: &Blob<Self>,
110 reader: &R,
111 ) -> Result<(), CollectionOperationError>
112 where
113 R: BlobStoreGet + BlobStoreMeta;
114
115 /// Return immutable representation dependencies missing from a resident
116 /// member root in this snapshot.
117 ///
118 /// Self-contained encodings need no extra work. Merkle encodings override
119 /// this narrow availability query so cover resolution can ignore an
120 /// incomplete compacted root and fall back to a finer support-equivalent
121 /// cover. Returned handles are only the blobs required to interpret this
122 /// representation, not optional attachments or semantic provenance.
123 ///
124 /// This is not semantic validation: it neither recomputes the member nor
125 /// proves its canonical bytes. It may read the named resident root, but
126 /// must not request missing dependencies, persist, or otherwise change
127 /// storage.
128 fn missing_representation_dependencies<R>(
129 _member: CollectionData,
130 _reader: &R,
131 ) -> Result<Vec<CollectionData>, CollectionOperationError>
132 where
133 R: BlobStoreGet + BlobStoreMeta,
134 {
135 Ok(Vec::new())
136 }
137
138 /// Compute the exact canonical join of two members.
139 ///
140 /// The implementation owns decoding and rejecting malformed inputs while
141 /// it performs this new work; warm resolution never calls this method.
142 ///
143 /// `reader` resolves immutable content-addressed dependencies named by the
144 /// inputs or by their canonical result. Availability may delay publication
145 /// but cannot alter the result bytes. Other resident content is not an
146 /// input to the join.
147 ///
148 fn join_members<R>(
149 descriptor: &Fragment,
150 low: &Blob<Self>,
151 high: &Blob<Self>,
152 reader: &R,
153 ) -> Result<Blob<Self>, CollectionOperationError>
154 where
155 R: BlobStoreGet + BlobStoreMeta;
156
157 /// Compute the exact canonical join of one or more members: the k-way
158 /// join an n-ary MERGE states.
159 ///
160 /// The default folds [`Self::join_members`] left to right, which is the
161 /// same canonical result by associativity; an encoding with a cheaper
162 /// k-way join overrides it. The same availability rules apply.
163 fn join_many<R>(
164 descriptor: &Fragment,
165 members: &[Blob<Self>],
166 reader: &R,
167 ) -> Result<Blob<Self>, CollectionOperationError>
168 where
169 R: BlobStoreGet + BlobStoreMeta,
170 {
171 let (first, rest) = members.split_first().ok_or_else(|| {
172 CollectionOperationError::Fatal("a join needs at least one member".to_owned())
173 })?;
174 let mut joined = first.clone();
175 for member in rest {
176 joined = Self::join_members(descriptor, &joined, member, reader)?;
177 }
178 Ok(joined)
179 }
180}
181
182/// The canonical incoming derivation owned by one target encoding.
183///
184/// The target names its one source encoding and the runtime argument carried
185/// by the mapping fragment embedded in its descriptor. Canonical builders
186/// normally derive the mapping entity id, but binding validates its algorithm
187/// and argument rather than its minting history. Implementations must be a
188/// join homomorphism:
189///
190/// `map(a join b) = map(a) join map(b)`.
191pub trait CollectionDerivation: CollectionEncoding {
192 /// Canonical source encoding.
193 type Source: CollectionEncoding;
194
195 /// Runtime argument which distinguishes concrete mappings of this
196 /// canonical source-to-target relation.
197 type Argument;
198
199 /// Canonical concrete mapping fragment embedded in a target descriptor.
200 ///
201 /// Parameterized derivations carry their argument in this fragment;
202 /// parameter-free derivations use `()`.
203 fn fragment(argument: &Self::Argument) -> Fragment;
204
205 /// Bind and validate the concrete mapping named by the target descriptor.
206 fn bind(
207 source: &Fragment,
208 target: &Fragment,
209 ) -> Result<Self::Argument, CollectionOperationError>;
210
211 /// Compute the canonical target image of one source member.
212 ///
213 /// `reader` is the same frozen store observation from which the
214 /// source was loaded. The mapping owns decoding and rejecting malformed
215 /// input while it performs new work. It may use `reader` only to resolve
216 /// dependencies named by `source` or its concrete argument. An explicit
217 /// collection reference is resolved using this observation's records and
218 /// capability evidence; unrelated store contents are not semantic input.
219 fn map<R>(
220 argument: &Self::Argument,
221 source: &Blob<Self::Source>,
222 reader: &R,
223 ) -> Result<Blob<Self>, CollectionOperationError>
224 where
225 R: StoreRead;
226
227 /// Join target images: the k-way join a MERGE of the derived collection
228 /// states. See [`DeriveMapping::join_images`].
229 fn join_images<R>(
230 _argument: &Self::Argument,
231 target_descriptor: &Fragment,
232 images: &[Blob<Self>],
233 reader: &R,
234 ) -> Result<Blob<Self>, CollectionOperationError>
235 where
236 R: BlobStoreGet + BlobStoreMeta,
237 {
238 Self::join_many(target_descriptor, images, reader)
239 }
240}
241
242/// One explicit parameterized mapping between collection encodings, whose
243/// images are derived into a collection of their own.
244///
245/// Its images are published as `DERIVE` leaves of the derived collection and
246/// replicate with it. Only a mapping not every reader of a node can compute
247/// is derived; an index that every reader of a node can compute for itself
248/// is a [`MapMapping`] instead. The contract an implementor takes on:
249///
250/// - It maps foundations only: a commit payload of a root source, or a leaf
251/// image of a derived one. It never maps a merge result: the derived
252/// collection carries its own leaf images into a lattice of its own, joined
253/// with [`Self::join_images`], and never reads its source's merges.
254/// - The target encoding's join is total: any two members that are each
255/// valid join, so every set of leaves has a join, whoever derived them.
256/// Several leaves may name one source foundation -- two derivations that
257/// disagree, or a re-derivation -- and the join keeps what each says. A
258/// join that cannot be represented in one member is `Capacity`, which
259/// leaves the finer cover standing; it is never `Fatal` for valid members.
260/// - A deterministic mapping is a join homomorphism into that join,
261/// `map(a join b) = map(a) join map(b)`, so the join of a collection's
262/// leaves is the image of everything the source's foundations hold. A
263/// mapping may also be non-deterministic -- an embedding that does not
264/// reproduce bit for bit -- and then the join of its leaves holds what
265/// each derivation observed.
266/// - It may be pinned to a class of host ([`Self::computable_here`]): a model
267/// whose output is canonical on one accelerator class only. Another host
268/// derives nothing through it and still carries the collection; its leaves
269/// arrive by replication.
270///
271/// Derivation is scheduled per source foundation by one rule, which every
272/// mapping in this crate follows: any admitted leaf suffices. Maintenance
273/// derives a foundation only when no believed leaf for its locator has an
274/// output that is here or can be fetched, whoever signed that leaf, and any
275/// key the derived collection admits may derive it on a host that can
276/// compute the mapping, not only the foundation's owner. A leaf whose output
277/// cannot be obtained does not count; whether it can is asked only of a
278/// store that reaches other holders, and a store that cannot waits for the
279/// output instead. A fetch that fails says the output is
280/// not available now, not that it is lost: an original output that arrives
281/// after its foundation was derived again stands beside the newer leaf,
282/// both are joined, and nothing further is derived. A leaf known to be bad
283/// is supplemented only on request
284/// ([`CollectionStoreExt::rederive_with`](super::CollectionStoreExt::rederive_with)),
285/// which adds a second leaf and never replaces one.
286///
287/// This is the coherence-safe extension point for mappings whose source and
288/// target encodings are both owned elsewhere. Prefer [`CollectionDerivation`]
289/// when a target encoding owns one canonical incoming derivation; explicit
290/// mappings are selected through the `*_with` collection-store methods.
291pub trait DeriveMapping: Sized {
292 /// Canonical source encoding.
293 type Source: CollectionEncoding;
294 /// Canonical target encoding.
295 type Target: CollectionEncoding;
296
297 /// Canonical concrete mapping fragment embedded in a target descriptor.
298 fn fragment(&self) -> Fragment;
299
300 /// Bind and validate the concrete mapping named by the target descriptor.
301 fn bind(source: &Fragment, target: &Fragment) -> Result<Self, CollectionOperationError>;
302
303 /// Compute the canonical target image of one source member.
304 ///
305 /// The reader freezes the source and any explicitly named dependencies
306 /// together, including collection records and authorization. A mapping may
307 /// resolve references carried by its descriptor or input, but must not
308 /// discover ambient inputs or advance the control-plane observation.
309 fn map<R>(
310 &self,
311 source: &Blob<Self::Source>,
312 reader: &R,
313 ) -> Result<Blob<Self::Target>, CollectionOperationError>
314 where
315 R: StoreRead;
316
317 /// Whether this host can compute the mapping: the class-pin hook.
318 ///
319 /// A mapping pinned to a class of host -- a model whose output is
320 /// canonical on one accelerator class, say -- answers `false` on every
321 /// other host. There maintenance derives nothing through it, raises no
322 /// error for it, and still carries the derived collection: its leaves
323 /// arrive by replication and are joined like any others. The default,
324 /// `true`, is every host.
325 fn computable_here(&self) -> bool {
326 true
327 }
328
329 /// Join target images: the k-way join one MERGE of the derived
330 /// collection states, which is how a derived collection carries its own
331 /// lattice.
332 ///
333 /// The result must be the encoding's canonical join
334 /// ([`CollectionEncoding::join_many`], the default), without constructing
335 /// upstream members or changing storage; a mapping may compute it through
336 /// its own backend. `Capacity` and `MissingDependency` leave the finer
337 /// cover standing.
338 fn join_images<R>(
339 &self,
340 target_descriptor: &Fragment,
341 images: &[Blob<Self::Target>],
342 reader: &R,
343 ) -> Result<Blob<Self::Target>, CollectionOperationError>
344 where
345 R: BlobStoreGet + BlobStoreMeta,
346 {
347 Self::Target::join_many(target_descriptor, images, reader)
348 }
349}
350
351/// Internal adapter from a target-owned derivation to the explicit mapping
352/// engine. Keeping this wrapper private leaves downstream coherence open.
353pub(crate) struct CanonicalDerivation<T: CollectionDerivation> {
354 argument: T::Argument,
355}
356
357impl<T: CollectionDerivation> CanonicalDerivation<T> {
358 pub(crate) fn new(argument: T::Argument) -> Self {
359 Self { argument }
360 }
361}
362
363impl<T: CollectionDerivation> DeriveMapping for CanonicalDerivation<T> {
364 type Source = T::Source;
365 type Target = T;
366
367 fn fragment(&self) -> Fragment {
368 T::fragment(&self.argument)
369 }
370
371 fn bind(source: &Fragment, target: &Fragment) -> Result<Self, CollectionOperationError> {
372 T::bind(source, target).map(Self::new)
373 }
374
375 fn map<R>(
376 &self,
377 source: &Blob<Self::Source>,
378 reader: &R,
379 ) -> Result<Blob<Self::Target>, CollectionOperationError>
380 where
381 R: StoreRead,
382 {
383 T::map(&self.argument, source, reader)
384 }
385
386 fn join_images<R>(
387 &self,
388 target_descriptor: &Fragment,
389 images: &[Blob<Self::Target>],
390 reader: &R,
391 ) -> Result<Blob<Self::Target>, CollectionOperationError>
392 where
393 R: BlobStoreGet + BlobStoreMeta,
394 {
395 T::join_images(&self.argument, target_descriptor, images, reader)
396 }
397}
398
399/// A mapping attached to another collection's nodes: an index of each node,
400/// not a collection of its own.
401///
402/// An attached collection's descriptor names its parent collection and one
403/// concrete mapping, and no policy: its records are `MAP(node ->
404/// attachment)` signed by the host, which no other host believes: one that
405/// reaches another store stays inert there. The contract an implementor
406/// takes on:
407///
408/// - It maps any node of its parent's lattice, a merge result as well as a
409/// foundation. The parent is a root collection: its nodes are
410/// `SimpleArchive`s.
411/// - It is deterministic. Its inputs are the node, immutable dependencies the
412/// node or the mapping names, and optionally the attachments of the same
413/// node in other attached collections of the same parent, named by
414/// [`Self::siblings`]. A dependency that is not resident is reported as
415/// [`CollectionOperationError::MissingDependency`], never read as absent.
416/// - Every host that can read the node can compute it: it is not pinned to a
417/// model, an accelerator class or any other property of a host. A mapping
418/// that is pinned cannot implement this trait, so it cannot be attached by
419/// mistake; it stays a [`DeriveMapping`].
420/// - Its images need no join. `Capacity` means this node's attachment cannot
421/// be represented; a reader then descends to finer nodes.
422///
423/// Its law is cover-query equivalence. A reader answers a query over a cover
424/// of attachments at mixed granularity -- one node's image beside the images
425/// of finer nodes, possibly overlapping -- and the answer must equal the
426/// query over the image of the union the cover stands for:
427///
428/// `view(cover { map(a_1), ..., map(a_n) }) = view(map(a_1 join ... join a_n))`.
429///
430/// Because the `a_i` may overlap, a view combines its per-node answers
431/// idempotently: a last-writer-wins register pairs partial rows across the
432/// cover, a text index takes each entity's maximum score, and a count or any
433/// other non-idempotent aggregate is never summed over overlapping supports.
434/// Every implementor is tested against this law over random covers.
435///
436/// The law holds for every cover when an image is built from single facts,
437/// as every implementor in this crate's is. A mapping whose image aggregates
438/// facts of several entities cannot tell from one node that an entity has
439/// more facts elsewhere, and then no combination of per-node images is
440/// exact once a cover splits that entity; nothing refuses such a node, so
441/// the answer can be wrong with an empty residual and nothing unread. For
442/// such a mapping the law is a premise about its input, which the
443/// implementor states. The one known is the Archive block BM25 mapping in
444/// faculties (`ArchiveBlockTextBm25Mapping`), whose document is a block and
445/// whose term frequency sums over the block's parts: it answers like the
446/// union for every cover whose nodes hold whole source units (a block with
447/// all its parts and their facts); a node holding part of a block scores
448/// that block from the part it holds, with no refusal and no residual to
449/// show it, so a cover that splits a block undercounts it. Faculties' own
450/// Archive importer commits whole source units, and a test there pins it:
451/// that is the premise for data it writes, not for another writer's commits
452/// or historical input, and the writer keeps the fragments it is given
453/// whole without checking that they hold whole blocks. The ignored
454/// faculties tests `a_block_whose_parts_sit_in_two_nodes_scores_like_their_union`,
455/// `a_block_extended_by_a_node_without_its_tag_scores_like_their_union` and
456/// `archive_block_covers_answer_like_the_union_when_parts_spread_over_nodes`
457/// are the acceptance tests of the structural fix: a per-fact text index
458/// over the content fact's payload, with blocks ranked at query time.
459pub trait MapMapping: Sized {
460 /// Encoding of the attachments.
461 type Target: CollectionEncoding;
462
463 /// Canonical concrete mapping fragment embedded in an attached
464 /// descriptor.
465 fn fragment(&self) -> Fragment;
466
467 /// Bind and validate the concrete mapping an attached descriptor names.
468 fn bind(parent: &Fragment, attached: &Fragment) -> Result<Self, CollectionOperationError>;
469
470 /// The attached collections of the same parent whose attachment of a
471 /// node this mapping reads, in the order [`Self::map`] receives them.
472 /// Their handles are named by the mapping's own descriptor, never
473 /// recomputed.
474 fn siblings(&self) -> Vec<CollectionHandle> {
475 Vec::new()
476 }
477
478 /// Compute the attachment of one node.
479 ///
480 /// `siblings` holds one usable attachment of this node in each collection
481 /// [`Self::siblings`] names. `reader` resolves immutable dependencies
482 /// named by the node, the siblings or the mapping; nothing else in the
483 /// store is an input.
484 fn map<R>(
485 &self,
486 node: &Blob<SimpleArchive>,
487 siblings: &[CollectionData],
488 reader: &R,
489 ) -> Result<Blob<Self::Target>, CollectionOperationError>
490 where
491 R: StoreRead;
492}
493
494/// The canonical attachment owned by one target encoding: the
495/// [`MapMapping`] counterpart of [`CollectionDerivation`], with the same
496/// contract and law.
497///
498/// The target names the runtime argument its mapping fragment carries;
499/// attaching it is [`crate::collection::CollectionStoreExt::attach`].
500pub trait CollectionAttachment: CollectionEncoding {
501 /// Runtime argument which distinguishes concrete mappings of this
502 /// canonical relation.
503 type Argument;
504
505 /// Canonical concrete mapping fragment embedded in an attached
506 /// descriptor.
507 fn fragment(argument: &Self::Argument) -> Fragment;
508
509 /// Bind and validate the concrete mapping an attached descriptor names.
510 fn bind(
511 parent: &Fragment,
512 attached: &Fragment,
513 ) -> Result<Self::Argument, CollectionOperationError>;
514
515 /// See [`MapMapping::siblings`].
516 fn siblings(_argument: &Self::Argument) -> Vec<CollectionHandle> {
517 Vec::new()
518 }
519
520 /// See [`MapMapping::map`].
521 fn map<R>(
522 argument: &Self::Argument,
523 node: &Blob<SimpleArchive>,
524 siblings: &[CollectionData],
525 reader: &R,
526 ) -> Result<Blob<Self>, CollectionOperationError>
527 where
528 R: StoreRead;
529}
530
531/// Internal adapter from a target-owned attachment to the explicit mapping
532/// engine, the counterpart of [`CanonicalDerivation`].
533pub(crate) struct CanonicalAttachment<T: CollectionAttachment> {
534 argument: T::Argument,
535}
536
537impl<T: CollectionAttachment> CanonicalAttachment<T> {
538 pub(crate) fn new(argument: T::Argument) -> Self {
539 Self { argument }
540 }
541}
542
543impl<T: CollectionAttachment> MapMapping for CanonicalAttachment<T> {
544 type Target = T;
545
546 fn fragment(&self) -> Fragment {
547 T::fragment(&self.argument)
548 }
549
550 fn bind(parent: &Fragment, attached: &Fragment) -> Result<Self, CollectionOperationError> {
551 T::bind(parent, attached).map(Self::new)
552 }
553
554 fn siblings(&self) -> Vec<CollectionHandle> {
555 T::siblings(&self.argument)
556 }
557
558 fn map<R>(
559 &self,
560 node: &Blob<SimpleArchive>,
561 siblings: &[CollectionData],
562 reader: &R,
563 ) -> Result<Blob<Self::Target>, CollectionOperationError>
564 where
565 R: StoreRead,
566 {
567 T::map(&self.argument, node, siblings, reader)
568 }
569}
570
571/// A descriptor does not denote the encoding requested by its Rust type.
572#[derive(Clone, Debug, Eq, PartialEq)]
573pub enum CollectionTypeError {
574 /// No tagged descriptor entity names the requested encoding.
575 WrongEncoding {
576 /// Encoding required by the Rust type.
577 expected: crate::id::Id,
578 },
579 /// The descriptor names the right encoding but supplies invalid context.
580 InvalidDescriptor(CollectionOperationError),
581}
582
583impl fmt::Display for CollectionTypeError {
584 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
585 match self {
586 Self::WrongEncoding { expected } => write!(
587 formatter,
588 "no collection descriptor names encoding {expected:X}",
589 ),
590 Self::InvalidDescriptor(source) => {
591 write!(formatter, "invalid collection encoding context: {source}")
592 }
593 }
594 }
595}
596
597impl Error for CollectionTypeError {
598 fn source(&self) -> Option<&(dyn Error + 'static)> {
599 match self {
600 Self::InvalidDescriptor(source) => Some(source),
601 Self::WrongEncoding { .. } => None,
602 }
603 }
604}
605
606/// Recognize the descriptor facts needed to interpret members as `E`.
607///
608/// A matching encoding fact is sufficient; other representations, annotations,
609/// and legacy fields do not invalidate it. Policy and lineage are separate
610/// queries at their respective consumers, not encoding prerequisites.
611pub(crate) fn validate_descriptor_type<E>(
612 descriptor_fragment: &Fragment,
613) -> Result<(), CollectionTypeError>
614where
615 E: CollectionEncoding,
616{
617 let expected = E::id();
618 if !exists!(pattern!(descriptor_fragment.facts(), [{ _?descriptor @
619 metadata::tag: KIND_COLLECTION_DESCRIPTOR,
620 collection_representation: expected,
621 }])) {
622 return Err(CollectionTypeError::WrongEncoding { expected });
623 }
624 E::validate_descriptor(descriptor_fragment).map_err(CollectionTypeError::InvalidDescriptor)
625}
626
627/// One exact collection descriptor, typed by its canonical member encoding.
628///
629/// The store owns the descriptor bytes. This is only its cheap, cloneable
630/// content address plus compile-time meaning; constructing it is restricted to
631/// descriptor-validation boundaries.
632pub struct Collection<E: CollectionEncoding> {
633 handle: CollectionHandle,
634 encoding: PhantomData<fn() -> E>,
635}
636
637impl<E: CollectionEncoding> Collection<E> {
638 pub(crate) const fn from_handle(handle: CollectionHandle) -> Self {
639 Self {
640 handle,
641 encoding: PhantomData,
642 }
643 }
644
645 /// Representation-neutral descriptor handle stored in dense records.
646 pub const fn handle(self) -> CollectionHandle {
647 self.handle
648 }
649}
650
651impl<E: CollectionEncoding> Clone for Collection<E> {
652 fn clone(&self) -> Self {
653 *self
654 }
655}
656
657impl<E: CollectionEncoding> Copy for Collection<E> {}
658
659impl<E: CollectionEncoding> fmt::Debug for Collection<E> {
660 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
661 formatter
662 .debug_tuple("Collection")
663 .field(&hex::encode_upper(self.handle.raw))
664 .finish()
665 }
666}
667
668impl<E: CollectionEncoding> PartialEq for Collection<E> {
669 fn eq(&self, other: &Self) -> bool {
670 self.handle == other.handle
671 }
672}
673
674impl<E: CollectionEncoding> Eq for Collection<E> {}
675
676impl<E: CollectionEncoding> PartialOrd for Collection<E> {
677 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
678 Some(self.cmp(other))
679 }
680}
681
682impl<E: CollectionEncoding> Ord for Collection<E> {
683 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
684 self.handle.cmp(&other.handle)
685 }
686}
687
688impl<E: CollectionEncoding> std::hash::Hash for Collection<E> {
689 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
690 self.handle.hash(state);
691 }
692}
693
694impl<E: CollectionEncoding> From<Collection<E>> for CollectionHandle {
695 fn from(collection: Collection<E>) -> Self {
696 collection.handle
697 }
698}