Repository: facet-rs/facet
Pull request #398: feat: tuple deserialisation
Author: login: lmmx
Labels: name: 📁 formats
description: facet-json, facet-yaml, facet-toml etc.
Created: 2025-04-25T14:13:42Z | Merged: 2025-04-26T07:21:31Z

Working on finished tuple deserialisation

- Follow up to #314

## Deserialisation tests

### Integration

- [x] i32 tuples
- [x] f32 tuples
- [x] i32/f32 mixed tuples
- [x] string tuple
- [x] nested tuple (tuple inside a tuple)

Also contains ignored integration/error tests for empty tuple handling, treating it as an edge case we don't handle (yet)

  - Tuple type includes unit type https://doc.rust-lang.org/reference/types/tuple.html

Snapshot test showing the error when the tuple is mis-sized could be made clearer (say "wrong size") but OK for now. Snapshot not included in this PR but can be seen if you un-ignore the 2 tests in `facet-json/tests/integration/err.rs`.

```
A: facet-json/tests/integration/snapshots/r#mod__integration__err__deserialize_tuple_wrong_size.snap
@@ -0,0 +1,11 @@
+---
+source: facet-json/tests/integration/err.rs
+expression: err
+---
+Error: Error at $
+   ╭─[ json:1:4 ]
+   │
+ 1 │ [10,20]
+   │    ┬  
+   │    ╰── Field error for shape (i32): No such static field
+───╯
```

I added another for when you read a '0-tuple' (unit type) but receive data in the `[...]`. This is giving the wrong error message for now, but this snapshot can be updated when it correctly says "hey that's not an empty container"

```
A: facet-json/tests/integration/snapshots/r#mod__integration__err__deserialize_tuple_empty.snap
@@ -0,0 +1,11 @@
+---
+source: facet-json/tests/integration/err.rs
+expression: err
+---
+Error: Error at $
+   ╭─[ json:1:1 ]
+   │
+ 1 │ [10]
+   │ ┬  
+   │ ╰── Unsupported type: got (), wanted array, list, or slice
+───╯
```


Diff:
diff --git a/facet-core/src/types/mod.rs b/facet-core/src/types/mod.rs
index aa1153a91..61bf83d9a 100644
--- a/facet-core/src/types/mod.rs
+++ b/facet-core/src/types/mod.rs
@@ -507,6 +507,14 @@ impl Def {
             _ => Err(self),
         }
     }
+    /// Returns the `TupleDef` wrapped in an `Ok` if this is a [`Def::Struct`] whose kind is
+    /// [`StructKind::Tuple`].
+    pub fn into_tuple(self) -> Result<StructDef, Self> {
+        match self {
+            Self::Struct(def) if def.kind == StructKind::Tuple => Ok(def),
+            _ => Err(self),
+        }
+    }
     /// Returns the `OptionDef` wrapped in an `Ok` if this is a [`Def::Option`].
     pub fn into_option(self) -> Result<OptionDef, Self> {
         match self {
diff --git a/facet-json/src/deserialize.rs b/facet-json/src/deserialize.rs
index fe4d31d39..301e8e790 100644
--- a/facet-json/src/deserialize.rs
+++ b/facet-json/src/deserialize.rs
@@ -3,7 +3,7 @@ use alloc::string::ToString;
 use alloc::vec;
 use facet_core::Characteristic;
 use facet_core::Def;
-use facet_core::Facet;
+use facet_core::{Facet, ScalarAffinity, StructKind};
 use facet_reflect::ReflectError;
 use facet_reflect::{HeapValue, Wip};
 use log::trace;
@@ -402,10 +402,60 @@ pub fn from_slice_wip<'input: 'facet, 'facet>(
                                         trace!("Array starting for list ({})!", wip.shape().blue());
                                         reflect!(put_default());
                                     }
+                                    Def::Struct(s) => {
+                                        if s.kind == StructKind::Tuple {
+                                            trace!(
+                                                "Array starting for tuple ({})!",
+                                                wip.shape().blue()
+                                            );
+                                            // Special handling for unit type ()
+                                            if s.fields.is_empty() {
+                                                // Check if the array is empty by peeking at the next token
+                                                let next_token = read_token!();
+                                                if next_token.node == Token::RBracket {
+                                                    // Empty array means unit type () - we're good
+                                                    reflect!(put_default());
+                                                } else {
+                                                    // Non-empty array is not valid for unit type
+                                                    bail!(JsonErrorKind::UnsupportedType {
+                                                        got: wip.innermost_shape(),
+                                                        wanted: "empty array",
+                                                    });
+                                                }
+                                            } else {
+                                                reflect!(put_default());
+                                            }
+                                        } else {
+                                            bail!(JsonErrorKind::UnsupportedType {
+                                                got: wip.shape(),
+                                                wanted: "array, list, tuple, or slice"
+                                            });
+                                        }
+                                    }
+                                    Def::Scalar(s)
+                                        if matches!(s.affinity, ScalarAffinity::Empty(_)) =>
+                                    {
+                                        trace!(
+                                            "Array starting for tuple ({})!",
+                                            wip.shape().blue()
+                                        );
+                                        // reflect!(put_default());
+                                        // Check if the array is empty by peeking at the next token
+                                        let next_token = read_token!();
+                                        if next_token.node == Token::RBracket {
+                                            // Empty array means unit type () - we're good
+                                        } else {
+                                            // Non-empty array is not valid for unit type
+                                            bail!(JsonErrorKind::UnsupportedType {
+                                                got: wip.innermost_shape(),
+                                                wanted: "empty array",
+                                            });
+                                        }
+                                    }
                                     _ => {
                                         bail!(JsonErrorKind::UnsupportedType {
                                             got: wip.innermost_shape(),
-                                            wanted: "array, list, or slice"
+                                            wanted: "array, list, tuple, or slice"
                                         });
                                     }
                                 }
diff --git a/facet-json/tests/integration/err.rs b/facet-json/tests/integration/err.rs
index e0156dcd7..35cd2161c 100644
--- a/facet-json/tests/integration/err.rs
+++ b/facet-json/tests/integration/err.rs
@@ -98,3 +98,23 @@ fn unknown_field_with_rename() -> eyre::Result<()> {
 
     Ok(())
 }
+
+#[test]
+#[ignore]
+fn test_deserialize_tuple_empty_but_isnt() -> eyre::Result<()> {
+    let result: Result<(), _> = from_str(r#"[10]"#);
+    let err = result.unwrap_err();
+    insta::assert_snapshot!(err);
+
+    Ok(())
+}
+
+#[test]
+#[ignore]
+fn test_deserialize_tuple_wrong_size_one_too_many() -> eyre::Result<()> {
+    let result: Result<(i32,), _> = from_str(r#"[10,20]"#);
+    let err = result.unwrap_err();
+    insta::assert_snapshot!(err);
+
+    Ok(())
+}
diff --git a/facet-json/tests/integration/read/tuple.rs b/facet-json/tests/integration/read/tuple.rs
index a617c8530..90801b21c 100644
--- a/facet-json/tests/integration/read/tuple.rs
+++ b/facet-json/tests/integration/read/tuple.rs
@@ -1,26 +1,155 @@
 use facet_json::from_str;
 
 #[test]
-#[ignore]
-fn test_deserialize_tuple() {
-    let result: Result<(&str, i32), _> = from_str(r#"["aaa",3]"#);
+fn test_deserialize_tuple_string() {
+    let result: Result<(String,), _> = from_str(r#"[""]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, "");
+
+    let result: Result<(String, String, String), _> = from_str(r#"["un","deux","trois"]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, "un");
+    assert_eq!(ok.1, "deux");
+    assert_eq!(ok.2, "trois");
+
+    let result: Result<(String, String, String), _> = from_str(r#"["🐑","🐑🐑","🐑🐑🐑"]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, "🐑");
+    assert_eq!(ok.1, "🐑🐑");
+    assert_eq!(ok.2, "🐑🐑🐑");
+}
+
+#[test]
+fn test_deserialize_tuple_i32() {
+    let result: Result<(i32,), _> = from_str(r#"[10]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10);
+
+    let result: Result<(i32, i32), _> = from_str(r#"[10,20]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10);
+    assert_eq!(ok.1, 20);
+
+    let result: Result<(i32, i32, i32), _> = from_str(r#"[10,20,30]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10);
+    assert_eq!(ok.1, 20);
+    assert_eq!(ok.2, 30);
+
+    let result: Result<(i32, i32, i32, i32), _> = from_str(r#"[10,20,30,40]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10);
+    assert_eq!(ok.1, 20);
+    assert_eq!(ok.2, 30);
+    assert_eq!(ok.3, 40);
+
+    let result: Result<(i32, i32, i32, i32, i32), _> = from_str(r#"[10,20,30,40,50]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10);
+    assert_eq!(ok.1, 20);
+    assert_eq!(ok.2, 30);
+    assert_eq!(ok.3, 40);
+    assert_eq!(ok.4, 50);
+
+    let result: Result<(i32, i32), _> = from_str(r#"[-1,-0]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, -1);
+    assert_eq!(ok.1, 0);
+}
+
+#[test]
+fn test_deserialize_tuple_f32() {
+    let result: Result<(f32,), _> = from_str(r#"[10]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10.0);
+
+    let result: Result<(f32, f32), _> = from_str(r#"[10,20]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10.0);
+    assert_eq!(ok.1, 20.0);
+
+    let result: Result<(f32, f32, f32), _> = from_str(r#"[10,20,30]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10.0);
+    assert_eq!(ok.1, 20.0);
+    assert_eq!(ok.2, 30.0);
+
+    let result: Result<(f32, f32, f32, f32), _> = from_str(r#"[10,20,30,40]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10.0);
+    assert_eq!(ok.1, 20.0);
+    assert_eq!(ok.2, 30.0);
+    assert_eq!(ok.3, 40.0);
+
+    let result: Result<(f32, f32, f32, f32, f32), _> = from_str(r#"[10,20,30,40,50]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10.0);
+    assert_eq!(ok.1, 20.0);
+    assert_eq!(ok.2, 30.0);
+    assert_eq!(ok.3, 40.0);
+    assert_eq!(ok.4, 50.0);
+
+    let result: Result<(f32, f32), _> = from_str(r#"[-1,-0]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, -1.0);
+    assert_eq!(ok.1, 0.0);
+}
+
+#[test]
+fn test_deserialize_tuple_mixed_string_i32() {
+    let result: Result<(String, i32), _> = from_str(r#"["aaa",100]"#);
     let ok = result.unwrap();
     assert_eq!(ok.0, "aaa");
-    assert_eq!(ok.1, 3);
+    assert_eq!(ok.1, 100);
+}
+
+#[test]
+fn test_deserialize_tuple_mixed_i32_f32() {
+    let result: Result<(i32, f32), _> = from_str(r#"[10,20]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10);
+    assert_eq!(ok.1, 20.0);
+
+    let result: Result<(f32, i32), _> = from_str(r#"[10,20]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10.0);
+    assert_eq!(ok.1, 20);
 
-    #[derive(facet::Facet)]
-    struct TestTuple(i32, String, bool);
-    let result: Result<TestTuple, _> = from_str(r#"[3,"aaa",true]"#);
+    let result: Result<(i32, f32, i32), _> = from_str(r#"[10,20,30]"#);
     let ok = result.unwrap();
-    assert_eq!(ok.0, 3);
-    assert_eq!(ok.1, "aaa");
-    assert!(ok.2);
+    assert_eq!(ok.0, 10);
+    assert_eq!(ok.1, 20.0);
+    assert_eq!(ok.2, 30);
+
+    let result: Result<(f32, i32, f32, i32), _> = from_str(r#"[10,20,30,40]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0, 10.0);
+    assert_eq!(ok.1, 20);
+    assert_eq!(ok.2, 30.0);
+    assert_eq!(ok.3, 40);
 }
 
 #[test]
-fn test_deserialize_list() {
-    let result: Result<Vec<i32>, _> = from_str(r#"[1,3]"#);
+#[ignore]
+fn test_deserialize_tuple_empty() {
+    let result: Result<((), ()), _> = from_str(r#"[[],[]]"#);
+    result.unwrap();
+}
+
+#[test]
+fn test_deserialize_tuple_nest() {
+    let result: Result<((String,),), _> = from_str(r#"[["hello"]]"#);
+    let ok = result.unwrap();
+    assert_eq!(ok.0.0, "hello");
+
+    type String1Tuple = (String,);
+    type IntFloatString3Tuple = (i32, f32, String);
+
+    let result: Result<(String1Tuple, IntFloatString3Tuple), _> =
+        from_str(r#"[["hello"],[1,2,"3"]]"#);
     let ok = result.unwrap();
-    assert_eq!(ok[0], 1);
-    assert_eq!(ok[1], 3);
+    assert_eq!(ok.0.0, "hello");
+    assert_eq!(ok.1.0, 1);
+    assert_eq!(ok.1.1, 2.0);
+    assert_eq!(ok.1.2, "3");
 }
diff --git a/facet-json/tests/integration/read/vec.rs b/facet-json/tests/integration/read/vec.rs
index 46d76ff05..27f1b73f3 100644
--- a/facet-json/tests/integration/read/vec.rs
+++ b/facet-json/tests/integration/read/vec.rs
@@ -131,23 +131,6 @@ fn test_nested_arrays() {
     assert_eq!(nested.matrix[2], vec![4, 5]);
 }
 
-#[test]
-#[ignore]
-fn test_deserialize_tuple() {
-    let result: Result<(&str, i32), _> = from_str(r#"["aaa",3]"#);
-    let ok = result.unwrap();
-    assert_eq!(ok.0, "aaa");
-    assert_eq!(ok.1, 3);
-
-    #[derive(facet::Facet)]
-    struct TestTuple(i32, String, bool);
-    let result: Result<TestTuple, _> = from_str(r#"[3,"aaa",true]"#);
-    let ok = result.unwrap();
-    assert_eq!(ok.0, 3);
-    assert_eq!(ok.1, "aaa");
-    assert!(ok.2);
-}
-
 #[test]
 fn test_deserialize_list() {
     let result: Result<Vec<i32>, _> = from_str(r#"[1,3]"#);
diff --git a/facet-reflect/src/wip/mod.rs b/facet-reflect/src/wip/mod.rs
index 42334d07e..82fa967bf 100644
--- a/facet-reflect/src/wip/mod.rs
+++ b/facet-reflect/src/wip/mod.rs
@@ -10,7 +10,8 @@ use alloc::{vec, vec::Vec};
 use bitflags::bitflags;
 use core::{fmt, marker::PhantomData};
 use facet_core::{
-    Def, DefaultInPlaceFn, Facet, FieldError, PtrConst, PtrMut, PtrUninit, Shape, Variant,
+    Def, DefaultInPlaceFn, Facet, FieldError, PtrConst, PtrMut, PtrUninit, ScalarAffinity, Shape,
+    Variant,
 };
 use flat_map::FlatMap;
 
@@ -1442,7 +1443,13 @@ impl<'facet_lifetime> Wip<'facet_lifetime> {
             });
         };
 
-        if !matches!(frame.shape.def, Def::List(_)) {
+        let is_tuple = if let Def::Struct(sd) = frame.shape.def {
+            sd.kind == facet_core::StructKind::Tuple
+        } else {
+            false
+        };
+
+        if !matches!(frame.shape.def, Def::List(_)) && !is_tuple {
             return Err(ReflectError::WasNotA {
                 expected: "list or array",
                 actual: frame.shape,
@@ -1505,29 +1512,69 @@ impl<'facet_lifetime> Wip<'facet_lifetime> {
         Ok(self)
     }
 
-    /// Pushes a new element onto the list/array
+    /// Pushes a new element onto the list/array/tuple
     ///
     /// This creates a new frame for the element. When this frame is popped,
     /// the element will be added to the list.
     pub fn push(mut self) -> Result<Self, ReflectError> {
-        // Make sure we're initializing a list
+        // Make sure we're initializing a list or a tuple
         let frame = self.frames.last().unwrap();
-        let list_shape = frame.shape;
+        let seq_shape = frame.shape;
 
-        if !matches!(list_shape.def, Def::List(_)) {
+        let is_tuple = if let Def::Struct(sd) = seq_shape.def {
+            sd.kind == facet_core::StructKind::Tuple
+        } else {
+            false
+        };
+
+        if !matches!(seq_shape.def, Def::List(_)) && !is_tuple {
             return Err(ReflectError::WasNotA {
-                expected: "list or array",
-                actual: list_shape,
+                expected: "list, array, or tuple",
+                actual: seq_shape,
             });
         }
 
-        // If the list isn't initialized yet, initialize it
-        if !frame.istate.fields.has(0) {
-            self = self.begin_pushback()?;
-        }
+        // For tuples, we need to find the next field to initialize
+        let element_shape = if is_tuple {
+            // We need to extract the info about the tuple now, before we potentially
+            // modify self with begin_pushback
+            if let Def::Struct(sd) = seq_shape.def {
+                // Find the next uninitialized field
+                let field_index = {
+                    let f = self.frames.last().unwrap();
+                    // IMPORTANT: Keep track of which index we're currently processing
+                    // Store this in the frame's istate for later use
+                    let next_idx = f.istate.list_index.unwrap_or(0);
+                    // Update the list_index for next time
+                    let frame = self.frames.last_mut().unwrap();
+                    frame.istate.list_index = Some(next_idx + 1);
+                    next_idx
+                };
 
-        // Get the element type
-        let element_shape = self.element_shape()?;
+                // Make sure the field index is valid
+                if field_index >= sd.fields.len() {
+                    return Err(ReflectError::FieldError {
+                        shape: seq_shape,
+                        field_error: FieldError::NoSuchField,
+                    });
+                }
+
+                // Get the field at that index
+                let field = &sd.fields[field_index];
+                field.shape()
+            } else {
+                // This shouldn't happen because we checked is_tuple earlier
+                unreachable!("Should have returned earlier if not a tuple");
+            }
+        } else {
+            // If the list isn't initialized yet, initialize it
+            if !frame.istate.fields.has(0) {
+                return self.begin_pushback()?.push();
+            }
+
+            // For lists, use the element_shape method
+            self.element_shape()?
+        };
 
         // Allocate memory for the element
         let element_data = element_shape
@@ -1549,10 +1596,11 @@ impl<'facet_lifetime> Wip<'facet_lifetime> {
         };
 
         trace!(
-            "[{}] Pushing element of type {} to list {}",
+            "[{}] Pushing element of type {} to {} {}",
             self.frames.len(),
             element_shape.green(),
-            list_shape.blue(),
+            if is_tuple { "tuple" } else { "list" },
+            seq_shape.blue(),
         );
 
         self.frames.push(element_frame);
@@ -1867,7 +1915,7 @@ impl<'facet_lifetime> Wip<'facet_lifetime> {
             // Handle list element frames
             FrameMode::ListElement => {
                 if frame.is_fully_initialized() {
-                    // This was a list element, so we need to push it to the parent list
+                    // This was a list or tuple element, so we need to push it to the parent
                     // Capture frame length and parent shape before mutable borrow
                     #[cfg(feature = "log")]
                     let frame_len = self.frames.len();
@@ -1876,32 +1924,106 @@ impl<'facet_lifetime> Wip<'facet_lifetime> {
                     let parent_frame = self.frames.last_mut().unwrap();
                     let parent_shape = parent_frame.shape;
 
-                    // Make sure the parent is a list
-                    match parent_shape.def {
-                        Def::List(_) => {
-                            // Get the list vtable from the ListDef
-                            if let Def::List(list_def) = parent_shape.def {
-                                let list_vtable = list_def.vtable;
-                                trace!(
-                                    "[{}] Pushing element to list {}",
-                                    frame_len,
-                                    parent_shape.blue()
+                    // Check if parent is a tuple
+                    let is_tuple = if let Def::Struct(sd) = parent_shape.def {
+                        sd.kind == facet_core::StructKind::Tuple
+                    } else {
+                        false
+                    };
+
+                    // Check if we're dealing with an empty unit type
+                    let is_empty_unit = match parent_shape.def {
+                        // Check for empty tuple struct
+                        Def::Struct(sd)
+                            if sd.kind == facet_core::StructKind::Tuple && sd.fields.is_empty() =>
+                        {
+                            true
+                        }
+                        // Check for scalar with Empty affinity
+                        Def::Scalar(s) if matches!(s.affinity, ScalarAffinity::Empty(_)) => true,
+                        _ => false,
+                    };
+
+                    // Make sure the parent is a list, array, or tuple
+                    if matches!(parent_shape.def, Def::List(_)) {
+                        // Get the list vtable from the ListDef
+                        if let Def::List(list_def) = parent_shape.def {
+                            let list_vtable = list_def.vtable;
+                            trace!(
+                                "[{}] Pushing element to list {}",
+                                frame_len,
+                                parent_shape.blue()
+                            );
+                            unsafe {
+                                // Convert the frame data pointer to Opaque and call push function from vtable
+                                (list_vtable.push)(
+                                    PtrMut::new(parent_frame.data.as_mut_byte_ptr()),
+                                    PtrMut::new(frame.data.as_mut_byte_ptr()),
                                 );
-                                unsafe {
-                                    // Convert the frame data pointer to Opaque and call push function from vtable
-                                    (list_vtable.push)(
-                                        PtrMut::new(parent_frame.data.as_mut_byte_ptr()),
-                                        PtrMut::new(frame.data.as_mut_byte_ptr()),
-                                    );
-                                    self.mark_moved_out_of(&mut frame);
-                                }
-                            } else {
-                                panic!("parent frame is not a list type");
+                                self.mark_moved_out_of(&mut frame);
                             }
+                        } else {
+                            panic!("parent frame is not a list type");
                         }
-                        _ => {
-                            panic!("Expected list or array, got {}", frame.shape);
+                    } else if is_empty_unit {
+                        trace!(
+                            "[{}] Handling empty unit type {}",
+                            frame_len,
+                            parent_shape.blue()
+                        );
+                        // We need to mark the unit type as fully initialized
+                        unsafe {
+                            parent_frame.mark_fully_initialized();
                         }
+                    } else if is_tuple {
+                        // For tuples, we need to set the field directly
+                        if let Def::Struct(sd) = parent_shape.def {
+                            // Get the field index from list_index we saved during push
+                            let previous_index = parent_frame.istate.list_index.unwrap_or(1);
+                            let field_index = previous_index - 1; // -1 because we already incremented in push
+
+                            // Make sure the field index is valid
+                            if field_index >= sd.fields.len() {
+                                panic!(
+                                    "Field index {} out of bounds for tuple with {} fields",
+                                    field_index,
+                                    sd.fields.len()
+                                );
+                            }
+
+                            // Use the correct field index directly
+                            let field = &sd.fields[field_index];
+                            trace!(
+                                "[{}] Setting tuple field {} of {}",
+                                frame_len,
+                                field_index,
+                                parent_shape.blue()
+                            );
+
+                            unsafe {
+                                // Copy the element data to the tuple field
+                                let field_ptr = parent_frame.data.field_uninit_at(field.offset);
+                                field_ptr
+                                    .copy_from(
+                                        PtrConst::new(frame.data.as_byte_ptr()),
+                                        field.shape(),
+                                    )
+                                    .unwrap();
+
+                                // Mark the field as initialized - we use field_index+1
+                                parent_frame.istate.fields.set(field_index + 1); // +1 because the first field (0) is for the struct itself
+
+                                // Mark the element as moved
+                                self.mark_moved_out_of(&mut frame);
+                            }
+                        } else {
+                            panic!(
+                                "Expected tuple (struct with StructKind::Tuple), got {}",
+                                frame.shape
+                            );
+                        }
+                    } else {
+                        panic!("Expected list or array, got {}", frame.shape);
                     }
                 }
             }


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T14:16:10Z
user: login: lmmx
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T14:25:03Z
user: login: fasterthanlime
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T14:28:16Z
user: login: lmmx
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T14:28:23Z
user: login: fasterthanlime
body: Looks good, I added some comments to keep in mind for further PRs. 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T07:19:22Z
user: login: lmmx
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T12:23:29Z

Review comments:
diff_hunk: @@ -456,6 +456,14 @@
             _ => Err(self),
         }
     }
+    /// Returns the `TupleDef` wrapped in an `Ok` if this is a [`Def::Struct`] whose kind is
+    /// [`StructKind::Tuple`].
+    pub fn into_tuple(self) -> Result<Struct, Self> {
path: facet-core/src/types/mod.rs
user: login: github-advanced-security[bot]
body: cannot find type `Struct` in this scope

[Show more details](https://github.com/facet-rs/facet/security/code-scanning/1476)
created_at: 2025-04-25T14:16:10Z
author_association: NONE
diff_hunk: @@ -456,6 +456,14 @@
             _ => Err(self),
         }
     }
+    /// Returns the `TupleDef` wrapped in an `Ok` if this is a [`Def::Struct`] whose kind is
+    /// [`StructKind::Tuple`].
+    pub fn into_tuple(self) -> Result<Struct, Self> {
path: facet-core/src/types/mod.rs
user: login: github-advanced-security[bot]
body: cannot find type `Struct` in this scope

[Show more details](https://github.com/facet-rs/facet/security/code-scanning/1477)
created_at: 2025-04-25T14:16:10Z
author_association: NONE
diff_hunk: @@ -456,6 +456,14 @@
             _ => Err(self),
         }
     }
+    /// Returns the `TupleDef` wrapped in an `Ok` if this is a [`Def::Struct`] whose kind is
+    /// [`StructKind::Tuple`].
+    pub fn into_tuple(self) -> Result<Struct, Self> {
path: facet-core/src/types/mod.rs
user: login: lmmx
body: oh, good call
created_at: 2025-04-25T14:25:03Z
author_association: MEMBER
diff_hunk: @@ -456,6 +456,14 @@
             _ => Err(self),
         }
     }
+    /// Returns the `TupleDef` wrapped in an `Ok` if this is a [`Def::Struct`] whose kind is
+    /// [`StructKind::Tuple`].
+    pub fn into_tuple(self) -> Result<Struct, Self> {
path: facet-core/src/types/mod.rs
user: login: fasterthanlime
body: yeah it's back to being called `StructDef` now.

but also.. `Tuple != Struct`? idk
created_at: 2025-04-25T14:28:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -456,6 +456,14 @@
             _ => Err(self),
         }
     }
+    /// Returns the `TupleDef` wrapped in an `Ok` if this is a [`Def::Struct`] whose kind is
+    /// [`StructKind::Tuple`].
+    pub fn into_tuple(self) -> Result<Struct, Self> {
path: facet-core/src/types/mod.rs
user: login: lmmx
body: fixed
created_at: 2025-04-25T14:28:23Z
author_association: MEMBER
diff_hunk: @@ -402,10 +402,60 @@ pub fn from_slice_wip<'input: 'facet, 'facet>(
                                         trace!("Array starting for list ({})!", wip.shape().blue());
                                         reflect!(put_default());
                                     }
+                                    Def::Struct(s) => {
+                                        if s.kind == StructKind::Tuple {
+                                            trace!(
+                                                "Array starting for tuple ({})!",
+                                                wip.shape().blue()
+                                            );
+                                            // Special handling for unit type ()
+                                            if s.fields.is_empty() {
+                                                // Check if the array is empty by peeking at the next token
path: facet-json/src/deserialize.rs
user: login: fasterthanlime
body: This is probably a fine approach, but I think I would have also... Another approach would have been to wait until we get another value, and then when we push it we get a reflect error, and then when we get a reflect error we... I don't know, at that time we generate a nice JSON error instead?
created_at: 2025-04-26T07:18:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -402,10 +402,60 @@ pub fn from_slice_wip<'input: 'facet, 'facet>(
                                         trace!("Array starting for list ({})!", wip.shape().blue());
                                         reflect!(put_default());
                                     }
+                                    Def::Struct(s) => {
+                                        if s.kind == StructKind::Tuple {
+                                            trace!(
+                                                "Array starting for tuple ({})!",
+                                                wip.shape().blue()
+                                            );
+                                            // Special handling for unit type ()
+                                            if s.fields.is_empty() {
+                                                // Check if the array is empty by peeking at the next token
+                                                let next_token = read_token!();
+                                                if next_token.node == Token::RBracket {
+                                                    // Empty array means unit type () - we're good
+                                                    reflect!(put_default());
+                                                } else {
+                                                    // Non-empty array is not valid for unit type
+                                                    bail!(JsonErrorKind::UnsupportedType {
+                                                        got: wip.innermost_shape(),
+                                                        wanted: "empty array",
+                                                    });
+                                                }
+                                            } else {
+                                                reflect!(put_default());
+                                            }
+                                        } else {
path: facet-json/src/deserialize.rs
user: login: fasterthanlime
body: In general, I would love to get to see more early returns in there to avoid such deep indentation. 
created_at: 2025-04-26T07:18:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -402,10 +402,60 @@ pub fn from_slice_wip<'input: 'facet, 'facet>(
                                         trace!("Array starting for list ({})!", wip.shape().blue());
                                         reflect!(put_default());
                                     }
+                                    Def::Struct(s) => {
+                                        if s.kind == StructKind::Tuple {
+                                            trace!(
+                                                "Array starting for tuple ({})!",
+                                                wip.shape().blue()
+                                            );
+                                            // Special handling for unit type ()
+                                            if s.fields.is_empty() {
+                                                // Check if the array is empty by peeking at the next token
+                                                let next_token = read_token!();
+                                                if next_token.node == Token::RBracket {
+                                                    // Empty array means unit type () - we're good
+                                                    reflect!(put_default());
+                                                } else {
+                                                    // Non-empty array is not valid for unit type
+                                                    bail!(JsonErrorKind::UnsupportedType {
+                                                        got: wip.innermost_shape(),
+                                                        wanted: "empty array",
+                                                    });
+                                                }
+                                            } else {
+                                                reflect!(put_default());
+                                            }
+                                        } else {
path: facet-json/src/deserialize.rs
user: login: lmmx
body: I returned to address this but see it's been modified (refactored to remove macros: 6e1d479) which I think resolved this complaint. Specifically [here](https://github.com/lmmx/facet/blob/22af2b63dd8aa63f1b1ff750f599732503087a1a/facet-json/src/deserialize.rs#L422C1-L430C38) there is now a `?` which will early return on error :smiley_cat: :+1: 
created_at: 2025-04-30T12:23:29Z
author_association: MEMBER

PR comments:
user: login: lmmx
created_at: 2025-04-25T20:01:46Z
body: @fasterthanlime having trouble working out how to fix this last bit, handling the unit type as a 0-tuple 7038d66
author_association: MEMBER
user: login: fasterthanlime
created_at: 2025-04-25T20:14:50Z
body: I'm on my mobile phone, but the code looks reasonable to me. What exactly you struggling with?
author_association: CONTRIBUTOR
user: login: lmmx
created_at: 2025-04-25T20:39:12Z
body: No worries, just am gonna be away from tomorrow so don't want to leave this half-finished ! If I can't get this tonight I'll ignore that test and walk back my attempt to include the `()` unit type, it shouldn't be a blocker.

I just need to work out how to say prematurely "that's all the elements in the tuple, close it" for the case of the unit type `()` which we are treating as a 0-tuple.

I seem to have correctly 'entered' the tuple via the `[` in the JSON but then am getting an error on reading the `]` straight after

```
louis 🌟 ~/dev/facet/facet $ just test "-p facet-json -E 'test(integration::read::tuple::test_deserialize_tuple_empty)'"
...
thread 'integration::read::tuple::test_deserialize_tuple_empty' panicked at facet-json/tests/integration/read/tuple.rs:135:12:
called `Result::unwrap()` on an `Err` value: Error: Error at $
   ╭─[ json:1:2 ]
   │
 1 │ []
   │  ┬  
   │  ╰── Wrong shape: expected list or array, but got ()
───╯
```

![Screenshot from 2025-04-25 21-38-12](https://github.com/user-attachments/assets/10886172-6779-4baf-94d1-b94e9b2aaf9c)
author_association: MEMBER
user: login: lmmx
created_at: 2025-04-25T21:09:52Z
body: Ah, I think I got it, now past the inside of the list - it was the pushback I hadn't updated with the unit type case :bulb: 

![Screenshot from 2025-04-25 22-08-59](https://github.com/user-attachments/assets/f2d3ea17-e973-45a8-af75-2159674796ca)

author_association: MEMBER
user: login: lmmx
created_at: 2025-04-25T22:09:25Z
body: Yeah, looks like I'm gonna have to walk this one back, can't see how to terminate it correctly :disappointed: I marked as fully initialised immediately, I don't get what I've missed. I can't pop a frame off to finish

It thinks it's reached EOF in a value when the value should have been finished (it's that "how to tell it it's finished" I am not getting)

![Screenshot from 2025-04-25 23-08-37](https://github.com/user-attachments/assets/32332211-3359-4521-88ec-2027c173ecf2)

**edit** on review (after walking back this approach entirely) I've got a hunch it failed because of this

https://github.com/lmmx/facet/blob/6b3cada782c02b2a96c4f7ab05cab999b3fae3e0/facet-json/src/deserialize.rs#L444

maybe by reading that token in the unit type case you consume what would be the signal to end the list... and it should be put back?

- Stored [here](https://github.com/lmmx/facet/tree/feat-tuple-deser-empty-tuple-draft) for now in case of re-drafting
author_association: MEMBER
user: login: lmmx
created_at: 2025-04-25T22:18:35Z
body: I'm sure it'll be achievable soon, but in the interest of not letting this go half baked I'm going to ignore the test

Also, I decided it's easier to interpret if you deserialize them as scalar types in a list:

```rust
let result: Result<((),()), _> = from_str(r#"[[],[]]"#);
```

so changed the ignored test to that

This PR introduces lots of tests for deserialising tuples and they all seem to work :-)

**edit** rolled this up into 1 commit and re-pushed for review at your leisure @fasterthanlime - should be good to merge as is
author_association: MEMBER