-
Notifications
You must be signed in to change notification settings - Fork 3
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
derive macro for complex enums #166
Comments
I started an implementation of general enum support, but I think we might need to discuss slightly how it should work :p Basic enum keysFor instance, a simple unit test of the node iteration: #[derive(PartialEq, Debug, Clone, Default, Tree)]
struct Inner {
data: u32,
}
#[derive(Debug, Clone, Tree)]
enum Either {
A
B(Inner)
}
#[derive(Debug, Clone, Tree)]
enum EitherDeep {
A
#[tree(depth = 1)
B(Inner)
}
#[test]
fn iterate_keys() {
let mut expected_keys = HashSet::from(["/A", "/B"]);
for node in Either::nodes::<Path<String, '/'>>() {
let (path, node) = node.unwrap();
assert!(node.is_leaf());
assert!(expected_keys.remove(path.as_str()));
}
assert!(expected_keys.is_empty());
}
#[test]
fn iterate_keys_deep() {
let mut expected_keys = HashSet::from(["/A", "/B/data"]);
for node in EitherDeep::nodes::<Path<String, '/'>>() {
let (path, node) = node.unwrap();
assert!(node.is_leaf());
assert!(expected_keys.remove(path.as_str()));
}
assert!(expected_keys.is_empty());
} Intuitively, I would expect both of these tests to pass. But what happens when we want to support all forms of enums (tree attr removed for simplicity): #[derive(Debug, Clone, Tree)]
enum EitherDeep {
A,
B(Inner),
C(Inner, Inner),
D { value: usize }
} Maybe the more correct way would be to have this result in keys: That way it should also be super simple to reuse the same code for supporting tuples, in the same manner as arrays? The issue is, that now enums of the format It would also be possible to go with something in between, where single field variants ( Obviously, another option is to only allow a single unnamed field in the Other issues or thoughtsAre there any other stuff I need to know about, or have not though about, before I venture down the rabbit hole of implementing this? |
Thanks for the interest, Mathias! From previous discussions and existing behavior of #[derive(Serialize, Deserialize)]
enum En0 {
Uni,
Tup(f32),
Var { field: f32 },
Ar(
[f32; 1],
),
}
#[derive(Tree)]
enum En1 {
Uni,
Tup(f32),
Var { field: f32 },
Ar(
[f32; 1],
),
}
#[derive(Tree)]
enum En2 {
Uni,
Tup(f32),
Var { field: f32 },
Ar(
[f32; 1],
),
}
#[derive(Tree)]
enum En3 {
Uni,
Tup(f32),
Var { field: f32 },
Ar(
#[tree(depth=1)]
[f32; 1],
),
}
#[derive(Tree)]
struct Stru {
// atomic serialize/deserialize, no Tree API usage, ability to switch variant through Miniconf
at: En0,
// one tree API level, one path hierarchy level
// atomic usage of variants,
// inability to switch variant through Miniconf, only through other means
#[tree(depth=1)]
depth1: En1,
#[tree(depth=2)]
depth2: En2,
#[tree(depth=3)]
depth3: En3,
}
assert_eq!(
Stru::nodes::<Path<String, '/'>>().map(Result::unwrap).collect::<Vec<_>>(),
[
"/at",
"/depth1/Uni", "/depth1/Tup", "/depth1/Var", "/depth1/Ar",
"/depth2/Uni", "/depth2/Tup/0", "/depth2/Var/field", "/depth2/Ar/0",
"/depth3/Uni", "/depth3/Tup/0", "/depth3/Var/field", "/depth3/Ar/0/0",
]
);
// Paths in/on variants that are not present are `Traversal::Absent` |
Maybe we can implement the |
Switching between variants can later be implemented via some "tagging" field outside (adjacent/external/internal like serde) that then implements the switch in its handler. |
Yeah, I thought about these as well, but as I see it, anything but the default externally tagged, would result in multiple keys in order to get one field in the enum, which I think would be difficult in the current design? |
The summary that @jordens has above for the examples makes the most sense with what already exists in
I'm not certain it's actually possible with Miniconf to guarantee that variants could be swapped. I think this is only possible if there's a single leaf within the enum variant that you want to swap to. I think to do this we would have to allow mutable access to the root of a complex enum. For example: enum Foo {
Bar,
#[tree(depth = 1)]
Qux{value: f32, other: f32},
}
// Has paths ["/Bar", "/Qux/value", "/Qux/other"]
#[derive(Tree, Default)]
struct Settings {
#[tree(depth = 1)]
foo: Foo,
}
let mut settings = Settings::default();
settings.foo = Foo::Bar;
// Read the current value of `foo`.
// Note: This is pseudo-code.
assert!(settings.get_json("/foo/Bar") == "null"); // What's the serialized unit type? Might be "" or "()"
assert!(settings.get_json("/foo/Qux/value").is_err());
// We now want to switch `settings.foo` to `Foo::Qux`. This is a bit awkward because `/foo` isn't actually a valid path
// because its an internal node.
settings.set_json("/foo", {"type": "Qux", "value": "10", "other": 9});
assert(settings.get_json("/foo/Qux/value") == 10);
assert!(settings.get_json("/foo/Bar").is_err()); I think both adjacent and external tagging would work fine, but we may need to make some modifications to |
My use-case might be slightly off from miniconf's intended behaviour, but I am mostly interested in using miniconf to turn my json tree into individually flattened key-value pairs of all leaf values individually (with few exceptions around arrays) I thought about this as well, and came to the conclusion that I would be okay with complex enums leaving "unused" keys in my key-value storage, as long as I know that they will be hit again next time the enum variant changes, such that there is a strict upper limit on this defined by the enum tree. |
I would think this would work like serialized json, where unit types act as a string serialization on the parent key?: Such that (using your snippet) |
This seems reasonable to me, since there's no other value represented. So for the simple unit enum case, we treat them how we're already treating enums (i.e. they become a terminal leaf node immediately). @jordens Does that sound appropriate to you? That would modify the above as follows: assert_eq!(
Stru::nodes::<Path<String, '/'>>().map(Result::unwrap).collect::<Vec<_>>(),
[
"/at",
- "/depth1/Uni", "/depth1/Tup", "/depth1/Var", "/depth/1/Ar",
- "/depth2/Uni", "/depth2/Tup/0", "/depth2/Var/field", "/depth2/Ar/0",
- "/depth3/Uni", "/depth3/Tup/0", "/depth3/Var/field", "/depth3/Ar/0/0",
+ "/depth1", "/depth1/Tup", "/depth1/Var", "/depth/1/Ar",
+ "/depth2", "/depth2/Tup/0", "/depth2/Var/field", "/depth2/Ar/0",
+ "/depth3", "/depth3/Tup/0", "/depth3/Var/field", "/depth3/Ar/0/0",
]
); Hmm but doesn't this introduce complexities, where |
@MathiasKoch I unintentionally editted your comment above, adding my reply here and reverting it shortly:
The goal of miniconf is simply to turn complex data structures into key-value pairs, so it sounds like you want the exact thing that we're trying to provide :)
I'm not sure I'm completely following what you mean here. Do you have a small example? In general, when you iterate over the keys of the tree, you will get a |
It's just occurred to me that there may be complications in loading complex enums that expose their members as subfields. When you're loading the values from some external key-value storage, you'd get something like ("/foo/Qux/inner", and "/foo/Qux/other"). How would |
I can try. Building on your example: enum Foo {
Bar,
#[tree(depth = 1)]
Qux{value: f32, other: f32},
}
// Using above notion on unit variants, this has paths ["/", "/Qux/value", "/Qux/other"]
#[derive(Tree, Default)]
struct Settings {
#[tree(depth = 1)]
foo: Foo,
}
let mut settings = Settings::default();
settings.foo = Foo::Bar;
assert!(settings.get_json("/foo") == "Bar");
assert!(settings.get_json("/foo/Qux/value").is_err());
// I would now persist these to e.g. `sequential-storage`, which persists following key-value pairs:
// [("/foo", "Bar")]
// We now want to switch `settings.foo` to `Foo::Qux`.
settings.set_json("/foo", {"type": "Qux", "value": "10", "other": 9}); // Not sure about how this should be handled, but pseudo-code for now
assert(settings.get_json("/foo/Qux/value") == 10);
assert!(settings.get_json("/foo/Bar").is_err());
// I would now persist these which persists following key-value pairs:
// [("/foo/Qux/value", 10), ("/foo/Qux/other", 9)]
// But i still have the key from `("/foo", "Bar")` which is kinda stranded until we update the config back |
But wouldn't this be exactly the same in struct cases (existing implementation)? |
Your code example makes sense, so you'd have the following key-values in storage [("/foo", "Bar"), ("/Qux/inner", "9"), ("/Qux/other", "10")]. There's really two possibilities:
(2) doesn't quite seem possible, so simply ignoring the errors seems the most normal case. We actually do this with
In the struct case, the tree already exists in memory when calling |
Alright, so to sum up so far, we are down to a few bigger (uncovered) issues here?
|
I believe this gets resolved based on the current value of the enum. If the value of the enum during iteration is the unit type, the node is a leaf. If the enum variant is non-unit, then the node will not be a leaf. I don't think this is actually a problem.
I would defer this to a future task for now. Is this something that you actively need? In any case, I would implement it separately from initial support for enums.
For now, I would say that the enum currently in the structure would be the only valid one. I.e. if the structure has the |
Makes sense overall. The only one I am seeing a slight issue with is the last one. Eg. device starts from clean state (reboot), loads the following key-values in storage [("/foo", "Bar"), ("/foo/Qux/inner", "9"), ("/foo/Qux/other", "10")]. (last persisted keys are Device is now unable to reconstruct the last persisted state? EDIT: |
You could also handle this outside of miniconf by detecting the |
I think the point is, that given above example, I don't see how it would ever return Absent? |
I would expect the behavior to be as follows. I.e. when attempting to set #[derive(tree)]
enum Foo {
Bar,
#[tree(depth = 1)]
Qux{value: f32, other: f32},
}
#[derive(Tree, Default)]
struct Settings {
#[tree(depth = 1)]
foo: Foo,
}
let mut settings = Settings::default();
settings.foo = Foo::Bar;
assert_eq!(settings.set_json("/foo/Qux/value", b"1"), Err(Error::Absent)) |
Thanks for the discussion! I think we can draw a couple conclusions. Let me know what you think.
Good point! For
Or you'd use
Both would work. Certainly not all of these features need to land at the same time. Let's boil it down to a MVP that doesn't preclude later completion. Maybe we want to kick |
This runs a bit counter to the thoughts above, where |
|
I don't understand why though. Could it not be the case where it is a leaf node when |
Ah that's right, I forgot that nodes are statically defined because all paths are present during iteration even if they're not run-time available. That kind of makes complex enums a little weird in comparison to normal enums though, but I guess this makes sense given the I guess that makes a unit enum variant somewhat similar to |
#[tree(flatten)] // to kill the "/Some"
enum Option<T> {
#[tree(skip)] None,
#[tree(flatten)] Some(T), // to kill the "/0"
}
|
The entire |
I've added derive support for a MVP in #233 |
would only change their variant at runtime outside
Miniconf
, i.e. be like option, untagged enumThey would implement exactly one depth: the max of the inner types.
The text was updated successfully, but these errors were encountered: