Help writing a library with the type state pattern

Rust's type state pattern is making me sad. It sounds like the solution to my problem, but every time I try to implement it I get some new wall to run into.
I'm trying to write a library for interacting with this fairly old API I use in my job. It has some common parameters, but most of the parameters depend on the function you want to call. So I was really trying to do something where you'd use it like:

let call = myapi::new().function(function1).param1(whatever)... and so on

Type state seemed like a good fit here, adding training wheels to the library so you can only call parameters that make sense. I had imagined it something like this:

//ton of unit structs representing the old api functions
struct func1 {}
struct func2 {}

//a trait to mark them all as valid
trait validfunc {}

//all the impl blocks
impl validfunc for func1 {}
impl validfunc for func2 {}

//the actual struct for my data
struct myapi<S: validfunc> {
    marker: PhantomData<S>,
    param1: Option<String>,
    param2: Option<u32>,

Something like that, then I could do impl blocks depending on what state it was in:

impl myapi<func1> {
    fn param1(input) {
        yada yada

impl myapi<func2> {
    fn param1(input) {
        yada yada
    fn param2(input) {
        yada yada again

Finally, this old API I'm interacting with allows you to use create your own custom calls, so one goal of my library was that a person could write a new unit struct and create their own impl blocks easily. I was even considering writing proc macros to generate the common parameters to make it easy to add them.

Clearly this doesn't work since I'm here asking. But I'm hoping someone can see what I was trying to accomplish and offer a different approach, because I'm sorta stuck.

Some other things that might matter.

  1. This old API has almost 200 functions, so there's quite a lot of variety in how it can be called.
  2. Once I create the call and send it, I get a stream of bytes back. How I decode that depends on what function was called, so I need to carry that state over to my response.
  3. If my library doesn't allow for custom calls, it's not worth it. There's other utilities out there for the standard set of functions, I need the ability for a user to add their own without it being too huge an effort.

Anyone have any ideas?

Don't use typestate for this. Typestate is useful for ensuring that functions are called in a particular order. For your use-case (product of optional parameters restricted to a subset), the naïve implementation will cause a combinatorial explosion of types (and I couldn't be bothered to spend time fixing a solution that's ill-suited to the problem anyway).

If you want to wrap an HTTP API, at a minimum, define a Request trait with an associated Response type (perhaps itself bounded by a Response trait), then make serialization and deserialization part of the request and response types' impls of the respective trait. Then define separate, concrete request types for every endpoint, and make a single, central, generic function for calling the API.

This will make your library trivially extensible as well.

Here's a previous, more detailed answer of mine, with a link to a concrete crate in which I employ the design laid out above.

1 Like

Thanks, I'm glad I asked here instead of continuing to beat my head against a idea that wasn't working. I don't immediately understand what you're saying in that post, but I'll look at your crate for a while until it clicks.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.