Repository: redbadger/crux (language: rust)

Issue:
crux_http and crux_kv: more ergonomic `Debug` on operations
The http requests and key value requests debug print as a giant list of numbers, which is not a great DX when used in tests. It would be nicer if they 

1. attempted to parse as UTF-8
2. otherwise printed as either numbers or hex pairs
3. truncated to the first N characters (maybe 50?)


Discussion / hints:

Fix patch:
diff --git a/crux_http/src/protocol.rs b/crux_http/src/protocol.rs
index 08a625a1..5c711cc9 100644
--- a/crux_http/src/protocol.rs
+++ b/crux_http/src/protocol.rs
@@ -16,7 +16,7 @@ pub struct HttpHeader {
     pub value: String,
 }
 
-#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq, Builder)]
+#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, Builder)]
 #[builder(
     custom_constructor,
     build_fn(private, name = "fallible_build"),
@@ -30,6 +30,30 @@ pub struct HttpRequest {
     pub body: Vec<u8>,
 }
 
+impl std::fmt::Debug for HttpRequest {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let body_repr = if let Ok(s) = std::str::from_utf8(&self.body) {
+            if s.len() < 50 {
+                format!("\"{s}\"")
+            } else {
+                format!("\"{}\"...", &s[..50])
+            }
+        } else {
+            format!("<binary data - {} bytes>", self.body.len())
+        };
+        let mut builder = f.debug_struct("HttpReqeuest");
+        builder
+            .field("method", &self.method)
+            .field("url", &self.url);
+        if !self.headers.is_empty() {
+            builder.field("headers", &self.headers);
+        };
+        builder
+            .field("body", &format_args!("{}", body_repr))
+            .finish()
+    }
+}
+
 macro_rules! http_method {
     ($name:ident, $method:expr) => {
         pub fn $name(url: impl Into<String>) -> HttpRequestBuilder {
@@ -267,4 +291,44 @@ mod tests {
             }
         );
     }
+
+    #[test]
+    fn test_http_request_debug_repr() {
+        {
+            // small
+            let req = HttpRequest::post("http://example.com")
+                .header("foo", "bar")
+                .body("hello world!")
+                .build();
+            let repr = format!("{req:?}");
+            assert_eq!(
+                repr,
+                r#"HttpReqeuest { method: "POST", url: "http://example.com", headers: [HttpHeader { name: "foo", value: "bar" }], body: "hello world!" }"#
+            );
+        }
+
+        {
+            // big
+            let req = HttpRequest::post("http://example.com")
+                .body("abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz")
+                .build();
+            let repr = format!("{req:?}");
+            assert_eq!(
+                repr,
+                r#"HttpReqeuest { method: "POST", url: "http://example.com", body: "abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvw"... }"#
+            );
+        }
+
+        {
+            // binary
+            let req = HttpRequest::post("http://example.com")
+                .body(vec![255, 254, 253, 252])
+                .build();
+            let repr = format!("{req:?}");
+            assert_eq!(
+                repr,
+                r#"HttpReqeuest { method: "POST", url: "http://example.com", body: <binary data - 4 bytes> }"#
+            );
+        }
+    }
 }
diff --git a/crux_kv/src/lib.rs b/crux_kv/src/lib.rs
index 08d36ae5..fba72f38 100644
--- a/crux_kv/src/lib.rs
+++ b/crux_kv/src/lib.rs
@@ -15,7 +15,7 @@ use error::KeyValueError;
 use value::Value;
 
 /// Supported operations
-#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
+#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
 pub enum KeyValueOperation {
     /// Read bytes stored under a key
     Get { key: String },
@@ -40,6 +40,40 @@ pub enum KeyValueOperation {
     },
 }
 
+impl std::fmt::Debug for KeyValueOperation {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            KeyValueOperation::Get { key } => f.debug_struct("Get").field("key", key).finish(),
+            KeyValueOperation::Set { key, value } => {
+                let body_repr = if let Ok(s) = std::str::from_utf8(&value) {
+                    if s.len() < 50 {
+                        format!("\"{s}\"")
+                    } else {
+                        format!("\"{}\"...", &s[..50])
+                    }
+                } else {
+                    format!("<binary data - {} bytes>", value.len())
+                };
+                f.debug_struct("Set")
+                    .field("key", key)
+                    .field("value", &format_args!("{}", body_repr))
+                    .finish()
+            }
+            KeyValueOperation::Delete { key } => {
+                f.debug_struct("Delete").field("key", key).finish()
+            }
+            KeyValueOperation::Exists { key } => {
+                f.debug_struct("Exists").field("key", key).finish()
+            }
+            KeyValueOperation::ListKeys { prefix, cursor } => f
+                .debug_struct("ListKeys")
+                .field("prefix", prefix)
+                .field("cursor", cursor)
+                .finish(),
+        }
+    }
+}
+
 /// The result of an operation on the store.
 ///
 /// Note: we can't use `Result` and `Option` here because generics are not currently


Test patch:
diff --git a/crux_kv/src/tests.rs b/crux_kv/src/tests.rs
index 4b0e645f..53ecfbb9 100644
--- a/crux_kv/src/tests.rs
+++ b/crux_kv/src/tests.rs
@@ -368,3 +368,53 @@ pub fn test_kv_async() -> Result<()> {
 
     Ok(())
 }
+
+#[test]
+fn test_kv_operation_debug_repr() {
+    {
+        // get
+        let op = KeyValueOperation::Get {
+            key: "my key".into(),
+        };
+        let repr = format!("{op:?}");
+        assert_eq!(repr, r#"Get { key: "my key" }"#);
+    }
+
+    {
+        // set small
+        let op = KeyValueOperation::Set {
+            key: "my key".into(),
+            value: b"my value".to_vec(),
+        };
+        let repr = format!("{op:?}");
+        assert_eq!(repr, r#"Set { key: "my key", value: "my value" }"#);
+    }
+
+    {
+        // set big
+        let op = KeyValueOperation::Set {
+            key: "my key".into(),
+            value:
+                b"abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz"
+                    .to_vec(),
+        };
+        let repr = format!("{op:?}");
+        assert_eq!(
+            repr,
+            r#"Set { key: "my key", value: "abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvw"... }"#
+        );
+    }
+
+    {
+        // set binary
+        let op = KeyValueOperation::Set {
+            key: "my key".into(),
+            value: vec![255, 255],
+        };
+        let repr = format!("{op:?}");
+        assert_eq!(
+            repr,
+            r#"Set { key: "my key", value: <binary data - 2 bytes> }"#
+        );
+    }
+}