Skip to main content

gtether/resource/source/
mod.rs

1//! Logic related to loading raw resource data from user-defined sources.
2//!
3//! [ResourceSources][rs] are responsible for two main actions: providing [raw data][rd] in the form
4//! of async data streams, and alerting upstream resource management when the underlying data they
5//! are responsible for changes. The latter is done via [sources][rs] "watching" certain [ids][rp],
6//! so that when the data for said [id][rp] changes - i.e. due to a file being modified or a remote
7//! data source updating itself - the [source][rs] can notify upstream of said changes.
8//!
9//! [ResourceSources][rs] can be layered, with one source containing many others. In order to
10//! facilitate this, sources use [SourceIndices][si] to represent what source any given
11//! [raw data][rd] came from. [SourceIndices][si] are nested indices, that are largely controlled
12//! by [sources][rs] themselves. See the documentation for [SourceIndex][si] for more.
13//!
14//! This module contains several pre-defined sources to use, covering cases such as sourcing from
15//! static application data, or utility layers such as combining multiple sub-sources under one
16//! sub-source.
17//!
18//! # Implementing a custom source
19//!
20//! All custom sources need to implement the async [ResourceSource::load()] method, which accepts
21//! an [id][rp] and outputs a result possibly containing [raw data][rd] in the form of
22//! [ResourceData]. [ResourceData] implements `From<Box<dyn AsyncRead>>`, so an
23//! implementation only needs to get a `Box<dyn AsyncRead>`, and then use `.into()`.
24//!
25//! If a given source does not have data for a given [id][rp], it is expected to output an `Err`
26//! wrapping [ResourceLoadError::NotFound].
27//!
28//! All resource sources should also maintain a [ResourceWatcher], and provide a reference to said
29//! watcher. This is used to keep track of what resources are currently requested from the source.
30//! See the [`watcher`](crate::resource::watcher) module for more.
31//!
32//! An example of a bare minimum source that simply yields the given id as raw bytes:
33//! ```
34//! use async_trait::async_trait;
35//! use gtether::resource::id::ResourceId;
36//! use gtether::resource::source::{ResourceData, ResourceDataResult , ResourceDataSource, ResourceSource};
37//! use gtether::resource::watcher::ResourceWatcher;
38//! use smol::io::Cursor;
39//!
40//! struct ReflectingSource {
41//!     watcher: ResourceWatcher,
42//! }
43//!
44//! #[async_trait]
45//! impl ResourceSource for ReflectingSource {
46//!     fn hash(&self, id: &ResourceId) -> Option<ResourceDataSource> {
47//!         Some(ResourceDataSource::new(
48//!             // The hash can simply be the ID, since the value won't change for the same ID
49//!             id.to_string().clone()
50//!         ))
51//!     }
52//!
53//!     async fn load(&self, id: &ResourceId) -> ResourceDataResult {
54//!         Ok(ResourceData::new(
55//!             Box::new(Cursor::new(id.to_string().into_bytes())),
56//!             // The hash can simply be the ID, since the value won't change for the same ID
57//!             id.to_string().clone(),
58//!         ))
59//!     }
60//!
61//!     fn watcher(&self) -> &ResourceWatcher {
62//!         &self.watcher
63//!     }
64//! }
65//! ```
66//!
67//! <div class="warning">
68//!
69//! Note that implementations of [ResourceSource] must use the [async_trait] attribute macro. See
70//! that crate for why this is needed.
71//!
72//! </div>
73//!
74//! # Implementing a middleware source
75//!
76//! Sometimes it may be useful to implement a middleware source, e.g. when extending the behavior of
77//! another source. In this case, some extra logic may need to be implemented, such as maintaining
78//! the child watchers for the middleware's watcher. Note that all methods should be passed to
79//! sub-sources, including [ResourceSource::sub_load()], even if it's not directly used by the
80//! wrapping source. This ensures any sub-sources can make use of [ResourceSource::sub_load()],
81//! since its default implementation is to defer to [ResourceSource::load()].
82//!
83//! Example wrapping a single sub-source:
84//! ```
85//! use ahash::HashSet;
86//! use async_trait::async_trait;
87//! use gtether::resource::id::ResourceId;
88//! use gtether::resource::source::{ResourceSource, ResourceDataResult, SourceIndex, ResourceDataSource};
89//! use gtether::resource::watcher::ResourceWatcher;
90//!
91//! struct WrapperSource<S: ResourceSource> {
92//!     watcher: ResourceWatcher,
93//!     inner: S,
94//! }
95//!
96//! impl<S: ResourceSource> WrapperSource<S> {
97//!     fn new(source: S) -> Self {
98//!         let watcher = ResourceWatcher::new(());
99//!         // The wrapped source's watcher needs to be added to this source's watcher
100//!         watcher.push_child(source.watcher());
101//!         Self {
102//!             watcher,
103//!             inner: source,
104//!         }
105//!     }
106//! }
107//!
108//! #[async_trait]
109//! impl<S: ResourceSource> ResourceSource for WrapperSource<S> {
110//!     fn hash(&self, id: &ResourceId) -> Option<ResourceDataSource> {
111//!         // Do any custom wrapper logic
112//!         let hash = self.inner.hash(id);
113//!         // Possibly modify result if necessary
114//!         hash
115//!     }
116//!
117//!     async fn load(&self, id: &ResourceId) -> ResourceDataResult {
118//!         // Do any custom wrapper logic
119//!         let result = self.inner.load(id).await;
120//!         // Possibly modify result if necessary
121//!         result
122//!     }
123//!
124//!     async fn sub_load(&self, id: &ResourceId, sub_idx: &SourceIndex) -> ResourceDataResult {
125//!         // Do any custom wrapper logic
126//!         let result = self.inner.sub_load(id, sub_idx).await;
127//!         // Possibly modify result if necessary
128//!         result
129//!     }
130//!
131//!     fn watcher(&self) -> &ResourceWatcher {
132//!         &self.watcher
133//!     }
134//! }
135//! ```
136//!
137//! If wrapping multiple other sources, [SourceIndex][si] sub-indices may be necessary, in order to
138//! distinguish between nested sources. It is also necessary to implement
139//! [ResourceSource::sub_load()], which is used for loading raw data from a particular sub-source.
140//!
141//! Example using sub-indices with multiple sub-sources:
142//! ```
143//! use ahash::HashSet;
144//! use async_trait::async_trait;
145//! use gtether::resource::id::ResourceId;
146//! use gtether::resource::ResourceLoadError;
147//! use gtether::resource::source::{ResourceSource, ResourceData, ResourceDataResult, SourceIndex, ResourceDataSource};
148//! use gtether::resource::watcher::ResourceWatcher;
149//!
150//! struct MultiSource<S: ResourceSource> {
151//!     watcher: ResourceWatcher,
152//!     sub_sources: Vec<S>,
153//! }
154//!
155//! impl<S: ResourceSource> MultiSource<S> {
156//!     fn new(sub_sources: impl IntoIterator<Item=S>) -> Self {
157//!         let watcher = ResourceWatcher::new(());
158//!         let sub_sources = sub_sources.into_iter().collect::<Vec<_>>();
159//!         for sub_source in &sub_sources {
160//!             // All sub-source watchers need to be added to this source's watcher
161//!             watcher.push_child(sub_source.watcher());
162//!         }
163//!         Self {
164//!             watcher,
165//!             sub_sources,
166//!         }
167//!     }
168//!
169//!     fn find_sub_hash(&self, id: &ResourceId) -> Option<(usize, ResourceDataSource)> {
170//!         unimplemented!("Yield a hash from a particular sub-source, as well as the sub-source's index")
171//!     }
172//!
173//!     fn find_sub_data(&self, id: &ResourceId) -> Result<(usize, ResourceData), ResourceLoadError> {
174//!         unimplemented!("Yield data from a particular sub-source, as well as the sub-source's index")
175//!     }
176//! }
177//!
178//! #[async_trait]
179//! impl<S: ResourceSource> ResourceSource for MultiSource<S> {
180//!     fn hash(&self, id: &ResourceId) -> Option<ResourceDataSource> {
181//!         let (idx, hash) = self.find_sub_hash(id)?;
182//!         // The inner sub-hash must be wrapped with its sub-sources index
183//!         Some(hash.wrap(idx))
184//!     }
185//!
186//!     async fn load(&self, id: &ResourceId) -> ResourceDataResult {
187//!         let (idx, data) = self.find_sub_data(id)?;
188//!         // The inner sub-data must be wrapped with its sub-sources index
189//!         Ok(data.wrap(idx))
190//!     }
191//!
192//!     async fn sub_load(&self, id: &ResourceId, sub_idx: &SourceIndex) -> ResourceDataResult {
193//!         let source = self.sub_sources.get(sub_idx.idx())
194//!             .ok_or(ResourceLoadError::NotFound(id.clone()))?;
195//!         match sub_idx.sub_idx() {
196//!             Some(sub_idx) => source.sub_load(id, sub_idx).await,
197//!             None => source.load(id).await,
198//!         }
199//!     }
200//!
201//!     fn watcher(&self) -> &ResourceWatcher {
202//!         &self.watcher
203//!     }
204//! }
205//! ```
206//!
207//! # Data Hashing
208//!
209//! Resource management uses hashing to identify raw data in a compact way. These hashes are used
210//! for many parts of the resource management lifecycle, including determining if a notified update
211//! can be ignored (i.e. if hashes match), or even for middleware caching (e.g. for a middleware
212//! source that caches remote data on-disk to prevent repeated downloads).
213//!
214//! It is highly recommended that a consistent hashing scheme is kept across all sources that are
215//! used. If two sources use a different hashing scheme for the same data, then that data will be
216//! treated as two separate sets of data, one from each source, which can cause caching to not
217//! function as well as it should.
218//!
219//! All pre-provided sources that this module provides use SHA256 hashing, encoded using base64 to
220//! turn it into a String. At this point in time, there are no ways to tell these sources to use a
221//! different hashing scheme, so it is recommended that all user-defined sources also use SHA256
222//! with base64 encoding, unless all of your sources are custom, and you can ensure they use the
223//! same hashing scheme.
224//!
225//! [rs]: ResourceSource
226//! [rsw]: ResourceSource::watch()
227//! [rsu]: ResourceSource::unwatch()
228//! [rd]: ResourceReadData
229//! [rp]: ResourceId
230//! [si]: SourceIndex
231//! [rw]: ResourceUpdaterTrait
232
233use ahash::HashMap;
234use async_trait::async_trait;
235use chrono::{DateTime, Utc};
236use smol::io::AsyncRead;
237use std::cmp::Ordering;
238use std::fmt::{Debug, Display, Formatter};
239
240use crate::resource::id::ResourceId;
241use crate::resource::watcher::ResourceWatcher;
242use crate::resource::{ResourceLoadError, ResourceReadData};
243
244pub mod constant;
245pub mod dynamic;
246pub mod fs;
247pub mod list;
248
249#[derive(Debug, Clone)]
250enum SubIndex {
251    None,
252    Some(Box<SourceIndex>),
253    Min,
254    Max,
255}
256
257/// Multi-layered index type that can be used to represent nested [ResourceSources][rs].
258///
259/// Comparable index type that can nest sub-indices. SourceIndices that have the same index will
260/// check their sub-indices for further comparison, if they have one. If only one SourceIndex has
261/// a sub-index, the SourceIndex without a sub-index takes priority (is ordered first).
262///
263/// # Examples
264/// ```
265/// use gtether::resource::source::SourceIndex;
266///
267/// let source_index = SourceIndex::new(1);
268/// let source_index: SourceIndex = 2.into();
269/// ```
270///
271/// With sub-indices:
272/// ```
273/// use gtether::resource::source::SourceIndex;
274///
275/// let base_index = SourceIndex::new(0);
276/// let total_index = base_index.with_sub_idx(Some(3));
277///
278/// let other_index = SourceIndex::new(4);
279/// let total_index_2 = total_index.clone().with_sub_idx(Some(other_index));
280/// // Remove the sub-index
281/// let total_index = total_index.with_sub_idx(None::<SourceIndex>);
282/// ```
283///
284/// [rs]: ResourceSource
285#[derive(Clone)]
286pub struct SourceIndex {
287    idx: usize,
288    sub_idx: SubIndex,
289}
290
291impl SourceIndex {
292    /// Create a minimum SourceIndex.
293    #[inline]
294    pub fn min() -> Self {
295        Self {
296            idx: usize::MIN,
297            sub_idx: SubIndex::Min,
298        }
299    }
300
301    /// Create a maximum SourceIndex.
302    #[inline]
303    pub fn max() -> Self {
304        Self {
305            idx: usize::MAX,
306            sub_idx: SubIndex::Max,
307        }
308    }
309
310    /// Create a new SourceIndex from an usize.
311    ///
312    /// SourceIndex also implements From<usize>.
313    #[inline]
314    pub fn new(idx: usize) -> Self {
315        Self {
316            idx,
317            sub_idx: SubIndex::None,
318        }
319    }
320
321    /// Consume this SourceIndex to create a new one with a given sub-index.
322    ///
323    /// Can erase the sub-index by specifying None.
324    #[inline]
325    pub fn with_sub_idx(mut self, sub_idx: Option<impl Into<SourceIndex>>) -> Self {
326        self.sub_idx = match sub_idx {
327            Some(sub_idx) => SubIndex::Some(Box::new(sub_idx.into())),
328            None => SubIndex::None,
329        };
330        self
331    }
332
333    /// Get the underlying index of this SourceIndex (ignoring sub-indices).
334    #[inline]
335    pub fn idx(&self) -> usize { self.idx }
336
337    /// Get the sub-index of this SourceIndex, if any.
338    #[inline]
339    pub fn sub_idx(&self) -> Option<&SourceIndex> {
340        match &self.sub_idx {
341            SubIndex::Some(sub_idx) => Some(Box::as_ref(sub_idx)),
342            _ => None,
343        }
344    }
345}
346
347impl Display for SourceIndex {
348    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
349        match &self.sub_idx {
350            SubIndex::Some(sub_idx) => {
351                write!(f, "{}:", self.idx)?;
352                Display::fmt(sub_idx, f)
353            },
354            SubIndex::None => write!(f, "{}", self.idx),
355            SubIndex::Min => write!(f, "{}:MIN", self.idx),
356            SubIndex::Max => write!(f, "{}:MAX", self.idx),
357        }
358    }
359}
360
361impl Debug for SourceIndex {
362    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
363        write!(f, "SourceIndex(")?;
364        Display::fmt(self, f)?;
365        write!(f, ")")
366    }
367}
368
369impl From<usize> for SourceIndex {
370    #[inline]
371    fn from(value: usize) -> Self {
372        Self::new(value)
373    }
374}
375
376impl FromIterator<usize> for SourceIndex {
377    fn from_iter<T: IntoIterator<Item=usize>>(iter: T) -> Self {
378        // need to collect into a vec first so we can reverse it
379        let mut indices = iter.into_iter().collect::<Vec<_>>().into_iter().rev();
380        let first_idx = indices.next().unwrap();
381        let mut src_idx = Self::new(first_idx);
382        for idx in indices {
383            src_idx = Self::new(idx).with_sub_idx(Some(src_idx));
384        }
385        src_idx
386    }
387}
388
389impl<const N: usize> From<[usize; N]> for SourceIndex {
390    #[inline]
391    fn from(value: [usize; N]) -> Self {
392        value.into_iter().collect()
393    }
394}
395
396impl PartialEq for SourceIndex {
397    fn eq(&self, other: &Self) -> bool {
398        if self.idx == other.idx {
399            match (&self.sub_idx, &other.sub_idx) {
400                (SubIndex::Some(sub_a), SubIndex::Some(sub_b)) => {
401                    sub_a == sub_b
402                },
403                (SubIndex::Min, SubIndex::Min) => true,
404                (SubIndex::Max, SubIndex::Max) => true,
405                (SubIndex::None, SubIndex::None) => true,
406                _ => false,
407            }
408        } else {
409            false
410        }
411    }
412}
413
414impl Eq for SourceIndex {}
415
416impl Ord for SourceIndex {
417    fn cmp(&self, other: &Self) -> Ordering {
418        match self.idx.cmp(&other.idx) {
419            Ordering::Equal => {
420                match (&self.sub_idx, &other.sub_idx) {
421                    (SubIndex::Some(sub_a), SubIndex::Some(sub_b)) => {
422                        sub_a.cmp(sub_b)
423                    },
424                    (SubIndex::Min, SubIndex::Min) => Ordering::Equal,
425                    (SubIndex::Max, SubIndex::Max) => Ordering::Equal,
426                    (SubIndex::Min, _) | (_, SubIndex::Max) => Ordering::Less,
427                    (_, SubIndex::Min) | (SubIndex::Max, _) => Ordering::Greater,
428                    (SubIndex::None, SubIndex::None) => Ordering::Equal,
429                    (SubIndex::None, SubIndex::Some(_)) => Ordering::Less,
430                    (SubIndex::Some(_), SubIndex::None) => Ordering::Greater,
431                }
432            },
433            order => order,
434        }
435    }
436}
437
438impl PartialOrd for SourceIndex {
439    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
440        Some(self.cmp(other))
441    }
442}
443
444pub(in crate::resource) struct SealedResourceDataSource {
445    pub hash: String,
446    pub idx: SourceIndex,
447}
448
449pub(in crate::resource) struct SealedResourceData {
450    pub data: ResourceReadData,
451    pub source: SealedResourceDataSource,
452}
453
454/// Pair of string hash and [SourceIndex] that represents a "source" of some data.
455#[derive(Debug, Clone)]
456pub struct ResourceDataSource {
457    hash: String,
458    idx: Option<SourceIndex>,
459}
460
461impl ResourceDataSource {
462    #[inline]
463    pub fn new(hash: String) -> Self {
464        Self {
465            hash,
466            idx: None,
467        }
468    }
469
470    pub(in crate::resource) fn seal(self, idx: usize) -> SealedResourceDataSource {
471        match self.idx {
472            Some(sub_idx) => SealedResourceDataSource {
473                hash: self.hash,
474                idx: SourceIndex::new(idx).with_sub_idx(Some(sub_idx)),
475            },
476            None => SealedResourceDataSource {
477                hash: self.hash,
478                idx: SourceIndex::new(idx),
479            }
480        }
481    }
482
483    /// Wrap this ResourceDataSource with another index.
484    ///
485    /// The resulting ResourceDataSource will use the given index as its top-level index in
486    /// [SourceIndex].
487    #[inline]
488    pub fn wrap(mut self, idx: usize) -> Self {
489        self.idx = Some(match self.idx {
490            Some(sub_idx) => SourceIndex::new(idx).with_sub_idx(Some(sub_idx)),
491            None => SourceIndex::new(idx),
492        });
493        self
494    }
495}
496
497/// Struct bundling raw resource data and the [SourceIndex] it came from.
498pub struct ResourceData {
499    data: ResourceReadData,
500    source: ResourceDataSource,
501}
502
503impl ResourceData {
504    /// Create ResourceData from raw data.
505    ///
506    /// It is recommended to use a SHA256 hash of the data as the hash. See
507    /// [module-level documentation][mod] for more.
508    ///
509    /// [more]: super::source#data-hashing
510    pub fn new(data: Box<dyn AsyncRead + Unpin + Send + 'static>, hash: String) -> Self {
511        Self {
512            data: Box::into_pin(data),
513            source: ResourceDataSource::new(hash),
514        }
515    }
516
517    pub(in crate::resource) fn seal(self, idx: usize) -> SealedResourceData {
518        SealedResourceData {
519            data: self.data,
520            source: self.source.seal(idx),
521        }
522    }
523
524    /// Wrap this ResourceData with another index.
525    ///
526    /// The resulting ResourceData will use the given index as its top-level index in [SourceIndex].
527    #[inline]
528    pub fn wrap(mut self, idx: usize) -> Self {
529        self.source = self.source.wrap(idx);
530        self
531    }
532}
533
534pub type ResourceDataResult = Result<ResourceData, ResourceLoadError>;
535pub(in crate::resource) type SealedResourceDataResult = Result<SealedResourceData, ResourceLoadError>;
536
537/// Update type for [ResourceUpdaters](ResourceUpdaterTrait).
538#[derive(Debug, Clone, PartialEq, Eq)]
539pub enum ResourceUpdate {
540    /// A resource was added with the specified [hash](super::source#data-hashing).
541    Added(String),
542
543    /// A resource was modified with the specified [hash](super::source#data-hashing).
544    Modified(String),
545
546    /// A resource was removed, and is no longer available.
547    Removed,
548
549    /// A resource moved from the specified [SourceIndex] to the new one specified by
550    /// [BulkResourceUpdate].
551    MovedSourceIndex(SourceIndex),
552}
553
554/// Bulk collection of updates for a single update timestamp.
555///
556/// Can be easily created via `HashMap<ResourceId, ResourceUpdate>::into()`.
557#[derive(Debug, Clone)]
558pub struct BulkResourceUpdate{
559    /// The timestamp of this bulk update.
560    pub timestamp: DateTime<Utc>,
561    /// Map of IDs to updates.
562    pub updates: HashMap<ResourceId, ResourceUpdate>,
563}
564
565impl From<HashMap<ResourceId, ResourceUpdate>> for BulkResourceUpdate {
566    #[inline]
567    fn from(updates: HashMap<ResourceId, ResourceUpdate>) -> Self {
568        Self {
569            timestamp: Utc::now(),
570            updates,
571        }
572    }
573}
574
575impl From<(ResourceId, ResourceUpdate)> for BulkResourceUpdate {
576    #[inline]
577    fn from(value: (ResourceId, ResourceUpdate)) -> Self {
578        let mut updates = HashMap::default();
579        updates.insert(value.0, value.1);
580        Self {
581            timestamp: Utc::now(),
582            updates,
583        }
584    }
585}
586
587impl FromIterator<(ResourceId, ResourceUpdate)> for BulkResourceUpdate {
588    #[inline]
589    fn from_iter<T: IntoIterator<Item=(ResourceId, ResourceUpdate)>>(iter: T) -> Self {
590        Self {
591            timestamp: Utc::now(),
592            updates: iter.into_iter().collect(),
593        }
594    }
595}
596
597/// User-defined source of [raw resource data][rd].
598///
599/// See [module-level][mod] docs for more details.
600///
601/// [rd]: ResourceReadData
602/// [mod]: super::source
603#[async_trait]
604pub trait ResourceSource: Send + Sync + 'static {
605    /// Given an [id](ResourceId), attempt to retrieve the [hash](super::source#data-hashing)
606    /// associated with it.
607    ///
608    /// If this source doesn't have data associated with the given id, yields `None`.
609    fn hash(&self, id: &ResourceId) -> Option<ResourceDataSource>;
610
611    /// Given an [id][rp], attempt to retrieve the [raw data][rd] associated with it.
612    ///
613    /// [rp]: ResourceId
614    /// [rd]: ResourceReadData
615    async fn load(&self, id: &ResourceId) -> ResourceDataResult;
616
617    /// Given an [id][rp] and [sub-idx][si], attempt to retrieve the [raw data][rd] associated with it.
618    ///
619    /// This version is intended to be used when a [source][rs] notifies that it has a data update.
620    /// In that case, this method will be called with the [sub-idx][si] that [source][rs]
621    /// reported with.
622    ///
623    /// Default implementation of this method is to delegate to [Self::load()].
624    ///
625    /// [rp]: ResourceId
626    /// [si]: SourceIndex
627    /// [rd]: ResourceReadData
628    /// [rs]: ResourceSource
629    // TODO: Is this still needed?
630    async fn sub_load(&self, id: &ResourceId, _sub_idx: &SourceIndex) -> ResourceDataResult {
631        self.load(id).await
632    }
633
634    fn watcher(&self) -> &ResourceWatcher;
635}
636
637#[async_trait]
638impl<S: ResourceSource + ?Sized> ResourceSource for Box<S> {
639    #[inline]
640    fn hash(&self, id: &ResourceId) -> Option<ResourceDataSource> {
641        (**self).hash(id)
642    }
643
644    #[inline]
645    async fn load(&self, id: &ResourceId) -> ResourceDataResult {
646        (**self).load(id).await
647    }
648
649    #[inline]
650    async fn sub_load(&self, id: &ResourceId, sub_idx: &SourceIndex) -> ResourceDataResult {
651        (**self).sub_load(id, sub_idx).await
652    }
653
654    #[inline]
655    fn watcher(&self) -> &ResourceWatcher {
656        (**self).watcher()
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663
664    #[test]
665    fn test_sub_idx_cmp() {
666        assert_eq!(SourceIndex::new(0), SourceIndex::new(0));
667        assert_eq!(SourceIndex::new(42), SourceIndex::new(42));
668        assert_eq!(SourceIndex::new(1).with_sub_idx(Some(2)), SourceIndex::new(1).with_sub_idx(Some(2)));
669
670        assert!(SourceIndex::new(3) < SourceIndex::new(5));
671        assert!(SourceIndex::new(5) > SourceIndex::new(3));
672        assert!(SourceIndex::new(4) < SourceIndex::new(4).with_sub_idx(Some(42)));
673        assert!(SourceIndex::new(4).with_sub_idx(Some(42)) > SourceIndex::new(4));
674    }
675
676    #[test]
677    fn test_sub_idx_cmp_min() {
678        assert_eq!(SourceIndex::min(), SourceIndex::min());
679
680        assert!(SourceIndex::min() < SourceIndex::new(usize::MIN));
681        assert!(SourceIndex::min() < SourceIndex::new(1));
682        assert!(SourceIndex::min() < SourceIndex::new(0).with_sub_idx(Some(0)));
683        assert!(SourceIndex::min() < SourceIndex::new(0).with_sub_idx(Some(SourceIndex::min())));
684        assert!(SourceIndex::min() < SourceIndex::max());
685
686        assert!(SourceIndex::new(usize::MIN) > SourceIndex::min());
687        assert!(SourceIndex::new(1) > SourceIndex::min());
688        assert!(SourceIndex::new(0).with_sub_idx(Some(0)) > SourceIndex::min());
689        assert!(SourceIndex::new(0).with_sub_idx(Some(SourceIndex::min())) > SourceIndex::min());
690        assert!(SourceIndex::max() > SourceIndex::min());
691    }
692
693    #[test]
694    fn test_sub_idx_cmp_max() {
695        assert_eq!(SourceIndex::max(), SourceIndex::max());
696
697        assert!(SourceIndex::max() > SourceIndex::new(usize::MAX));
698        assert!(SourceIndex::max() > SourceIndex::new(1));
699        assert!(SourceIndex::max() > SourceIndex::new(0).with_sub_idx(Some(0)));
700        assert!(SourceIndex::max() > SourceIndex::new(0).with_sub_idx(Some(SourceIndex::max())));
701        assert!(SourceIndex::max() > SourceIndex::min());
702
703        assert!(SourceIndex::new(usize::MAX) < SourceIndex::max());
704        assert!(SourceIndex::new(1) < SourceIndex::max());
705        assert!(SourceIndex::new(0).with_sub_idx(Some(0)) < SourceIndex::max());
706        assert!(SourceIndex::new(0).with_sub_idx(Some(SourceIndex::max())) < SourceIndex::max());
707        assert!(SourceIndex::min() < SourceIndex::max());
708    }
709}