Skip to content

Commit

Permalink
Documentation and cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
tclem committed Jul 21, 2023
1 parent 1201e2b commit 0e1ba34
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 19 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ all: build lint test
build:
cargo build --features test-support

.PHONY: test
test:
cargo test --features test-support

.PHONY: lint
lint:
cargo fmt --all -- --check
Expand Down
52 changes: 40 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[Twirp is an RPC protocol](https://twitchtv.github.io/twirp/docs/spec_v7.html) based on HTTP and Protocol Buffers (proto). The protocol uses HTTP URLs to specify the RPC endpoints, and sends/receives proto messages as HTTP request/response bodies. Services are defined in a [.proto file](https://developers.google.com/protocol-buffers/docs/proto3), allowing easy implementation of RPC services with auto-generated clients and servers in different languages.

The [canonical implementation](https://github.com/twitchtv/twirp) is in Golang, this is a Rust implementation of the protocol. Currently, this crate only supports server generation, client generation is a future TODO.
The [canonical implementation](https://github.com/twitchtv/twirp) is in Golang, this is a Rust implementation of the protocol.

## Usage

Expand Down Expand Up @@ -43,28 +43,56 @@ fn main() {

This generates code that you can find in `target/build/your-project-*/out/example.service.rs`. In order to use this code, you'll need to implement the trait for the proto defined service and wire up the service handlers to a hyper web server. See [the example `main.rs`]( example/src/main.rs) for details.

Essentially, you need to include the generate code, create a router, register your service, and then serve those routes in the hyper server:
Include the generated code, create a router, register your service, and then serve those routes in the hyper server:

```rust
pub mod service {
pub mod haberdash {
pub mod v1 {
include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs"));
}
}
mod haberdash {
include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs"));
}
use service::haberdash::v1:: as haberdash;
use haberdash

#[tokio::main]
pub async fn main() {
let mut router = Router::default();
let example = Arc::new(HaberdasherAPIServer {});
haberdash::add_service(&mut router, example.clone());
let server = Arc::new(HaberdasherAPIServer {});
haberdash::add_service(&mut router, server.clone());
let router = Arc::new(router);
let service = make_service_fn(move |_| {
let router = router.clone();
async { Ok::<_, GenericError>(service_fn(move |req| twirp::serve(router.clone(), req))) }
});
// ... now start the server...

let addr = ([127, 0, 0, 1], 3000).into();
let server = Server::bind(&addr).serve(service);
server.await.expect("server error")
}

// Define the server and implement the trait.
struct HaberdasherAPIServer;

#[async_trait]
impl haberdash::HaberdasherAPI for HaberdasherAPIServer {
async fn make_hat(&self, req: MakeHatRequest) -> Result<MakeHatResponse, TwirpErrorResponse> {
todo!()
}
}
```

## Usage (client side)

On the client side, you also get a generated twirp client (based on the rpc endpoints in your proto). Include the generated code, create a client, and start making rpc calls:

``` rust
mod haberdash {
include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs"));
}

use haberdash::{HaberdasherAPIClient, MakeHatRequest, MakeHatResponse};

#[tokio::main]
pub async fn main() {
let client = Client::from_base_url(Url::parse("http://localhost:3000/twirp/")?)?;
let resp = client.make_hat(MakeHatRequest { inches: 1 }).await;
eprintln!("{:?}", resp);
}
```
3 changes: 2 additions & 1 deletion crates/twirp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use url::Url;
use crate::headers::*;
use crate::{error::*, to_proto_body};

/// A twirp client error.
#[derive(Debug, Error)]
pub enum ClientError {
#[error(transparent)]
Expand Down Expand Up @@ -41,6 +42,7 @@ pub enum ClientError {

pub type Result<T> = core::result::Result<T, ClientError>;

/// Use ClientBuilder to build a TwirpClient.
pub struct ClientBuilder {
base_url: Url,
builder: reqwest::ClientBuilder,
Expand Down Expand Up @@ -164,7 +166,6 @@ impl Client {
let status = resp.status();
let content_type = resp.headers().get(CONTENT_TYPE).cloned();

// TODO: Include more info in the error cases: request path, content-type, etc.
match (status, content_type) {
(status, Some(ct)) if status.is_success() && ct.as_bytes() == CONTENT_TYPE_PROTOBUF => {
O::decode(resp.bytes().await?).map_err(|e| e.into())
Expand Down
2 changes: 1 addition & 1 deletion crates/twirp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub use server::*;
// Re-export `reqwest` so that it's easy to implement middleware.
pub use reqwest;

// Re-export `url so that the generated code works without additional dependencies beyond just the `twirp` crate.
// Re-export `url` so that the generated code works without additional dependencies beyond just the `twirp` crate.
pub use url;

pub(crate) fn to_proto_body<T>(m: T) -> hyper::Body
Expand Down
10 changes: 7 additions & 3 deletions crates/twirp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,20 @@ use crate::error::*;
use crate::headers::*;
use crate::to_proto_body;

/// A function that handles a request and returns a response.
type HandlerFn = Box<dyn Fn(Request<Body>) -> HandlerResponse + Send + Sync>;

/// Type alias for a handler response.
type HandlerResponse =
Box<dyn Future<Output = Result<Response<Body>, GenericError>> + Unpin + Send>;

type HandlerFn = Box<dyn Fn(Request<Body>) -> HandlerResponse + Send + Sync>;

/// A Router maps a request to a handler.
/// A Router maps a request (method, path) tuple to a handler.
pub struct Router {
routes: HashMap<(Method, String), HandlerFn>,
prefix: &'static str,
}

/// The canonical twirp path prefix. You don't have to use this, but it's the default.
pub const DEFAULT_TWIRP_PATH_PREFIX: &str = "/twirp";

impl Default for Router {
Expand Down Expand Up @@ -101,6 +104,7 @@ impl Router {
}
}

/// Serve a request using the given router.
pub async fn serve(
router: Arc<Router>,
req: Request<Body>,
Expand Down
2 changes: 0 additions & 2 deletions example/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ fn main() {
prost_build
.service_generator(twirp_build::service_generator())
.type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]")
.type_attribute(".service.example.MakeHatRequest", "#[serde(default)]")
.type_attribute(".service.example.MakeHatResponse", "#[serde(default)]")
.extern_path(".google.protobuf.Timestamp", "::prost_wkt_types::Timestamp")
.file_descriptor_set_path(&descriptor_file)
.compile_protos(&proto_source_files, &["./proto"])
Expand Down

0 comments on commit 0e1ba34

Please sign in to comment.