diff --git a/pico_proc_macro/src/lib.rs b/pico_proc_macro/src/lib.rs index ad0ea81af5316311e145984fae230236656c44d3..2cde5dd5c9ebb7281755dbba5ff49c61db654127 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 d0867f3a18dbf8933fefa7105c95ca400adf67de..ada952ab1c758752fde6a7ad9fe8c380d62bc071 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 0000000000000000000000000000000000000000..379898e9f1578f51ff9c79e4e55d7af6a464d749 --- /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 ab03f652fa6e2f46fa48d03709f65f46e570c64b..c97bc38427a792ef664b18b71c598f5996d56e58 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;