-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Zoe Spellman
committed
Mar 7, 2024
1 parent
f8d54f7
commit dc2ba87
Showing
17 changed files
with
414 additions
and
29 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,121 @@ | ||
//! API endpoint definitions for different entry methods | ||
pub mod grpc; | ||
pub mod rest; | ||
|
||
use std::convert::Infallible; | ||
use std::pin::Pin; | ||
|
||
use axum::body::Bytes; | ||
use axum::response::IntoResponse; | ||
use futures::ready; | ||
#[allow(unused_imports)] | ||
pub use grpc::{GrpcService, GrpcServiceBuilder}; | ||
use http_body::combinators::UnsyncBoxBody; | ||
use hyper::{header::CONTENT_TYPE, Request}; | ||
pub use rest::{RestService, RestServiceBuilder}; | ||
use thiserror::Error; | ||
use tower::Service; | ||
|
||
use self::grpc::GrpcError; | ||
|
||
/// Any error that can occur internally to our service | ||
#[derive(Debug, Error)] | ||
pub enum AppCenterRatingsError { | ||
/// An error from the GRPC endpoints | ||
#[error("an error from the GRPC service occurred: {0}")] | ||
GrpcError(#[from] GrpcError), | ||
/// Technically, an error from the Rest endpoints, but they're infallible | ||
#[error("cannot happen")] | ||
RestError(#[from] Infallible), | ||
} | ||
|
||
/// The general service for our app, containing all our endpoints | ||
#[derive(Clone, Debug)] | ||
#[allow(clippy::missing_docs_in_private_items)] | ||
pub struct AppCenterRatingsService { | ||
grpc_service: GrpcService, | ||
grpc_ready: bool, | ||
rest_service: RestService, | ||
rest_ready: bool, | ||
} | ||
|
||
impl AppCenterRatingsService { | ||
/// Constructs the service with all the default service endpoints for REST and GRPC | ||
pub fn with_default_routes() -> AppCenterRatingsService { | ||
Self { | ||
grpc_service: GrpcServiceBuilder::default().build(), | ||
grpc_ready: false, | ||
rest_service: RestServiceBuilder::default().build(), | ||
rest_ready: false, | ||
} | ||
} | ||
} | ||
|
||
/// A type definition which is simply a future that's in a pinned location in the heap. | ||
type BoxFuture<'a, T> = Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>; | ||
|
||
impl Service<hyper::Request<hyper::Body>> for AppCenterRatingsService { | ||
type Response = hyper::Response<UnsyncBoxBody<Bytes, axum::Error>>; | ||
|
||
type Error = AppCenterRatingsError; | ||
|
||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>; | ||
|
||
fn poll_ready( | ||
&mut self, | ||
cx: &mut std::task::Context<'_>, | ||
) -> std::task::Poll<Result<(), Self::Error>> { | ||
loop { | ||
match (self.grpc_ready, self.rest_ready) { | ||
(true, true) => return std::task::Poll::Ready(Ok(())), | ||
(false, _) => { | ||
ready!(self.grpc_service.poll_ready(cx))?; | ||
self.grpc_ready = true | ||
} | ||
(_, false) => { | ||
ready!(self.rest_service.poll_ready(cx)).unwrap(); | ||
self.rest_ready = true | ||
} | ||
} | ||
} | ||
} | ||
|
||
fn call(&mut self, req: hyper::Request<hyper::Body>) -> Self::Future { | ||
assert!( | ||
self.grpc_ready, | ||
"grpc service not ready. Did you forget to call `poll_ready`?" | ||
); | ||
assert!( | ||
self.rest_ready, | ||
"rest service not ready. Did you forget to call `poll_ready`?" | ||
); | ||
|
||
// if we get a grpc request call the grpc service, otherwise call the rest service | ||
// when calling a service it becomes not-ready so we have drive readiness again | ||
if is_grpc_request(&req) { | ||
self.grpc_ready = false; | ||
let future = self.grpc_service.call(req); | ||
Box::pin(async move { | ||
let res = future.await?; | ||
Ok(res.into_response()) | ||
}) | ||
} else { | ||
self.rest_ready = false; | ||
let future = self.rest_service.call(req); | ||
Box::pin(async move { | ||
let res = future.await?; | ||
Ok(res.into_response()) | ||
}) | ||
} | ||
} | ||
} | ||
|
||
/// Checks to see if this request has a GRPC header (if not we assume REST) | ||
fn is_grpc_request<B>(req: &Request<B>) -> bool { | ||
req.headers() | ||
.get(CONTENT_TYPE) | ||
.map(|content_type| content_type.as_bytes()) | ||
.filter(|content_type| content_type.starts_with(b"application/grpc")) | ||
.is_some() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
//! The interface for serving on REST endpoints | ||
use std::{convert::Infallible, pin::Pin}; | ||
|
||
use axum::{body::Bytes, response::IntoResponse, Router}; | ||
use http_body::combinators::UnsyncBoxBody; | ||
use hyper::StatusCode; | ||
use tower::Service; | ||
|
||
use crate::features::admin::log_level::service::LogLevelService; | ||
|
||
/// The base path appended to all our internal endpoints | ||
const BASE_ROUTE: &str = "/v1/"; | ||
|
||
/// Dispatches to our web endpoints | ||
#[derive(Clone, Debug)] | ||
pub struct RestService { | ||
/// The axum router we use for dispatching to endpoints | ||
router: Router, | ||
} | ||
|
||
/// A type definition which is simply a future that's in a pinned location in the heap. | ||
type BoxFuture<'a, T> = Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>; | ||
|
||
impl Service<hyper::Request<hyper::Body>> for RestService { | ||
type Response = hyper::Response<UnsyncBoxBody<Bytes, axum::Error>>; | ||
|
||
type Error = Infallible; | ||
|
||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>; | ||
|
||
fn poll_ready( | ||
&mut self, | ||
cx: &mut std::task::Context<'_>, | ||
) -> std::task::Poll<Result<(), Self::Error>> { | ||
self.router | ||
.poll_ready(cx) | ||
.map_err(|_| unreachable!("error is infallible")) | ||
} | ||
|
||
fn call(&mut self, req: hyper::Request<hyper::Body>) -> Self::Future { | ||
let future = self.router.call(req); | ||
Box::pin(future) | ||
} | ||
} | ||
|
||
/// Handles any missing paths | ||
async fn handler_404() -> impl IntoResponse { | ||
(StatusCode::NOT_FOUND, "no such API endpoint") | ||
} | ||
|
||
/// Builds the REST service | ||
pub struct RestServiceBuilder { | ||
/// The underlying axum router we're building up | ||
router: Router, | ||
} | ||
|
||
impl RestServiceBuilder { | ||
/// Creates a new builder with an empty path, | ||
/// you probably actually want [`RestServiceBuilder::default`], | ||
/// since that seeds the default API endpoint paths. | ||
pub fn new() -> Self { | ||
Self { | ||
router: Router::default(), | ||
} | ||
} | ||
|
||
/// Adds the log service | ||
pub fn with_log_level(self) -> Self { | ||
Self { | ||
router: self | ||
.router | ||
.nest(BASE_ROUTE, LogLevelService.register_axum_route()), | ||
} | ||
} | ||
|
||
/// Builds the REST service, applying all configured paths and | ||
/// forcing the others to 404. | ||
pub fn build(self) -> RestService { | ||
RestService { | ||
router: self.router.fallback(handler_404), | ||
} | ||
} | ||
} | ||
|
||
impl Default for RestServiceBuilder { | ||
fn default() -> Self { | ||
Self::new().with_log_level() | ||
} | ||
} |
Oops, something went wrong.