From 145a9ba8ddc82a3ee693305a8d2eaa955e7ab926 Mon Sep 17 00:00:00 2001
From: Georgy Moshkin <gmoshkin@picodata.io>
Date: Wed, 13 Mar 2024 13:37:37 +0300
Subject: [PATCH] feat: Introspection::set_field_from_yaml

---
 pico_proc_macro/src/lib.rs | 274 +++++++++++++++++++++++++++++++++++--
 src/config.rs              |   1 +
 src/introspection.rs       | 269 ++++++++++++++++++++++++++++++++++++
 src/lib.rs                 |   1 +
 4 files changed, 537 insertions(+), 8 deletions(-)
 create mode 100644 src/introspection.rs

diff --git a/pico_proc_macro/src/lib.rs b/pico_proc_macro/src/lib.rs
index ad0ea81af5..2cde5dd5c9 100644
--- a/pico_proc_macro/src/lib.rs
+++ b/pico_proc_macro/src/lib.rs
@@ -1,18 +1,50 @@
 use quote::quote;
 
+macro_rules! unwrap_or_compile_error {
+    ($expr:expr) => {
+        match $expr {
+            Ok(v) => v,
+            Err(e) => {
+                return e.to_compile_error().into();
+            }
+        }
+    };
+}
+
 #[allow(clippy::single_match)]
-#[proc_macro_derive(Introspection)]
+#[proc_macro_derive(Introspection, attributes(introspection))]
 pub fn derive_introspection(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let input = syn::parse_macro_input!(input as syn::DeriveInput);
     let name = &input.ident;
 
+    let args = unwrap_or_compile_error!(Args::from_attributes(input.attrs));
+
+    let mut context = Context {
+        args,
+        fields: vec![],
+    };
     let mut field_names = vec![];
-    match &input.data {
-        syn::Data::Struct(ds) => match &ds.fields {
+    match input.data {
+        syn::Data::Struct(ds) => match ds.fields {
             syn::Fields::Named(fs) => {
-                for field in &fs.named {
-                    let field_name = field.ident.as_ref().unwrap();
-                    field_names.push(field_name.to_string());
+                for mut field in fs.named {
+                    let attrs = std::mem::take(&mut field.attrs);
+                    let attrs = unwrap_or_compile_error!(FieldAttrs::from_attributes(attrs));
+                    if attrs.ignore {
+                        continue;
+                    }
+
+                    let name = field_name(&field);
+                    field_names.push(name.clone());
+                    context.fields.push(FieldInfo {
+                        name,
+                        ident: field
+                            .ident
+                            .clone()
+                            .expect("Fields::Named has fields with names"),
+                        attrs,
+                        field,
+                    });
                 }
             }
             _ => {}
@@ -20,12 +52,238 @@ 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 crate_ = &context.args.crate_;
     quote! {
-        impl #name {
-            pub const FIELD_NAMES: &'static [&'static str] = &[
+        #[automatically_derived]
+        impl #crate_::introspection::Introspection for #name {
+            const FIELD_NAMES: &'static [&'static str] = &[
                 #( #field_names, )*
             ];
+
+            fn set_field_from_yaml(&mut self, path: &str, yaml: &str) -> ::std::result::Result<(), #crate_::introspection::IntrospectionError> {
+                use #crate_::introspection::IntrospectionError;
+                #body_for_set_field_from_yaml
+            }
         }
     }
     .into()
 }
+
+fn generate_body_for_set_field_from_yaml(context: &Context) -> proc_macro2::TokenStream {
+    let mut set_non_nestable = quote! {};
+    let mut set_nestable = quote! {};
+    let mut error_if_nestable = 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 assigning to a non-nestable field
+            set_non_nestable.extend(quote! {
+                #name => {
+                    match serde_yaml::from_str(yaml) {
+                        Ok(v) => {
+                            self.#ident = v;
+                            return Ok(());
+                        }
+                        Err(error) => return Err(IntrospectionError::SerdeYaml { field: path.into(), error }),
+                    }
+                }
+            });
+        } else {
+            // Handle assigning to a nested sub-field
+            set_nestable.extend(quote! {
+                #name => {
+                    return self.#ident.set_field_from_yaml(tail, yaml)
+                        .map_err(|e| e.with_prepended_prefix(head));
+                }
+            });
+
+            // Handle if trying to assign to field marked with `#[introspection(nested)]`
+            // This is not currently supported, all of it's subfields must be assigned individually
+            error_if_nestable.extend(quote! {
+                #name => return Err(IntrospectionError::AssignToNested {
+                    field: path.into(),
+                    example: #Type::FIELD_NAMES.get(0).unwrap_or(&"<actually there's no fields in this struct :(>"),
+                }),
+            })
+        }
+    }
+
+    // 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.extend(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
+                    #set_nestable
+                    _ => {
+                        return Err(IntrospectionError::NoSuchField {
+                            parent: "".into(),
+                            field: head.into(),
+                            expected: Self::FIELD_NAMES,
+                        });
+                    }
+                }
+            }
+            None => {
+                match path {
+                    #set_non_nestable
+                    #error_if_nestable
+                    _ => {
+                        return Err(IntrospectionError::NoSuchField {
+                            parent: "".into(),
+                            field: path.into(),
+                            expected: Self::FIELD_NAMES,
+                        });
+                    }
+                }
+            }
+        }
+    }
+}
+
+struct Context {
+    fields: Vec<FieldInfo>,
+    args: Args,
+}
+
+struct FieldInfo {
+    name: String,
+    ident: syn::Ident,
+    attrs: FieldAttrs,
+    #[allow(unused)]
+    field: syn::Field,
+}
+
+struct Args {
+    crate_: syn::Path,
+}
+
+impl Args {
+    fn from_attributes(attrs: Vec<syn::Attribute>) -> Result<Self, syn::Error> {
+        let mut result = Self {
+            crate_: syn::parse2(quote!(crate)).unwrap(),
+        };
+
+        for attr in attrs {
+            if !attr.path.is_ident("introspection") {
+                continue;
+            }
+
+            let meta: PathKeyValue = attr.parse_args()?;
+            if meta.key.is_ident("crate") {
+                result.crate_ = meta.value;
+            }
+        }
+
+        Ok(result)
+    }
+}
+
+struct FieldAttrs {
+    ignore: bool,
+    nested: bool,
+}
+
+impl FieldAttrs {
+    fn from_attributes(attrs: Vec<syn::Attribute>) -> Result<Self, syn::Error> {
+        let mut result = Self {
+            ignore: false,
+            nested: false,
+        };
+
+        for attr in attrs {
+            if !attr.path.is_ident("introspection") {
+                continue;
+            }
+
+            attr.parse_args_with(|input: syn::parse::ParseStream| {
+                // `input` is a stream of those tokens right there
+                // `#[introspection(foo, bar, ...)]`
+                //                  ^^^^^^^^^^^^^
+                while !input.is_empty() {
+                    let ident = input.parse::<syn::Ident>()?;
+                    if ident == "ignore" {
+                        result.ignore = true;
+                    } else if ident == "nested" {
+                        result.nested = true;
+                    } else {
+                        return Err(syn::Error::new(
+                            ident.span(),
+                            format!("unknown attribute argument `{ident}`, expected one of `ignore`, `nested`"),
+                        ));
+                    }
+
+                    if !input.is_empty() {
+                        input.parse::<syn::Token![,]>()?;
+                    }
+                }
+
+                Ok(())
+            })?;
+        }
+
+        Ok(result)
+    }
+}
+
+fn field_name(field: &syn::Field) -> String {
+    // TODO: consider using `quote::format_ident!` instead
+    let mut name = field.ident.as_ref().unwrap().to_string();
+    if name.starts_with("r#") {
+        // Remove 2 leading characters
+        name.remove(0);
+        name.remove(0);
+    }
+    name
+}
+
+#[derive(Debug)]
+struct PathKeyValue {
+    key: syn::Path,
+    #[allow(unused)]
+    eq_token: syn::Token![=],
+    value: syn::Path,
+}
+
+impl syn::parse::Parse for PathKeyValue {
+    fn parse(input: syn::parse::ParseStream) -> Result<Self, syn::Error> {
+        Ok(Self {
+            key: input.parse()?,
+            eq_token: input.parse()?,
+            value: input.parse()?,
+        })
+    }
+}
diff --git a/src/config.rs b/src/config.rs
index d0867f3a18..ada952ab1c 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -2,6 +2,7 @@ use crate::address::Address;
 use crate::cli::args;
 use crate::failure_domain::FailureDomain;
 use crate::instance::InstanceId;
+use crate::introspection::Introspection;
 use crate::replicaset::ReplicasetId;
 use crate::storage;
 use crate::tier::Tier;
diff --git a/src/introspection.rs b/src/introspection.rs
new file mode 100644
index 0000000000..379898e9f1
--- /dev/null
+++ b/src/introspection.rs
@@ -0,0 +1,269 @@
+//! This module contains items intended to help with runtime introspection of
+//! rust types. Currently it's mainly used for the [`PicodataConfig`] struct
+//! to simplify the management of configuration parameters and relatetd stuff.
+//!
+//! The main functionality is implemented via the [`Introspection`] trait and
+//! the corresponding derive macro. This trait currently facilitates the ability
+//! to set and get fields of the struct via a path known at runtime. Also it
+//! supports some basic struct field information.
+//!
+//! [`PicodataConfig`]: crate::config::PicodataConfig
+pub use pico_proc_macro::Introspection;
+
+pub trait Introspection {
+    const FIELD_NAMES: &'static [&'static str];
+
+    /// Assign field of `self` described by `path` to value parsed from a given
+    /// `yaml` expression.
+    ///
+    /// When using the `#[derive(Introspection)]` derive macro the implementation
+    /// uses the `serde_yaml` to decode values from yaml. This may change in the
+    /// future.
+    ///
+    /// # Examples:
+    /// ```
+    /// use picodata::introspection::Introspection;
+    ///
+    /// #[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: f32,
+    /// }
+    ///
+    /// let mut s = MyStruct::default();
+    /// s.set_field_from_yaml("number", "420").unwrap();
+    /// s.set_field_from_yaml("text", "hello world").unwrap();
+    /// s.set_field_from_yaml("nested.sub_field", "3.14").unwrap();
+    /// ```
+    fn set_field_from_yaml(&mut self, path: &str, yaml: &str) -> Result<(), IntrospectionError>;
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum IntrospectionError {
+    #[error("{}", Self::no_such_field_error_message(.parent, .field, .expected))]
+    NoSuchField {
+        parent: String,
+        field: String,
+        expected: &'static [&'static str],
+    },
+
+    #[error("incorrect value for field '{field}': {error}")]
+    SerdeYaml {
+        field: String,
+        error: serde_yaml::Error,
+    },
+
+    #[error("{expected} '{path}'")]
+    InvalidPath {
+        path: String,
+        expected: &'static str,
+    },
+
+    #[error("field '{field}' has no nested sub-fields")]
+    NotNestable { field: String },
+
+    #[error("field '{field}' cannot be assigned directly, must choose a sub-field (for example '{field}.{example}')")]
+    AssignToNested {
+        field: String,
+        example: &'static str,
+    },
+}
+
+impl IntrospectionError {
+    fn no_such_field_error_message(parent: &str, field: &str, expected: &[&str]) -> String {
+        let mut res = String::with_capacity(128);
+        if !parent.is_empty() {
+            _ = write!(&mut res, "{parent}: ");
+        }
+        use std::fmt::Write;
+        _ = write!(&mut res, "unknown field `{field}`");
+
+        let mut fields = expected.iter();
+        if let Some(first) = fields.next() {
+            _ = write!(&mut res, ", expected one of `{first}`");
+            for next in fields {
+                _ = write!(&mut res, ", `{next}`");
+            }
+        } else {
+            _ = write!(&mut res, ", there are no fields at all");
+        }
+
+        res
+    }
+
+    pub fn prepend_prefix(&mut self, prefix: &str) {
+        match self {
+            Self::NotNestable { field } => {
+                *field = format!("{prefix}.{field}");
+            }
+            Self::InvalidPath { path, .. } => {
+                *path = format!("{prefix}.{path}");
+            }
+            Self::NoSuchField { parent, .. } => {
+                if parent.is_empty() {
+                    *parent = prefix.into();
+                } else {
+                    *parent = format!("{prefix}.{parent}");
+                }
+            }
+            Self::SerdeYaml { field, .. } => {
+                *field = format!("{prefix}.{field}");
+            }
+            Self::AssignToNested { field, .. } => {
+                *field = format!("{prefix}.{field}");
+            }
+        }
+    }
+
+    #[inline(always)]
+    pub fn with_prepended_prefix(mut self, prefix: &str) -> Self {
+        self.prepend_prefix(prefix);
+        self
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[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 Empty {}
+
+        let mut s = S::default();
+
+        //
+        // Check error cases
+        //
+        let e = s.set_field_from_yaml("a", "foo").unwrap_err();
+        assert_eq!(
+            e.to_string(),
+            "unknown field `a`, expected one of `x`, `y`, `s`, `v`, `struct`"
+        );
+
+        let e = s.set_field_from_yaml(".x", "foo").unwrap_err();
+        assert_eq!(e.to_string(), "expected a field name before '.x'");
+
+        let e = s.set_field_from_yaml("&-*%?!", "foo").unwrap_err();
+        assert_eq!(
+            e.to_string(),
+            "unknown field `&-*%?!`, expected one of `x`, `y`, `s`, `v`, `struct`"
+        );
+
+        let e = s.set_field_from_yaml("x.foo.bar", "foo").unwrap_err();
+        assert_eq!(e.to_string(), "field 'x' has no nested sub-fields");
+
+        let e = s.set_field_from_yaml("struct", "foo").unwrap_err();
+        assert_eq!(e.to_string(), "field 'struct' cannot be assigned directly, must choose a sub-field (for example 'struct.a')");
+
+        let e = s.set_field_from_yaml("struct.empty", "foo").unwrap_err();
+        assert_eq!(e.to_string(), "field 'struct.empty' cannot be assigned directly, must choose a sub-field (for example 'struct.empty.<actually there's no fields in this struct :(>')");
+
+        let e = s.set_field_from_yaml("struct.foo", "foo").unwrap_err();
+        assert_eq!(
+            e.to_string(),
+            "struct: unknown field `foo`, expected one of `a`, `b`, `empty`"
+        );
+
+        let e = s.set_field_from_yaml("struct.a.bar", "foo").unwrap_err();
+        assert_eq!(e.to_string(), "field 'struct.a' has no nested sub-fields");
+
+        let e = s.set_field_from_yaml("struct.a..", "foo").unwrap_err();
+        assert_eq!(e.to_string(), "expected a field name after 'struct.a.'");
+
+        let e = s.set_field_from_yaml("x.", "foo").unwrap_err();
+        assert_eq!(e.to_string(), "expected a field name after 'x.'");
+
+        let e = s.set_field_from_yaml("x", "foo").unwrap_err();
+        assert_eq!(
+            e.to_string(),
+            "incorrect value for field 'x': invalid type: string \"foo\", expected i32"
+        );
+
+        let e = s.set_field_from_yaml("x", "'420'").unwrap_err();
+        assert_eq!(
+            e.to_string(),
+            "incorrect value for field 'x': invalid type: string \"420\", expected i32"
+        );
+
+        let e = s.set_field_from_yaml("x", "'420'").unwrap_err();
+        assert_eq!(
+            e.to_string(),
+            "incorrect value for field 'x': invalid type: string \"420\", expected i32"
+        );
+
+        let e = s.set_field_from_yaml("ignored", "foo").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
+            .set_field_from_yaml("struct.empty.foo", "bar")
+            .unwrap_err();
+        assert_eq!(
+            e.to_string(),
+            "struct.empty: unknown field `foo`, there are no fields at all"
+        );
+
+        //
+        // Check success cases
+        //
+        s.set_field_from_yaml("v", "[1, 2, 3]").unwrap();
+        assert_eq!(&s.v, &["1", "2", "3"]);
+        s.set_field_from_yaml("v", "['foo', \"bar\", baz]").unwrap();
+        assert_eq!(&s.v, &["foo", "bar", "baz"]);
+
+        s.set_field_from_yaml("x", "420").unwrap();
+        assert_eq!(s.x, 420);
+
+        s.set_field_from_yaml("y", "13").unwrap();
+        assert_eq!(s.y, 13.0);
+        s.set_field_from_yaml("y", "13.37").unwrap();
+        assert_eq!(s.y, 13.37);
+
+        s.set_field_from_yaml("s", "13.37").unwrap();
+        assert_eq!(s.s, "13.37");
+        s.set_field_from_yaml("s", "foo bar").unwrap();
+        assert_eq!(s.s, "foo bar");
+        s.set_field_from_yaml("s", "'foo bar'").unwrap();
+        assert_eq!(s.s, "foo bar");
+
+        s.set_field_from_yaml("  struct  .  a  ", "aaaa").unwrap();
+        assert_eq!(s.r#struct.a, "aaaa");
+        s.set_field_from_yaml("struct.b", "  0xbbbb  ").unwrap();
+        assert_eq!(s.r#struct.b, 0xbbbb);
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index ab03f652fa..c97bc38427 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -47,6 +47,7 @@ pub mod governor;
 pub mod http_server;
 pub mod info;
 pub mod instance;
+pub mod introspection;
 pub mod ipc;
 pub mod kvcell;
 pub mod r#loop;
-- 
GitLab