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

feat: #[introspection(config_default = ".")] + Introspection::get_field_default_value_as_rmpv

parent 7563bf31
No related branches found
No related tags found
1 merge request!913feat: pico.config() now shows sources for provided parameters
......@@ -66,6 +66,9 @@ pub fn derive_introspection(input: proc_macro::TokenStream) -> proc_macro::Token
let body_for_get_field_as_rmpv = generate_body_for_get_field_as_rmpv(&context);
let body_for_get_field_default_value_as_rmpv =
generate_body_for_get_field_default_value_as_rmpv(&context);
let crate_ = &context.args.crate_;
quote! {
#[automatically_derived]
......@@ -88,6 +91,11 @@ pub fn derive_introspection(input: proc_macro::TokenStream) -> proc_macro::Token
use #crate_::introspection::IntrospectionError;
#body_for_get_field_as_rmpv
}
fn get_field_default_value_as_rmpv(&self, path: &str) -> Result<Option<#crate_::introspection::RmpvValue>, #crate_::introspection::IntrospectionError> {
use #crate_::introspection::IntrospectionError;
#body_for_get_field_default_value_as_rmpv
}
}
}
.into()
......@@ -347,6 +355,133 @@ fn generate_body_for_get_field_as_rmpv(context: &Context) -> proc_macro2::TokenS
}
}
fn generate_body_for_get_field_default_value_as_rmpv(
context: &Context,
) -> proc_macro2::TokenStream {
let crate_ = &context.args.crate_;
let mut default_for_non_nestable = quote! {};
let mut default_for_whole_nestable = quote! {};
let mut default_for_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 default for a non-nestable field
if let Some(default) = &field.attrs.config_default {
default_for_non_nestable.extend(quote! {
#name => {
match #crate_::introspection::to_rmpv_value(&(#default)) {
Err(e) => {
return Err(IntrospectionError::ToRmpvValue { field: path.into(), details: e });
}
Ok(value) => return Ok(Some(value)),
}
}
});
} else {
default_for_non_nestable.extend(quote! {
#name => { return Ok(None); }
});
}
} else {
// Handle getting a field marked with `#[introspection(nested)]`.
default_for_whole_nestable.extend(quote! {
#name => {
use #crate_::introspection::RmpvValue;
let field_infos = #Type::FIELD_INFOS;
let mut fields = Vec::with_capacity(field_infos.len());
for field in field_infos {
let value = self.#ident.get_field_default_value_as_rmpv(field.name)
.map_err(|e| e.with_prepended_prefix(#name))?;
let Some(value) = value else {
continue;
};
fields.push((RmpvValue::from(field.name), value));
}
if fields.is_empty() {
return Ok(None);
}
return Ok(Some(RmpvValue::Map(fields)));
}
});
// Handle getting a nested field
default_for_nested_subfield.extend(quote! {
#name => {
return self.#ident.get_field_default_value_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
#default_for_nested_subfield
_ => {
return Err(IntrospectionError::NoSuchField {
parent: "".into(),
field: head.into(),
expected: Self::FIELD_INFOS,
});
}
}
}
None => {
match path {
#default_for_non_nestable
#default_for_whole_nestable
_ => {
return Err(IntrospectionError::NoSuchField {
parent: "".into(),
field: path.into(),
expected: Self::FIELD_INFOS,
});
}
}
}
}
}
}
struct Context {
fields: Vec<FieldInfo>,
args: Args,
......@@ -385,17 +520,16 @@ impl Args {
}
}
#[derive(Default)]
struct FieldAttrs {
ignore: bool,
nested: bool,
config_default: Option<syn::Expr>,
}
impl FieldAttrs {
fn from_attributes(attrs: Vec<syn::Attribute>) -> Result<Self, syn::Error> {
let mut result = Self {
ignore: false,
nested: false,
};
let mut result = Self::default();
for attr in attrs {
if !attr.path.is_ident("introspection") {
......@@ -412,10 +546,18 @@ impl FieldAttrs {
result.ignore = true;
} else if ident == "nested" {
result.nested = true;
} else if ident == "config_default" {
if result.config_default.is_some() {
return Err(syn::Error::new(ident.span(), "duplicate `config_default` specified"));
}
input.parse::<syn::Token![=]>()?;
result.config_default = Some(input.parse::<syn::Expr>()?);
} else {
return Err(syn::Error::new(
ident.span(),
format!("unknown attribute argument `{ident}`, expected one of `ignore`, `nested`"),
format!("unknown attribute argument `{ident}`, expected one of `ignore`, `nested`, `config_default`"),
));
}
......
......@@ -711,6 +711,7 @@ impl ClusterConfig {
#[derive(PartialEq, Default, Debug, Clone, serde::Deserialize, serde::Serialize, Introspection)]
pub struct InstanceConfig {
#[introspection(config_default = ".")]
pub data_dir: Option<String>,
pub service_password_file: Option<String>,
pub config_file: Option<String>,
......
......@@ -130,6 +130,67 @@ pub trait Introspection {
/// 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>;
/// Get a value which was specified via the `#[introspection(config_default = expr)]`
/// attribute converted to a [`rmpv::Value`].
///
/// Returns `Ok(None)` if `config_default` attribute wasn't provided for
/// given field.
///
/// Note that the value is not type checked, instead it is immediately
/// converted to a rmpv::Value, so it's the user's responsibility to make
/// sure the resulting value has the appropriate type. This however makes it
/// a bit simpler to specify values for nested types like `Option<String>`,
/// etc.
///
/// The `expr` in `#[introspection(config_default = expr)]` may contain
/// references to `self`, which allows for default values of some parameters
/// to be dependent on values of other parameters
/// (see [`InstanceConfig::advertise_address`] for example).
///
/// Also note that this function doesn't do any special checks to see if the
/// values are set or not in the actual struct. See what happens in
/// [`PicodataConfig::set_defaults_explicitly`] for a real-life example.
///
/// When using the `#[derive(Introspection)]` derive macro the implementation
/// converts the value to msgpack using `rmp_serde` and then decodes that
/// msgpack into a `rmpv::Value`. This is sub-optimal with respect to performance,
/// but for our use cases this is fine.
///
/// # Examples:
/// ```
/// use picodata::introspection::Introspection;
/// use rmpv::Value;
///
/// #[derive(Introspection, Default)]
/// #[introspection(crate = picodata)]
/// struct MyStruct {
/// #[introspection(config_default = 69105)]
/// number: i32,
/// #[introspection(config_default = format!("there's {} leaves in the pile", self.number))]
/// text: String,
/// doesnt_have_default: bool,
/// }
///
/// let mut s = MyStruct {
/// number: 2,
/// ..Default::default()
/// };
///
/// assert_eq!(s.get_field_default_value_as_rmpv("number").unwrap(), Some(Value::from(69105)));
///
/// // Note here the actual value of `s.number` is used, not the config_default one. --------V
/// assert_eq!(s.get_field_default_value_as_rmpv("text").unwrap(), Some(Value::from("there's 2 leaves in the pile")));
///
/// assert_eq!(s.get_field_default_value_as_rmpv("doesnt_have_default").unwrap(), None);
/// ```
///
/// [`InstanceConfig::advertise_address`]: crate::config::InstanceConfig::advertise_address
/// [`PicodataConfig::set_defaults_explicitly`]: crate::config::PicodataConfig::set_defaults_explicitly
fn get_field_default_value_as_rmpv(
&self,
path: &str,
) -> Result<Option<rmpv::Value>, IntrospectionError>;
}
/// Information about a single struct field. This is the type which is stored
......@@ -322,8 +383,11 @@ mod test {
#[derive(Default, Debug, Introspection, PartialEq)]
struct S {
x: i32,
#[introspection(config_default = self.x as f32 * 0.5)]
y: f32,
#[introspection(config_default = "this is a &str but it still works")]
s: String,
#[introspection(config_default = &["this", "also", "works"])]
v: Vec<String>,
#[introspection(nested)]
r#struct: Nested,
......@@ -334,7 +398,9 @@ mod test {
#[derive(Default, Debug, Introspection, PartialEq)]
struct Nested {
#[introspection(config_default = "nested of course works")]
a: String,
#[introspection(config_default = format!("{}, but type safety is missing unfortunately", self.a))]
b: i64,
#[introspection(nested)]
empty: Empty,
......@@ -661,4 +727,88 @@ mod test {
assert_eq!(leaf_field_paths::<Nested>(), &["a", "b", "empty"]);
}
#[test]
fn get_field_default_value_as_rmpv() {
let s = S {
x: 999,
y: 8.88,
s: "S".into(),
v: vec!["V0".into(), "V1".into(), "V2".into()],
r#struct: Nested {
a: "self.a which was set explicitly".into(),
b: 0xb,
empty: Empty {},
},
ignored: serde_yaml::Value::default(),
};
//
// Error cases are mostly covered in tests above
//
let e = s
.get_field_default_value_as_rmpv("struct.no_such_field")
.unwrap_err();
assert_eq!(
e.to_string(),
"struct: unknown field `no_such_field`, expected one of `a`, `b`, `empty`"
);
let e = s.get_field_default_value_as_rmpv("ignored").unwrap_err();
assert_eq!(
e.to_string(),
"unknown field `ignored`, expected one of `x`, `y`, `s`, `v`, `struct`"
);
//
// Success cases
//
assert_eq!(s.get_field_default_value_as_rmpv("x").unwrap(), None);
// Remember, it's `self.x * 0.5`
assert_eq!(
s.get_field_default_value_as_rmpv("y").unwrap(),
Some(rmpv::Value::F32(499.5))
);
assert_eq!(
s.get_field_default_value_as_rmpv("s").unwrap(),
Some(rmpv::Value::from("this is a &str but it still works"))
);
assert_eq!(
s.get_field_default_value_as_rmpv("v").unwrap(),
Some(rmpv::Value::Array(vec![
rmpv::Value::from("this"),
rmpv::Value::from("also"),
rmpv::Value::from("works"),
]))
);
assert_eq!(
s.get_field_default_value_as_rmpv("struct.a").unwrap(),
Some(rmpv::Value::from("nested of course works"))
);
assert_eq!(
s.get_field_default_value_as_rmpv("struct.b").unwrap(),
Some(rmpv::Value::from(
"self.a which was set explicitly, but type safety is missing unfortunately"
))
);
assert_eq!(
s.get_field_default_value_as_rmpv("struct.empty").unwrap(),
None
);
// For some reason we even allow getting the default for the whole subsection.
#[rustfmt::skip]
assert_eq!(
s.get_field_default_value_as_rmpv("struct").unwrap(),
Some(rmpv::Value::Map(vec![
(rmpv::Value::from("a"), rmpv::Value::from("nested of course works")),
(rmpv::Value::from("b"), rmpv::Value::from("self.a which was set explicitly, but type safety is missing unfortunately")),
])),
);
}
}
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