Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proc macro for tedge config #1936

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
375bc6c
POC proc macro for tedge config
jarhodes314 Apr 28, 2023
fb59fda
Make example realistic
jarhodes314 Apr 28, 2023
5e9b80d
Refactor tests and example into appropriate places
jarhodes314 May 2, 2023
f4c67a1
Ensure missing key error contains the field name
jarhodes314 May 2, 2023
1aab677
Store the key regardless of success/failure
jarhodes314 May 2, 2023
771db4c
Replace all or nothing logic in macro with library function
jarhodes314 May 3, 2023
80fe910
Add doc comments and support for read only configurations
jarhodes314 May 3, 2023
ffff8d1
Begin to use new macro in tedge_config
jarhodes314 May 9, 2023
8d59006
Migrate old accessors to new DTOs
jarhodes314 May 9, 2023
c7490e9
Fix test failures in tedge
jarhodes314 May 9, 2023
1868294
Merge remote-tracking branch 'upstream/main' into feat/tedge-config-r…
jarhodes314 May 9, 2023
86af093
Update tedge config set to use the new dto
jarhodes314 May 9, 2023
28dcee3
Migrate `tedge config unset` to new dto
jarhodes314 May 9, 2023
8cd46ad
Fix clippy warning
jarhodes314 May 9, 2023
6471e1f
Migrate `tedge config list` to new dto/reader
jarhodes314 May 9, 2023
98f4a85
Run formatter
jarhodes314 May 9, 2023
6e2e3d0
Actually support renaming
jarhodes314 May 9, 2023
6556d1c
Update references to renamed keys
jarhodes314 May 9, 2023
98e5201
Migrate `tedge cert` to new dto
jarhodes314 May 10, 2023
5b513a0
Fix integration tests and add some tests for backwards compatibility
jarhodes314 May 10, 2023
4696954
Merge remote-tracking branch 'upstream/main' into feat/tedge-config-r…
jarhodes314 May 11, 2023
5d3e4e4
Finish implementing private reader fields and optional paths
jarhodes314 May 11, 2023
81b7193
Support renaming groups
jarhodes314 May 11, 2023
ce77b22
Log only if we actually migrate tedge.toml
jarhodes314 May 11, 2023
b2ef8fa
Add more documentation and tests for the macro, fix clippy errors
jarhodes314 May 11, 2023
3b95375
Add documentation
jarhodes314 May 12, 2023
501b756
Fix unused dependency error
jarhodes314 May 15, 2023
d2063d5
Merge remote-tracking branch 'upstream/main' into feat/tedge-config-r…
jarhodes314 May 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions crates/common/tedge_config_macros/impl/src/input/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,4 @@ pub struct FieldDtoSettings {
}

#[derive(FromMeta, Debug, Default)]
pub struct ReaderSettings {
#[darling(default)]
pub all_or_nothing: bool,
}
pub struct ReaderSettings {}
113 changes: 7 additions & 106 deletions crates/common/tedge_config_macros/impl/src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,15 @@ pub fn try_generate(
root_name: proc_macro2::Ident,
items: &[FieldOrGroup],
) -> syn::Result<TokenStream> {
let structs = generate_structs(&root_name, items, false)?;
let structs = generate_structs(&root_name, items)?;
let conversions = generate_conversions(&root_name, items, vec![], items)?;
Ok(quote! {
#structs
#conversions
})
}

fn generate_structs(
name: &proc_macro2::Ident,
items: &[FieldOrGroup],
parent_all_or_nothing: bool,
) -> syn::Result<TokenStream> {
fn generate_structs(name: &proc_macro2::Ident, items: &[FieldOrGroup]) -> syn::Result<TokenStream> {
let mut idents = Vec::new();
let mut tys = Vec::<syn::Type>::new();
let mut sub_readers = Vec::new();
Expand All @@ -46,34 +42,18 @@ fn generate_structs(
let ty = field.ty();
attrs.push(field.attrs().to_vec());
idents.push(field.ident());
if Some(&FieldDefault::None) == field.read_write().map(|f| &f.default)
&& !parent_all_or_nothing
{
if Some(&FieldDefault::None) == field.read_write().map(|f| &f.default) {
tys.push(parse_quote_spanned!(ty.span()=> OptionalConfig<#ty>));
} else {
tys.push(ty.to_owned());
}
sub_readers.push(None);
}
FieldOrGroup::Group(group) if parent_all_or_nothing => {
return Err(syn::Error::new(
group.ident.span(),
"Sub-groups inside `#[reader(all_or_nothing)]` are not supported",
));
}
FieldOrGroup::Group(group) => {
let sub_reader_name = prefixed_type_name(name, group);
idents.push(&group.ident);
if group.reader.all_or_nothing {
tys.push(parse_quote_spanned!(group.ident.span()=> OptionalConfigGroup<#sub_reader_name>));
} else {
tys.push(parse_quote_spanned!(group.ident.span()=> #sub_reader_name));
}
sub_readers.push(Some(generate_structs(
&sub_reader_name,
&group.contents,
group.reader.all_or_nothing,
)?));
tys.push(parse_quote_spanned!(group.ident.span()=> #sub_reader_name));
sub_readers.push(Some(generate_structs(&sub_reader_name, &group.contents)?));
attrs.push(group.attrs.to_vec());
}
}
Expand Down Expand Up @@ -242,11 +222,8 @@ fn generate_conversions(
let mut parents = parents.clone();
parents.push(group.ident.clone());
field_conversions.push(quote!(#name: #sub_reader_name::from_dto(dto, location)));
let sub_conversions = if group.reader.all_or_nothing {
generate_all_or_nothing_struct(&sub_reader_name, &group.contents, parents)
} else {
generate_conversions(&sub_reader_name, &group.contents, parents, root_fields)
}?;
let sub_conversions =
generate_conversions(&sub_reader_name, &group.contents, parents, root_fields)?;
rest.push(sub_conversions);
}
}
Expand All @@ -266,79 +243,3 @@ fn generate_conversions(
#(#rest)*
})
}

fn generate_all_or_nothing_struct(
name: &proc_macro2::Ident,
items: &[FieldOrGroup],
parents: Vec<syn::Ident>,
) -> syn::Result<TokenStream> {
let fields = items
.iter()
.filter_map(|field| match field {
FieldOrGroup::Field(f) => Some(f),
_ => None,
})
.collect::<Vec<_>>();

ensure_defaults_not_set_for(&fields)?;

let names = fields.iter().map(|f| f.ident()).collect::<Vec<_>>();
let parentss = std::iter::repeat(&parents)
.take(names.len())
.collect::<Vec<_>>();
let nones = std::iter::repeat(quote!(None)).take(names.len());
// TODO this should ideally cope with the fields being renamed
let field = parents
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(".");
let field_strings = names.iter().map(|name| format!("{field}.{name}"));
let error = format!("Missing some fields in {field}. ");

Ok(quote! {
impl #name {
#[automatically_derived]
pub fn from_dto(dto: &TEdgeConfigDto, location: &TEdgeConfigLocation) -> OptionalConfigGroup<Self> {
match (#(dto.#(#parentss).*.#names),*) {
(#(Some(#names)),*) => OptionalConfigGroup::Present(Self {
#(#names,)*
}),
(#(#nones),*) => OptionalConfigGroup::Empty(#field),
(#(#names),*) => {
let mut error = #error.to_owned();
let mut filled_fields = vec![];
let mut vacant_fields = vec![];
#(
match #names {
Some(_) => filled_fields.push(#field_strings),
None => vacant_fields.push(#field_strings),
};
)*
error.push_str(&format!("Either set {:?}, or unset {:?}", vacant_fields, filled_fields));
OptionalConfigGroup::Partial(error)
}
}
}
}
})
}

fn ensure_defaults_not_set_for(fields: &Vec<&ConfigurableField>) -> Result<(), syn::Error> {
let mut error = OptionalError::default();
for field in fields {
match field.read_write().map(|f| &f.default) {
Some(FieldDefault::None) => (),
Some(d) => error.combine(syn::Error::new(
// Safe to unwrap because default is not FieldDefault::None
d.possible_span().unwrap(),
"sub-fields of all or nothing groups are not allowed to have defaults",
)),
None => error.combine(syn::Error::new(
field.ident().span(),
"read-only values are not allowed as all or nothing sub-fields",
)),
}
}
error.try_throw()
}
87 changes: 87 additions & 0 deletions crates/common/tedge_config_macros/src/all_or_nothing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use crate::OptionalConfig;

pub fn all_or_nothing<T, U>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding correctly, the function takes only two arguments, what would it look like if we wanted to use it with 3 or 4 arguments? Do we duplicate the function however many times we need or is there a way to solve this more generically, which we'll use when we need it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment I'm trying to keep this bit simple as this is an area where we have a lot of scope for solving problems we don't really have. I will refactor this to a single tuple argument via a trait so we at least don't need to create a different all_or_nothing function for each cardinality, but for now, I'm going to avoid coming up with any particularly clever solutions to generalising this, as I'm not convinced we really have a use case.

t: OptionalConfig<T>,
u: OptionalConfig<U>,
) -> Result<Option<(T, U)>, String> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely simpler and better to have this defined as a regular function instead of a macro.

Beyond this specific case, I see this simplification as a better separation of the responsibilities. Adding constraints on what makes sense should be done at the application level while the config should be focus on a more syntactic level (what is set, what is missing, what is ill-formed).

use OptionalConfig::*;

match (t, u) {
(Present { value: t, .. }, Present { value: u, .. }) => Ok(Some((t, u))),
(Empty(..), Empty(..)) => Ok(None),
(t, u) => {
let all_settings = [t.key(), u.key()];
let present = [t.key_if_present(), u.key_if_present()]
.into_iter()
.filter_map(|id| id)
.collect::<Vec<_>>();
let missing = [t.key_if_empty(), u.key_if_empty()]
.into_iter()
.filter_map(|id| id)
.collect::<Vec<_>>();
Err(format!(
"The thin-edge configuration is invalid. The settings {all_settings:?} must either all be configured, or all unset. Currently {present:?} are set, and {missing:?} are unset."))
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn all_or_nothing_returns_some_when_both_values_are_configured() {
assert_eq!(
all_or_nothing(
OptionalConfig::Present {
value: "first",
key: "test.key"
},
OptionalConfig::Present {
value: "second",
key: "test.key2"
}
),
Ok(Some(("first", "second")))
)
}

#[test]
fn all_or_nothing_returns_none_when_both_values_when_neither_value_is_configured() {
assert_eq!(
all_or_nothing::<String, String>(
OptionalConfig::Empty("first.key"),
OptionalConfig::Empty("second.key"),
),
Ok(None)
)
}

#[test]
fn all_or_nothing_returns_an_error_if_only_the_first_value_is_configured() {
assert!(matches!(
all_or_nothing::<_, String>(
OptionalConfig::Present {
value: "test",
key: "first.key"
},
OptionalConfig::Empty("second.key"),
),
Err(_)
))
}

#[test]
fn all_or_nothing_returns_an_error_if_only_the_second_value_is_configured() {
assert!(matches!(
all_or_nothing::<String, _>(
OptionalConfig::Empty("first.key"),
OptionalConfig::Present {
value: "test",
key: "second.key"
},
),
Err(_)
))
}
}
1 change: 1 addition & 0 deletions crates/common/tedge_config_macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#[doc(inline)]
pub use tedge_config_macros_macro::define_tedge_config;

pub use all_or_nothing::*;
pub use connect_url::*;
pub use default::*;
pub use doku_aliases::*;
Expand Down
31 changes: 20 additions & 11 deletions crates/common/tedge_config_macros/src/option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ impl<T> From<OptionalConfig<T>> for Option<T> {
}
}

pub enum OptionalConfigGroup<T> {
Present(T),
Empty(&'static str),
Partial(String),
}

#[derive(thiserror::Error, Debug)]
#[error(
r#"A value for '{key}' is missing.\n\
Expand All @@ -53,15 +47,30 @@ impl<T> OptionalConfig<T> {
Self::Empty(key) => Err(ConfigNotSet { key }),
}
}
}

impl<T: doku::Document> doku::Document for OptionalConfig<T> {
fn ty() -> doku::Type {
Option::<T>::ty()
pub fn key(&self) -> &'static str {
match self {
Self::Present { key, .. } => key,
Self::Empty(key) => key,
}
}

pub fn key_if_present(&self) -> Option<&'static str> {
match self {
Self::Present { key, .. } => Some(key),
Self::Empty(..) => None,
}
}

pub fn key_if_empty(&self) -> Option<&'static str> {
match self {
Self::Empty(key) => Some(key),
Self::Present { .. } => None,
}
}
}

impl<T: doku::Document> doku::Document for OptionalConfigGroup<T> {
impl<T: doku::Document> doku::Document for OptionalConfig<T> {
fn ty() -> doku::Type {
Option::<T>::ty()
}
Expand Down