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}