rustwasm / gloo

A modular toolkit for building fast, reliable Web applications and libraries with Rust and WASM
https://gloo-rs.web.app
Apache License 2.0
1.77k stars 145 forks source link

[Draft RFC] Make `gloo_net` more idiomatic #335

Open Alextopher opened 1 year ago

Alextopher commented 1 year ago

:warning: This PR is still a bit of a draft, sorry about that, I'm about to start a new job and need to relearn C++ :smile: But hopefully there is enough here to start a discussion.

Summary

Make gloo_net more idiomatic by increasing ownership and borrowing constraints. As is the Rust way, this will prevent certain classes of bugs and make our implementations flexible for future improvements.

This will require increasing the complexity of wrapper types.

Motivation

In JavaScript the bodies of requests and responses can only be read once, if you read the same response body more than once you'll just receive an empty buffer, without an error. This behavior is almost always a bug, and rust ownership semantics can prevent this. By requiring certain methods to take ownership of the underlying Request/Response, this bug is prevented.

JsValue backed types allow interior mutability through shared references. If we do the same with our wrapper types (Request, Response, RequestBuilder, etc.) we're adding unnecessary constraints to future implementations, and our APIs don't communicate mutability where it exists. One example is rather than having Header be a wrapper over web_sys::Header we could choose to wrap a http::HeaderMap and implement From<Header> for web_sys::Header. In that future, we'd be happier to have committed to

Header::set(&mut self, key: &str, value: &str) 

Over

Header::set(&self, key: &str, value: &str) 

These changes can also open the door to improve https://rust-lang.github.io/api-guidelines/interoperability.html#interoperability.

Detailed Explanation

Request and Response

In #312 we added RequestBuilder and ResponseBuilder those types map to RequestInit and ResponseInit. I don't believe that made it into an RFC, I would like to suggest minor changes to those new APIs.

// mutable
pub struct RequestBuilder(...);

impl RequestBuilder {
    pub fn from_raw(raw: websys::RequestInit) -> Self;
    pub fn into_raw(self) -> websys::RequestInit;

    // Creates a new request with the provided method
    pub fn get(url: &str) -> RequestBuilder;
    pub fn post(url: &str) -> RequestBuilder;
    // ...

    // Setters
    pub fn header(self, header: http::HeaderName, value: http::HeaderValue) -> Self; // or strings
    pub fn headers(self, headers: http::HeadersMap) -> Self;
    // ...

    // Getters

    // I'm still investigating if these builders are infailable. They might not be.
    pub fn body(self, body: Body) -> Request;
    pub fn json<T: serde::Serialize>(self, value: &T) -> Request;
    pub fn binary(self, body: Vec<u8>) -> Request;
    pub fn build() -> Request 
}

ResponseBuilder has a similar API.

Response and Request map into web_sys::Response and web_sys::Request.

// immutable
pub struct Request(...);

impl Request {
    pub fn from_raw(raw: web_sys::Request) -> Self;
    pub fn into_raw(self) -> web_sys::Request;

    // To construct a Request should make a builder
    pub fn get(url: &str) -> RequestBuilder;
    pub fn post(url: &str) -> RequestBuilder;
    // ...

    // I'm propsing changes to these getters to return _borrowed_ values
    // In my opinion a Request should be immutable after creation, like other libraries
    pub fn headers(&self) -> &Headers;
    pub fn method(&self) -> &http::Method;
    pub fn url(&self) -> &String;
    // ...

    // You can extract the body of a request/response, only once
    pub async fn body(self) -> Body;
    pub async fn text(self) -> Result<String, Error>;
    pub async fn binary(self) -> Result<Vec<u8>, Error>;

    // `fetch()`
    pub async fn send(self) -> Result<Response, Error>;
}

impl Clone for Request {
    // This is https://developer.mozilla.org/en-US/docs/Web/API/Request/clone
    // It works by `tee`-ing the body of the request, in JS clone misbehaves if
    // the body has already been consumed. Since you can only extract the body
    // once, `clone` should always work
}

Headers

Remove Headers and replace it with http::HeadersMap.

TODO: explain why

Drawbacks, Rationale, and Alternatives

This RFC includes API breaking changes to basically every type in gloo_net. Since most changes involve ownership and borrowing, cargo check should communicate most of the fixes

When wrapping a web_sys type, we exclusively get owned values back, for example

// web_sys::Request
#[doc = "Getter for the `method` field of this object."]
#[doc = ""]
#[doc = "[MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Request/method)"]
#[doc = ""]
#[doc = "*This API requires the following crate features to be activated: `Request`*"]
pub fn method(this: &Request) -> String; // Returns an owned value

// Our wrapper
pub fn method(&self) -> &http::Method; // Returns a borrowed value

I'm proposing adding a new Rust type RequestOptions that stores rust representations of values we used to keep in a web_sys::RequestInit.

Like so:

#[derive(Debug)]
struct RequestOptions {
    method: http::Method,
    headers: http::HeaderMap, // or our current `Headers` type
    body: Option<Body>,
    cache: web_sys::RequestCache,
    credentials: web_sys::RequestCredentials,
    integrity: String,
    mode: web_sys::RequestMode,
    redirect: web_sys::RequestRedirect,
    referrer: String,
    referrer_policy: web_sys::ReferrerPolicy,
    // signal: Option<&'a web_sys::AbortSignal>, TODO: our requests could be made cancellable 
}

impl From<RequestOptions> for web_sys::RequestInit {...} // straightforward
impl From<web_sys::RequestInit> for RequestOptions {...} // makes significant use of reflection.

// Which makes the builder
pub struct RequestBuilder {
    // url
    url: String,
    init: RequestOptions,
}

// And to support the getters in the Request
pub struct Request {
    inner: web_sys::Request,
    // Cached Rust representations of the request inners.
    url: String,
    init: RequestOptions,
}

Unresolved Questions

We can maybe remove Request::inner and instead lazily create it when well call send or try to process the body.

TODO

skeet70 commented 2 weeks ago

How is pub fn binary(self, body: Vec<u8>) -> Request; accomplished in the current version?

Alextopher commented 2 weeks ago

It's been a long time since I've thought about this issue. If improvements have been added since I published this issue then it might be worth closing ❤️