[Solved] Joining together Either in futures

I'm using futures 0.1.14 and I have some code that parses an incoming request body that is multipart into tuples (String, String) consisting of a key and either the text of the field or the file path for the temporary file:

fn call(multi: Multipart<Body> /* stream of Fields */) -> Future<Item=Response, Error=hyper::Error> {
  let read_field = |field: Field<Body>| if field.is_text() {
    Either::A(field.data.read_text().map(|field| -> (String, String) {
      (field.headers.name.clone(), field.text)
    }))
  } else {
    Either::B(field.data.read_file().map(|field| -> (String, String) {
      (field.headers.name.clone(), field.filename)
    }))
  };

  multi.map(read_file) /* something here */ .then(|_| futures::future::ok(Response::new()))
}

I'm stuck figuring out how to merge the results of the Map of Either's into a Map or stream or something of (String, String). If I map the result of read file, it iterates over the futures of wrapper type Either, which doesn't seem to help me because I need to resolve those futures.

1 Like

Disclaimer, I am not sure I understand your issue but wouldn't this work:

multi.map(read_file)
.and_then(|(key, value)|  { /* ... */ })
.then(|_| ....)

Unfortunately no, because the value that comes into the and_then is the Either future, unresolved. The Eithers need to be resolved somehow first.

Either<A, B> is a Future only if A and B are futures; otherwise it’s an ordinary enum.

It sounds like your case ends up using it as just an enum. As such, you may as well remove Either and just return the tuple. Then your stream’s type will be the tuple and not the wrapper Either.

That doesn't work because the futures in each branch of the if/else (which handle things differently based on whether the Field is a text or file field) are different future types, which is why the Either is necessary to wrap them =/

So do field.data.read_text() and field.data.read_file() return futures? I originally didn't think so but I guess so based on what you're saying. In that case, you need to make sure your mapping function internally returns a future as well, so that Either<A, B> becomes a future. For instance:

let read_field = |field: Field<Body>| if field.is_text() {
    Either::A(field.data.read_text().map(|field| -> (String, String) {
      Ok((field.headers.name.clone(), field.text)).into_future() <== make this a FutureResult, which is a Future
    }))
  } else {
    Either::B(field.data.read_file().map(|field| -> (String, String) {
      Ok((field.headers.name.clone(), field.filename)).into_future() <== ditto here
    }))
  };

Ok I see what you're saying, but even when I make sure that I'm returning FutureResults, it doesn't seem that Either is taking on the Future trait, even though both branches are in fact futures. This is the type that comes out if I then on the Map of read_field:

std::result::Result<
  futures::future::Either<futures::Map<multipart::server::ReadTextField<multipart::server::FieldData<hyper::Body>, [closure@src/lib.rs:178:66: 181:30]>, futures::Then<futures::stream::Fold<multipart::server::FieldData<hyper::Body>, [closure@src/lib.rs:185:67: 188:30], futures::FutureResult<std::vec::Vec<u8>, hyper::Error>, std::vec::Vec<u8>>, futures::FutureResult<(std::string::String, std::string::String), _>, [closure@src/lib.rs:188:37: 190:30]>>, hyper::
Error>

This is what the full code for the if/else looks like now (read_file was an abstraction to make reading the code easier, this is how I'm actually doing it):

let read_field = |field: Field<Body>| if field.headers.is_text() {
  Either::A(field.data.read_text().map(|field| -> FutureResult<(String, String), hyper::Error> {
      futures::future::ok((field.headers.name.clone(), field.text))
  }))
} else {
  Either::B(field.data.fold(Vec::new(), |mut acc, chunk| -> FutureResult<Vec<u8>, hyper::Error> {
      chunk.iter().for_each(|x| acc.push(x.clone()));
      futures::future::ok(acc)
  }).then(|v| {
      futures::future::ok(("image".to_owned(), String::from_utf8_lossy(v.unwrap().as_slice()).to_string()))
  }))
};

It should be something like this:

multi.and_then(read_field).map(|(key, value)| ...)

Ok, we're getting somewhere here, doing that resolves the Either into a FutureResult, so now that map is getting a FutureResult<(String, String), hyper::Error> though and I can't seem to figure out how to get that resolved.

No, something's not right though if that's what you're seeing.

multi.and_then(read_field) is supposed to transform the multi into a Stream<Item=(String, String), Error=hyper::Error>. It should do that by ensuring the closure you pass returns something that's IntoFuture<Item=(String, String), Error=hyper::Error>.

Either<A,B> is the type returned from your closure - it implements Future if A and B do (as we discussed upthread). Any F: Future<...> is automatically an IntoFuture as well.

So A and B need to be futures that also yield Item=(String, String). The A and B are your two arms of the if/else.

I'll take a closer look later.

1 Like

Ah, so if I make the map of each Either branch return (String, String) instead of a FutureResult, it looks like the correct thing is reaching the map after and_then. I think at that point I was just double wrapping the future.

Thanks so much for your help!

For anyone else that might run into this, this is what the working code looks like:

let read_field = |field: Field<Body>| if field.headers.is_text() {
  Either::A(field.data.read_text().map(|field| -> (String, String) {
      (field.headers.name.clone(), field.text)
  }))
} else {
  Either::B(field.data.fold(Vec::new(), |mut acc, chunk| -> FutureResult<Vec<u8>, hyper::Error> {
      chunk.iter().for_each(|x| acc.push(x.clone()));
      futures::future::ok(acc)
  }).map(|v| {
      ("image".to_owned(), String::from_utf8_lossy(v.as_slice()).to_string())
  }))
};

multi.and_then(read_field).map(|(key, value)| /* something here */)

Shouldn’t

then(|v| {
      ("image".to_owned(), String::from_utf8_lossy(v.unwrap().as_slice()).to_string())
  }))

be

then(|v| {
      Ok(("image".to_owned(), String::from_utf8_lossy(v.unwrap().as_slice()).to_string()))
  }))

then() needs the closure to return an IntoFuture.

Also, you don’t need this then() - it can be replaced with a map(), in which case you can return the tuple directly.

Yep, you're right, I had changed it to map in my local code but forgot to change it here. I fixed it above. Thanks!

1 Like