From 0e1ba34356cd5f7861f8f87279fbc6cd68dd32d4 Mon Sep 17 00:00:00 2001 From: Timothy Clem Date: Fri, 21 Jul 2023 16:59:40 -0700 Subject: [PATCH] Documentation and cleanup --- Makefile | 4 +++ README.md | 52 +++++++++++++++++++++++++++++--------- crates/twirp/src/client.rs | 3 ++- crates/twirp/src/lib.rs | 2 +- crates/twirp/src/server.rs | 10 +++++--- example/build.rs | 2 -- 6 files changed, 54 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index 5125320..c6a9be2 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 2ebe42d..90c4221 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 { + 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); } ``` diff --git a/crates/twirp/src/client.rs b/crates/twirp/src/client.rs index 3841546..b826c3c 100644 --- a/crates/twirp/src/client.rs +++ b/crates/twirp/src/client.rs @@ -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)] @@ -41,6 +42,7 @@ pub enum ClientError { pub type Result = core::result::Result; +/// Use ClientBuilder to build a TwirpClient. pub struct ClientBuilder { base_url: Url, builder: reqwest::ClientBuilder, @@ -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()) diff --git a/crates/twirp/src/lib.rs b/crates/twirp/src/lib.rs index aa62c0c..9f85b00 100644 --- a/crates/twirp/src/lib.rs +++ b/crates/twirp/src/lib.rs @@ -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(m: T) -> hyper::Body diff --git a/crates/twirp/src/server.rs b/crates/twirp/src/server.rs index 90dd381..4f405cc 100644 --- a/crates/twirp/src/server.rs +++ b/crates/twirp/src/server.rs @@ -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) -> HandlerResponse + Send + Sync>; + +/// Type alias for a handler response. type HandlerResponse = Box, GenericError>> + Unpin + Send>; -type HandlerFn = Box) -> 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 { @@ -101,6 +104,7 @@ impl Router { } } +/// Serve a request using the given router. pub async fn serve( router: Arc, req: Request, diff --git a/example/build.rs b/example/build.rs index a3c522f..c980ce0 100644 --- a/example/build.rs +++ b/example/build.rs @@ -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"])