genco icon indicating copy to clipboard operation
genco copied to clipboard

FormatInto object safety - or how can I make a collection of Token Streams?

Open rrichardson opened this issue 1 year ago • 6 comments

I am creating a custom HTTP handler class from a schema specification which has about 100 routes.

Because of reasons, my main dispatcher is basically a match from a usize to an function invocation: e.g.


match index {
     0 => {  
         let input = FromRequest::from_request(req).await;
         let res = handler_0(input).await; 
         IntoResponse::into_response(res).await
      }, 
      1  =>   {  
         let input = FromRequest::from_request(req).await;
         let res = handler_0(input).await; 
         IntoResponse::into_response(res).await
      }, 
      N => {
          ... 
      }
}

I want to create a Vec<Box<dyn FormatInto<Rust>> of handlers and then iterate through them and create each match block.
However, I can't, because format_into takes self

It would be nice if there was an alternative to format_into that didn't consume self. Or maybe there is a better overall approach?

What is the recommended way to create a vector of token streams that I can inject into here?

rrichardson avatar Sep 14 '22 22:09 rrichardson

Interesting.

Have you tried to write your code so that you use a Vec<impl FormatInto> instead? It would require uniform types, but this can be accomplished by using say enums and matching over them.

If you want to use it multiple times you could also do: Vec<impl FormatInto + Copy>. I usually find it to be easy enough to have it be Copy since it just requires that you only use references in the corresponding quote_fn!. Or even Clone.

I appreciate you reporting on your experience and please continue doing so! This might feed into future choices on API design. I can definitely see how support for trait objects might be legitimately useful.

udoprog avatar Sep 15 '22 01:09 udoprog

Actually looking over the docs of FromFn it seems to be missing a forwarding Clone and Copy impl. I was sure it had one but I'm clearly misremembering. That makes the pattern I described a bit harder to implement since it can only be used for types for which you impl FormatInto manually.

I'd suggest adding those impls ASAP. I'll look closer into this tomorrow.

udoprog avatar Sep 15 '22 02:09 udoprog

Also of you have the time: posting a representative example of how you are trying to use genco would also be helpful. I might be able to suggest a different way to write it.

Looking at this repo and how it uses genco might also be helpful!

udoprog avatar Sep 15 '22 02:09 udoprog

Adding the + Clone constraint to the trait object was the first thing that I tried. However, I think it'd need to be + Sized as well, otherwise the compiler wouldn't know how big to make the self to move out of. Fundamentally, I'm actually not sure it would be possible. I think making a FormatAs that works like FormatInto, but copies the internal bits instead of moving them, might be the most flexible approach.

I was able to get my code working by using a for loop calling quote_in in succession on a mut Token<Rust>. It works, but it doesn't have the beautiful, functional flavor that my first attempt had. :)

Overall my experience with genco has been great. No surprises, it is well documented and does what it says.

rrichardson avatar Sep 15 '22 05:09 rrichardson

Cheers!

I was able to get my code working by using a for loop calling quote_in in succession on a mut Token. It works, but it doesn't have the beautiful, functional flavor that my first attempt had. :)

So maybe this is helpful. The typical way I'd do that pattern is instead of doing something like this:

let mut out = Tokens::new();

for thing in things {
    match thing {
        Thing::First => quote_in!(out => /* do first*/),
        Thing::Second => quote_in!(out => /* do second */),
    }
}

I'd do:

let mut out = Tokens::new();

quote_in! { out =>
    $(for thing in things join ($['\n']) => match thing {
        Thing::First => /* do first */,
        Thing::Second => /* do second */,
    })
}

And if one of them was a bit more involved, I'd break it out into a function:

fn do_third() -> impl FormatInto<Rust> {
    quote_fn! {
        /* do third */
    }
}

quote_in! { out =>
    $(for thing in things => match thing {
        Thing::First => /* do first */,
        Thing::Second => /* do second */,
        Thing::Third => $(do_third()),
    })
}

Note that due to the design of the FormatInto trait it doesn't require additional allocations to do so, since it will interact with the same Tokens collection.

I hope this helps!

udoprog avatar Sep 15 '22 11:09 udoprog

I don't know what the match cases are at compile time, only after I parse them out of the schema. So I have this:

       let mut fn_matches = rust::Tokens::new();
       for (idx, op) in ops.iter().enumerate() { 
           let fun = to_snake_case(op.name.as_str());
           let input = &op.input; 
           if input == "()" {
               quote_in!{ fn_matches =>
                   $idx => { 
                       $fun(bucket, key, req)
                           .and_then($into_resp::into_response).await 
                   },$['\n']
               };
           }else {
               quote_in!{ fn_matches =>
                   $idx => { 
                       $shapes::$input::from_request(req)
                           .and_then(|req| $fun(bucket, key, req))
                           .and_then($into_resp::into_response).await 
                   },$['\n']
               }; 
           }
       }
       ```

rrichardson avatar Sep 15 '22 14:09 rrichardson