use std::fmt::Write as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RpcHttpMethod {
Get,
Post,
Put,
Delete,
Patch,
}
impl RpcHttpMethod {
#[inline]
pub fn as_str(self) -> &'static str {
match self {
Self::Get => "GET",
Self::Post => "POST",
Self::Put => "PUT",
Self::Delete => "DELETE",
Self::Patch => "PATCH",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RpcMethodDef {
pub operation: String,
pub path: String,
pub method: RpcHttpMethod,
pub summary: Option<String>,
pub request_type: Option<String>,
pub response_type: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RpcServiceDef {
pub service: String,
pub methods: Vec<RpcMethodDef>,
}
pub fn emit_service_trait(def: &RpcServiceDef) -> String {
let mut out = String::new();
let _ = writeln!(
out,
"//! Generated RPC service trait for `{}`.\n\
//!\n\
//! Emit source via `id_effect_rpc::codegen::emit_service_trait`.",
def.service
);
let _ = writeln!(out, "#![allow(dead_code, missing_docs)]");
let _ = writeln!(out);
let _ = writeln!(out, "use id_effect::Effect;");
let _ = writeln!(out, "use id_effect_rpc::RpcError;");
let _ = writeln!(out);
let _ = writeln!(
out,
"/// RPC service trait — implement on your domain type or Axum state."
);
let _ = writeln!(out, "pub trait {} {{", def.service);
for method in &def.methods {
let req = method.request_type.as_deref().unwrap_or("()");
let resp = method.response_type.as_deref().unwrap_or("()");
let doc = match &method.summary {
Some(s) => format!("{} {} {}", method.method.as_str(), method.path, s),
None => format!("{} {}", method.method.as_str(), method.path),
};
let _ = writeln!(out, " /// {doc}");
let _ = writeln!(
out,
" fn {}(&self, request: {req}) -> Effect<{resp}, RpcError, ()>;",
method.operation
);
let _ = writeln!(out);
}
let _ = writeln!(out, "}}");
out
}
#[cfg(test)]
mod tests {
use super::*;
fn greet_fixture() -> RpcServiceDef {
RpcServiceDef {
service: "GreetService".to_owned(),
methods: vec![RpcMethodDef {
operation: "greet".to_owned(),
path: "/greet".to_owned(),
method: RpcHttpMethod::Post,
summary: Some("Say hello".to_owned()),
request_type: Some("GreetRequest".to_owned()),
response_type: Some("GreetResponse".to_owned()),
}],
}
}
#[test]
fn emit_service_trait_includes_trait_name_and_methods() {
let src = emit_service_trait(&greet_fixture());
assert!(src.contains("pub trait GreetService"));
assert!(src.contains("fn greet(&self, request: GreetRequest)"));
assert!(src.contains("Effect<GreetResponse, RpcError, ()>"));
assert!(src.contains("POST /greet"));
}
#[test]
fn emit_service_trait_uses_unit_when_types_omitted() {
let def = RpcServiceDef {
service: "PingService".to_owned(),
methods: vec![RpcMethodDef {
operation: "ping".to_owned(),
path: "/ping".to_owned(),
method: RpcHttpMethod::Get,
summary: None,
request_type: None,
response_type: None,
}],
};
let src = emit_service_trait(&def);
assert!(src.contains("fn ping(&self, request: ())"));
assert!(src.contains("Effect<(), RpcError, ()>"));
}
}