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