From 672b5cab6934b4d7159a729d1c4ad9d6e6acaf2c Mon Sep 17 00:00:00 2001
From: Alejandro Perea <alexpro820@gmail.com>
Date: Mon, 2 May 2022 13:29:21 +0200
Subject: [PATCH] Refactor `get_attrs` (#219)

* Refactor `get_attrs`

* Add explanation
---
 src/animation.rs            |  11 +-
 src/image.rs                |  17 ++--
 src/layers/mod.rs           |  24 ++---
 src/layers/object.rs        |   8 +-
 src/layers/tile/finite.rs   |  10 +-
 src/layers/tile/infinite.rs |  25 +++--
 src/layers/tile/mod.rs      |  11 +-
 src/map.rs                  |  26 +++--
 src/objects.rs              |  56 +++++------
 src/properties.rs           |  17 ++--
 src/tile.rs                 |  15 ++-
 src/tileset.rs              |  60 +++++------
 src/util.rs                 | 196 +++++++++++++++++++++++++++++-------
 13 files changed, 288 insertions(+), 188 deletions(-)

diff --git a/src/animation.rs b/src/animation.rs
index a7bb686..6ffccbb 100644
--- a/src/animation.rs
+++ b/src/animation.rs
@@ -22,12 +22,11 @@ pub struct Frame {
 impl Frame {
     pub(crate) fn new(attrs: Vec<OwnedAttribute>) -> Result<Frame> {
         let (tile_id, duration) = get_attrs!(
-            attrs,
-            required: [
-                ("tileid", tile_id, |v:String| v.parse().ok()),
-                ("duration", duration, |v:String| v.parse().ok()),
-            ],
-            Error::MalformedAttributes("A frame must have tileid and duration".to_string())
+            for v in attrs {
+                "tileid" => tile_id ?= v.parse::<u32>(),
+                "duration" => duration ?= v.parse::<u32>(),
+            }
+            (tile_id, duration)
         );
         Ok(Frame { tile_id, duration })
     }
diff --git a/src/image.rs b/src/image.rs
index 3301429..b29a310 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -84,16 +84,13 @@ impl Image {
         path_relative_to: impl AsRef<Path>,
     ) -> Result<Image> {
         let (c, (s, w, h)) = get_attrs!(
-            attrs,
-            optionals: [
-                ("trans", trans, |v:String| v.parse().ok()),
-            ],
-            required: [
-                ("source", source, Some),
-                ("width", width, |v:String| v.parse().ok()),
-                ("height", height, |v:String| v.parse().ok()),
-            ],
-            Error::MalformedAttributes("Image must have a source, width and height with correct types".to_string())
+            for v in attrs {
+                Some("trans") => trans ?= v.parse(),
+                "source" => source = v,
+                "width" => width ?= v.parse::<i32>(),
+                "height" => height ?= v.parse::<i32>(),
+            }
+            (trans, (source, width, height))
         );
 
         parse_tag!(parser, "image", {});
diff --git a/src/layers/mod.rs b/src/layers/mod.rs
index 1272cf4..ab0e9f5 100644
--- a/src/layers/mod.rs
+++ b/src/layers/mod.rs
@@ -71,18 +71,18 @@ impl LayerData {
         tilesets: &[MapTilesetGid],
     ) -> Result<Self> {
         let (opacity, tint_color, visible, offset_x, offset_y, parallax_x, parallax_y, name, id) = get_attrs!(
-            attrs,
-            optionals: [
-                ("opacity", opacity, |v:String| v.parse().ok()),
-                ("tintcolor", tint_color, |v:String| v.parse().ok()),
-                ("visible", visible, |v:String| v.parse().ok().map(|x:i32| x == 1)),
-                ("offsetx", offset_x, |v:String| v.parse().ok()),
-                ("offsety", offset_y, |v:String| v.parse().ok()),
-                ("parallaxx", parallax_x, |v:String| v.parse().ok()),
-                ("parallaxy", parallax_y, |v:String| v.parse().ok()),
-                ("name", name, Some),
-                ("id", id, |v:String| v.parse().ok()),
-            ]
+            for v in attrs {
+                Some("opacity") => opacity ?= v.parse(),
+                Some("tintcolor") => tint_color ?= v.parse(),
+                Some("visible") => visible ?= v.parse().map(|x:i32| x == 1),
+                Some("offsetx") => offset_x ?= v.parse(),
+                Some("offsety") => offset_y ?= v.parse(),
+                Some("parallaxx") => parallax_x ?= v.parse(),
+                Some("parallaxy") => parallax_y ?= v.parse(),
+                Some("name") => name = v,
+                Some("id") => id ?= v.parse(),
+            }
+            (opacity, tint_color, visible, offset_x, offset_y, parallax_x, parallax_y, name, id)
         );
 
         let (ty, properties) = match tag {
diff --git a/src/layers/object.rs b/src/layers/object.rs
index 45c08d8..1d6c636 100644
--- a/src/layers/object.rs
+++ b/src/layers/object.rs
@@ -25,10 +25,10 @@ impl ObjectLayerData {
         tilesets: Option<&[MapTilesetGid]>,
     ) -> Result<(ObjectLayerData, Properties)> {
         let c = get_attrs!(
-            attrs,
-            optionals: [
-                ("color", colour, |v:String| v.parse().ok()),
-            ]
+            for v in attrs {
+                Some("color") => color ?= v.parse(),
+            }
+            color
         );
         let mut objects = Vec::new();
         let mut properties = HashMap::new();
diff --git a/src/layers/tile/finite.rs b/src/layers/tile/finite.rs
index 0e018fc..590ea33 100644
--- a/src/layers/tile/finite.rs
+++ b/src/layers/tile/finite.rs
@@ -46,11 +46,11 @@ impl FiniteTileLayerData {
         tilesets: &[MapTilesetGid],
     ) -> Result<Self> {
         let (e, c) = get_attrs!(
-            attrs,
-            optionals: [
-                ("encoding", encoding, Some),
-                ("compression", compression, Some),
-            ]
+            for v in attrs {
+                Some("encoding") => encoding = v,
+                Some("compression") => compression = v,
+            }
+            (encoding, compression)
         );
 
         let tiles = parse_data_line(e, c, parser, tilesets)?;
diff --git a/src/layers/tile/infinite.rs b/src/layers/tile/infinite.rs
index 9c79e5b..0fc4111 100644
--- a/src/layers/tile/infinite.rs
+++ b/src/layers/tile/infinite.rs
@@ -28,11 +28,11 @@ impl InfiniteTileLayerData {
         tilesets: &[MapTilesetGid],
     ) -> Result<Self> {
         let (e, c) = get_attrs!(
-            attrs,
-            optionals: [
-                ("encoding", encoding, Some),
-                ("compression", compression, Some),
-            ]
+            for v in attrs {
+                Some("encoding") => encoding = v,
+                Some("compression") => compression = v,
+            }
+            (encoding, compression)
         );
 
         let mut chunks = HashMap::<(i32, i32), Chunk>::new();
@@ -191,14 +191,13 @@ impl InternalChunk {
         tilesets: &[MapTilesetGid],
     ) -> Result<Self> {
         let (x, y, width, height) = get_attrs!(
-            attrs,
-            required: [
-                ("x", x, |v: String| v.parse().ok()),
-                ("y", y, |v: String| v.parse().ok()),
-                ("width", width, |v: String| v.parse().ok()),
-                ("height", height, |v: String| v.parse().ok()),
-            ],
-            Error::MalformedAttributes("chunk must have x, y, width & height attributes".to_string())
+            for v in attrs {
+                "x" => x ?= v.parse::<i32>(),
+                "y" => y ?= v.parse::<i32>(),
+                "width" => width ?= v.parse::<u32>(),
+                "height" => height ?= v.parse::<u32>(),
+            }
+            (x, y, width, height)
         );
 
         let tiles = parse_data_line(encoding, compression, parser, tilesets)?;
diff --git a/src/layers/tile/mod.rs b/src/layers/tile/mod.rs
index ff32120..109554c 100644
--- a/src/layers/tile/mod.rs
+++ b/src/layers/tile/mod.rs
@@ -101,12 +101,11 @@ impl TileLayerData {
         tilesets: &[MapTilesetGid],
     ) -> Result<(Self, Properties)> {
         let (width, height) = get_attrs!(
-            attrs,
-            required: [
-                ("width", width, |v: String| v.parse().ok()),
-                ("height", height, |v: String| v.parse().ok()),
-            ],
-            Error::MalformedAttributes("layer parsing error, width and height attributes required".to_string())
+            for v in attrs {
+                "width" => width ?= v.parse::<u32>(),
+                "height" => height ?= v.parse::<u32>(),
+            }
+            (width, height)
         );
         let mut result = Self::Finite(Default::default());
         let mut properties = HashMap::new();
diff --git a/src/map.rs b/src/map.rs
index 8871985..2343803 100644
--- a/src/map.rs
+++ b/src/map.rs
@@ -158,20 +158,17 @@ impl Map {
         cache: &mut impl ResourceCache,
     ) -> Result<Map> {
         let ((c, infinite), (v, o, w, h, tw, th)) = get_attrs!(
-            attrs,
-            optionals: [
-                ("backgroundcolor", colour, |v:String| v.parse().ok()),
-                ("infinite", infinite, |v:String| Some(v == "1")),
-            ],
-            required: [
-                ("version", version, Some),
-                ("orientation", orientation, |v:String| v.parse().ok()),
-                ("width", width, |v:String| v.parse().ok()),
-                ("height", height, |v:String| v.parse().ok()),
-                ("tilewidth", tile_width, |v:String| v.parse().ok()),
-                ("tileheight", tile_height, |v:String| v.parse().ok()),
-            ],
-            Error::MalformedAttributes("map must have version, width, height, tilewidth, tileheight and orientation with correct types".to_string())
+            for v in attrs {
+                Some("backgroundcolor") => colour ?= v.parse(),
+                Some("infinite") => infinite = v == "1",
+                "version" => version = v,
+                "orientation" => orientation ?= v.parse::<Orientation>(),
+                "width" => width ?= v.parse::<u32>(),
+                "height" => height ?= v.parse::<u32>(),
+                "tilewidth" => tile_width ?= v.parse::<u32>(),
+                "tileheight" => tile_height ?= v.parse::<u32>(),
+            }
+            ((colour, infinite), (version, orientation, width, height, tile_width, tile_height))
         );
 
         let infinite = infinite.unwrap_or(false);
@@ -278,6 +275,7 @@ pub enum Orientation {
 }
 
 impl FromStr for Orientation {
+    // TODO(0.11): Change error type to OrientationParseErr or similar
     type Err = ();
 
     fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
diff --git a/src/objects.rs b/src/objects.rs
index b2f8281..d56ccec 100644
--- a/src/objects.rs
+++ b/src/objects.rs
@@ -82,24 +82,22 @@ impl ObjectData {
         tilesets: Option<&[MapTilesetGid]>,
     ) -> Result<ObjectData> {
         let ((id, tile, n, t, w, h, v, r), (x, y)) = get_attrs!(
-            attrs,
-            optionals: [
-                ("id", id, |v:String| v.parse().ok()),
-                ("gid", tile, |v:String| v.parse().ok()
-                                            .and_then(|bits| LayerTileData::from_bits(bits, tilesets?))),
-                ("name", name, |v:String| v.parse().ok()),
-                ("type", obj_type, |v:String| v.parse().ok()),
-                ("width", width, |v:String| v.parse().ok()),
-                ("height", height, |v:String| v.parse().ok()),
-                ("visible", visible, |v:String| v.parse().ok().map(|x:i32| x == 1)),
-                ("rotation", rotation, |v:String| v.parse().ok()),
-            ],
-            required: [
-                ("x", x, |v:String| v.parse().ok()),
-                ("y", y, |v:String| v.parse().ok()),
-            ],
-            Error::MalformedAttributes("objects must have an x and a y number".to_string())
+            for v in attrs {
+                Some("id") => id ?= v.parse(),
+                Some("gid") => tile ?= v.parse(),
+                Some("name") => name ?= v.parse(),
+                Some("type") => obj_type ?= v.parse(),
+                Some("width") => width ?= v.parse(),
+                Some("height") => height ?= v.parse(),
+                Some("visible") => visible ?= v.parse().map(|x:i32| x == 1),
+                Some("rotation") => rotation ?= v.parse(),
+
+                "x" => x ?= v.parse::<f32>(),
+                "y" => y ?= v.parse::<f32>(),
+            }
+            ((id, tile, name, obj_type, width, height, visible, rotation), (x, y))
         );
+        let tile = tile.and_then(|bits| LayerTileData::from_bits(bits, tilesets?));
         let visible = v.unwrap_or(true);
         let width = w.unwrap_or(0f32);
         let height = h.unwrap_or(0f32);
@@ -158,26 +156,22 @@ impl ObjectData {
 
 impl ObjectData {
     fn new_polyline(attrs: Vec<OwnedAttribute>) -> Result<ObjectShape> {
-        let s = get_attrs!(
-            attrs,
-            required: [
-                ("points", points, Some),
-            ],
-            Error::MalformedAttributes("A polyline must have points".to_string())
+        let points = get_attrs!(
+            for v in attrs {
+                "points" => points ?= ObjectData::parse_points(v),
+            }
+            points
         );
-        let points = ObjectData::parse_points(s)?;
         Ok(ObjectShape::Polyline { points })
     }
 
     fn new_polygon(attrs: Vec<OwnedAttribute>) -> Result<ObjectShape> {
-        let s = get_attrs!(
-            attrs,
-            required: [
-                ("points", points, Some),
-            ],
-            Error::MalformedAttributes("A polygon must have points".to_string())
+        let points = get_attrs!(
+            for v in attrs {
+                "points" => points ?= ObjectData::parse_points(v),
+            }
+            points
         );
-        let points = ObjectData::parse_points(s)?;
         Ok(ObjectShape::Polygon { points })
     }
 
diff --git a/src/properties.rs b/src/properties.rs
index 63335ea..27ab438 100644
--- a/src/properties.rs
+++ b/src/properties.rs
@@ -135,16 +135,13 @@ pub(crate) fn parse_properties(
     let mut p = HashMap::new();
     parse_tag!(parser, "properties", {
         "property" => |attrs:Vec<OwnedAttribute>| {
-            let ((t, v_attr), k) = get_attrs!(
-                attrs,
-                optionals: [
-                    ("type", property_type, Some),
-                    ("value", value, Some),
-                ],
-                required: [
-                    ("name", key, Some),
-                ],
-                Error::MalformedAttributes("property must have a name and a value".to_string())
+            let (t, v_attr, k) = get_attrs!(
+                for attr in attrs {
+                    Some("type") => obj_type = attr,
+                    Some("value") => value = attr,
+                    "name" => name = attr
+                }
+                (obj_type, value, name)
             );
             let t = t.unwrap_or_else(|| "string".to_owned());
 
diff --git a/src/tile.rs b/src/tile.rs
index ddb5413..b697b64 100644
--- a/src/tile.rs
+++ b/src/tile.rs
@@ -66,15 +66,12 @@ impl TileData {
         path_relative_to: &Path,
     ) -> Result<(TileId, TileData)> {
         let ((tile_type, probability), id) = get_attrs!(
-            attrs,
-            optionals: [
-                ("type", tile_type, |v:String| v.parse().ok()),
-                ("probability", probability, |v:String| v.parse().ok()),
-            ],
-            required: [
-                ("id", id, |v:String| v.parse::<u32>().ok()),
-            ],
-            Error::MalformedAttributes("tile must have an id with the correct type".to_string())
+            for v in attrs {
+                Some("type") => tile_type ?= v.parse(),
+                Some("probability") => probability ?= v.parse(),
+                "id" => id ?= v.parse::<u32>(),
+            }
+            ((tile_type, probability), id)
         );
 
         let mut image = Option::None;
diff --git a/src/tileset.rs b/src/tileset.rs
index d907aa7..441be44 100644
--- a/src/tileset.rs
+++ b/src/tileset.rs
@@ -138,20 +138,17 @@ impl Tileset {
         map_path: &Path,
     ) -> Result<EmbeddedParseResult> {
         let ((spacing, margin, columns, name), (tilecount, first_gid, tile_width, tile_height)) = get_attrs!(
-           attrs,
-           optionals: [
-                ("spacing", spacing, |v:String| v.parse().ok()),
-                ("margin", margin, |v:String| v.parse().ok()),
-                ("columns", columns, |v:String| v.parse().ok()),
-                ("name", name, Some),
-            ],
-           required: [
-                ("tilecount", tilecount, |v:String| v.parse().ok()),
-                ("firstgid", first_gid, |v:String| v.parse().ok().map(Gid)),
-                ("tilewidth", width, |v:String| v.parse().ok()),
-                ("tileheight", height, |v:String| v.parse().ok()),
-            ],
-            Error::MalformedAttributes("tileset must have a firstgid, tilecount, tilewidth, and tileheight with correct types".to_string())
+           for v in attrs {
+            Some("spacing") => spacing ?= v.parse(),
+            Some("margin") => margin ?= v.parse(),
+            Some("columns") => columns ?= v.parse(),
+            Some("name") => name = v,
+            "tilecount" => tilecount ?= v.parse::<u32>(),
+            "firstgid" => first_gid ?= v.parse::<u32>().map(Gid),
+            "tilewidth" => tile_width ?= v.parse::<u32>(),
+            "tileheight" => tile_height ?= v.parse::<u32>(),
+           }
+           ((spacing, margin, columns, name), (tilecount, first_gid, tile_width, tile_height))
         );
 
         let root_path = map_path.parent().ok_or(Error::PathIsNotFile)?.to_owned();
@@ -180,12 +177,11 @@ impl Tileset {
         map_path: &Path,
     ) -> Result<EmbeddedParseResult> {
         let (first_gid, source) = get_attrs!(
-            attrs,
-            required: [
-                ("firstgid", first_gid, |v:String| v.parse().ok().map(Gid)),
-                ("source", name, Some),
-            ],
-            Error::MalformedAttributes("Tileset reference must have a firstgid and source with correct types".to_string())
+            for v in attrs {
+                "firstgid" => first_gid ?= v.parse::<u32>().map(Gid),
+                "source" => source = v,
+            }
+            (first_gid, source)
         );
 
         let tileset_path = map_path.parent().ok_or(Error::PathIsNotFile)?.join(source);
@@ -202,19 +198,17 @@ impl Tileset {
         path: &Path,
     ) -> Result<Tileset> {
         let ((spacing, margin, columns, name), (tilecount, tile_width, tile_height)) = get_attrs!(
-            attrs,
-            optionals: [
-                ("spacing", spacing, |v:String| v.parse().ok()),
-                ("margin", margin, |v:String| v.parse().ok()),
-                ("columns", columns, |v:String| v.parse().ok()),
-                ("name", name, Some),
-            ],
-            required: [
-                ("tilecount", tilecount, |v:String| v.parse().ok()),
-                ("tilewidth", width, |v:String| v.parse().ok()),
-                ("tileheight", height, |v:String| v.parse().ok()),
-            ],
-            Error::MalformedAttributes("tileset must have a name, tile width and height with correct types".to_string())
+            for v in attrs {
+                Some("spacing") => spacing ?= v.parse(),
+                Some("margin") => margin ?= v.parse(),
+                Some("columns") => columns ?= v.parse(),
+                Some("name") => name = v,
+
+                "tilecount" => tilecount ?= v.parse::<u32>(),
+                "tilewidth" => tile_width ?= v.parse::<u32>(),
+                "tileheight" => tile_height ?= v.parse::<u32>(),
+            }
+            ((spacing, margin, columns, name), (tilecount, tile_width, tile_height))
         );
 
         let root_path = path.parent().ok_or(Error::PathIsNotFile)?.to_owned();
diff --git a/src/util.rs b/src/util.rs
index 4e874f2..a10222d 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -1,55 +1,182 @@
 /// Loops through the attributes once and pulls out the ones we ask it to. It
-/// will check that the required ones are there. This could have been done with
-/// attrs.find but that would be inefficient.
+/// will check that the required ones are there.
+///
+/// The syntax is:
+/// ```ignore
+/// get_attrs!(
+///     for $attr in $attributes {
+///         $($branch),*
+///     }
+///     $expression_to_return
+/// )
+/// ```
+/// Where `$attributes` is anything that implements `Iterator<Item = OwnedAttribute>`,
+/// and `$attr` is the value of the attribute (a String) going to be used in each branch.
+///
+/// Each branch indicates a variable to be set once a certain attribute is found.
+/// Its syntax is as follows:
+/// ```ignore
+/// "attribute name" => variable_name = expression_using_$attr,
+/// ```
+///
+/// For instance:
+/// ```ignore
+/// "source" => source = v,
+/// ```
+/// The variable set has an inferred type `T`. In this case, `source` is inferred to be a `String`,
+/// and `$attr` has been named `v`.
+///
+/// If `Some` encapsulates the attribute name (like so: `Some("attribute name")`) then the attribute
+/// is meant to be optional, which will make the variable an `Option<T>` rather than `T`. Even if it
+/// is technically an Option, the assignment is still done *as if it was `T`*, for instance:
+/// ```ignore
+/// Some("name") => name = v,
+/// ```
+///
+/// Finally, branches can also use `?=` instead of `=`, which will make them accept a `Result<T, E>`
+/// instead. If the expression results in an Err, the error will be handled internally and the
+/// iteration will return early with a `Result<T, crate::Error>`.
+///
+/// Here are some examples of valid branches:
+/// ```ignore
+/// Some("spacing") => spacing ?= v.parse(),
+/// Some("margin") => margin ?= v.parse(),
+/// Some("columns") => columns ?= v.parse(),
+/// Some("name") => name = v,
+///
+/// "tilecount" => tilecount ?= v.parse::<u32>(),
+/// "tilewidth" => tile_width ?= v.parse::<u32>(),
+/// "tileheight" => tile_height ?= v.parse::<u32>(),
+/// ```
+///
+/// Finally, after the `for` block, `$expression_to_return` indicates what to return once the
+/// iteration has finished. It may refer to variables declared previously.
+///
+/// ## Example
+/// ```ignore
+/// let ((c, infinite), (v, o, w, h, tw, th)) = get_attrs!(
+///     for v in attrs {
+///         Some("backgroundcolor") => colour ?= v.parse(),
+///         Some("infinite") => infinite = v == "1",
+///         "version" => version = v,
+///         "orientation" => orientation ?= v.parse::<Orientation>(),
+///         "width" => width ?= v.parse::<u32>(),
+///         "height" => height ?= v.parse::<u32>(),
+///         "tilewidth" => tile_width ?= v.parse::<u32>(),
+///         "tileheight" => tile_height ?= v.parse::<u32>(),
+///     }
+///     ((colour, infinite), (version, orientation, width, height, tile_width, tile_height))
+/// );
+/// ```
 macro_rules! get_attrs {
-    ($attrs:expr, optionals: [$(($oName:pat, $oVar:ident, $oMethod:expr)),+ $(,)*]
-     , required: [$(($name:pat, $var:ident, $method:expr)),+ $(,)*], $err:expr) => {
+    (
+        for $attr:ident in $attrs:ident {
+            $($branches:tt)*
+        }
+        $ret_expr:expr
+    ) => {
         {
-            $(let mut $oVar = None;)*
-            $(let mut $var = None;)*
-            $crate::util::match_attrs!($attrs, match: [$(($oName, $oVar, $oMethod)),+, $(($name, $var, $method)),+]);
+            $crate::util::let_attr_branches!($($branches)*);
 
-            if !(true $(&& $var.is_some())*) {
-                return Err($err);
+            for attr in $attrs.iter() {
+                let $attr = attr.value.clone();
+                $crate::util::process_attr_branches!(attr; $($branches)*);
             }
-            (
-                    ($($oVar),*),
-                    ($($var.unwrap()),*)
-            )
+
+            $crate::util::handle_attr_branches!($($branches)*);
+
+            $ret_expr
         }
     };
-    ($attrs:expr, optionals: [$(($oName:pat, $oVar:ident, $oMethod:expr)),+ $(,)*]) => {
-        {
-            $(let mut $oVar = None;)+
-            $crate::util::match_attrs!($attrs, match: [$(($oName, $oVar, $oMethod)),+]);
-            ($($oVar),*)
+}
+
+macro_rules! let_attr_branches {
+    () => {};
+
+    (Some($attr_pat_opt:literal) => $opt_var:ident $(?)?= $opt_expr:expr $(, $($tail:tt)*)?) => {
+        let mut $opt_var = None;
+        $crate::util::let_attr_branches!($($($tail)*)?);
+    };
+
+    ($attr_pat_opt:literal => $opt_var:ident $(?)?= $opt_expr:expr $(, $($tail:tt)*)?) => {
+        let mut $opt_var = None;
+        $crate::util::let_attr_branches!($($($tail)*)?);
+    };
+}
+
+pub(crate) use let_attr_branches;
+
+macro_rules! process_attr_branches {
+    ($attr:ident; ) => {};
+
+    ($attr:ident; Some($attr_pat_opt:literal) => $opt_var:ident = $opt_expr:expr $(, $($tail:tt)*)?) => {
+        if(&$attr.name.local_name == $attr_pat_opt) {
+            $opt_var = Some($opt_expr);
+        }
+        else {
+            $crate::util::process_attr_branches!($attr; $($($tail)*)?);
         }
     };
-    ($attrs:expr, required: [$(($name:pat, $var:ident, $method:expr)),+ $(,)*], $err:expr) => {
-        {
-            $(let mut $var = None;)*
-            $crate::util::match_attrs!($attrs, match: [$(($name, $var, $method)),+]);
 
-            if !(true $(&& $var.is_some())*) {
-                return Err($err);
-            }
+    ($attr:ident; Some($attr_pat_opt:literal) => $opt_var:ident ?= $opt_expr:expr $(, $($tail:tt)*)?) => {
+        if(&$attr.name.local_name == $attr_pat_opt) {
+            $opt_var = Some($opt_expr.map_err(|_|
+                $crate::Error::MalformedAttributes(
+                    concat!("Error parsing optional attribute '", $attr_pat_opt, "'").to_owned()
+                )
+            )?);
+        }
+        else {
+            $crate::util::process_attr_branches!($attr; $($($tail)*)?);
+        }
+    };
 
-            ($($var.unwrap()),*)
+    ($attr:ident; $attr_pat_opt:literal => $opt_var:ident = $opt_expr:expr $(, $($tail:tt)*)?) => {
+        if(&$attr.name.local_name == $attr_pat_opt) {
+            $opt_var = Some($opt_expr);
+        }
+        else {
+            $crate::util::process_attr_branches!($attr; $($($tail)*)?);
         }
     };
-}
 
-macro_rules! match_attrs {
-    ($attrs:expr, match: [$(($name:pat, $var:ident, $method:expr)),*]) => {
-        for attr in $attrs.iter() {
-            match <String as AsRef<str>>::as_ref(&attr.name.local_name) {
-                $($name => $var = $method(attr.value.clone()),)*
-                _ => {}
-            }
+    ($attr:ident; $attr_pat_opt:literal => $opt_var:ident ?= $opt_expr:expr $(, $($tail:tt)*)?) => {
+        if(&$attr.name.local_name == $attr_pat_opt) {
+            $opt_var = Some($opt_expr.map_err(|_|
+                $crate::Error::MalformedAttributes(
+                    concat!("Error parsing attribute '", $attr_pat_opt, "'").to_owned()
+                )
+            )?);
+        }
+        else {
+            $crate::util::process_attr_branches!($attr; $($($tail)*)?);
         }
     }
 }
 
+pub(crate) use process_attr_branches;
+
+macro_rules! handle_attr_branches {
+    () => {};
+
+    (Some($attr_pat_opt:literal) => $opt_var:ident $(?)?= $opt_expr:expr $(, $($tail:tt)*)?) => {
+        $crate::util::handle_attr_branches!($($($tail)*)?);
+    };
+
+    ($attr_pat_opt:literal => $opt_var:ident $(?)?= $opt_expr:expr $(, $($tail:tt)*)?) => {
+        let $opt_var = $opt_var
+            .ok_or_else(||
+                Error::MalformedAttributes(
+                    concat!("Missing attribute: ", $attr_pat_opt).to_owned()
+                )
+            )?;
+
+        $crate::util::handle_attr_branches!($($($tail)*)?);
+    };
+}
+
+pub(crate) use handle_attr_branches;
+
 /// Goes through the children of the tag and will call the correct function for
 /// that child. Closes the tag.
 macro_rules! parse_tag {
@@ -112,7 +239,6 @@ macro_rules! map_wrapper {
 
 pub(crate) use get_attrs;
 pub(crate) use map_wrapper;
-pub(crate) use match_attrs;
 pub(crate) use parse_tag;
 
 use crate::{Gid, MapTilesetGid};
-- 
GitLab