bevy_asset/io/
source.rs

1use crate::{
2    io::{processor_gated::ProcessorGatedReader, AssetSourceEvent, AssetWatcher},
3    processor::AssetProcessorData,
4};
5use alloc::{
6    boxed::Box,
7    string::{String, ToString},
8    sync::Arc,
9};
10use atomicow::CowArc;
11use bevy_ecs::resource::Resource;
12use bevy_platform::collections::HashMap;
13use core::{fmt::Display, hash::Hash, time::Duration};
14use thiserror::Error;
15use tracing::{error, warn};
16
17use super::{ErasedAssetReader, ErasedAssetWriter};
18
19/// A reference to an "asset source", which maps to an [`AssetReader`](crate::io::AssetReader) and/or [`AssetWriter`](crate::io::AssetWriter).
20///
21/// * [`AssetSourceId::Default`] corresponds to "default asset paths" that don't specify a source: `/path/to/asset.png`
22/// * [`AssetSourceId::Name`] corresponds to asset paths that _do_ specify a source: `remote://path/to/asset.png`, where `remote` is the name.
23#[derive(Default, Clone, Debug, Eq)]
24pub enum AssetSourceId<'a> {
25    /// The default asset source.
26    #[default]
27    Default,
28    /// A non-default named asset source.
29    Name(CowArc<'a, str>),
30}
31
32impl<'a> Display for AssetSourceId<'a> {
33    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34        match self.as_str() {
35            None => write!(f, "AssetSourceId::Default"),
36            Some(v) => write!(f, "AssetSourceId::Name({v})"),
37        }
38    }
39}
40
41impl<'a> AssetSourceId<'a> {
42    /// Creates a new [`AssetSourceId`]
43    pub fn new(source: Option<impl Into<CowArc<'a, str>>>) -> AssetSourceId<'a> {
44        match source {
45            Some(source) => AssetSourceId::Name(source.into()),
46            None => AssetSourceId::Default,
47        }
48    }
49
50    /// Returns [`None`] if this is [`AssetSourceId::Default`] and [`Some`] containing the
51    /// name if this is [`AssetSourceId::Name`].
52    pub fn as_str(&self) -> Option<&str> {
53        match self {
54            AssetSourceId::Default => None,
55            AssetSourceId::Name(v) => Some(v),
56        }
57    }
58
59    /// If this is not already an owned / static id, create one. Otherwise, it will return itself (with a static lifetime).
60    pub fn into_owned(self) -> AssetSourceId<'static> {
61        match self {
62            AssetSourceId::Default => AssetSourceId::Default,
63            AssetSourceId::Name(v) => AssetSourceId::Name(v.into_owned()),
64        }
65    }
66
67    /// Clones into an owned [`AssetSourceId<'static>`].
68    /// This is equivalent to `.clone().into_owned()`.
69    #[inline]
70    pub fn clone_owned(&self) -> AssetSourceId<'static> {
71        self.clone().into_owned()
72    }
73}
74
75impl AssetSourceId<'static> {
76    /// Indicates this [`AssetSourceId`] should have a static lifetime.
77    #[inline]
78    pub fn as_static(self) -> Self {
79        match self {
80            Self::Default => Self::Default,
81            Self::Name(value) => Self::Name(value.as_static()),
82        }
83    }
84
85    /// Constructs an [`AssetSourceId`] with a static lifetime.
86    #[inline]
87    pub fn from_static(value: impl Into<Self>) -> Self {
88        value.into().as_static()
89    }
90}
91
92impl<'a> From<&'a str> for AssetSourceId<'a> {
93    fn from(value: &'a str) -> Self {
94        AssetSourceId::Name(CowArc::Borrowed(value))
95    }
96}
97
98impl<'a, 'b> From<&'a AssetSourceId<'b>> for AssetSourceId<'b> {
99    fn from(value: &'a AssetSourceId<'b>) -> Self {
100        value.clone()
101    }
102}
103
104impl<'a> From<Option<&'a str>> for AssetSourceId<'a> {
105    fn from(value: Option<&'a str>) -> Self {
106        match value {
107            Some(value) => AssetSourceId::Name(CowArc::Borrowed(value)),
108            None => AssetSourceId::Default,
109        }
110    }
111}
112
113impl From<String> for AssetSourceId<'static> {
114    fn from(value: String) -> Self {
115        AssetSourceId::Name(value.into())
116    }
117}
118
119impl<'a> Hash for AssetSourceId<'a> {
120    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
121        self.as_str().hash(state);
122    }
123}
124
125impl<'a> PartialEq for AssetSourceId<'a> {
126    fn eq(&self, other: &Self) -> bool {
127        self.as_str().eq(&other.as_str())
128    }
129}
130
131/// Metadata about an "asset source", such as how to construct the [`AssetReader`](crate::io::AssetReader) and [`AssetWriter`](crate::io::AssetWriter) for the source,
132/// and whether or not the source is processed.
133#[derive(Default)]
134pub struct AssetSourceBuilder {
135    /// The [`ErasedAssetReader`] to use on the unprocessed asset.
136    pub reader: Option<Box<dyn FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync>>,
137    /// The [`ErasedAssetWriter`] to use on the unprocessed asset.
138    pub writer: Option<Box<dyn FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync>>,
139    /// The [`AssetWatcher`] to use for unprocessed assets, if any.
140    pub watcher: Option<
141        Box<
142            dyn FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
143                + Send
144                + Sync,
145        >,
146    >,
147    /// The [`ErasedAssetReader`] to use for processed assets.
148    pub processed_reader: Option<Box<dyn FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync>>,
149    /// The [`ErasedAssetWriter`] to use for processed assets.
150    pub processed_writer:
151        Option<Box<dyn FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync>>,
152    /// The [`AssetWatcher`] to use for processed assets, if any.
153    pub processed_watcher: Option<
154        Box<
155            dyn FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
156                + Send
157                + Sync,
158        >,
159    >,
160    /// The warning message to display when watching an unprocessed asset fails.
161    pub watch_warning: Option<&'static str>,
162    /// The warning message to display when watching a processed asset fails.
163    pub processed_watch_warning: Option<&'static str>,
164}
165
166impl AssetSourceBuilder {
167    /// Builds a new [`AssetSource`] with the given `id`. If `watch` is true, the unprocessed source will watch for changes.
168    /// If `watch_processed` is true, the processed source will watch for changes.
169    pub fn build(
170        &mut self,
171        id: AssetSourceId<'static>,
172        watch: bool,
173        watch_processed: bool,
174    ) -> Option<AssetSource> {
175        let reader = self.reader.as_mut()?();
176        let writer = self.writer.as_mut().and_then(|w| w(false));
177        let processed_writer = self.processed_writer.as_mut().and_then(|w| w(true));
178        let mut source = AssetSource {
179            id: id.clone(),
180            reader,
181            writer,
182            processed_reader: self.processed_reader.as_mut().map(|r| r()),
183            processed_writer,
184            event_receiver: None,
185            watcher: None,
186            processed_event_receiver: None,
187            processed_watcher: None,
188        };
189
190        if watch {
191            let (sender, receiver) = crossbeam_channel::unbounded();
192            match self.watcher.as_mut().and_then(|w| w(sender)) {
193                Some(w) => {
194                    source.watcher = Some(w);
195                    source.event_receiver = Some(receiver);
196                }
197                None => {
198                    if let Some(warning) = self.watch_warning {
199                        warn!("{id} does not have an AssetWatcher configured. {warning}");
200                    }
201                }
202            }
203        }
204
205        if watch_processed {
206            let (sender, receiver) = crossbeam_channel::unbounded();
207            match self.processed_watcher.as_mut().and_then(|w| w(sender)) {
208                Some(w) => {
209                    source.processed_watcher = Some(w);
210                    source.processed_event_receiver = Some(receiver);
211                }
212                None => {
213                    if let Some(warning) = self.processed_watch_warning {
214                        warn!("{id} does not have a processed AssetWatcher configured. {warning}");
215                    }
216                }
217            }
218        }
219        Some(source)
220    }
221
222    /// Will use the given `reader` function to construct unprocessed [`AssetReader`](crate::io::AssetReader) instances.
223    pub fn with_reader(
224        mut self,
225        reader: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
226    ) -> Self {
227        self.reader = Some(Box::new(reader));
228        self
229    }
230
231    /// Will use the given `writer` function to construct unprocessed [`AssetWriter`](crate::io::AssetWriter) instances.
232    pub fn with_writer(
233        mut self,
234        writer: impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync + 'static,
235    ) -> Self {
236        self.writer = Some(Box::new(writer));
237        self
238    }
239
240    /// Will use the given `watcher` function to construct unprocessed [`AssetWatcher`] instances.
241    pub fn with_watcher(
242        mut self,
243        watcher: impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
244            + Send
245            + Sync
246            + 'static,
247    ) -> Self {
248        self.watcher = Some(Box::new(watcher));
249        self
250    }
251
252    /// Will use the given `reader` function to construct processed [`AssetReader`](crate::io::AssetReader) instances.
253    pub fn with_processed_reader(
254        mut self,
255        reader: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
256    ) -> Self {
257        self.processed_reader = Some(Box::new(reader));
258        self
259    }
260
261    /// Will use the given `writer` function to construct processed [`AssetWriter`](crate::io::AssetWriter) instances.
262    pub fn with_processed_writer(
263        mut self,
264        writer: impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync + 'static,
265    ) -> Self {
266        self.processed_writer = Some(Box::new(writer));
267        self
268    }
269
270    /// Will use the given `watcher` function to construct processed [`AssetWatcher`] instances.
271    pub fn with_processed_watcher(
272        mut self,
273        watcher: impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
274            + Send
275            + Sync
276            + 'static,
277    ) -> Self {
278        self.processed_watcher = Some(Box::new(watcher));
279        self
280    }
281
282    /// Enables a warning for the unprocessed source watcher, which will print when watching is enabled and the unprocessed source doesn't have a watcher.
283    pub fn with_watch_warning(mut self, warning: &'static str) -> Self {
284        self.watch_warning = Some(warning);
285        self
286    }
287
288    /// Enables a warning for the processed source watcher, which will print when watching is enabled and the processed source doesn't have a watcher.
289    pub fn with_processed_watch_warning(mut self, warning: &'static str) -> Self {
290        self.processed_watch_warning = Some(warning);
291        self
292    }
293
294    /// Returns a builder containing the "platform default source" for the given `path` and `processed_path`.
295    /// For most platforms, this will use [`FileAssetReader`](crate::io::file::FileAssetReader) / [`FileAssetWriter`](crate::io::file::FileAssetWriter),
296    /// but some platforms (such as Android) have their own default readers / writers / watchers.
297    pub fn platform_default(path: &str, processed_path: Option<&str>) -> Self {
298        let default = Self::default()
299            .with_reader(AssetSource::get_default_reader(path.to_string()))
300            .with_writer(AssetSource::get_default_writer(path.to_string()))
301            .with_watcher(AssetSource::get_default_watcher(
302                path.to_string(),
303                Duration::from_millis(300),
304            ))
305            .with_watch_warning(AssetSource::get_default_watch_warning());
306        if let Some(processed_path) = processed_path {
307            default
308                .with_processed_reader(AssetSource::get_default_reader(processed_path.to_string()))
309                .with_processed_writer(AssetSource::get_default_writer(processed_path.to_string()))
310                .with_processed_watcher(AssetSource::get_default_watcher(
311                    processed_path.to_string(),
312                    Duration::from_millis(300),
313                ))
314                .with_processed_watch_warning(AssetSource::get_default_watch_warning())
315        } else {
316            default
317        }
318    }
319}
320
321/// A [`Resource`] that hold (repeatable) functions capable of producing new [`AssetReader`](crate::io::AssetReader) and [`AssetWriter`](crate::io::AssetWriter) instances
322/// for a given asset source.
323#[derive(Resource, Default)]
324pub struct AssetSourceBuilders {
325    sources: HashMap<CowArc<'static, str>, AssetSourceBuilder>,
326    default: Option<AssetSourceBuilder>,
327}
328
329impl AssetSourceBuilders {
330    /// Inserts a new builder with the given `id`
331    pub fn insert(&mut self, id: impl Into<AssetSourceId<'static>>, source: AssetSourceBuilder) {
332        match AssetSourceId::from_static(id) {
333            AssetSourceId::Default => {
334                self.default = Some(source);
335            }
336            AssetSourceId::Name(name) => {
337                self.sources.insert(name, source);
338            }
339        }
340    }
341
342    /// Gets a mutable builder with the given `id`, if it exists.
343    pub fn get_mut<'a, 'b>(
344        &'a mut self,
345        id: impl Into<AssetSourceId<'b>>,
346    ) -> Option<&'a mut AssetSourceBuilder> {
347        match id.into() {
348            AssetSourceId::Default => self.default.as_mut(),
349            AssetSourceId::Name(name) => self.sources.get_mut(&name.into_owned()),
350        }
351    }
352
353    /// Builds a new [`AssetSources`] collection. If `watch` is true, the unprocessed sources will watch for changes.
354    /// If `watch_processed` is true, the processed sources will watch for changes.
355    pub fn build_sources(&mut self, watch: bool, watch_processed: bool) -> AssetSources {
356        let mut sources = <HashMap<_, _>>::default();
357        for (id, source) in &mut self.sources {
358            if let Some(data) = source.build(
359                AssetSourceId::Name(id.clone_owned()),
360                watch,
361                watch_processed,
362            ) {
363                sources.insert(id.clone_owned(), data);
364            }
365        }
366
367        AssetSources {
368            sources,
369            default: self
370                .default
371                .as_mut()
372                .and_then(|p| p.build(AssetSourceId::Default, watch, watch_processed))
373                .expect(MISSING_DEFAULT_SOURCE),
374        }
375    }
376
377    /// Initializes the default [`AssetSourceBuilder`] if it has not already been set.
378    pub fn init_default_source(&mut self, path: &str, processed_path: Option<&str>) {
379        self.default
380            .get_or_insert_with(|| AssetSourceBuilder::platform_default(path, processed_path));
381    }
382}
383
384/// A collection of unprocessed and processed [`AssetReader`](crate::io::AssetReader), [`AssetWriter`](crate::io::AssetWriter), and [`AssetWatcher`] instances
385/// for a specific asset source, identified by an [`AssetSourceId`].
386pub struct AssetSource {
387    id: AssetSourceId<'static>,
388    reader: Box<dyn ErasedAssetReader>,
389    writer: Option<Box<dyn ErasedAssetWriter>>,
390    processed_reader: Option<Box<dyn ErasedAssetReader>>,
391    processed_writer: Option<Box<dyn ErasedAssetWriter>>,
392    watcher: Option<Box<dyn AssetWatcher>>,
393    processed_watcher: Option<Box<dyn AssetWatcher>>,
394    event_receiver: Option<crossbeam_channel::Receiver<AssetSourceEvent>>,
395    processed_event_receiver: Option<crossbeam_channel::Receiver<AssetSourceEvent>>,
396}
397
398impl AssetSource {
399    /// Starts building a new [`AssetSource`].
400    pub fn build() -> AssetSourceBuilder {
401        AssetSourceBuilder::default()
402    }
403
404    /// Returns this source's id.
405    #[inline]
406    pub fn id(&self) -> AssetSourceId<'static> {
407        self.id.clone()
408    }
409
410    /// Return's this source's unprocessed [`AssetReader`](crate::io::AssetReader).
411    #[inline]
412    pub fn reader(&self) -> &dyn ErasedAssetReader {
413        &*self.reader
414    }
415
416    /// Return's this source's unprocessed [`AssetWriter`](crate::io::AssetWriter), if it exists.
417    #[inline]
418    pub fn writer(&self) -> Result<&dyn ErasedAssetWriter, MissingAssetWriterError> {
419        self.writer
420            .as_deref()
421            .ok_or_else(|| MissingAssetWriterError(self.id.clone_owned()))
422    }
423
424    /// Return's this source's processed [`AssetReader`](crate::io::AssetReader), if it exists.
425    #[inline]
426    pub fn processed_reader(
427        &self,
428    ) -> Result<&dyn ErasedAssetReader, MissingProcessedAssetReaderError> {
429        self.processed_reader
430            .as_deref()
431            .ok_or_else(|| MissingProcessedAssetReaderError(self.id.clone_owned()))
432    }
433
434    /// Return's this source's processed [`AssetWriter`](crate::io::AssetWriter), if it exists.
435    #[inline]
436    pub fn processed_writer(
437        &self,
438    ) -> Result<&dyn ErasedAssetWriter, MissingProcessedAssetWriterError> {
439        self.processed_writer
440            .as_deref()
441            .ok_or_else(|| MissingProcessedAssetWriterError(self.id.clone_owned()))
442    }
443
444    /// Return's this source's unprocessed event receiver, if the source is currently watching for changes.
445    #[inline]
446    pub fn event_receiver(&self) -> Option<&crossbeam_channel::Receiver<AssetSourceEvent>> {
447        self.event_receiver.as_ref()
448    }
449
450    /// Return's this source's processed event receiver, if the source is currently watching for changes.
451    #[inline]
452    pub fn processed_event_receiver(
453        &self,
454    ) -> Option<&crossbeam_channel::Receiver<AssetSourceEvent>> {
455        self.processed_event_receiver.as_ref()
456    }
457
458    /// Returns true if the assets in this source should be processed.
459    #[inline]
460    pub fn should_process(&self) -> bool {
461        self.processed_writer.is_some()
462    }
463
464    /// Returns a builder function for this platform's default [`AssetReader`](crate::io::AssetReader). `path` is the relative path to
465    /// the asset root.
466    pub fn get_default_reader(
467        _path: String,
468    ) -> impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync {
469        move || {
470            #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
471            return Box::new(super::file::FileAssetReader::new(&_path));
472            #[cfg(target_arch = "wasm32")]
473            return Box::new(super::wasm::HttpWasmAssetReader::new(&_path));
474            #[cfg(target_os = "android")]
475            return Box::new(super::android::AndroidAssetReader);
476        }
477    }
478
479    /// Returns a builder function for this platform's default [`AssetWriter`](crate::io::AssetWriter). `path` is the relative path to
480    /// the asset root. This will return [`None`] if this platform does not support writing assets by default.
481    pub fn get_default_writer(
482        _path: String,
483    ) -> impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync {
484        move |_create_root: bool| {
485            #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
486            return Some(Box::new(super::file::FileAssetWriter::new(
487                &_path,
488                _create_root,
489            )));
490            #[cfg(any(target_arch = "wasm32", target_os = "android"))]
491            return None;
492        }
493    }
494
495    /// Returns the default non-existent [`AssetWatcher`] warning for the current platform.
496    pub fn get_default_watch_warning() -> &'static str {
497        #[cfg(target_arch = "wasm32")]
498        return "Web does not currently support watching assets.";
499        #[cfg(target_os = "android")]
500        return "Android does not currently support watching assets.";
501        #[cfg(all(
502            not(target_arch = "wasm32"),
503            not(target_os = "android"),
504            not(feature = "file_watcher")
505        ))]
506        return "Consider enabling the `file_watcher` feature.";
507        #[cfg(all(
508            not(target_arch = "wasm32"),
509            not(target_os = "android"),
510            feature = "file_watcher"
511        ))]
512        return "Consider adding an \"assets\" directory.";
513    }
514
515    /// Returns a builder function for this platform's default [`AssetWatcher`]. `path` is the relative path to
516    /// the asset root. This will return [`None`] if this platform does not support watching assets by default.
517    /// `file_debounce_time` is the amount of time to wait (and debounce duplicate events) before returning an event.
518    /// Higher durations reduce duplicates but increase the amount of time before a change event is processed. If the
519    /// duration is set too low, some systems might surface events _before_ their filesystem has the changes.
520    #[cfg_attr(
521        any(
522            not(feature = "file_watcher"),
523            target_arch = "wasm32",
524            target_os = "android"
525        ),
526        expect(
527            unused_variables,
528            reason = "The `path` and `file_debounce_wait_time` arguments are unused when on WASM, Android, or if the `file_watcher` feature is disabled."
529        )
530    )]
531    pub fn get_default_watcher(
532        path: String,
533        file_debounce_wait_time: Duration,
534    ) -> impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
535           + Send
536           + Sync {
537        move |sender: crossbeam_channel::Sender<AssetSourceEvent>| {
538            #[cfg(all(
539                feature = "file_watcher",
540                not(target_arch = "wasm32"),
541                not(target_os = "android")
542            ))]
543            {
544                let path = super::file::get_base_path().join(path.clone());
545                if path.exists() {
546                    Some(Box::new(
547                        super::file::FileWatcher::new(
548                            path.clone(),
549                            sender,
550                            file_debounce_wait_time,
551                        )
552                        .unwrap_or_else(|e| {
553                            panic!("Failed to create file watcher from path {path:?}, {e:?}")
554                        }),
555                    ))
556                } else {
557                    warn!("Skip creating file watcher because path {path:?} does not exist.");
558                    None
559                }
560            }
561            #[cfg(any(
562                not(feature = "file_watcher"),
563                target_arch = "wasm32",
564                target_os = "android"
565            ))]
566            return None;
567        }
568    }
569
570    /// This will cause processed [`AssetReader`](crate::io::AssetReader) futures (such as [`AssetReader::read`](crate::io::AssetReader::read)) to wait until
571    /// the [`AssetProcessor`](crate::AssetProcessor) has finished processing the requested asset.
572    pub fn gate_on_processor(&mut self, processor_data: Arc<AssetProcessorData>) {
573        if let Some(reader) = self.processed_reader.take() {
574            self.processed_reader = Some(Box::new(ProcessorGatedReader::new(
575                self.id(),
576                reader,
577                processor_data,
578            )));
579        }
580    }
581}
582
583/// A collection of [`AssetSource`]s.
584pub struct AssetSources {
585    sources: HashMap<CowArc<'static, str>, AssetSource>,
586    default: AssetSource,
587}
588
589impl AssetSources {
590    /// Gets the [`AssetSource`] with the given `id`, if it exists.
591    pub fn get<'a, 'b>(
592        &'a self,
593        id: impl Into<AssetSourceId<'b>>,
594    ) -> Result<&'a AssetSource, MissingAssetSourceError> {
595        match id.into().into_owned() {
596            AssetSourceId::Default => Ok(&self.default),
597            AssetSourceId::Name(name) => self
598                .sources
599                .get(&name)
600                .ok_or(MissingAssetSourceError(AssetSourceId::Name(name))),
601        }
602    }
603
604    /// Iterates all asset sources in the collection (including the default source).
605    pub fn iter(&self) -> impl Iterator<Item = &AssetSource> {
606        self.sources.values().chain(Some(&self.default))
607    }
608
609    /// Mutably iterates all asset sources in the collection (including the default source).
610    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut AssetSource> {
611        self.sources.values_mut().chain(Some(&mut self.default))
612    }
613
614    /// Iterates all processed asset sources in the collection (including the default source).
615    pub fn iter_processed(&self) -> impl Iterator<Item = &AssetSource> {
616        self.iter().filter(|p| p.should_process())
617    }
618
619    /// Mutably iterates all processed asset sources in the collection (including the default source).
620    pub fn iter_processed_mut(&mut self) -> impl Iterator<Item = &mut AssetSource> {
621        self.iter_mut().filter(|p| p.should_process())
622    }
623
624    /// Iterates over the [`AssetSourceId`] of every [`AssetSource`] in the collection (including the default source).
625    pub fn ids(&self) -> impl Iterator<Item = AssetSourceId<'static>> + '_ {
626        self.sources
627            .keys()
628            .map(|k| AssetSourceId::Name(k.clone_owned()))
629            .chain(Some(AssetSourceId::Default))
630    }
631
632    /// This will cause processed [`AssetReader`](crate::io::AssetReader) futures (such as [`AssetReader::read`](crate::io::AssetReader::read)) to wait until
633    /// the [`AssetProcessor`](crate::AssetProcessor) has finished processing the requested asset.
634    pub fn gate_on_processor(&mut self, processor_data: Arc<AssetProcessorData>) {
635        for source in self.iter_processed_mut() {
636            source.gate_on_processor(processor_data.clone());
637        }
638    }
639}
640
641/// An error returned when an [`AssetSource`] does not exist for a given id.
642#[derive(Error, Debug, Clone, PartialEq, Eq)]
643#[error("Asset Source '{0}' does not exist")]
644pub struct MissingAssetSourceError(AssetSourceId<'static>);
645
646/// An error returned when an [`AssetWriter`](crate::io::AssetWriter) does not exist for a given id.
647#[derive(Error, Debug, Clone)]
648#[error("Asset Source '{0}' does not have an AssetWriter.")]
649pub struct MissingAssetWriterError(AssetSourceId<'static>);
650
651/// An error returned when a processed [`AssetReader`](crate::io::AssetReader) does not exist for a given id.
652#[derive(Error, Debug, Clone, PartialEq, Eq)]
653#[error("Asset Source '{0}' does not have a processed AssetReader.")]
654pub struct MissingProcessedAssetReaderError(AssetSourceId<'static>);
655
656/// An error returned when a processed [`AssetWriter`](crate::io::AssetWriter) does not exist for a given id.
657#[derive(Error, Debug, Clone)]
658#[error("Asset Source '{0}' does not have a processed AssetWriter.")]
659pub struct MissingProcessedAssetWriterError(AssetSourceId<'static>);
660
661const MISSING_DEFAULT_SOURCE: &str =
662    "A default AssetSource is required. Add one to `AssetSourceBuilders`";