From 7c072c9fe5e612b6b3ed20397b6a5ab6b65c9f1b Mon Sep 17 00:00:00 2001
From: Alejandro Perea <alexpro820@gmail.com>
Date: Mon, 14 Mar 2022 17:27:31 +0100
Subject: [PATCH] Add loader type (#193)

* Add `Loader` and map loading functions

* Update examples, doc & tests

* Add tileset loading functions

* Fix unrelated broken intradoc link

* Fix intradoc link

* Fix more docs

* Update changelog and readme

* Fix warnings

* Change version, remove dead code
---
 CHANGELOG.md             |  11 ++-
 Cargo.toml               |   2 +-
 README.md                |  14 ++--
 examples/main.rs         |  10 +--
 examples/sfml/main.rs    |  22 ++---
 src/cache.rs             |   5 +-
 src/error.rs             |   2 +-
 src/lib.rs               |   3 +
 src/loader.rs            | 171 +++++++++++++++++++++++++++++++++++++++
 src/map.rs               |  34 ++------
 src/parse/mod.rs         |   1 +
 src/parse/xml/map.rs     |  35 ++++++++
 src/parse/xml/mod.rs     |   4 +
 src/parse/xml/tileset.rs |  28 +++++++
 src/tileset.rs           |  26 +-----
 tests/lib.rs             | 116 +++++++++++++-------------
 16 files changed, 350 insertions(+), 134 deletions(-)
 create mode 100644 src/loader.rs
 create mode 100644 src/parse/mod.rs
 create mode 100644 src/parse/xml/map.rs
 create mode 100644 src/parse/xml/mod.rs
 create mode 100644 src/parse/xml/tileset.rs

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 09bb815..c38e7d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,16 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [Unreleased]
+## [0.10.1]
+### Added
+- `Loader` type for loading map and tileset files without having to necessarily mention the cache
+to use.
+
+### Deprecated
+- `Map::parse_reader`: Use `Loader::parse_tmx_map_from` instead.
+- `Map::parse_file`: Use `Loader::load_tmx_map` instead.
+- `Tileset::parse_reader`: Use `Loader::load_tsx_tileset` instead.
+
 ### Fixed
 - Fix message when a tileset is missing the `tilecount` attribute (#194).
 
diff --git a/Cargo.toml b/Cargo.toml
index ba1ea3e..aa63910 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "tiled"
-version = "0.10.0"
+version = "0.10.1"
 description = "A rust crate for loading maps created by the Tiled editor"
 categories = ["game-development"]
 keywords = ["gamedev", "tiled", "tmx", "map"]
diff --git a/README.md b/README.md
index 5340a31..2d1cc54 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # rs-tiled
 ```toml
-tiled = "0.10.0"
+tiled = "0.10.1"
 ```
 
 [![Rust](https://github.com/mapeditor/rs-tiled/actions/workflows/rust.yml/badge.svg)](https://github.com/mapeditor/rs-tiled/actions/workflows/rust.yml)
@@ -18,16 +18,16 @@ The minimum supported TMX version is 0.13.
 ### Example
 
 ```rust
-use tiled::{FilesystemResourceCache, Map};
+use tiled::Loader;
 
 fn main() {
-    let map = Map::parse_file(
-        "assets/tiled_base64_zlib.tmx",
-        &mut FilesystemResourceCache::new(),
-    )
-    .unwrap();
+    let mut loader = Loader::new();
+    let map = loader.load_tmx_map("assets/tiled_base64_zlib.tmx").unwrap();
     println!("{:?}", map);
     println!("{:?}", map.tilesets()[0].get_tile(0).unwrap().probability);
+    
+    let tileset = loader.load_tsx_tileset("assets/tilesheet.tsx").unwrap();
+    assert_eq!(*map.tilesets()[0], tileset);
 }
 
 ```
diff --git a/examples/main.rs b/examples/main.rs
index e59c588..34a7a45 100644
--- a/examples/main.rs
+++ b/examples/main.rs
@@ -1,17 +1,13 @@
 use std::path::PathBuf;
 
-use tiled::{FilesystemResourceCache, Map};
+use tiled::Loader;
 
 fn main() {
-    // Create a new resource cache. This is a structure that holds references to loaded
-    // assets such as tilesets so that they only get loaded once.
-    // [`FilesystemResourceCache`] is a implementation of [`tiled::ResourceCache`] that
-    // identifies resources by their path in the filesystem.
-    let mut cache = FilesystemResourceCache::new();
+    let mut loader = Loader::new();
 
     let map_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
         .join("assets/tiled_base64_zlib.tmx");
-    let map = Map::parse_file(map_path, &mut cache).unwrap();
+    let map = loader.load_tmx_map(map_path).unwrap();
 
     for layer in map.layers() {
         print!("Layer \"{}\":\n\t", layer.name);
diff --git a/examples/sfml/main.rs b/examples/sfml/main.rs
index bde0f36..1b6c720 100644
--- a/examples/sfml/main.rs
+++ b/examples/sfml/main.rs
@@ -13,7 +13,7 @@ use sfml::{
     window::{ContextSettings, Key, Style},
 };
 use std::{env, path::PathBuf, time::Duration};
-use tiled::{FilesystemResourceCache, FiniteTileLayer, Map};
+use tiled::{FiniteTileLayer, Loader, Map};
 use tilesheet::Tilesheet;
 
 /// A path to the map to display.
@@ -94,17 +94,17 @@ impl Drawable for Level {
 }
 
 fn main() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let map = Map::parse_file(
-        PathBuf::from(
-            env::var("CARGO_MANIFEST_DIR")
-                .expect("To run the example, use `cargo run --example sfml`"),
+    let mut loader = Loader::new();
+
+    let map = loader
+        .load_tmx_map(
+            PathBuf::from(
+                env::var("CARGO_MANIFEST_DIR")
+                    .expect("To run the example, use `cargo run --example sfml`"),
+            )
+            .join(MAP_PATH),
         )
-        .join(MAP_PATH),
-        &mut cache,
-    )
-    .unwrap();
+        .unwrap();
     let level = Level::from_map(map);
 
     let mut window = create_window();
diff --git a/src/cache.rs b/src/cache.rs
index de53736..a650dd1 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -12,7 +12,10 @@ pub type ResourcePath = Path;
 pub type ResourcePathBuf = PathBuf;
 
 /// A trait identifying a data type that holds resources (such as tilesets) and maps them to a
-/// [`ResourcePath`] to prevent loading them more than once.
+/// [`ResourcePath`] to prevent loading them more than once. Normally you don't need to use this
+/// type yourself unless you want to create a custom caching solution to, for instance, integrate
+/// with your own.
+/// If you simply want to load a map or tileset, use the [`Loader`](crate::Loader) type.
 pub trait ResourceCache {
     /// Obtains a tileset from the cache, if it exists.
     ///
diff --git a/src/error.rs b/src/error.rs
index b7fcbfa..d83d492 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -51,7 +51,7 @@ pub enum Error {
     },
 }
 
-/// A result with an error variant of [`tiled::error::Error`].
+/// A result with an error variant of [`crate::Error`].
 pub type Result<T> = std::result::Result<T, Error>;
 
 impl fmt::Display for Error {
diff --git a/src/lib.rs b/src/lib.rs
index 951c971..6287c73 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -10,8 +10,10 @@ mod cache;
 mod error;
 mod image;
 mod layers;
+mod loader;
 mod map;
 mod objects;
+mod parse;
 mod properties;
 mod tile;
 mod tileset;
@@ -22,6 +24,7 @@ pub use cache::*;
 pub use error::*;
 pub use image::*;
 pub use layers::*;
+pub use loader::*;
 pub use map::*;
 pub use objects::*;
 pub use properties::*;
diff --git a/src/loader.rs b/src/loader.rs
new file mode 100644
index 0000000..939e1ff
--- /dev/null
+++ b/src/loader.rs
@@ -0,0 +1,171 @@
+use std::{fs::File, io::Read, path::Path};
+
+use crate::{Error, FilesystemResourceCache, Map, ResourceCache, Result, Tileset};
+
+/// A type used for loading [`Map`]s and [`Tileset`]s.
+///
+/// Internally, it holds a [`ResourceCache`] that, as its name implies, caches intermediate loading
+/// artifacts, most notably map tilesets.
+///
+/// ## Reasoning
+/// This type is used for loading operations because they require a [`ResourceCache`] for
+/// intermediate artifacts, so using a type for creation can ensure that the cache is reused if
+/// loading more than one object is required.
+#[derive(Debug, Clone)]
+pub struct Loader<Cache: ResourceCache = FilesystemResourceCache> {
+    cache: Cache,
+}
+
+impl Loader<FilesystemResourceCache> {
+    /// Creates a new loader, creating a default ([`FilesystemResourceCache`]) resource cache in the process.
+    pub fn new() -> Self {
+        Self {
+            cache: FilesystemResourceCache::new(),
+        }
+    }
+}
+
+impl<Cache: ResourceCache> Loader<Cache> {
+    /// Creates a new loader using a specific resource cache.
+    ///
+    /// ## Example
+    /// ```
+    /// # fn main() -> tiled::Result<()> {
+    /// use std::{sync::Arc, path::Path};
+    ///
+    /// use tiled::{Loader, ResourceCache};
+    ///
+    /// /// An example resource cache that doesn't actually cache any resources at all.
+    /// struct NoopResourceCache;
+    ///
+    /// impl ResourceCache for NoopResourceCache {
+    ///     fn get_tileset(
+    ///         &self,
+    ///         _path: impl AsRef<tiled::ResourcePath>,
+    ///     ) -> Option<std::sync::Arc<tiled::Tileset>> {
+    ///         None
+    ///     }
+    ///
+    ///     fn get_or_try_insert_tileset_with<F, E>(
+    ///         &mut self,
+    ///         _path: tiled::ResourcePathBuf,
+    ///         f: F,
+    ///     ) -> Result<std::sync::Arc<tiled::Tileset>, E>
+    ///     where
+    ///         F: FnOnce() -> Result<tiled::Tileset, E>,
+    ///     {
+    ///         f().map(Arc::new)
+    ///     }
+    /// }
+    ///
+    /// let mut loader = Loader::with_cache(NoopResourceCache);
+    ///
+    /// let map = loader.load_tmx_map("assets/tiled_base64_external.tmx")?;
+    ///
+    /// assert_eq!(
+    ///     map.tilesets()[0].image.as_ref().unwrap().source,
+    ///     Path::new("assets/tilesheet.png")
+    /// );
+    ///
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn with_cache(cache: Cache) -> Self {
+        Self { cache }
+    }
+
+    /// Parses a file hopefully containing a Tiled map and tries to parse it. All external files
+    /// will be loaded relative to the path given.
+    ///
+    /// All intermediate objects such as map tilesets will be stored in the [internal loader cache].
+    ///
+    /// If you need to parse a reader object instead, use [Loader::load_tmx_map_from()].
+    ///
+    /// [internal loader cache]: Loader::cache()
+    pub fn load_tmx_map(&mut self, path: impl AsRef<Path>) -> Result<Map> {
+        let reader = File::open(path.as_ref()).map_err(|err| Error::CouldNotOpenFile {
+            path: path.as_ref().to_owned(),
+            err,
+        })?;
+        crate::parse::xml::parse_map(reader, path.as_ref(), &mut self.cache)
+    }
+
+    /// Parses a map out of a reader hopefully containing the contents of a Tiled file.
+    ///
+    /// This augments [`load_tmx_map`] with a custom reader: some engines (e.g. Amethyst) simply
+    /// hand over a byte stream and file location for parsing, in which case this function may be
+    /// required.
+    ///
+    /// If you need to parse a file in the filesystem instead, [`load_tmx_map`] might be
+    /// more convenient.
+    ///
+    /// The path is used for external dependencies such as tilesets or images. It is required.
+    /// If the map if fully embedded and doesn't refer to external files, you may input an arbitrary
+    /// path; the library won't read from the filesystem if it is not required to do so.
+    ///
+    /// All intermediate objects such as map tilesets will be stored in the [internal loader cache].
+    ///
+    /// [internal loader cache]: Loader::cache()
+    /// [`load_tmx_map`]: Loader::load_tmx_map()
+    pub fn load_tmx_map_from(&mut self, reader: impl Read, path: impl AsRef<Path>) -> Result<Map> {
+        crate::parse::xml::parse_map(reader, path.as_ref(), &mut self.cache)
+    }
+
+    /// Parses a file hopefully containing a Tiled tileset and tries to parse it. All external files
+    /// will be loaded relative to the path given.
+    ///
+    /// Unless you specifically want to load a tileset, you won't need to call this function. If
+    /// you are trying to load a map, simply use [`Loader::load_tmx_map`] or
+    /// [`Loader::load_tmx_map_from`].
+    ///
+    /// If you need to parse a reader object instead, use [Loader::load_tsx_tileset_from()].
+    pub fn load_tsx_tileset(&mut self, path: impl AsRef<Path>) -> Result<Tileset> {
+        let reader = File::open(path.as_ref()).map_err(|err| Error::CouldNotOpenFile {
+            path: path.as_ref().to_owned(),
+            err,
+        })?;
+        crate::parse::xml::parse_tileset(reader, path.as_ref())
+    }
+
+    /// Parses a tileset out of a reader hopefully containing the contents of a Tiled tileset.
+    /// Uses the `path` parameter as the root for any relative paths found in the tileset.
+    ///
+    /// Unless you specifically want to load a tileset, you won't need to call this function. If
+    /// you are trying to load a map, simply use [`Loader::load_tmx_map`] or
+    /// [`Loader::load_tmx_map_from`].
+    ///
+    /// ## Example
+    /// ```
+    /// use std::fs::File;
+    /// use std::path::PathBuf;
+    /// use std::io::BufReader;
+    /// use tiled::Loader;
+    ///
+    /// let path = "assets/tilesheet.tsx";
+    /// // Note: This is just an example, if you actually need to load a file use `load_tsx_tileset`
+    /// // instead.
+    /// let reader = BufReader::new(File::open(path).unwrap());
+    /// let mut loader = Loader::new();
+    /// let tileset = loader.load_tsx_tileset_from(reader, path).unwrap();
+    ///
+    /// assert_eq!(tileset.image.unwrap().source, PathBuf::from("assets/tilesheet.png"));
+    /// ```
+    pub fn load_tsx_tileset_from(
+        &self,
+        reader: impl Read,
+        path: impl AsRef<Path>,
+    ) -> Result<Tileset> {
+        // This function doesn't need the cache right now, but will do once template support is in
+        crate::parse::xml::parse_tileset(reader, path.as_ref())
+    }
+
+    /// Returns a reference to the loader's internal [`ResourceCache`].
+    pub fn cache(&self) -> &Cache {
+        &self.cache
+    }
+
+    /// Consumes the loader and returns its internal [`ResourceCache`].
+    pub fn into_cache(self) -> Cache {
+        self.cache
+    }
+}
diff --git a/src/map.rs b/src/map.rs
index 4890f53..9fb04d0 100644
--- a/src/map.rs
+++ b/src/map.rs
@@ -2,7 +2,7 @@
 
 use std::{collections::HashMap, fmt, fs::File, io::Read, path::Path, str::FromStr, sync::Arc};
 
-use xml::{attribute::OwnedAttribute, reader::XmlEvent, EventReader};
+use xml::attribute::OwnedAttribute;
 
 use crate::{
     error::{Error, Result},
@@ -70,46 +70,26 @@ impl Map {
     /// the library won't read from the filesystem if it is not required to do so.
     ///
     /// The tileset cache is used to store and refer to any tilesets found along the way.
+    #[deprecated(since = "0.10.1", note = "Use `Loader::parse_tmx_map_from` instead")]
     pub fn parse_reader<R: Read>(
         reader: R,
         path: impl AsRef<Path>,
         cache: &mut impl ResourceCache,
     ) -> Result<Self> {
-        let mut parser = EventReader::new(reader);
-        loop {
-            match parser.next().map_err(Error::XmlDecodingError)? {
-                XmlEvent::StartElement {
-                    name, attributes, ..
-                } => {
-                    if name.local_name == "map" {
-                        return Self::parse_xml(
-                            &mut parser.into_iter(),
-                            attributes,
-                            path.as_ref(),
-                            cache,
-                        );
-                    }
-                }
-                XmlEvent::EndDocument => {
-                    return Err(Error::PrematureEnd(
-                        "Document ended before map was parsed".to_string(),
-                    ))
-                }
-                _ => {}
-            }
-        }
+        crate::parse::xml::parse_map(reader, path.as_ref(), cache)
     }
 
     /// Parse a file hopefully containing a Tiled map and try to parse it.  All external
     /// files will be loaded relative to the path given.
     ///
     /// The tileset cache is used to store and refer to any tilesets found along the way.
+    #[deprecated(since = "0.10.1", note = "Use `Loader::parse_tmx_map` instead")]
     pub fn parse_file(path: impl AsRef<Path>, cache: &mut impl ResourceCache) -> Result<Self> {
         let reader = File::open(path.as_ref()).map_err(|err| Error::CouldNotOpenFile {
             path: path.as_ref().to_owned(),
             err,
         })?;
-        Self::parse_reader(reader, path.as_ref(), cache)
+        crate::parse::xml::parse_map(reader, path.as_ref(), cache)
     }
 
     /// The TMX format version this map was saved to. Equivalent to the map file's `version`
@@ -146,7 +126,7 @@ impl Map {
 }
 
 impl Map {
-    fn parse_xml(
+    pub(crate) fn parse_xml(
         parser: &mut impl Iterator<Item = XmlEventResult>,
         attrs: Vec<OwnedAttribute>,
         map_path: &Path,
@@ -184,7 +164,7 @@ impl Map {
                 match res.result_type {
                     EmbeddedParseResultType::ExternalReference { tileset_path } => {
                         let file = File::open(&tileset_path).map_err(|err| Error::CouldNotOpenFile{path: tileset_path.clone(), err })?;
-                        let tileset = cache.get_or_try_insert_tileset_with(tileset_path.clone(), || Tileset::parse_reader(file, &tileset_path))?;
+                        let tileset = cache.get_or_try_insert_tileset_with(tileset_path.clone(), || crate::parse::xml::parse_tileset(file, &tileset_path))?;
                         tilesets.push(MapTilesetGid{first_gid: res.first_gid, tileset});
                     }
                     EmbeddedParseResultType::Embedded { tileset } => {
diff --git a/src/parse/mod.rs b/src/parse/mod.rs
new file mode 100644
index 0000000..2910ec6
--- /dev/null
+++ b/src/parse/mod.rs
@@ -0,0 +1 @@
+pub mod xml;
diff --git a/src/parse/xml/map.rs b/src/parse/xml/map.rs
new file mode 100644
index 0000000..05ce67a
--- /dev/null
+++ b/src/parse/xml/map.rs
@@ -0,0 +1,35 @@
+use std::{io::Read, path::Path};
+
+use xml::{reader::XmlEvent, EventReader};
+
+use crate::{Error, Map, ResourceCache, Result};
+
+pub fn parse_map(
+    reader: impl Read,
+    path: &Path,
+    cache: &mut impl ResourceCache,
+) -> Result<Map> {
+    let mut parser = EventReader::new(reader);
+    loop {
+        match parser.next().map_err(Error::XmlDecodingError)? {
+            XmlEvent::StartElement {
+                name, attributes, ..
+            } => {
+                if name.local_name == "map" {
+                    return Map::parse_xml(
+                        &mut parser.into_iter(),
+                        attributes,
+                        path,
+                        cache,
+                    );
+                }
+            }
+            XmlEvent::EndDocument => {
+                return Err(Error::PrematureEnd(
+                    "Document ended before map was parsed".to_string(),
+                ))
+            }
+            _ => {}
+        }
+    }
+}
diff --git a/src/parse/xml/mod.rs b/src/parse/xml/mod.rs
new file mode 100644
index 0000000..5b8107c
--- /dev/null
+++ b/src/parse/xml/mod.rs
@@ -0,0 +1,4 @@
+mod map;
+pub use map::*;
+mod tileset;
+pub use tileset::*;
diff --git a/src/parse/xml/tileset.rs b/src/parse/xml/tileset.rs
new file mode 100644
index 0000000..df6bbe6
--- /dev/null
+++ b/src/parse/xml/tileset.rs
@@ -0,0 +1,28 @@
+use std::{io::Read, path::Path};
+
+use xml::{reader::XmlEvent, EventReader};
+
+use crate::{Error, Result, Tileset};
+
+pub fn parse_tileset<R: Read>(reader: R, path: &Path) -> Result<Tileset> {
+    let mut tileset_parser = EventReader::new(reader);
+    loop {
+        match tileset_parser.next().map_err(Error::XmlDecodingError)? {
+            XmlEvent::StartElement {
+                name, attributes, ..
+            } if name.local_name == "tileset" => {
+                return Tileset::parse_external_tileset(
+                    &mut tileset_parser.into_iter(),
+                    &attributes,
+                    path,
+                );
+            }
+            XmlEvent::EndDocument => {
+                return Err(Error::PrematureEnd(
+                    "Tileset Document ended before map was parsed".to_string(),
+                ))
+            }
+            _ => {}
+        }
+    }
+}
diff --git a/src/tileset.rs b/src/tileset.rs
index 34d2d53..5437606 100644
--- a/src/tileset.rs
+++ b/src/tileset.rs
@@ -3,8 +3,6 @@ use std::io::Read;
 use std::path::{Path, PathBuf};
 
 use xml::attribute::OwnedAttribute;
-use xml::reader::XmlEvent;
-use xml::EventReader;
 
 use crate::error::{Error, Result};
 use crate::image::Image;
@@ -99,27 +97,9 @@ impl Tileset {
     ///
     /// assert_eq!(tileset.image.unwrap().source, PathBuf::from("assets/tilesheet.png"));
     /// ```
+    #[deprecated(since = "0.10.1", note = "Use `Loader::parse_tsx_tileset` instead")]
     pub fn parse_reader<R: Read>(reader: R, path: impl AsRef<Path>) -> Result<Self> {
-        let mut tileset_parser = EventReader::new(reader);
-        loop {
-            match tileset_parser.next().map_err(Error::XmlDecodingError)? {
-                XmlEvent::StartElement {
-                    name, attributes, ..
-                } if name.local_name == "tileset" => {
-                    return Self::parse_external_tileset(
-                        &mut tileset_parser.into_iter(),
-                        &attributes,
-                        path.as_ref(),
-                    );
-                }
-                XmlEvent::EndDocument => {
-                    return Err(Error::PrematureEnd(
-                        "Tileset Document ended before map was parsed".to_string(),
-                    ))
-                }
-                _ => {}
-            }
-        }
+        crate::parse::xml::parse_tileset(reader, path.as_ref())
     }
 
     /// Gets the tile with the specified ID from the tileset.
@@ -216,7 +196,7 @@ impl Tileset {
         })
     }
 
-    fn parse_external_tileset(
+    pub(crate) fn parse_external_tileset(
         parser: &mut impl Iterator<Item = XmlEventResult>,
         attrs: &Vec<OwnedAttribute>,
         path: &Path,
diff --git a/tests/lib.rs b/tests/lib.rs
index e682be7..4698c00 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -1,7 +1,7 @@
 use std::path::PathBuf;
 use tiled::{
-    Color, FilesystemResourceCache, FiniteTileLayer, GroupLayer, Layer, LayerType, Map,
-    ObjectLayer, PropertyValue, ResourceCache, TileLayer,
+    Color, FiniteTileLayer, GroupLayer, Layer, LayerType, Loader, Map, ObjectLayer, PropertyValue,
+    ResourceCache, TileLayer,
 };
 
 fn as_tile_layer<'map>(layer: Layer<'map>) -> TileLayer<'map> {
@@ -51,12 +51,14 @@ fn compare_everything_but_tileset_sources(r: &Map, e: &Map) {
 
 #[test]
 fn test_gzip_and_zlib_encoded_and_raw_are_the_same() {
-    let mut cache = FilesystemResourceCache::new();
-    let z = Map::parse_file("assets/tiled_base64_zlib.tmx", &mut cache).unwrap();
-    let g = Map::parse_file("assets/tiled_base64_gzip.tmx", &mut cache).unwrap();
-    let r = Map::parse_file("assets/tiled_base64.tmx", &mut cache).unwrap();
-    let zstd = Map::parse_file("assets/tiled_base64_zstandard.tmx", &mut cache).unwrap();
-    let c = Map::parse_file("assets/tiled_csv.tmx", &mut cache).unwrap();
+    let mut loader = Loader::new();
+    let z = loader.load_tmx_map("assets/tiled_base64_zlib.tmx").unwrap();
+    let g = loader.load_tmx_map("assets/tiled_base64_gzip.tmx").unwrap();
+    let r = loader.load_tmx_map("assets/tiled_base64.tmx").unwrap();
+    let zstd = loader
+        .load_tmx_map("assets/tiled_base64_zstandard.tmx")
+        .unwrap();
+    let c = Loader::new().load_tmx_map("assets/tiled_csv.tmx").unwrap();
     compare_everything_but_tileset_sources(&z, &g);
     compare_everything_but_tileset_sources(&z, &r);
     compare_everything_but_tileset_sources(&z, &c);
@@ -77,21 +79,24 @@ fn test_gzip_and_zlib_encoded_and_raw_are_the_same() {
 
 #[test]
 fn test_external_tileset() {
-    let mut cache = FilesystemResourceCache::new();
+    let mut loader = Loader::new();
 
-    let r = Map::parse_file("assets/tiled_base64.tmx", &mut cache).unwrap();
-    let e = Map::parse_file("assets/tiled_base64_external.tmx", &mut cache).unwrap();
+    let r = loader.load_tmx_map("assets/tiled_base64.tmx").unwrap();
+    let e = loader
+        .load_tmx_map("assets/tiled_base64_external.tmx")
+        .unwrap();
     compare_everything_but_tileset_sources(&r, &e);
 }
 
 #[test]
 fn test_sources() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let e = Map::parse_file("assets/tiled_base64_external.tmx", &mut cache).unwrap();
+    let mut loader = Loader::new();
+    let e = loader
+        .load_tmx_map("assets/tiled_base64_external.tmx")
+        .unwrap();
     assert_eq!(
         e.tilesets()[0],
-        cache.get_tileset("assets/tilesheet.tsx").unwrap()
+        loader.cache().get_tileset("assets/tilesheet.tsx").unwrap()
     );
     assert_eq!(
         e.tilesets()[0].image.as_ref().unwrap().source,
@@ -101,20 +106,21 @@ fn test_sources() {
 
 #[test]
 fn test_just_tileset() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/tiled_base64_external.tmx", &mut cache).unwrap();
+    let mut loader = Loader::new();
+    let r = loader
+        .load_tmx_map("assets/tiled_base64_external.tmx")
+        .unwrap();
     assert_eq!(
         r.tilesets()[0],
-        cache.get_tileset("assets/tilesheet.tsx").unwrap()
+        loader.cache().get_tileset("assets/tilesheet.tsx").unwrap()
     );
 }
 
 #[test]
 fn test_infinite_tileset() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/tiled_base64_zlib_infinite.tmx", &mut cache).unwrap();
+    let r = Loader::new()
+        .load_tmx_map("assets/tiled_base64_zlib_infinite.tmx")
+        .unwrap();
 
     if let TileLayer::Infinite(inf) = &as_tile_layer(r.get_layer(1).unwrap()) {
         assert_eq!(inf.get_tile(2, 10).unwrap().id(), 5);
@@ -150,9 +156,9 @@ fn test_infinite_tileset() {
 
 #[test]
 fn test_image_layers() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/tiled_image_layers.tmx", &mut cache).unwrap();
+    let r = Loader::new()
+        .load_tmx_map("assets/tiled_image_layers.tmx")
+        .unwrap();
     assert_eq!(r.layers().len(), 2);
     let mut image_layers = r.layers().map(|layer| {
         if let LayerType::ImageLayer(img) = layer.layer_type() {
@@ -186,9 +192,9 @@ fn test_image_layers() {
 
 #[test]
 fn test_tile_property() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/tiled_base64.tmx", &mut cache).unwrap();
+    let r = Loader::new()
+        .load_tmx_map("assets/tiled_base64.tmx")
+        .unwrap();
     let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) = r.tilesets()[0]
         .get_tile(1)
         .unwrap()
@@ -204,9 +210,9 @@ fn test_tile_property() {
 
 #[test]
 fn test_layer_property() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/tiled_base64.tmx", &mut cache).unwrap();
+    let r = Loader::new()
+        .load_tmx_map("assets/tiled_base64.tmx")
+        .unwrap();
     let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) =
         r.get_layer(0).unwrap().properties.get("prop3")
     {
@@ -219,9 +225,9 @@ fn test_layer_property() {
 
 #[test]
 fn test_object_group_property() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/tiled_object_groups.tmx", &mut cache).unwrap();
+    let r = Loader::new()
+        .load_tmx_map("assets/tiled_object_groups.tmx")
+        .unwrap();
     let group_layer = r.get_layer(1).unwrap();
     let group_layer = as_group_layer(group_layer);
     let sub_layer = group_layer.get_layer(0).unwrap();
@@ -236,9 +242,9 @@ fn test_object_group_property() {
 }
 #[test]
 fn test_tileset_property() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/tiled_base64.tmx", &mut cache).unwrap();
+    let r = Loader::new()
+        .load_tmx_map("assets/tiled_base64.tmx")
+        .unwrap();
     let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) =
         r.tilesets()[0].properties.get("tileset property")
     {
@@ -251,9 +257,9 @@ fn test_tileset_property() {
 
 #[test]
 fn test_flipped() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/tiled_flipped.tmx", &mut cache).unwrap();
+    let r = Loader::new()
+        .load_tmx_map("assets/tiled_flipped.tmx")
+        .unwrap();
     let layer = as_tile_layer(r.get_layer(0).unwrap());
 
     let t1 = layer.get_tile(0, 0).unwrap();
@@ -279,9 +285,9 @@ fn test_flipped() {
 
 #[test]
 fn test_ldk_export() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/ldk_tiled_export.tmx", &mut cache).unwrap();
+    let r = Loader::new()
+        .load_tmx_map("assets/ldk_tiled_export.tmx")
+        .unwrap();
     let layer = as_finite(as_tile_layer(r.get_layer(0).unwrap()));
     {
         assert_eq!(layer.width(), 8);
@@ -293,9 +299,9 @@ fn test_ldk_export() {
 
 #[test]
 fn test_parallax_layers() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/tiled_parallax.tmx", &mut cache).unwrap();
+    let r = Loader::new()
+        .load_tmx_map("assets/tiled_parallax.tmx")
+        .unwrap();
     for (i, layer) in r.layers().enumerate() {
         match i {
             0 => {
@@ -320,9 +326,9 @@ fn test_parallax_layers() {
 
 #[test]
 fn test_object_property() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/tiled_object_property.tmx", &mut cache).unwrap();
+    let r = Loader::new()
+        .load_tmx_map("assets/tiled_object_property.tmx")
+        .unwrap();
     let layer = r.get_layer(1).unwrap();
     let prop_value = if let Some(PropertyValue::ObjectValue(v)) = as_object_layer(layer)
         .get_object(0)
@@ -339,9 +345,9 @@ fn test_object_property() {
 
 #[test]
 fn test_tint_color() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/tiled_image_layers.tmx", &mut cache).unwrap();
+    let r = Loader::new()
+        .load_tmx_map("assets/tiled_image_layers.tmx")
+        .unwrap();
     assert_eq!(
         r.get_layer(0).unwrap().tint_color,
         Some(Color {
@@ -364,9 +370,9 @@ fn test_tint_color() {
 
 #[test]
 fn test_group_layers() {
-    let mut cache = FilesystemResourceCache::new();
-
-    let r = Map::parse_file("assets/tiled_group_layers.tmx", &mut cache).unwrap();
+    let r = Loader::new()
+        .load_tmx_map("assets/tiled_group_layers.tmx")
+        .unwrap();
 
     // Depth = 0
     let layer_tile_1 = r.get_layer(0).unwrap();
-- 
GitLab