Oh whoops; missed your other thread.
For the visitor:
I think you probably want a TypePath
visitor that you feed the ty
field in the Field
s on your ItemStruct
.
I'm still a little spooked at all the different variants on syn::Type
but ultimately all roads do seem to lead back to TypePath
. Filtering out Path
s that aren't single path element + no path arguments and then comparing Ident
s with the list of generic Ident
s does seem like it'd work just fine.
Edit: I just re-read what you wrote and I think this is exactly what you were describing . Just in case though: I think you probably want to have a visitor that just overrides Visit::visit_field
that then shells out to another visitor that just overrides Visit::visit_type_path
; that way you can associate the type paths that are generic params that you discover with a particular field.
A few other things, though:
- a single field can reference multiple generic params so you'll want to collect a list of params per field
- as in, your input struct could look like this:
struct Foo<A, B> { f1: (A, B), f2: usize }
- for which you'd want to produce something like this:
impl<F2> FooBuilder<Unset, F2> { fn f1<A, B>(self, val: (A, B)) -> FooBuilder<Set<(A, B)>, F2> { ... } }
- as in, your input struct could look like this:
- if type parameters on your source struct have bounds you'll need to replicate these on your Builder's setters or at least on your Builder's build method (the thing that goes from
FooBuilder
toFoo
:- i.e. for:
struct Foo<A: Clone, B: Hash> { f1: A, f2: B }
- if you produced:
impl<A, B> FooBuilder<Set<A>, Set<B>> { fn build(self) -> Foo<A, B> { ... } }
- you'd get a type error since
A
andB
in the latter code block above aren't necessarilyClone
andHash
respectively - to get this to work you'd need to do this at the minimum:
impl<A: Clone, B: Hash> FooBuilder<Set<A>, Set<B>> { fn build(self) -> Foo<A, B> { ... } }
- and you'd probably want to copy the bounds to the individual setter functions too (raising the type error only when you call
build
makes it less immediately apparent to users what the problematic field/builder call was, I think)
- i.e. for:
- the above case isn't so bad since you can just literally copy the bounds to a couple of places but it actually gets worse (sorry)
- consider:
struct Foo<A: Clone, B: ?Sized + dyn FnOnce(A)> { a: A, b: Box<B> }
- here you have to copy
A
everywhereB
is used since the bound forB
referencesA
; this is especially problematic because it means that you can't setb
before settinga
(sinceA
isn't a type onFooBuilder
yet)
- here you have to copy
- and also:
struct Foo<'s, A: Clone + 's, B: ?Sized + Fn(&'s dyn FnOnce(&'s Box<A>))> { a: &'s A, b: Box<B> }
- here there's now a lifetime parameter that has to be copied around where it's used
- it's the same challenge as the previous example but with some indirection; you have to infer that
B
's bound involves's
and that's
is used ina
thereforeb
can't be set untila
is set - if you add multiple usages for parameters this gets worse; i.e.
struct Foo<'s, A: 's, B: Fn(&'s u8)> { a: &'s A, b: Box<B>, c: &'s u8 }
- here setting
c
or settinga
should be enough to allow settingb
but I don't think we can even represent this in the type system without specialization
- here setting
- and finally, const generics
- these can be used in other types (i.e. arrays like
[u8; N]
) or used nowhere else in the struct's fields' types at all; not even in aPhantomData
equivalent because the compiler doesn't need to infer variance for them - this means for types like:
struct Foo<A, B, const N: usize> { a: A, b: B }
- you'll have to know to copy
const N: usize
onto thebuild
method or to carry it along on theBuilder
type - for types where the const params are used in other types/bounds, you'll need to infer that too
- these can be used in other types (i.e. arrays like
- consider:
Apologies; that was definitely way longer than I intended. Anyways the point is that I think just carrying the type parameters on the Builder
type lets you sidestep having to handle lots of different edge cases.
That said, It's totally understandable if you ultimately decide that you don't want to support all the different things described above; while I don't think they're particularly esoteric bits of Rust code it's probably valid to decide that it's unlikely someone will want an auto-generated builder for something with lifetime bounds in it.
One last thing though. I think this was what you were referring to re: limitations with putting the type parameters on the builder type.
I just wanted to note that it's totally possible to do what you were describing; i.e. pass around incomplete builders without having to concretely specify types for fields that haven't been set yet. For the example in the previous message:
fn half_set<P>() -> FooBuilder<P, Unset, Set<u16>> {
FooBuilder::new().field2(23)
}
fn main() {
let f: Foo<_> = half_set().field1("👋").build();
}
Not sure if that's good enough for your use case but I thought I'd mention it.
Regardless, I'd love to know what you ultimately choose to go with/how it turns out! I'm emotionally invested now .