use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::net::{AddrParseError, SocketAddr};
use std::sync::Arc;
use serde_json::json;
use tonic::{Request, Response, Status};
use super::service::Service;
use super::session::Session;
#[derive(Clone, prost::Message)]
pub struct GrpcRequest {
#[prost(string, tag = "1")]
pub command: String,
#[prost(string, tag = "2")]
pub input: String, #[prost(map = "string, string", tag = "3")]
pub session_variables: HashMap<String, String>,
}
#[derive(Clone, prost::Message)]
pub struct GrpcResponse {
#[prost(uint32, tag = "1")]
pub status: u32,
#[prost(string, tag = "2")]
pub body: String, }
#[derive(Clone, prost::Message)]
pub struct HealthRequest {}
#[derive(Clone, prost::Message)]
pub struct HealthResponse {
#[prost(bool, tag = "1")]
pub ok: bool,
#[prost(string, repeated, tag = "2")]
pub commands: Vec<String>,
}
include!(concat!(
env!("OUT_DIR"),
"/sourced.microsvc.CommandService.rs"
));
pub use command_service_client::CommandServiceClient;
pub use command_service_server::{CommandService, CommandServiceServer};
#[derive(Debug)]
pub enum GrpcServeError {
InvalidAddress {
addr: String,
source: AddrParseError,
},
Transport(tonic::transport::Error),
}
impl fmt::Display for GrpcServeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GrpcServeError::InvalidAddress { addr, source } => {
write!(f, "invalid gRPC bind address `{addr}`: {source}")
}
GrpcServeError::Transport(source) => write!(f, "gRPC transport error: {source}"),
}
}
}
impl Error for GrpcServeError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
GrpcServeError::InvalidAddress { source, .. } => Some(source),
GrpcServeError::Transport(source) => Some(source),
}
}
}
impl From<tonic::transport::Error> for GrpcServeError {
fn from(source: tonic::transport::Error) -> Self {
GrpcServeError::Transport(source)
}
}
pub struct GrpcHandler {
service: Arc<Service>,
}
impl GrpcHandler {
pub fn new(service: Arc<Service>) -> Self {
Self { service }
}
}
#[tonic::async_trait]
impl CommandService for GrpcHandler {
async fn dispatch(
&self,
request: Request<GrpcRequest>,
) -> Result<Response<GrpcResponse>, Status> {
let metadata = request.metadata().clone();
let req = request.into_inner();
let input: serde_json::Value = match serde_json::from_str(&req.input) {
Ok(value) => value,
Err(e) => {
return Ok(Response::new(GrpcResponse {
status: 400,
body: json!({ "error": format!("invalid JSON input: {e}") }).to_string(),
}));
}
};
let session = build_session(&metadata, req.session_variables);
match self.service.dispatch(&req.command, input, session).await {
Ok(value) => Ok(Response::new(GrpcResponse {
status: 200,
body: value.to_string(),
})),
Err(e) => {
let status = e.status_code();
if status >= 500 {
eprintln!("microsvc command `{}` failed: {e}", req.command);
}
Ok(Response::new(GrpcResponse {
status: status as u32,
body: json!({ "error": e.client_facing_message() }).to_string(),
}))
}
}
}
async fn health(
&self,
_request: Request<HealthRequest>,
) -> Result<Response<HealthResponse>, Status> {
let commands: Vec<String> = self
.service
.command_names()
.into_iter()
.map(|s| s.to_string())
.collect();
Ok(Response::new(HealthResponse { ok: true, commands }))
}
}
fn build_session(
metadata: &tonic::metadata::MetadataMap,
payload_vars: HashMap<String, String>,
) -> Session {
let mut vars = HashMap::new();
for (k, v) in payload_vars {
vars.insert(k, v);
}
for kv in metadata.iter() {
if let tonic::metadata::KeyAndValueRef::Ascii(key, value) = kv {
if let Ok(v) = value.to_str() {
vars.insert(key.as_str().to_string(), v.to_string());
}
}
}
Session::from_map(vars)
}
pub fn grpc_server(service: Arc<Service>) -> CommandServiceServer<GrpcHandler> {
CommandServiceServer::new(GrpcHandler::new(service))
}
pub async fn serve_grpc(service: Arc<Service>, addr: &str) -> Result<(), GrpcServeError> {
let addr: SocketAddr = addr
.parse()
.map_err(|source| GrpcServeError::InvalidAddress {
addr: addr.to_string(),
source,
})?;
tonic::transport::Server::builder()
.add_service(grpc_server(service))
.serve(addr)
.await?;
Ok(())
}