use axum::http::Method;
use prost_reflect::{DynamicMessage, ExtensionDescriptor, MethodDescriptor, Value};
use super::request::BodyMapping;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RouteMethod {
One(Method),
Any,
}
impl RouteMethod {
pub(crate) fn as_str(&self) -> &str {
match self {
Self::One(method) => method.as_str(),
Self::Any => "*",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct HttpBinding {
pub(crate) method: RouteMethod,
pub(crate) path: String,
pub(crate) body: BodyMapping,
pub(crate) response_body: Option<String>,
}
pub(crate) fn http_bindings(
method: &MethodDescriptor,
http_ext: &ExtensionDescriptor,
) -> Vec<HttpBinding> {
let options = method.options();
if !options.has_extension(http_ext) {
return Vec::new();
}
match options.get_extension(http_ext).as_ref() {
Value::Message(rule) => collect_bindings(rule),
_ => Vec::new(),
}
}
pub(crate) fn collect_bindings(rule: &DynamicMessage) -> Vec<HttpBinding> {
let mut bindings = Vec::new();
bindings.extend(parse_http_rule(rule));
if let Some(field) = rule.get_field_by_name("additional_bindings") {
if let Value::List(list) = field.as_ref() {
for item in list {
if let Value::Message(sub) = item {
bindings.extend(parse_http_rule(sub));
}
}
}
}
bindings
}
fn parse_http_rule(rule: &DynamicMessage) -> Option<HttpBinding> {
let (method, path) = standard_pattern(rule).or_else(|| custom_pattern(rule))?;
let body = string_field(rule, "body")
.map(|body| BodyMapping::parse(&body))
.unwrap_or(BodyMapping::None);
Some(HttpBinding {
method,
path,
body,
response_body: string_field(rule, "response_body"),
})
}
fn standard_pattern(rule: &DynamicMessage) -> Option<(RouteMethod, String)> {
[
("get", Method::GET),
("put", Method::PUT),
("post", Method::POST),
("delete", Method::DELETE),
("patch", Method::PATCH),
]
.into_iter()
.find_map(|(name, method)| Some((RouteMethod::One(method), string_field(rule, name)?)))
}
fn custom_pattern(rule: &DynamicMessage) -> Option<(RouteMethod, String)> {
let custom = rule.get_field_by_name("custom")?;
let Value::Message(custom) = custom.as_ref() else {
return None;
};
let kind = string_field(custom, "kind")?;
let path = string_field(custom, "path")?;
let method = if kind == "*" {
RouteMethod::Any
} else {
match Method::from_bytes(kind.as_bytes()) {
Ok(method) => RouteMethod::One(method),
Err(_) => {
tracing::warn!(%kind, %path, "custom HTTP rule kind is not a method token; skipping it");
return None;
}
}
};
Some((method, path))
}
fn string_field(msg: &DynamicMessage, name: &str) -> Option<String> {
match msg.get_field_by_name(name)?.as_ref() {
Value::String(s) if !s.is_empty() => Some(s.clone()),
_ => None,
}
}
#[cfg(test)]
mod tests;