axum-connect/README.md

252 lines
8.9 KiB
Markdown
Raw Normal View History

2023-03-03 04:33:18 +00:00
# Axum Connect-Web
Brings the protobuf-based [Connect-Web RPC
framework](https://connect.build/docs/introduction) to Rust via idiomatic
2023-04-29 17:08:06 +00:00
[Axum](https://github.com/tokio-rs/axum).
2023-12-24 23:24:48 +00:00
# Axum Version
- `axum-connect:0.3` works with `axum:0.7`
- `axum-connect:0.2` works with `axum:0.6`
2023-12-24 23:24:48 +00:00
2023-04-29 17:08:06 +00:00
# Features 🔍
- Integrates into existing Axum HTTP applications seamlessly
- Closely mirrors Axum's API
- Extract State and other `parts` that impl `RpcFromRequestParts` just like
with Axum.
- Return any type that impl `RpcIntoResponse` just like Axum.
- Generated types and service handlers are strongly typed and...
- Handlers enforce semantically correct HTTP 'parts' access.
- Allows users to derive `RpcIntoResponse` and `RpcFromRequestParts` just like
Axum.
- Note: These must be derivatives of Axum's types because they are more
restrictive; you're not dealing with arbitrary HTTP any more, you're
speaking `connect-web` RPC **over** HTTP.
- Wrap `connect-web` error handling in idiomatic Axum/Rust.
- Codegen from `*.proto` files in a separate crate.
- All the other amazing benefits that come with Axum, like the community,
documentation and performance!
2023-10-04 00:05:45 +00:00
# Caution ⚠️
We use `axum-connect` in production, but I don't kow that anyone with more sense
does. It's written in Rust which obviously offers some amazing compiler
guarantees, but it's not well tested or battle-proven yet. Do what you will with
that information.
Please let me know if you're using `axum-connect`! And open issues if you find a
bug.
2023-04-29 17:08:06 +00:00
# Getting Started 🤓
2023-03-03 04:33:18 +00:00
_Prior knowledge with [Protobuf](https://github.com/protocolbuffers/protobuf)
(both the IDL and it's use in RPC frameworks) and
[Axum](https://github.com/tokio-rs/axum) are assumed._
## Dependencies 👀
2023-04-29 17:08:06 +00:00
You'll need 2 `axum-connect` crates, one for code-gen and one for runtime use.
2023-04-29 17:08:06 +00:00
Because of how prost works, you'll also need to add it to your own project.
You'll obviously also need `axum` and `tokio`.
```sh
# Note: axum-connect-build will fetch `protoc` for you.
2023-04-29 17:08:06 +00:00
cargo add --build axum-connect-build
cargo add axum-connect prost axum
cargo add tokio --features full
```
## Protobuf File 🥱
Start by creating the obligatory 'hello world' proto service definition.
2023-03-03 04:33:18 +00:00
`proto/hello.proto`
```protobuf
syntax = "proto3";
2023-04-29 17:21:06 +00:00
package hello;
2023-03-03 04:33:18 +00:00
2023-04-29 17:21:06 +00:00
message HelloRequest { string name = 1; }
2023-03-03 04:33:18 +00:00
2023-04-29 17:21:06 +00:00
message HelloResponse { string message = 1; }
2023-03-03 04:33:18 +00:00
service HelloWorldService {
2023-04-29 17:21:06 +00:00
rpc SayHello(HelloRequest) returns (HelloResponse) {}
2023-03-03 04:33:18 +00:00
}
```
2023-04-29 17:08:06 +00:00
## Codegen 🤔
Use the `axum_connect_codegen` crate to generate Rust code from the proto IDL.
> Currently all codegen is done by having the proto files locally on-disk, and
> using a `build.rs` file. Someday I hope to support more of Buf's idioms like
> [Remote Code Gen](https://buf.build/docs/bsr/remote-plugins/usage).
2023-03-03 04:33:18 +00:00
`build.rs`
```rust
use axum_connect_build::{axum_connect_codegen, AxumConnectGenSettings};
2023-03-03 04:33:18 +00:00
fn main() {
// This helper will use `proto` as the import path, and globs all .proto
2023-10-04 00:05:45 +00:00
// files in the `proto` directory.
//
// Note that you might need to re-save the `build.rs` file after updating
// a proto file to get rust-analyzer to pickup the change. I haven't put
// time into looking for a fix to that yet.
let settings = AxumConnectGenSettings::from_directory_recursive("proto")
.expect("failed to glob proto files");
2023-10-04 00:05:45 +00:00
axum_connect_codegen(settings).unwrap();
2023-03-03 04:33:18 +00:00
}
```
2023-04-29 17:08:06 +00:00
## The Fun Part 😁
With the boring stuff out of the way, let's implement our service using Axum!
2023-03-03 04:33:18 +00:00
```rust
2023-12-28 16:52:42 +00:00
use async_stream::stream;
2023-03-03 04:33:18 +00:00
use axum::{extract::Host, Router};
2023-12-28 16:52:42 +00:00
use axum_connect::{futures::Stream, prelude::*};
2023-04-29 17:08:06 +00:00
use proto::hello::*;
2023-03-03 04:33:18 +00:00
mod proto {
2023-04-29 17:08:06 +00:00
pub mod hello {
include!(concat!(env!("OUT_DIR"), "/hello.rs"));
}
2023-03-03 04:33:18 +00:00
}
#[tokio::main]
async fn main() {
2023-12-28 16:52:42 +00:00
// Build our application with a route. Note the `rpc` method which was added by `axum-connect`.
// It expect a service method handler, wrapped in it's respective type. The handler (below) is
// just a normal Rust function. Just like Axum, it also supports extractors!
let app = Router::new()
.rpc(HelloWorldService::say_hello(say_hello_success))
.rpc(HelloWorldService::say_hello_stream(say_hello_stream));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3030")
2023-03-03 04:33:18 +00:00
.await
.unwrap();
2023-12-28 16:52:42 +00:00
println!("listening on http://{:?}", listener.local_addr());
axum::serve(listener, app).await.unwrap();
2023-03-03 04:33:18 +00:00
}
async fn say_hello_success(
Host(host): Host,
request: HelloRequest
) -> HelloResponse {
2023-03-03 04:33:18 +00:00
HelloResponse {
message: format!(
"Hello {}! You're addressing the hostname: {}.",
request.name, host
),
}
}
```
2023-04-29 17:08:06 +00:00
## SEND IT 🚀
To test it out, try hitting the endpoint manually.
```sh
curl --location 'http://localhost:3030/hello.HelloWorldService/SayHello' \
--header 'Content-Type: application/json' \
--data '{ "name": "Alec" }'
```
From here you can stand up a `connect-web` TypeScript/Go project to call your
API with end-to-end typed RPCs.
2023-05-30 05:01:18 +00:00
# Request/Response Parts 🙍‍♂️
Both the request and response types are derived in `axum-connect`. This might
seem redundant at first.
Let's go over the easier one first, `RpcIntoResponse`. Connect RPCs are not
arbitrary HTML requests, they cannot return arbitrary HTML responses. For
example, streaming responses MUST return an HTTP 200 regardless of error state.
Keeping with Axum's (fantastic) paradigm, that is enforced by the type system.
2023-05-30 05:01:18 +00:00
RPC handlers may not return arbitrary HTML, but instead must return something
that `axum-connect` knows how to turn into a valid Connect response.
Somewhat less intuitively, `axum-connect` derives `RpcFromRequestParts`, which
is _almost_ identical to Axum's `FromRequestParts`. Importantly though,
`FromRequestParts` can return back an error of arbitrary HTML responses, which
is a problem for the same reason.
Axum also allows a `FromRequest` to occupy the last parameter in a handler which
consumed the remainder of the HTTP request (including the body). `axum-connect`
2023-05-30 05:01:18 +00:00
needs to handle the request input itself, so there is no equivalent for RPCs
handlers.
# Roadmap / Stated Non-Goals 🛣️
2023-04-29 17:08:06 +00:00
2023-05-30 05:01:18 +00:00
- Explore better typing than `RpcFromRequestParts`
- Ideally clients only need to provide an `RpcIntoError`, but I haven't fully
2023-05-30 05:01:18 +00:00
thought this problem through. I just know that having to specific both a
`FromRequestParts` and an `RpcFromRequestParts` on custom types is a PITA.
- Rework error responses to allow for multiple errors
2023-04-29 17:08:06 +00:00
- Version checking between generated and runtime code
- A plan for forward-compatibility
- Bring everything in-line with `connect-web` and...
- Comprehensive integration tests
2023-04-29 17:08:06 +00:00
## More Distant Goals 🌜
2023-04-29 17:08:06 +00:00
- I would love to also support a WASM-ready client library
- Use `buf.build` to support remote codegen and streamlined proto handling
- Support gRPC calls
- I don't think this is hard to do, I just have no personal use-case for it
- Possibly maybe-someday support BiDi streaming over WebRTC
- This would require `connect-web` picking up support for the same
- WebRTC streams because they are DTLS/SRTP and are resilient
- Replace Prost (with something custom and simpler)
2023-04-29 17:08:06 +00:00
## Non-goals 🙅
2023-04-29 17:08:06 +00:00
- To support every feature gRPC does
- You get a lot of this already with Axum, but gRPC is a monster that I
don't wish to reproduce. That complexity is useful for Google, and gets in
the way for pretty much everyone else.
- To do everything and integrate with everything
- I plan on keeping `axum-connect` highly focused. Good at what it does and
nothing else.
- This is idiomatic Rust. Do one thing well, and leave the rest to other
crates.
# Prost and Protobuf 📖
## Protoc Version
The installed version of `protoc` can be configured in the
`AxumConnectGenSettings` if you need/wish to do so. Setting the value to `None`
will disable the download entirely.
## Reasoning
Prost stopped shipping `protoc` binaries (a decision I disagree with) so
`axum-connect-build` internally uses
2023-10-04 00:05:45 +00:00
[protoc-fetcher](https://crates.io/crates/protoc-fetcher) to download and
resolve a copy of `protoc`. This is far more turnkey than forcing every build
environment (often Heroku and/or Docker) to have a recent `protoc` binary
pre-installed. This behavior can be disabled if you disagree, or if you need to
comply with corporate policy, or your build environment is offline.
I would someday like to replace all of it with a new 'lean and
mean' protoc library for the Rust community. One with a built-in parser, that
supports only the latest proto3 syntax as well as the canonical JSON
serialization format and explicitly doesn't support many of the rarely used
features. But that day is not today.
2023-04-29 17:08:06 +00:00
# License 🧾
2023-03-03 04:33:18 +00:00
Axum-Connect is dual licensed (at your option)
- MIT License ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))