Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft RFC] Make gloo_net more idiomatic #335

Open
Alextopher opened this issue May 10, 2023 · 0 comments
Open

[Draft RFC] Make gloo_net more idiomatic #335

Alextopher opened this issue May 10, 2023 · 0 comments

Comments

@Alextopher
Copy link

⚠️ 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++ 😄 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant