Repository: rojo-rbx/rbx-dom (rust)

Issue:
Add support for `EnumItem`-typed attributes
[Roblox added support for `EnumItem` attributes in 570](https://create.roblox.com/docs/release-notes/release-notes-570). rbx-dom currently lacks support for them, but they're probably uncommon since they're not yet available in Roblox Studio's attribute type picker UI. 

Pull request:
Implement enum attributes
This PR closes #383 by by adding a new tagged enum variant, `EnumItem`, that contains the name of the `EnumItem` as well as its value, and implementing the attributes reader/writer against the [specification at rojo.dom.space](https://dom.rojo.space/attributes.html#enumitem).

Additionally, it adds a codec to rbx_dom_lua to handle the new `EnumItem` variant.

It also updates some docs, since the link that's currently there doesn't work anymore.

I went back and forth on what the fields should be named in the serde representation, and settled on `type` and `value`. This more or less mirrors `EnumItem.EnumType` and `EnumItem.Value` on Roblox - but maybe `type` should be `enumType` instead?

Patch:
diff --git a/docs/attributes.md b/docs/attributes.md
index fda407f..ff51327 100644
--- a/docs/attributes.md
+++ b/docs/attributes.md
@@ -18,7 +18,7 @@ This document describes the Attribute binary format. In this format there is no
     - [Vector2](#vector2)
     - [Vector3](#vector3)
     - [CFrame](#cframe)
-	- [EnumItem](#EnumItem)
+	- [EnumItem](#enumitem)
     - [NumberSequence](#numbersequence)
     - [ColorSequence](#colorsequence)
     - [NumberRange](#numberrange)
@@ -27,7 +27,7 @@ This document describes the Attribute binary format. In this format there is no
 
 ## Document Conventions
 
-This document assumes a basic understanding of Rust's convention for numeric types. For example: 
+This document assumes a basic understanding of Rust's convention for numeric types. For example:
 
 - `u32` is an unsigned 32-bit integer
 - `f32` is a 32-bit floating point
@@ -189,7 +189,7 @@ Demonstrating the axis-aligned rotation matrix case, a `CFrame` with the value `
 ### EnumItem
 **Type ID `0x15`**
 
-The `EnumItem` type is composed of two parts: 
+The `EnumItem` type is composed of two parts:
 
 | Field Name | Format              | Value                                                  |
 |:-----------|:--------------------|:-------------------------------------------------------|
diff --git a/rbx_binary/src/serializer/state.rs b/rbx_binary/src/serializer/state.rs
index 168ee81..e83e11d 100644
--- a/rbx_binary/src/serializer/state.rs
+++ b/rbx_binary/src/serializer/state.rs
@@ -9,8 +9,8 @@ use ahash::{HashMap, HashMapExt, HashSetExt};
 use rbx_dom_weak::{
     types::{
         Attributes, Axes, BinaryString, BrickColor, CFrame, Color3, Color3uint8, ColorSequence,
-        ColorSequenceKeypoint, Content, Enum, Faces, Font, MaterialColors, Matrix3, NumberRange,
-        NumberSequence, NumberSequenceKeypoint, PhysicalProperties, Ray, Rect, Ref,
+        ColorSequenceKeypoint, Content, Enum, EnumItem, Faces, Font, MaterialColors, Matrix3,
+        NumberRange, NumberSequence, NumberSequenceKeypoint, PhysicalProperties, Ray, Rect, Ref,
         SecurityCapabilities, SharedString, Tags, UDim, UDim2, UniqueId, Variant, VariantType,
         Vector2, Vector3, Vector3int16,
     },
@@ -970,10 +970,10 @@ impl<'dom, 'db, W: Write> SerializerState<'dom, 'db, W> {
                         let mut buf = Vec::with_capacity(values.len());
 
                         for (i, rbx_value) in values {
-                            if let Variant::Enum(value) = rbx_value.as_ref() {
-                                buf.push(value.to_u32());
-                            } else {
-                                return type_mismatch(i, &rbx_value, "Enum");
+                            match rbx_value.as_ref() {
+                                Variant::Enum(value) => buf.push(value.to_u32()),
+                                Variant::EnumItem(EnumItem { value, .. }) => buf.push(*value),
+                                _ => return type_mismatch(i, &rbx_value, "Enum or EnumItem"),
                             }
                         }
 
diff --git a/rbx_dom_lua/src/EncodedValue.lua b/rbx_dom_lua/src/EncodedValue.lua
index f2e9e34..67d78e4 100644
--- a/rbx_dom_lua/src/EncodedValue.lua
+++ b/rbx_dom_lua/src/EncodedValue.lua
@@ -205,6 +205,19 @@ types = {
 		end,
 	},
 
+	EnumItem = {
+		fromPod = function(pod)
+			return Enum[pod.type]:FromValue(pod.value)
+		end,
+
+		toPod = function(roblox)
+			return {
+				type = tostring(roblox.EnumType),
+				value = roblox.Value,
+			}
+		end,
+	},
+
 	Faces = {
 		fromPod = function(pod)
 			local faces = {}
diff --git a/rbx_dom_lua/src/allValues.json b/rbx_dom_lua/src/allValues.json
index 9b07d7b..59e531f 100644
--- a/rbx_dom_lua/src/allValues.json
+++ b/rbx_dom_lua/src/allValues.json
@@ -15,6 +15,12 @@
             0.0
           ]
         },
+        "TestEnumItem": {
+          "EnumItem": {
+            "type": "Material",
+            "value": 256
+          }
+        },
         "TestNumber": {
           "Float64": 1337.0
         },
@@ -182,6 +188,15 @@
     },
     "ty": "Enum"
   },
+  "EnumItem": {
+    "value": {
+      "EnumItem": {
+        "type": "Material",
+        "value": 256
+      }
+    },
+    "ty": "EnumItem"
+  },
   "Faces": {
     "value": {
       "Faces": [
diff --git a/rbx_reflector/src/cli/values.rs b/rbx_reflector/src/cli/values.rs
index 387cfbc..3cccb13 100644
--- a/rbx_reflector/src/cli/values.rs
+++ b/rbx_reflector/src/cli/values.rs
@@ -4,10 +4,10 @@ use anyhow::bail;
 use clap::Parser;
 use rbx_types::{
     Attributes, Axes, BinaryString, BrickColor, CFrame, Color3, Color3uint8, ColorSequence,
-    ColorSequenceKeypoint, Content, CustomPhysicalProperties, Enum, Faces, Font, MaterialColors,
-    Matrix3, NumberRange, NumberSequence, NumberSequenceKeypoint, PhysicalProperties, Ray, Rect,
-    Region3int16, Tags, TerrainMaterials, UDim, UDim2, Variant, VariantType, Vector2, Vector2int16,
-    Vector3, Vector3int16,
+    ColorSequenceKeypoint, Content, CustomPhysicalProperties, Enum, EnumItem, Faces, Font,
+    MaterialColors, Matrix3, NumberRange, NumberSequence, NumberSequenceKeypoint,
+    PhysicalProperties, Ray, Rect, Region3int16, Tags, TerrainMaterials, UDim, UDim2, Variant,
+    VariantType, Vector2, Vector2int16, Vector3, Vector3int16,
 };
 use serde::Serialize;
 
@@ -45,6 +45,13 @@ impl ValuesSubcommand {
                     "TestUDim2",
                     UDim2::new(UDim::new(1.0, 2), UDim::new(3.0, 4)),
                 )
+                .with(
+                    "TestEnumItem",
+                    EnumItem {
+                        ty: "Material".into(),
+                        value: 256,
+                    },
+                )
                 .into(),
         );
         values.insert("Axes", Axes::all().into());
@@ -80,6 +87,14 @@ impl ValuesSubcommand {
         );
         values.insert("Content", Content::from("rbxassetid://12345").into());
         values.insert("Enum", Enum::from_u32(1234).into());
+        values.insert(
+            "EnumItem",
+            EnumItem {
+                ty: "Material".into(),
+                value: 256,
+            }
+            .into(),
+        );
         values.insert("Faces", Faces::all().into());
         values.insert("Float32", 15.0f32.into());
         values.insert("Float64", 15123.0f64.into());
diff --git a/rbx_types/CHANGELOG.md b/rbx_types/CHANGELOG.md
index c4c4ae3..79a578c 100644
--- a/rbx_types/CHANGELOG.md
+++ b/rbx_types/CHANGELOG.md
@@ -5,7 +5,9 @@
 # 1.10.0 (2024-08-22)
 * Add support for `Int32` values within `Attributes` ([#439])
 * Add `len` and `is_empty` to `Tags` ([#438])
+* Added support for `EnumItem` attributes. [#470]
 
+[#470]: https://github.com/rojo-rbx/rbx-dom/pull/470
 [#438]: https://github.com/rojo-rbx/rbx-dom/pull/438
 [#439]: https://github.com/rojo-rbx/rbx-dom/pull/439
 
diff --git a/rbx_types/src/attributes/error.rs b/rbx_types/src/attributes/error.rs
index 0638623..aba6324 100644
--- a/rbx_types/src/attributes/error.rs
+++ b/rbx_types/src/attributes/error.rs
@@ -30,6 +30,9 @@ pub(crate) enum AttributeError {
     #[error(transparent)]
     Io(#[from] std::io::Error),
 
+    #[error(transparent)]
+    Utf8(#[from] std::string::FromUtf8Error),
+
     #[error(transparent)]
     BadAttributeValue(#[from] crate::Error),
 
diff --git a/rbx_types/src/attributes/reader.rs b/rbx_types/src/attributes/reader.rs
index 74c17ad..a1b4d67 100644
--- a/rbx_types/src/attributes/reader.rs
+++ b/rbx_types/src/attributes/reader.rs
@@ -4,7 +4,7 @@ use std::{
 };
 
 use crate::{
-    BinaryString, BrickColor, CFrame, Color3, ColorSequence, ColorSequenceKeypoint, Font,
+    BinaryString, BrickColor, CFrame, Color3, ColorSequence, ColorSequenceKeypoint, EnumItem, Font,
     FontStyle, FontWeight, Matrix3, NumberRange, NumberSequence, NumberSequenceKeypoint, Rect,
     UDim, UDim2, Variant, VariantType, Vector2, Vector3,
 };
@@ -202,6 +202,17 @@ pub(crate) fn read_attributes<R: Read>(
             }
             .into(),
 
+            VariantType::EnumItem => {
+                let enum_type = read_string(&mut value)?;
+                let value = read_u32(&mut value)?;
+
+                EnumItem {
+                    ty: String::from_utf8(enum_type)?,
+                    value,
+                }
+            }
+            .into(),
+
             other => return Err(AttributeError::UnsupportedVariantType(other)),
         };
 
diff --git a/rbx_types/src/attributes/type_id.rs b/rbx_types/src/attributes/type_id.rs
index 2af23ec..af59572 100644
--- a/rbx_types/src/attributes/type_id.rs
+++ b/rbx_types/src/attributes/type_id.rs
@@ -41,7 +41,7 @@ type_ids! {
     // ??? => 0x12,
     // ??? => 0x13,
     CFrame => 0x14,
-    // ??? => 0x15,
+    EnumItem => 0x15,
     // ??? => 0x16,
     NumberSequence => 0x17,
     // ??? => 0x18,
diff --git a/rbx_types/src/attributes/writer.rs b/rbx_types/src/attributes/writer.rs
index bd32da7..2684cad 100644
--- a/rbx_types/src/attributes/writer.rs
+++ b/rbx_types/src/attributes/writer.rs
@@ -99,6 +99,10 @@ pub(crate) fn write_attributes<W: Write>(
                     font.cached_face_id.as_deref().unwrap_or_default(),
                 )?;
             }
+            Variant::EnumItem(enum_item) => {
+                write_string(&mut writer, &enum_item.ty)?;
+                write_u32(&mut writer, enum_item.value)?;
+            }
 
             other_variant => unreachable!("variant {:?} was not implemented", other_variant),
         }
diff --git a/rbx_types/src/basic_types.rs b/rbx_types/src/basic_types.rs
index 4d60131..174a431 100644
--- a/rbx_types/src/basic_types.rs
+++ b/rbx_types/src/basic_types.rs
@@ -2,13 +2,13 @@ use thiserror::Error;
 
 use crate::Error;
 
-/// Represents any Roblox enum value.
+/// Represents any Roblox EnumItem.
 ///
 /// Roblox enums are not strongly typed, so the meaning of a value depends on
 /// where they're assigned.
 ///
 /// A list of all enums and their values are available [on the Roblox Developer
-/// Hub](https://developer.roblox.com/en-us/api-reference/enum).
+/// Hub](https://create.roblox.com/docs/reference/engine/enums).
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 #[cfg_attr(
     feature = "serde",
@@ -29,6 +29,26 @@ impl Enum {
     }
 }
 
+/// Represents a specific Roblox EnumItem.
+///
+/// A list of all enums and their values are available [on the Roblox Developer
+/// Hub](https://create.roblox.com/docs/reference/engine/enums).
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize,))]
+pub struct EnumItem {
+    #[serde(rename = "type")]
+    pub ty: String,
+    pub value: u32,
+}
+
+impl From<EnumItem> for Enum {
+    fn from(enum_item: EnumItem) -> Self {
+        Self {
+            value: enum_item.value,
+        }
+    }
+}
+
 /// The standard 2D vector type used in Roblox.
 ///
 /// ## See Also
@@ -718,4 +738,15 @@ mod serde_test {
             "[[1.0,2.0,3.0],[4.0,5.0,6.0],[7.0,8.0,9.0]]",
         );
     }
+
+    #[test]
+    fn tagged_enum_json() {
+        test_ser(
+            EnumItem {
+                ty: "PlayTag".to_string(),
+                value: 3,
+            },
+            r#"{"type":"PlayTag","value":3}"#,
+        );
+    }
 }
diff --git a/rbx_types/src/variant.rs b/rbx_types/src/variant.rs
index 597c874..801a41b 100644
--- a/rbx_types/src/variant.rs
+++ b/rbx_types/src/variant.rs
@@ -1,8 +1,8 @@
 use crate::{
     Attributes, Axes, BinaryString, BrickColor, CFrame, Color3, Color3uint8, ColorSequence,
-    Content, Enum, Faces, Font, MaterialColors, NumberRange, NumberSequence, PhysicalProperties,
-    Ray, Rect, Ref, Region3, Region3int16, SecurityCapabilities, SharedString, Tags, UDim, UDim2,
-    UniqueId, Vector2, Vector2int16, Vector3, Vector3int16,
+    Content, Enum, EnumItem, Faces, Font, MaterialColors, NumberRange, NumberSequence,
+    PhysicalProperties, Ray, Rect, Ref, Region3, Region3int16, SecurityCapabilities, SharedString,
+    Tags, UDim, UDim2, UniqueId, Vector2, Vector2int16, Vector3, Vector3int16,
 };
 
 /// Reduces boilerplate from listing different values of Variant by wrapping
@@ -129,6 +129,7 @@ make_variant! {
     UniqueId(UniqueId),
     MaterialColors(MaterialColors),
     SecurityCapabilities(SecurityCapabilities),
+    EnumItem(EnumItem),
 }
 
 impl From<&'_ str> for Variant {
diff --git a/rbx_xml/src/conversion.rs b/rbx_xml/src/conversion.rs
index d84fc04..0e99276 100644
--- a/rbx_xml/src/conversion.rs
+++ b/rbx_xml/src/conversion.rs
@@ -4,6 +4,7 @@
 use std::borrow::{Borrow, Cow};
 use std::convert::TryInto;
 
+use rbx_dom_weak::types::Enum;
 use rbx_dom_weak::{
     types::{Attributes, BrickColor, Color3uint8, MaterialColors, Tags, Variant, VariantType},
     Ustr,
@@ -96,6 +97,9 @@ rbx-dom may require changes to fully support this property. Please open an issue
                     }
                 }
             }
+            (Variant::EnumItem(enum_item), VariantType::Enum) => {
+                Ok(Cow::Owned(Enum::from_u32(enum_item.value).into()))
+            }
             (_, _) => Ok(value),
         }
     }


Test patch:
diff --git a/rbx_binary/src/tests/models.rs b/rbx_binary/src/tests/models.rs
index 5105d6b..d47df84 100644
--- a/rbx_binary/src/tests/models.rs
+++ b/rbx_binary/src/tests/models.rs
@@ -66,4 +66,5 @@ binary_tests! {
     folder_with_font_attribute,
     number_values_with_security_capabilities,
     lighting_with_int32_attribute,
+    folder_with_enum_attribute,
 }
diff --git a/rbx_binary/src/tests/snapshots/rbx_binary__tests__util__folder-with-enum-attribute__decoded.snap b/rbx_binary/src/tests/snapshots/rbx_binary__tests__util__folder-with-enum-attribute__decoded.snap
new file mode 100644
index 0000000..a6ce755
--- /dev/null
+++ b/rbx_binary/src/tests/snapshots/rbx_binary__tests__util__folder-with-enum-attribute__decoded.snap
@@ -0,0 +1,23 @@
+---
+source: rbx_binary/src/tests/util.rs
+expression: decoded_viewed
+---
+- referent: referent-0
+  name: Folder
+  class: Folder
+  properties:
+    Attributes:
+      Attributes:
+        AnEnumValue:
+          EnumItem:
+            type: Material
+            value: 512
+    Capabilities:
+      SecurityCapabilities: 0
+    Sandboxed:
+      Bool: false
+    SourceAssetId:
+      Int64: -1
+    Tags:
+      Tags: []
+  children: []
diff --git a/rbx_binary/src/tests/snapshots/rbx_binary__tests__util__folder-with-enum-attribute__encoded.snap b/rbx_binary/src/tests/snapshots/rbx_binary__tests__util__folder-with-enum-attribute__encoded.snap
new file mode 100644
index 0000000..51d6284
--- /dev/null
+++ b/rbx_binary/src/tests/snapshots/rbx_binary__tests__util__folder-with-enum-attribute__encoded.snap
@@ -0,0 +1,55 @@
+---
+source: rbx_binary/src/tests/util.rs
+expression: text_roundtrip
+---
+num_types: 1
+num_instances: 1
+chunks:
+  - Inst:
+      type_id: 0
+      type_name: Folder
+      object_format: 0
+      referents:
+        - 0
+  - Prop:
+      type_id: 0
+      prop_name: AttributesSerialize
+      prop_type: String
+      values:
+        - "\u0001\u0000\u0000\u0000\u000b\u0000\u0000\u0000AnEnumValue\u0015\b\u0000\u0000\u0000Material\u0000\u0002\u0000\u0000"
+  - Prop:
+      type_id: 0
+      prop_name: Capabilities
+      prop_type: SecurityCapabilities
+      values:
+        - 0
+  - Prop:
+      type_id: 0
+      prop_name: Name
+      prop_type: String
+      values:
+        - Folder
+  - Prop:
+      type_id: 0
+      prop_name: DefinesCapabilities
+      prop_type: Bool
+      values:
+        - false
+  - Prop:
+      type_id: 0
+      prop_name: SourceAssetId
+      prop_type: Int64
+      values:
+        - -1
+  - Prop:
+      type_id: 0
+      prop_name: Tags
+      prop_type: String
+      values:
+        - ""
+  - Prnt:
+      version: 0
+      links:
+        - - 0
+          - -1
+  - End
diff --git a/rbx_binary/src/tests/snapshots/rbx_binary__tests__util__folder-with-enum-attribute__input.snap b/rbx_binary/src/tests/snapshots/rbx_binary__tests__util__folder-with-enum-attribute__input.snap
new file mode 100644
index 0000000..815d413
--- /dev/null
+++ b/rbx_binary/src/tests/snapshots/rbx_binary__tests__util__folder-with-enum-attribute__input.snap
@@ -0,0 +1,59 @@
+---
+source: rbx_binary/src/tests/util.rs
+expression: text_decoded
+---
+num_types: 1
+num_instances: 1
+chunks:
+  - Meta:
+      entries:
+        - - ExplicitAutoJoints
+          - "true"
+  - Inst:
+      type_id: 0
+      type_name: Folder
+      object_format: 0
+      referents:
+        - 0
+  - Prop:
+      type_id: 0
+      prop_name: AttributesSerialize
+      prop_type: String
+      values:
+        - "\u0001\u0000\u0000\u0000\u000b\u0000\u0000\u0000AnEnumValue\u0015\b\u0000\u0000\u0000Material\u0000\u0002\u0000\u0000"
+  - Prop:
+      type_id: 0
+      prop_name: Capabilities
+      prop_type: SecurityCapabilities
+      values:
+        - 0
+  - Prop:
+      type_id: 0
+      prop_name: DefinesCapabilities
+      prop_type: Bool
+      values:
+        - false
+  - Prop:
+      type_id: 0
+      prop_name: Name
+      prop_type: String
+      values:
+        - Folder
+  - Prop:
+      type_id: 0
+      prop_name: SourceAssetId
+      prop_type: Int64
+      values:
+        - -1
+  - Prop:
+      type_id: 0
+      prop_name: Tags
+      prop_type: String
+      values:
+        - ""
+  - Prnt:
+      version: 0
+      links:
+        - - 0
+          - -1
+  - End
diff --git a/rbx_dom_lua/test b/rbx_dom_lua/test
old mode 100644
new mode 100755
index 9ac1092..09577d6
--- a/rbx_dom_lua/test
+++ b/rbx_dom_lua/test
@@ -1,4 +1,5 @@
 #!/bin/sh
 
+cargo run --bin rbx_reflector values ./src/allValues.json
 rojo build test-place.project.json -o TestPlace.rbxlx
-run-in-roblox --script run-tests.lua --place TestPlace.rbxlx
\ No newline at end of file
+run-in-roblox --script run-tests.lua --place TestPlace.rbxlx
diff --git a/rbx_xml/src/tests/basic.rs b/rbx_xml/src/tests/basic.rs
index e4540eb..7169844 100644
--- a/rbx_xml/src/tests/basic.rs
+++ b/rbx_xml/src/tests/basic.rs
@@ -2,11 +2,11 @@
 
 use rbx_dom_weak::types::{
     Attributes, BinaryString, BrickColor, Color3, Color3uint8, ColorSequence,
-    ColorSequenceKeypoint, Enum, Font, MaterialColors, NumberRange, NumberSequence,
-    NumberSequenceKeypoint, Rect, Tags, TerrainMaterials, UDim, UDim2, UniqueId, Variant, Vector2,
-    Vector3,
+    ColorSequenceKeypoint, Enum, EnumItem, Font, MaterialColors, NumberRange, NumberSequence,
+    NumberSequenceKeypoint, Rect, Tags, TerrainMaterials, UDim, UDim2, UniqueId, Variant,
+    VariantType, Vector2, Vector3,
 };
-use rbx_dom_weak::{InstanceBuilder, WeakDom};
+use rbx_dom_weak::{ustr, InstanceBuilder, WeakDom};
 
 #[test]
 fn with_bool() {
@@ -352,3 +352,28 @@ fn bad_migrated_property() {
     crate::to_writer_default(&mut encoded, &tree, &[tree.root_ref()]).unwrap();
     insta::assert_snapshot!(std::str::from_utf8(&encoded).unwrap());
 }
+
+#[test]
+fn enum_item_to_enum() {
+    let tree = WeakDom::new(InstanceBuilder::new("Part").with_property(
+        "Material",
+        EnumItem {
+            ty: "Material".into(),
+            value: 256,
+        },
+    ));
+
+    let mut encoded = Vec::new();
+    crate::to_writer_default(&mut encoded, &tree, &[tree.root_ref()]).unwrap();
+
+    let decoded = crate::from_reader_default(encoded.as_slice()).unwrap();
+    let prop_type = decoded
+        .get_by_ref(*decoded.root().children().first().unwrap())
+        .unwrap()
+        .properties
+        .get(&ustr("Material"))
+        .unwrap()
+        .ty();
+
+    assert_eq!(prop_type, VariantType::Enum);
+}
diff --git a/rbx_xml/src/tests/models.rs b/rbx_xml/src/tests/models.rs
index eaffd82..b6c58ab 100644
--- a/rbx_xml/src/tests/models.rs
+++ b/rbx_xml/src/tests/models.rs
@@ -67,4 +67,5 @@ model_tests! {
     folder_with_font_attribute,
     number_values_with_security_capabilities,
     lighting_with_int32_attribute,
+    folder_with_enum_attribute,
 }
diff --git a/rbx_xml/src/tests/snapshots/rbx_xml__tests__folder-with-enum-attribute__decoded.snap b/rbx_xml/src/tests/snapshots/rbx_xml__tests__folder-with-enum-attribute__decoded.snap
new file mode 100644
index 0000000..27baa1d
--- /dev/null
+++ b/rbx_xml/src/tests/snapshots/rbx_xml__tests__folder-with-enum-attribute__decoded.snap
@@ -0,0 +1,23 @@
+---
+source: rbx_xml/src/tests/mod.rs
+expression: "DomViewer::new().view_children(&decoded)"
+---
+- referent: referent-0
+  name: Folder
+  class: Folder
+  properties:
+    Attributes:
+      Attributes:
+        AnEnumValue:
+          EnumItem:
+            type: Material
+            value: 512
+    Capabilities:
+      SecurityCapabilities: 0
+    Sandboxed:
+      Bool: false
+    SourceAssetId:
+      Int64: -1
+    Tags:
+      Tags: []
+  children: []
diff --git a/rbx_xml/src/tests/snapshots/rbx_xml__tests__folder-with-enum-attribute__roundtrip.snap b/rbx_xml/src/tests/snapshots/rbx_xml__tests__folder-with-enum-attribute__roundtrip.snap
new file mode 100644
index 0000000..7f3f270
--- /dev/null
+++ b/rbx_xml/src/tests/snapshots/rbx_xml__tests__folder-with-enum-attribute__roundtrip.snap
@@ -0,0 +1,23 @@
+---
+source: rbx_xml/src/tests/mod.rs
+expression: "DomViewer::new().view_children(&roundtrip)"
+---
+- referent: referent-0
+  name: Folder
+  class: Folder
+  properties:
+    Attributes:
+      Attributes:
+        AnEnumValue:
+          EnumItem:
+            type: Material
+            value: 512
+    Capabilities:
+      SecurityCapabilities: 0
+    Sandboxed:
+      Bool: false
+    SourceAssetId:
+      Int64: -1
+    Tags:
+      Tags: []
+  children: []