Skip to content
Snippets Groups Projects
Commit 145a9ba8 authored by Georgy Moshkin's avatar Georgy Moshkin :speech_balloon:
Browse files

feat: Introspection::set_field_from_yaml

parent dbbf1937
No related branches found
No related tags found
1 merge request!900Gmoshkin/config.yaml cli consistent naming
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()?,
})
}
}
......@@ -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;
......
//! 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);
}
}
......@@ -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;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment