From 8856b1c6347fd73ed7cba14ae648d6f4a05da005 Mon Sep 17 00:00:00 2001
From: Georgy Moshkin <gmoshkin@picodata.io>
Date: Tue, 19 Mar 2024 13:25:03 +0300
Subject: [PATCH] feat: Introspection::get_field_as_rmpv

---
 Cargo.toml                 |   2 +-
 pico_proc_macro/src/lib.rs | 119 ++++++++++++++++++-
 src/introspection.rs       | 237 +++++++++++++++++++++++++++++++------
 3 files changed, 321 insertions(+), 37 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index bf2dc155d0..050f83dd50 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,7 +32,7 @@ once_cell = "1.17.1"
 blake3 = "=1.3.3"
 rustyline = "12.0.0"
 rustyline-derive = "0.10.0"
-rmpv = "1.0.0"
+rmpv = { version = "1.0.0", features = ["with-serde"] }
 comfy-table = "7.0.1"
 va_list = ">=0.1.4"
 chrono = "0.4.31"
diff --git a/pico_proc_macro/src/lib.rs b/pico_proc_macro/src/lib.rs
index 2cde5dd5c9..e77daf472c 100644
--- a/pico_proc_macro/src/lib.rs
+++ b/pico_proc_macro/src/lib.rs
@@ -53,6 +53,7 @@ pub fn derive_introspection(input: proc_macro::TokenStream) -> proc_macro::Token
     }
 
     let body_for_set_field_from_yaml = generate_body_for_set_field_from_yaml(&context);
+    let body_for_get_field_as_rmpv = generate_body_for_get_field_as_rmpv(&context);
 
     let crate_ = &context.args.crate_;
     quote! {
@@ -66,6 +67,11 @@ pub fn derive_introspection(input: proc_macro::TokenStream) -> proc_macro::Token
                 use #crate_::introspection::IntrospectionError;
                 #body_for_set_field_from_yaml
             }
+
+            fn get_field_as_rmpv(&self, path: &str) -> ::std::result::Result<#crate_::introspection::RmpvValue, #crate_::introspection::IntrospectionError> {
+                use #crate_::introspection::IntrospectionError;
+                #body_for_get_field_as_rmpv
+            }
         }
     }
     .into()
@@ -93,7 +99,7 @@ fn generate_body_for_set_field_from_yaml(context: &Context) -> proc_macro2::Toke
                             self.#ident = v;
                             return Ok(());
                         }
-                        Err(error) => return Err(IntrospectionError::SerdeYaml { field: path.into(), error }),
+                        Err(error) => return Err(IntrospectionError::FromSerdeYaml { field: path.into(), error }),
                     }
                 }
             });
@@ -174,6 +180,117 @@ fn generate_body_for_set_field_from_yaml(context: &Context) -> proc_macro2::Toke
     }
 }
 
+fn generate_body_for_get_field_as_rmpv(context: &Context) -> proc_macro2::TokenStream {
+    let crate_ = &context.args.crate_;
+
+    let mut get_non_nestable = quote! {};
+    let mut get_whole_nestable = quote! {};
+    let mut get_nested_subfield = quote! {};
+    let mut non_nestable_names = vec![];
+    for field in &context.fields {
+        let name = &field.name;
+        let ident = &field.ident;
+        #[allow(non_snake_case)]
+        let Type = &field.field.ty;
+
+        if !field.attrs.nested {
+            non_nestable_names.push(name);
+
+            // Handle getting a non-nestable field
+            get_non_nestable.extend(quote! {
+                #name => {
+                    match #crate_::introspection::to_rmpv_value(&self.#ident) {
+                        Err(e) => {
+                            return Err(IntrospectionError::ToRmpvValue { field: path.into(), details: e });
+                        }
+                        Ok(value) => return Ok(value),
+                    }
+                }
+            });
+        } else {
+            // Handle getting a field marked with `#[introspection(nested)]`.
+            get_whole_nestable.extend(quote! {
+                #name => {
+                    use #crate_::introspection::RmpvValue;
+                    let field_names = #Type::FIELD_NAMES;
+                    let mut fields = Vec::with_capacity(field_names.len());
+                    for sub_field in field_names {
+                        let key = RmpvValue::from(*sub_field);
+                        let value = self.#ident.get_field_as_rmpv(sub_field)
+                            .map_err(|e| e.with_prepended_prefix(#name))?;
+                        fields.push((key, value));
+                    }
+                    return Ok(RmpvValue::Map(fields));
+                }
+            });
+
+            // Handle getting a nested field
+            get_nested_subfield.extend(quote! {
+                #name => {
+                    return self.#ident.get_field_as_rmpv(tail)
+                        .map_err(|e| e.with_prepended_prefix(head));
+                }
+            });
+        }
+    }
+
+    // Handle if a nested path is specified for non-nestable field
+    let mut error_if_non_nestable = quote! {};
+    if !non_nestable_names.is_empty() {
+        error_if_non_nestable = quote! {
+            #( #non_nestable_names )|* => {
+                return Err(IntrospectionError::NotNestable { field: head.into() })
+            }
+        };
+    }
+
+    // Actual generated body:
+    quote! {
+        match path.split_once('.') {
+            Some((head, tail)) => {
+                let head = head.trim();
+                if head.is_empty() {
+                    return Err(IntrospectionError::InvalidPath {
+                        expected: "expected a field name before",
+                        path: format!(".{tail}"),
+                    })
+                }
+                let tail = tail.trim();
+                if !tail.chars().next().map_or(false, char::is_alphabetic) {
+                    return Err(IntrospectionError::InvalidPath {
+                        expected: "expected a field name after",
+                        path: format!("{head}."),
+                    })
+                }
+                match head {
+                    #error_if_non_nestable
+                    #get_nested_subfield
+                    _ => {
+                        return Err(IntrospectionError::NoSuchField {
+                            parent: "".into(),
+                            field: head.into(),
+                            expected: Self::FIELD_NAMES,
+                        });
+                    }
+                }
+            }
+            None => {
+                match path {
+                    #get_non_nestable
+                    #get_whole_nestable
+                    _ => {
+                        return Err(IntrospectionError::NoSuchField {
+                            parent: "".into(),
+                            field: path.into(),
+                            expected: Self::FIELD_NAMES,
+                        });
+                    }
+                }
+            }
+        }
+    }
+}
+
 struct Context {
     fields: Vec<FieldInfo>,
     args: Args,
diff --git a/src/introspection.rs b/src/introspection.rs
index 379898e9f1..a05934d096 100644
--- a/src/introspection.rs
+++ b/src/introspection.rs
@@ -8,6 +8,7 @@
 //! supports some basic struct field information.
 //!
 //! [`PicodataConfig`]: crate::config::PicodataConfig
+use crate::traft::error::Error;
 pub use pico_proc_macro::Introspection;
 
 pub trait Introspection {
@@ -45,6 +46,64 @@ pub trait Introspection {
     /// s.set_field_from_yaml("nested.sub_field", "3.14").unwrap();
     /// ```
     fn set_field_from_yaml(&mut self, path: &str, yaml: &str) -> Result<(), IntrospectionError>;
+
+    /// Get field of `self` described by `path` as a generic msgpack value in
+    /// form of [`rmpv::Value`].
+    ///
+    /// When using the `#[derive(Introspection)]` derive macro the implementation
+    /// converts the value to msgpack using [`to_rmpv_value`].
+    ///
+    /// In the future we may want to get values as some other enums (maybe
+    /// serde_yaml::Value, or our custom one), but for now we've chosen rmpv
+    /// because we're using this to convert values to msgpack.
+    ///
+    /// # Examples:
+    /// ```
+    /// use picodata::introspection::Introspection;
+    /// use rmpv::Value;
+    ///
+    /// #[derive(Introspection, Default)]
+    /// #[introspection(crate = picodata)]
+    /// struct MyStruct {
+    ///     number: i32,
+    ///     text: String,
+    ///     #[introspection(nested)]
+    ///     nested: NestedStruct,
+    /// }
+    ///
+    /// #[derive(Introspection, Default)]
+    /// #[introspection(crate = picodata)]
+    /// struct NestedStruct {
+    ///     sub_field: f64,
+    /// }
+    ///
+    /// let mut s = MyStruct {
+    ///     number: 13,
+    ///     text: "hello".into(),
+    ///     nested: NestedStruct {
+    ///         sub_field: 2.71,
+    ///     },
+    /// };
+    ///
+    /// assert_eq!(s.get_field_as_rmpv("number").unwrap(), Value::from(13));
+    /// assert_eq!(s.get_field_as_rmpv("text").unwrap(), Value::from("hello"));
+    /// assert_eq!(s.get_field_as_rmpv("nested.sub_field").unwrap(), Value::from(2.71));
+    /// ```
+    fn get_field_as_rmpv(&self, path: &str) -> Result<rmpv::Value, IntrospectionError>;
+}
+
+/// A public reimport for use in the derive macro.
+pub use rmpv::Value as RmpvValue;
+
+/// Converts a generic serde serializable value to [`rmpv::Value`]. This
+/// function is just needed to be called from the derived
+/// [`Introspection::get_field_as_rmpv`] implementations.
+#[inline(always)]
+pub fn to_rmpv_value<T>(v: &T) -> Result<rmpv::Value, Error>
+where
+    T: serde::Serialize,
+{
+    crate::to_rmpv_named::to_rmpv_named(v).map_err(Error::other)
 }
 
 #[derive(Debug, thiserror::Error)]
@@ -57,7 +116,7 @@ pub enum IntrospectionError {
     },
 
     #[error("incorrect value for field '{field}': {error}")]
-    SerdeYaml {
+    FromSerdeYaml {
         field: String,
         error: serde_yaml::Error,
     },
@@ -76,6 +135,9 @@ pub enum IntrospectionError {
         field: String,
         example: &'static str,
     },
+
+    #[error("failed converting '{field}' to a msgpack value: {details}")]
+    ToRmpvValue { field: String, details: Error },
 }
 
 impl IntrospectionError {
@@ -100,8 +162,8 @@ impl IntrospectionError {
         res
     }
 
-    pub fn prepend_prefix(&mut self, prefix: &str) {
-        match self {
+    pub fn with_prepended_prefix(mut self, prefix: &str) -> Self {
+        match &mut self {
             Self::NotNestable { field } => {
                 *field = format!("{prefix}.{field}");
             }
@@ -115,18 +177,16 @@ impl IntrospectionError {
                     *parent = format!("{prefix}.{parent}");
                 }
             }
-            Self::SerdeYaml { field, .. } => {
+            Self::FromSerdeYaml { field, .. } => {
                 *field = format!("{prefix}.{field}");
             }
             Self::AssignToNested { field, .. } => {
                 *field = format!("{prefix}.{field}");
             }
+            Self::ToRmpvValue { field, .. } => {
+                *field = format!("{prefix}.{field}");
+            }
         }
-    }
-
-    #[inline(always)]
-    pub fn with_prepended_prefix(mut self, prefix: &str) -> Self {
-        self.prepend_prefix(prefix);
         self
     }
 }
@@ -134,37 +194,38 @@ impl IntrospectionError {
 #[cfg(test)]
 mod test {
     use super::*;
+    use pretty_assertions::assert_eq;
+
+    #[derive(Default, Debug, Introspection)]
+    struct S {
+        x: i32,
+        y: f32,
+        s: String,
+        v: Vec<String>,
+        #[introspection(nested)]
+        r#struct: Nested,
+
+        #[introspection(ignore)]
+        ignored: serde_yaml::Value,
+    }
 
-    #[test]
-    fn derive_set_field_from_yaml() {
-        #[derive(Default, Debug, Introspection)]
-        struct S {
-            x: i32,
-            y: f32,
-            s: String,
-            v: Vec<String>,
-            #[introspection(nested)]
-            r#struct: Nested,
-
-            #[introspection(ignore)]
-            ignored: serde_yaml::Value,
-        }
-
-        #[derive(Default, Debug, Introspection)]
-        struct Nested {
-            a: String,
-            b: i64,
-            #[introspection(nested)]
-            empty: Empty,
-        }
+    #[derive(Default, Debug, Introspection)]
+    struct Nested {
+        a: String,
+        b: i64,
+        #[introspection(nested)]
+        empty: Empty,
+    }
 
-        #[derive(Default, Debug, Introspection)]
-        struct Empty {}
+    #[derive(Default, Debug, Introspection)]
+    struct Empty {}
 
+    #[test]
+    fn set_field_from_yaml() {
         let mut s = S::default();
 
         //
-        // Check error cases
+        // Check `set_field_from_yaml` error cases
         //
         let e = s.set_field_from_yaml("a", "foo").unwrap_err();
         assert_eq!(
@@ -239,7 +300,7 @@ mod test {
         );
 
         //
-        // Check success cases
+        // Check `set_field_from_yaml` success cases
         //
         s.set_field_from_yaml("v", "[1, 2, 3]").unwrap();
         assert_eq!(&s.v, &["1", "2", "3"]);
@@ -266,4 +327,110 @@ mod test {
         s.set_field_from_yaml("struct.b", "  0xbbbb  ").unwrap();
         assert_eq!(s.r#struct.b, 0xbbbb);
     }
+
+    #[test]
+    fn get_field_as_rmpv() {
+        let s = S {
+            x: 111,
+            y: 2.22,
+            s: "sssss".into(),
+            v: vec!["v".into(), "vv".into(), "vvv".into()],
+            r#struct: Nested {
+                a: "aaaaaa".into(),
+                b: 0xbbbbbb,
+                empty: Empty {},
+            },
+            ignored: serde_yaml::Value::default(),
+        };
+
+        //
+        // Check `get_field_as_rmpv` error cases
+        //
+        let e = s.get_field_as_rmpv("a").unwrap_err();
+        assert_eq!(
+            e.to_string(),
+            "unknown field `a`, expected one of `x`, `y`, `s`, `v`, `struct`"
+        );
+
+        let e = s.get_field_as_rmpv(".x").unwrap_err();
+        assert_eq!(e.to_string(), "expected a field name before '.x'");
+
+        let e = s.get_field_as_rmpv("&-*%?!").unwrap_err();
+        assert_eq!(
+            e.to_string(),
+            "unknown field `&-*%?!`, expected one of `x`, `y`, `s`, `v`, `struct`"
+        );
+
+        let e = s.get_field_as_rmpv("x.foo.bar").unwrap_err();
+        assert_eq!(e.to_string(), "field 'x' has no nested sub-fields");
+
+        let e = s.get_field_as_rmpv("struct.foo").unwrap_err();
+        assert_eq!(
+            e.to_string(),
+            "struct: unknown field `foo`, expected one of `a`, `b`, `empty`"
+        );
+
+        let e = s.get_field_as_rmpv("struct.a.bar").unwrap_err();
+        assert_eq!(e.to_string(), "field 'struct.a' has no nested sub-fields");
+
+        let e = s.get_field_as_rmpv("struct.a..").unwrap_err();
+        assert_eq!(e.to_string(), "expected a field name after 'struct.a.'");
+
+        let e = s.get_field_as_rmpv("x.").unwrap_err();
+        assert_eq!(e.to_string(), "expected a field name after 'x.'");
+
+        let e = s.get_field_as_rmpv("ignored").unwrap_err();
+        assert_eq!(
+            e.to_string(),
+            "unknown field `ignored`, expected one of `x`, `y`, `s`, `v`, `struct`"
+        );
+        assert_eq!(s.ignored, serde_yaml::Value::default());
+
+        let e = s.get_field_as_rmpv("struct.empty.foo").unwrap_err();
+        assert_eq!(
+            e.to_string(),
+            "struct.empty: unknown field `foo`, there are no fields at all"
+        );
+
+        //
+        // Check `get_field_as_rmpv` success cases
+        //
+        assert_eq!(s.get_field_as_rmpv("x").unwrap(), rmpv::Value::from(111));
+        assert_eq!(s.get_field_as_rmpv("y").unwrap(), rmpv::Value::F32(2.22));
+        assert_eq!(
+            s.get_field_as_rmpv("s").unwrap(),
+            rmpv::Value::from("sssss")
+        );
+        assert_eq!(
+            s.get_field_as_rmpv("v").unwrap(),
+            rmpv::Value::Array(vec![
+                rmpv::Value::from("v"),
+                rmpv::Value::from("vv"),
+                rmpv::Value::from("vvv"),
+            ])
+        );
+
+        assert_eq!(
+            s.get_field_as_rmpv("struct.a").unwrap(),
+            rmpv::Value::from("aaaaaa")
+        );
+        assert_eq!(
+            s.get_field_as_rmpv("struct.b").unwrap(),
+            rmpv::Value::from(0xbbbbbb)
+        );
+        assert_eq!(
+            s.get_field_as_rmpv("struct.empty").unwrap(),
+            rmpv::Value::Map(vec![])
+        );
+
+        // We can also get the entire `struct` sub-field if we wanted:
+        assert_eq!(
+            s.get_field_as_rmpv("struct").unwrap(),
+            rmpv::Value::Map(vec![
+                (rmpv::Value::from("a"), rmpv::Value::from("aaaaaa")),
+                (rmpv::Value::from("b"), rmpv::Value::from(0xbbbbbb)),
+                (rmpv::Value::from("empty"), rmpv::Value::Map(vec![])),
+            ])
+        );
+    }
 }
-- 
GitLab