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

refactor: add support to define_tedge_config for (optionally) multi-value fields #3126

Merged
merged 32 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a9aad21
Start PoC for named c8ys in tedge config
jarhodes314 Sep 18, 2024
1517688
Generate enums that mostly work with multi keys
jarhodes314 Sep 19, 2024
5e50c0e
Fix the remaining new compiler errors
jarhodes314 Sep 19, 2024
0efaa46
Replace HashMap with Multi enum, to allow unnamed c8ys to persist
jarhodes314 Sep 19, 2024
d3ea36e
Add another .get call to fix a compiler error message
jarhodes314 Sep 20, 2024
7eb7515
Make multi example compile
jarhodes314 Sep 20, 2024
5dcdfaf
Update tedge config API usage so the existing tedge config works
jarhodes314 Sep 20, 2024
a879f50
Run formatter and fix some remaining errors
jarhodes314 Sep 20, 2024
56525cc
Add a proper example for multi c8y config
jarhodes314 Sep 20, 2024
0baeb4c
Refactor some of the ID generation logic
jarhodes314 Sep 20, 2024
10ccd17
Refactor reader ID generation
jarhodes314 Sep 20, 2024
7d4745e
Rename `get` to `try_get`
jarhodes314 Sep 20, 2024
b962d72
Fix clippy and test errors
jarhodes314 Sep 20, 2024
66ea8c7
Update `FromStr` implementation to cope with multi-value fields
jarhodes314 Sep 23, 2024
e3ee82a
Add support for iterating over readable keys
jarhodes314 Sep 24, 2024
8666582
Add support for stringifying multi-value fields correctly
jarhodes314 Sep 25, 2024
9c816da
Tidy up code following the new multi-value logic
jarhodes314 Sep 25, 2024
615d0f8
Remove dead code
jarhodes314 Sep 25, 2024
d923fc3
Tidy up most of the remaining TODOs
jarhodes314 Sep 25, 2024
c786ae2
Run formatter
jarhodes314 Sep 25, 2024
3860292
Fix doc test
jarhodes314 Sep 26, 2024
fdc4751
Fix imports in docs
jarhodes314 Sep 26, 2024
d508c90
Run formatter
jarhodes314 Sep 26, 2024
edc2d61
Rename map -> map_keys
jarhodes314 Sep 30, 2024
acdd944
Improve error messages for multi fields
jarhodes314 Oct 1, 2024
582f1f6
Use named fields inside the multi
jarhodes314 Oct 1, 2024
63fee73
Allow writing to novel tedge config multi keys
jarhodes314 Oct 1, 2024
8bbf918
Update crates/common/tedge_config_macros/src/multi.rs
jarhodes314 Oct 2, 2024
64ddb22
Fix tests and add more tests around multi
jarhodes314 Oct 2, 2024
85b30f8
Fix issues with normal groups inside multi-value groups
jarhodes314 Oct 2, 2024
87da9c3
Refactoring
jarhodes314 Oct 2, 2024
3936b7d
Support converting between single and multi fields
jarhodes314 Oct 3, 2024
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
23 changes: 19 additions & 4 deletions crates/common/tedge_config_macros/examples/multi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,19 @@ impl<T> AppendRemoveItem for T {
define_tedge_config! {
#[tedge_config(multi)]
c8y: {
#[tedge_config(example = "your-tenant.cumulocity.com")]
#[tedge_config(reader(private))]
url: String,

#[tedge_config(default(from_optional_key = "c8y.url"))]
http: String,

smartrest: {
/// Switch using 501-503 (without operation ID) or 504-506 (with operation ID) SmartREST messages for operation status update
#[tedge_config(example = "true", default(value = true))]
use_operation_id: bool,
},

#[tedge_config(multi)]
something: {
test: String,
Expand Down Expand Up @@ -76,19 +87,19 @@ fn main() {
.try_get(Some("cloud"))
.unwrap_err()
.to_string(),
"You are trying to access a named field (cloud) of c8y, but the fields are not named"
"You are trying to access a profile `cloud` of c8y, but profiles are not enabled for c8y"
);
assert_eq!(
multi_c8y_reader
.c8y
.try_get(Some("unknown"))
.unwrap_err()
.to_string(),
"Key c8y.unknown not found in multi-value group"
"Unknown profile `unknown` for the multi-profile property c8y"
);
assert_eq!(
multi_c8y_reader.c8y.try_get(None).unwrap_err().to_string(),
"You need a name for the field c8y"
"A profile is required for the multi-profile property c8y"
);

assert_eq!(
Expand Down Expand Up @@ -134,10 +145,14 @@ fn main() {
assert_eq!(
keys,
[
"c8y.cloud.http",
"c8y.cloud.smartrest.use_operation_id",
"c8y.cloud.something.test",
"c8y.cloud.url",
"c8y.edge.http",
"c8y.edge.smartrest.use_operation_id",
"c8y.edge.something.test",
"c8y.edge.url",
"c8y.edge.url"
]
);
}
4 changes: 2 additions & 2 deletions crates/common/tedge_config_macros/impl/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ fn key_iterators(
));
let ident = &g.ident;
exprs.push_back(parse_quote! {
self.#ident.#function_name(#(#args),*)
self.#ident.#function_name(#(#args.clone()),*)
});
}
Some(FieldOrGroup::Field(f)) => {
Expand Down Expand Up @@ -1159,7 +1159,7 @@ mod tests {

impl TEdgeConfigReaderC8y {
pub fn readable_keys(&self, c8y: Option<String>) -> impl Iterator<Item = ReadableKey> + '_ {
self.nested.readable_keys(c8y)
self.nested.readable_keys(c8y.clone())
}
}

Expand Down
238 changes: 221 additions & 17 deletions crates/common/tedge_config_macros/impl/src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,16 +289,22 @@ fn reader_value_for_field<'a>(
parse_quote!(#key_str.into())
} else {
let mut id_gen = SequentialIdGenerator::default();
let elems = parents.iter().map::<syn::Expr, _>(|p| match p {
PathItem::Static(p) => {
let p_str = p.to_string();
parse_quote!(Some(#p_str))
}
PathItem::Dynamic(span) => {
let ident = id_gen.next_id(*span);
parse_quote!(#ident)
}
});
let elems = parents
.iter()
.map::<syn::Expr, _>(|p| match p {
PathItem::Static(p) => {
let p_str = p.to_string();
parse_quote!(Some(#p_str))
}
PathItem::Dynamic(span) => {
let ident = id_gen.next_id(*span);
parse_quote!(#ident)
}
})
.chain({
let name = name.to_string();
iter::once(parse_quote!(Some(#name)))
});
parse_quote!([#(#elems),*].into_iter().filter_map(|id| id).collect::<Vec<_>>().join(".").into())
};
let read_path = read_field(parents);
Expand Down Expand Up @@ -334,14 +340,50 @@ fn reader_value_for_field<'a>(
}
FieldDefault::FromKey(default_key) | FieldDefault::FromOptionalKey(default_key) => {
observed_keys.push(default_key);
// TODO error if the field touches an unrelated multi?
jarhodes314 marked this conversation as resolved.
Show resolved Hide resolved
// Trace through the key to make sure we pick up dynamic paths as we expect
let mut parents = parents.iter().peekable();
let mut fields = root_fields;
let mut new_parents = vec![];
for (i, field) in default_key.iter().take(default_key.len() - 1).enumerate() {
new_parents.push(PathItem::Static(field.to_owned()));
if let Some(PathItem::Static(s)) = parents.next() {
if field == s {
if let Some(PathItem::Dynamic(span)) = parents.peek() {
parents.next();
new_parents.push(PathItem::Dynamic(*span));
}
} else {
while parents.next().is_some() {}
}
}
let root_field = fields.iter().find(|fog| fog.ident() == field).unwrap();
match root_field {
FieldOrGroup::Multi(m) => {
if matches!(new_parents.last().unwrap(), PathItem::Dynamic(_)) {
fields = &m.contents;
} else {
let multi_key = default_key
.iter()
.take(i + 1)
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(".");
return Err(syn::Error::new(
default_key.span(),
format!("{multi_key} is a multi-value field"),
));
}
}
FieldOrGroup::Group(g) => {
fields = &g.contents;
}
FieldOrGroup::Field(_) => {}
}
}
let default = reader_value_for_field(
find_field(root_fields, default_key)?,
&default_key
.iter()
.take(default_key.len() - 1)
.map(<_>::to_owned)
.map(PathItem::Static)
.collect::<Vec<_>>(),
&new_parents,
root_fields,
observed_keys,
)?;
Expand Down Expand Up @@ -432,7 +474,17 @@ fn generate_conversions(

let mut parents = parents.clone();
parents.push(PathItem::Static(group.ident.clone()));
field_conversions.push(quote!(#name: #sub_reader_name::from_dto(dto, location)));
let mut id_gen = SequentialIdGenerator::default();
let extra_call_args: Vec<_> = parents
.iter()
.filter_map(|path_item| match path_item {
PathItem::Static(_) => None,
PathItem::Dynamic(span) => Some(id_gen.next_id(*span)),
})
.collect();
field_conversions.push(
quote!(#name: #sub_reader_name::from_dto(dto, location, #(#extra_call_args),*)),
);
let sub_conversions =
generate_conversions(&sub_reader_name, &group.contents, parents, root_fields)?;
rest.push(sub_conversions);
Expand Down Expand Up @@ -491,3 +543,155 @@ fn generate_conversions(
#(#rest)*
})
}

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

#[test]
fn from_optional_key_reuses_multi_fields() {
let input: crate::input::Configuration = parse_quote!(
#[tedge_config(multi)]
c8y: {
url: String,

#[tedge_config(default(from_optional_key = "c8y.url"))]
http: String,
}
);
let FieldOrGroup::Multi(m) = &input.groups[0] else {
unreachable!()
};
let http = m.contents[1].field().unwrap();
let actual = reader_value_for_field(
http,
&[
PathItem::Static(parse_quote!(c8y)),
PathItem::Dynamic(Span::call_site()),
],
&input.groups,
vec![],
)
.unwrap();
let actual: syn::File = parse_quote!(fn dummy() { #actual });
let c8y_http_key = quote! {
[Some("c8y"), key0, Some("http")]
.into_iter()
.filter_map(|id| id)
.collect::<Vec<_>>()
.join(".")
.into()
};
let c8y_url_key = quote! {
[Some("c8y"), key0, Some("url")]
.into_iter()
.filter_map(|id| id)
.collect::<Vec<_>>()
.join(".")
.into()
};
let expected: syn::Expr = parse_quote!(match &dto.c8y.try_get(key0, "c8y").unwrap().http {
Some(value) => {
OptionalConfig::Present {
value: value.clone(),
key: #c8y_http_key,
}
}
None => {
match &dto.c8y.try_get(key0, "c8y").unwrap().url {
None => OptionalConfig::Empty(#c8y_url_key),
Some(value) => OptionalConfig::Present {
value: value.clone(),
key: #c8y_url_key,
},
}
.map(|v| v.into())
}
});
let expected = parse_quote!(fn dummy() { #expected });
pretty_assertions::assert_eq!(
prettyplease::unparse(&actual),
prettyplease::unparse(&expected),
)
}

#[test]
fn from_optional_key_returns_error_with_invalid_multi() {
let input: crate::input::Configuration = parse_quote!(
#[tedge_config(multi)]
c8y: {
url: String,
},
az: {
// We can't derive this from c8y.url, as we don't have a profile to select from c8y
#[tedge_config(default(from_optional_key = "c8y.url"))]
url: String,
}
);

let FieldOrGroup::Group(g) = &input.groups[1] else {
unreachable!()
};
let az_url = g.contents[0].field().unwrap();
let error = reader_value_for_field(
az_url,
&[PathItem::Static(parse_quote!(az))],
&input.groups,
vec![],
)
.unwrap_err();

assert_eq!(error.to_string(), "c8y is a multi-value field");
}

#[test]
fn generate_conversions_() {
let input: crate::input::Configuration = parse_quote!(
#[tedge_config(multi)]
c8y: {
smartrest: {
templates: TemplatesSet,
}
},
);
let actual = generate_conversions(
&parse_quote!(TEdgeConfigReader),
&input.groups,
Vec::new(),
&input.groups,
)
.unwrap();
let file: syn::File = syn::parse2(actual).unwrap();
let r#impl = file
.items
.into_iter()
.find_map(|item| match item {
syn::Item::Impl(i) if i.self_ty == parse_quote!(TEdgeConfigReaderC8y) => Some(i),
_ => None,
})
.unwrap();

let expected = parse_quote! {
impl TEdgeConfigReaderC8y {
#[allow(unused, clippy::clone_on_copy, clippy::useless_conversion)]
#[automatically_derived]
/// Converts the provided [TEdgeConfigDto] into a reader
pub fn from_dto(
dto: &TEdgeConfigDto,
location: &TEdgeConfigLocation,
key0: Option<&str>,
) -> Self {
Self {
smartrest: TEdgeConfigReaderC8ySmartrest::from_dto(dto, location, key0)
}
}
}
};

pretty_assertions::assert_eq!(
prettyplease::unparse(&parse_quote!(#r#impl)),
prettyplease::unparse(&expected)
)
}
}
24 changes: 21 additions & 3 deletions crates/common/tedge_config_macros/src/multi.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use itertools::Either;

#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum MultiDto<T> {
Expand Down Expand Up @@ -50,7 +52,7 @@ pub enum MultiError {
SingleNotMulti(String, String),
#[error("A profile is required for the multi-profile property {0}")]
MultiNotSingle(String),
#[error("Unknown profile {1} for the multi-profile property {0}")]
#[error("Unknown profile `{1}` for the multi-profile property {0}")]
MultiKeyNotFound(String, String),
}

Expand Down Expand Up @@ -109,6 +111,22 @@ impl<T> MultiReader<T> {
}
}
}

pub fn entries(&self) -> impl Iterator<Item = (Option<&str>, &T)> {
match self {
Self::Single { value, .. } => Either::Left(std::iter::once((None, value))),
Self::Multi { map, .. } => {
Either::Right(map.iter().map(|(k, v)| (Some(k.as_str()), v)))
}
}
}

pub fn values(&self) -> impl Iterator<Item = &T> {
match self {
Self::Single { value, .. } => Either::Left(std::iter::once(value)),
Self::Multi { map, .. } => Either::Right(map.iter().map(|(_, v)| v)),
}
}
}

impl<T> MultiDto<T> {
Expand Down Expand Up @@ -271,7 +289,7 @@ mod tests {

assert_eq!(
val.try_get(Some("unknown"), "c8y").unwrap_err().to_string(),
"Unknown profile unknown for the multi-profile property c8y"
"Unknown profile `unknown` for the multi-profile property c8y"
);
}

Expand All @@ -284,7 +302,7 @@ mod tests {

assert_eq!(
val.try_get(Some("unknown")).unwrap_err().to_string(),
"Unknown profile unknown for the multi-profile property c8y"
"Unknown profile `unknown` for the multi-profile property c8y"
);
}

Expand Down