use crate::analysis::{OperationInfo, RequestBodyContent, SchemaAnalysis};
use crate::config::ServerSection;
use crate::generator::{GeneratedFile, GeneratorConfig};
use super::{OperationIndex, Selector};
use heck::{ToPascalCase, ToSnakeCase};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use std::collections::BTreeMap;
use std::path::PathBuf;
pub fn reachable_schemas(
analysis: &SchemaAnalysis,
ops: &[&OperationInfo],
) -> std::collections::BTreeSet<String> {
let mut keep: std::collections::BTreeSet<String> = Default::default();
let mut queue: Vec<String> = Vec::new();
let seed =
|name: &str, queue: &mut Vec<String>, keep: &mut std::collections::BTreeSet<String>| {
if !name.is_empty() && keep.insert(name.to_string()) {
queue.push(name.to_string());
}
};
for op in ops {
if let Some(rb) = &op.request_body
&& let Some(name) = rb.schema_name()
{
seed(name, &mut queue, &mut keep);
}
for ty in op.response_schemas.values() {
seed(ty, &mut queue, &mut keep);
}
for p in &op.parameters {
if let Some(name) = &p.schema_ref {
seed(name, &mut queue, &mut keep);
}
}
}
let ref_named = collect_all_ref_names(analysis);
for name in analysis.schemas.keys() {
if !ref_named.contains(name) && keep.insert(name.clone()) {
queue.push(name.clone());
}
}
while let Some(name) = queue.pop() {
if let Some(schema) = analysis.schemas.get(&name) {
collect_refs(&schema.original, &mut queue, &mut keep);
for dep in &schema.dependencies {
seed(dep, &mut queue, &mut keep);
}
}
}
keep
}
fn collect_all_ref_names(analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
let mut out: std::collections::HashSet<String> = Default::default();
for schema in analysis.schemas.values() {
gather_refs(&schema.original, &mut out);
}
for op in analysis.operations.values() {
if let Some(rb) = &op.request_body
&& let Some(name) = rb.schema_name()
{
out.insert(name.to_string());
}
for ty in op.response_schemas.values() {
out.insert(ty.clone());
}
for p in &op.parameters {
if let Some(name) = &p.schema_ref {
out.insert(name.clone());
}
}
}
out
}
fn gather_refs(value: &serde_json::Value, out: &mut std::collections::HashSet<String>) {
match value {
serde_json::Value::Object(map) => {
for (k, v) in map {
if k == "$ref"
&& let Some(s) = v.as_str()
&& let Some(name) = s.strip_prefix("#/components/schemas/")
{
out.insert(name.to_string());
}
gather_refs(v, out);
}
}
serde_json::Value::Array(items) => items.iter().for_each(|v| gather_refs(v, out)),
_ => {}
}
}
fn collect_refs(
value: &serde_json::Value,
queue: &mut Vec<String>,
keep: &mut std::collections::BTreeSet<String>,
) {
match value {
serde_json::Value::Object(map) => {
for (k, v) in map {
if k == "$ref"
&& let Some(s) = v.as_str()
&& let Some(name) = s.strip_prefix("#/components/schemas/")
&& keep.insert(name.to_string())
{
queue.push(name.to_string());
}
collect_refs(v, queue, keep);
}
}
serde_json::Value::Array(items) => {
for v in items {
collect_refs(v, queue, keep);
}
}
_ => {}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ServerCodegenError {
#[error("server selector: {0}")]
Parse(#[from] super::SelectorParseError),
#[error("server selector: {0}")]
Resolve(#[from] super::SelectorResolveError),
#[error("internal: {0}")]
Internal(String),
}
pub struct ServerCodegen<'a> {
config: &'a GeneratorConfig,
analysis: &'a SchemaAnalysis,
server: &'a ServerSection,
}
impl<'a> ServerCodegen<'a> {
pub fn new(
config: &'a GeneratorConfig,
analysis: &'a SchemaAnalysis,
server: &'a ServerSection,
) -> Self {
Self {
config,
analysis,
server,
}
}
pub fn generate(&self) -> Result<Vec<GeneratedFile>, ServerCodegenError> {
if self.server.operations.is_empty() {
return Ok(Vec::new());
}
let index = OperationIndex::from_analysis(self.analysis);
let selectors: Vec<Selector> = self
.server
.operations
.iter()
.map(|s| Selector::parse(s))
.collect::<Result<_, _>>()?;
let resolution = super::resolve(&selectors, &index)?;
let ops: Vec<&OperationInfo> = resolution
.operations
.iter()
.map(|s| {
self.analysis
.operations
.get(&s.operation_id)
.ok_or_else(|| {
ServerCodegenError::Internal(format!(
"operation `{}` resolved but missing from analysis",
s.operation_id
))
})
})
.collect::<Result<_, _>>()?;
let groups = group_by_tag(&ops);
let api_rs = self.emit_api(&groups);
let errors_rs = self.emit_errors(&ops);
let router_rs = self.emit_router(&groups);
let mod_rs = self.emit_mod();
Ok(vec![
GeneratedFile {
path: PathBuf::from("server").join("mod.rs"),
content: format_or_raw(mod_rs),
},
GeneratedFile {
path: PathBuf::from("server").join("api.rs"),
content: format_or_raw(api_rs),
},
GeneratedFile {
path: PathBuf::from("server").join("errors.rs"),
content: format_or_raw(errors_rs),
},
GeneratedFile {
path: PathBuf::from("server").join("router.rs"),
content: format_or_raw(router_rs),
},
])
}
fn emit_mod(&self) -> TokenStream {
let _ = &self.config; quote! {
pub mod api;
pub mod errors;
pub mod router;
pub use api::*;
pub use errors::*;
pub use router::*;
}
}
fn emit_router(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
let factories: Vec<TokenStream> = groups
.iter()
.map(|(tag, ops)| self.emit_router_for_trait(tag, ops))
.collect();
let query_structs: Vec<TokenStream> = groups
.values()
.flatten()
.filter_map(|op| self.emit_query_struct(op))
.collect();
let combined = if groups.len() > 1 {
Some(self.emit_combined_router(groups))
} else {
None
};
quote! {
use super::api::*;
use super::errors::*;
#[allow(unused_imports)]
use super::super::types::*;
#(#query_structs)*
#(#factories)*
#combined
}
}
fn emit_combined_router(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
let entries: Vec<(syn::Ident, syn::Ident, syn::Ident)> = groups
.keys()
.enumerate()
.map(|(i, tag)| {
let trait_ident = trait_ident_for_tag(tag);
let factory = format_ident!("{}_router", trait_ident.to_string().to_snake_case());
let generic = format_ident!("T{}", i + 1);
(trait_ident, factory, generic)
})
.collect();
let generics: Vec<&syn::Ident> = entries.iter().map(|(_, _, g)| g).collect();
let args: Vec<TokenStream> = entries
.iter()
.map(|(trait_ident, _, g)| {
let arg_ident = format_ident!("{}", trait_ident.to_string().to_snake_case());
quote! { #arg_ident: #g }
})
.collect();
let bounds: Vec<TokenStream> = entries
.iter()
.map(|(trait_ident, _, g)| {
quote! { #g: #trait_ident + Clone + Send + Sync + 'static }
})
.collect();
let first = &entries[0];
let first_arg = format_ident!("{}", first.0.to_string().to_snake_case());
let first_factory = &first.1;
let rest = entries
.iter()
.skip(1)
.map(|(trait_ident, factory, _)| {
let arg = format_ident!("{}", trait_ident.to_string().to_snake_case());
quote! { .merge(#factory(#arg)) }
})
.collect::<Vec<_>>();
let trait_names: Vec<String> = entries.iter().map(|(t, _, _)| t.to_string()).collect();
let doc = format!(
" Combined router spanning {} traits: {}.",
entries.len(),
trait_names.join(", "),
);
quote! {
#[doc = #doc]
pub fn build_router<#(#generics),*>(
#(#args),*
) -> ::axum::Router
where
#(#bounds),*
{
#first_factory(#first_arg) #(#rest)*
}
}
}
fn emit_router_for_trait(&self, tag: &str, ops: &[&OperationInfo]) -> TokenStream {
let trait_ident = trait_ident_for_tag(tag);
let fn_ident = format_ident!("{}_router", trait_ident.to_string().to_snake_case());
let routes: Vec<TokenStream> = ops
.iter()
.map(|op| {
let method = axum_method_call(&op.method);
let handler = format_ident!("{}_handler", op.operation_id.to_snake_case());
let path = openapi_to_axum_path(&op.path);
quote! { .route(#path, ::axum::routing::#method(#handler::<T>)) }
})
.collect();
let handlers: Vec<TokenStream> = ops
.iter()
.map(|op| self.emit_axum_handler(&trait_ident, op))
.collect();
let doc = format!(" Build an axum::Router for the `{trait_ident}` trait.");
quote! {
#[doc = #doc]
pub fn #fn_ident<T>(api: T) -> ::axum::Router
where
T: #trait_ident + Clone + Send + Sync + 'static,
{
::axum::Router::new()
#(#routes)*
.with_state(api)
}
#(#handlers)*
}
}
fn emit_axum_handler(&self, trait_ident: &syn::Ident, op: &OperationInfo) -> TokenStream {
let handler_ident = format_ident!("{}_handler", op.operation_id.to_snake_case());
let trait_method = format_ident!("{}", op.operation_id.to_snake_case());
let mut extractors: Vec<TokenStream> =
vec![quote! { ::axum::extract::State(api): ::axum::extract::State<T> }];
let mut call_args: Vec<TokenStream> = Vec::new();
let path_params: Vec<&_> = op
.parameters
.iter()
.filter(|p| p.location == "path")
.collect();
if !path_params.is_empty() {
let idents: Vec<syn::Ident> = path_params
.iter()
.map(|p| format_ident!("{}", p.name.to_snake_case()))
.collect();
let types: Vec<TokenStream> = path_params
.iter()
.map(|p| parse_type(&p.rust_type))
.collect();
if path_params.len() == 1 {
let i = &idents[0];
let t = &types[0];
extractors.push(quote! { ::axum::extract::Path(#i): ::axum::extract::Path<#t> });
} else {
extractors.push(quote! { ::axum::extract::Path((#(#idents),*)): ::axum::extract::Path<(#(#types),*)> });
}
for i in &idents {
call_args.push(quote! { #i });
}
}
let query_params: Vec<&_> = op
.parameters
.iter()
.filter(|p| p.location == "query")
.collect();
let mut required_query_checks: Vec<TokenStream> = Vec::new();
if !query_params.is_empty() {
let query_ident = format_ident!("{}Query", op.operation_id.to_pascal_case());
extractors.push(quote! {
::axum::extract::Query(__q): ::axum::extract::Query<#query_ident>
});
for p in &query_params {
let f = format_ident!("{}", p.name.to_snake_case());
let wire = p.name.as_str();
if p.required {
let missing_msg = format!("missing required query parameter `{wire}`");
required_query_checks.push(quote! {
let #f = match __q.#f {
Some(v) => v,
None => return ::axum::response::IntoResponse::into_response(
(
::axum::http::StatusCode::BAD_REQUEST,
::axum::Json(::serde_json::json!({
"error": #missing_msg
})),
)
),
};
});
call_args.push(quote! { #f });
} else {
call_args.push(quote! { __q.#f });
}
}
}
let header_params: Vec<&_> = op
.parameters
.iter()
.filter(|p| p.location == "header")
.collect();
let mut required_header_checks: Vec<TokenStream> = Vec::new();
if !header_params.is_empty() {
extractors.push(quote! { __headers: ::axum::http::HeaderMap });
for p in &header_params {
let wire = p.name.as_str();
let ident = format_ident!("{}", header_param_ident(&p.name));
if p.required {
let missing_msg = format!("missing required header `{wire}`");
required_header_checks.push(quote! {
let #ident = match __headers
.get(#wire)
.and_then(|v| v.to_str().ok())
.map(::std::string::String::from)
{
Some(v) => v,
None => return ::axum::response::IntoResponse::into_response(
(
::axum::http::StatusCode::BAD_REQUEST,
::axum::Json(::serde_json::json!({
"error": #missing_msg
})),
)
),
};
});
call_args.push(quote! { #ident });
} else {
call_args.push(quote! {
__headers
.get(#wire)
.and_then(|v| v.to_str().ok())
.map(::std::string::String::from)
});
}
}
}
let body_ty_opt = body_type(op);
if let Some(body_ty) = &body_ty_opt {
let body_ty_tokens = parse_type(body_ty);
if op.request_body_required {
extractors.push(quote! {
::axum::Json(body): ::axum::Json<#body_ty_tokens>
});
call_args.push(quote! { body });
} else {
extractors.push(quote! {
body: ::std::option::Option<::axum::Json<#body_ty_tokens>>
});
call_args.push(quote! { body.map(|::axum::Json(b)| b) });
}
}
let _ = format_ident!("{}Response", op.operation_id.to_pascal_case());
let _ = trait_ident;
quote! {
async fn #handler_ident<T>(
#(#extractors),*
) -> ::axum::response::Response
where
T: super::api::#trait_ident + Clone + Send + Sync + 'static,
{
#(#required_query_checks)*
#(#required_header_checks)*
::axum::response::IntoResponse::into_response(
api.#trait_method(#(#call_args),*).await,
)
}
}
}
fn emit_api(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
let _ = &self.config; let traits: Vec<TokenStream> = groups
.iter()
.map(|(tag, ops)| self.emit_trait(tag, ops))
.collect();
let mut emitted: std::collections::BTreeSet<String> = Default::default();
let mut param_enums: Vec<TokenStream> = Vec::new();
for op in groups.values().flatten() {
for p in &op.parameters {
if let Some(values) = &p.enum_values {
if emitted.insert(p.rust_type.clone()) {
param_enums.push(emit_param_enum(&p.rust_type, values));
}
}
}
}
quote! {
#![allow(clippy::too_many_arguments)]
use super::errors::*;
#[allow(unused_imports)]
use super::super::types::*;
#(#param_enums)*
#(#traits)*
}
}
fn emit_trait(&self, tag: &str, ops: &[&OperationInfo]) -> TokenStream {
let trait_ident = trait_ident_for_tag(tag);
let methods: Vec<TokenStream> = ops.iter().map(|op| self.emit_method_sig(op)).collect();
let doc = format!(" Operations under the `{tag}` tag.");
quote! {
#[doc = #doc]
#[axum::async_trait]
pub trait #trait_ident: Send + Sync + 'static {
#(#methods)*
}
}
}
fn emit_method_sig(&self, op: &OperationInfo) -> TokenStream {
let name = format_ident!("{}", op.operation_id.to_snake_case());
let response_ty = format_ident!("{}Response", op.operation_id.to_pascal_case());
let mut params: Vec<TokenStream> = Vec::new();
for p in &op.parameters {
if p.location == "path" {
let ident = format_ident!("{}", p.name.to_snake_case());
let ty = parse_type(&p.rust_type);
params.push(quote! { #ident: #ty });
}
}
for p in &op.parameters {
if p.location == "query" {
let ident = format_ident!("{}", p.name.to_snake_case());
let ty = parse_type(&p.rust_type);
if p.required {
params.push(quote! { #ident: #ty });
} else {
params.push(quote! { #ident: ::std::option::Option<#ty> });
}
}
}
for p in &op.parameters {
if p.location == "header" {
let ident = format_ident!("{}", header_param_ident(&p.name));
if p.required {
params.push(quote! { #ident: String });
} else {
params.push(quote! { #ident: ::std::option::Option<String> });
}
}
}
if let Some(body) = body_type(op) {
let body_ty = parse_type(&body);
if op.request_body_required {
params.push(quote! { body: #body_ty });
} else {
params.push(quote! { body: Option<#body_ty> });
}
}
let summary_doc = op
.summary
.as_deref()
.map(|s| format!(" {s}"))
.unwrap_or_default();
let route_doc = format!(" `{} {}`", op.method, op.path);
quote! {
#[doc = #summary_doc]
#[doc = ""]
#[doc = #route_doc]
async fn #name(&self, #(#params),*) -> #response_ty;
}
}
fn emit_query_struct(&self, op: &OperationInfo) -> Option<TokenStream> {
let query_params: Vec<&_> = op
.parameters
.iter()
.filter(|p| p.location == "query")
.collect();
if query_params.is_empty() {
return None;
}
let ident = format_ident!("{}Query", op.operation_id.to_pascal_case());
let fields: Vec<TokenStream> = query_params
.iter()
.map(|p| {
let f_ident = format_ident!("{}", p.name.to_snake_case());
let ty = parse_type(&p.rust_type);
let serde_rename = if p.name.to_snake_case() == p.name {
quote! {}
} else {
let wire = p.name.as_str();
quote! { #[serde(rename = #wire)] }
};
quote! {
#serde_rename
#[serde(default)]
pub #f_ident: ::std::option::Option<#ty>
}
})
.collect();
let doc = format!(
" Query parameters for `{} {}` (operationId `{}`).",
op.method, op.path, op.operation_id
);
Some(quote! {
#[doc = #doc]
#[derive(Debug, Default, ::serde::Deserialize)]
pub struct #ident {
#(#fields),*
}
})
}
fn emit_errors(&self, ops: &[&OperationInfo]) -> TokenStream {
let _ = &self.config; let any_streaming = ops.iter().any(|op| op.supports_streaming);
let enums: Vec<TokenStream> = ops.iter().map(|op| self.emit_response_enum(op)).collect();
let stream_alias = if any_streaming {
quote! {
pub type ServerEventStream = ::std::pin::Pin<
Box<
dyn ::futures_core::Stream<
Item = ::std::result::Result<
::axum::response::sse::Event,
::std::convert::Infallible,
>,
> + ::std::marker::Send
+ 'static,
>,
>;
pub fn sse_response<S>(stream: S) -> ::axum::response::sse::Sse<ServerEventStream>
where
S: ::futures_core::Stream<
Item = ::std::result::Result<
::axum::response::sse::Event,
::std::convert::Infallible,
>,
> + ::std::marker::Send
+ 'static,
{
::axum::response::sse::Sse::new(Box::pin(stream))
}
}
} else {
quote! {}
};
quote! {
#![allow(clippy::large_enum_variant)]
use axum::{
http::StatusCode,
response::IntoResponse,
Json,
};
#[allow(unused_imports)]
use super::super::types::*;
#stream_alias
#(#enums)*
}
}
fn emit_response_enum(&self, op: &OperationInfo) -> TokenStream {
let enum_ident = format_ident!("{}Response", op.operation_id.to_pascal_case());
let mut variants: Vec<TokenStream> = Vec::new();
let mut arms: Vec<TokenStream> = Vec::new();
for (status, schema_name) in &op.response_schemas {
let variant = format_ident!("{}", status_variant_name(status));
let body_ty = parse_type(schema_name);
variants.push(quote! { #variant(#body_ty) });
let status_expr = status_token(status);
arms.push(quote! {
Self::#variant(body) => (#status_expr, Json(body)).into_response()
});
}
if op.supports_streaming {
variants.push(quote! {
OkStream(::axum::response::sse::Sse<ServerEventStream>)
});
arms.push(quote! {
Self::OkStream(sse) => sse.into_response()
});
}
if variants.is_empty() {
variants.push(quote! { Empty });
arms.push(quote! {
Self::Empty => StatusCode::NO_CONTENT.into_response()
});
}
let doc = format!(
" Response for `{} {}` (operationId `{}`).",
op.method, op.path, op.operation_id
);
quote! {
#[doc = #doc]
pub enum #enum_ident {
#(#variants),*
}
impl IntoResponse for #enum_ident {
fn into_response(self) -> ::axum::response::Response {
match self {
#(#arms),*
}
}
}
}
}
}
fn header_param_ident(name: &str) -> String {
name.replace('-', "_").to_snake_case()
}
fn emit_param_enum(name: &str, values: &[String]) -> TokenStream {
let enum_ident = format_ident!("{}", name);
let variants: Vec<TokenStream> = values
.iter()
.enumerate()
.map(|(i, raw)| {
let pascal = raw.to_pascal_case();
let starts_with_digit = pascal
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(true);
let v_name = if pascal.is_empty() || starts_with_digit {
format!("Variant{i}")
} else {
pascal
};
let v_ident = format_ident!("{}", v_name);
let default_marker = if i == 0 {
quote! { #[default] }
} else {
quote! {}
};
quote! {
#default_marker
#[serde(rename = #raw)]
#v_ident
}
})
.collect();
quote! {
#[derive(Debug, Clone, PartialEq, Eq, ::serde::Deserialize, ::serde::Serialize, Default)]
pub enum #enum_ident {
#(#variants),*
}
}
}
fn axum_method_call(method: &str) -> TokenStream {
match method.to_ascii_uppercase().as_str() {
"GET" => quote! { get },
"POST" => quote! { post },
"PUT" => quote! { put },
"PATCH" => quote! { patch },
"DELETE" => quote! { delete },
"HEAD" => quote! { head },
"OPTIONS" => quote! { options },
_ => quote! { any },
}
}
fn openapi_to_axum_path(p: &str) -> String {
p.to_string()
}
fn body_type(op: &OperationInfo) -> Option<String> {
match &op.request_body {
Some(RequestBodyContent::Json { schema_name })
| Some(RequestBodyContent::FormUrlEncoded { schema_name }) => Some(schema_name.clone()),
_ => None,
}
}
fn group_by_tag<'a>(ops: &[&'a OperationInfo]) -> BTreeMap<String, Vec<&'a OperationInfo>> {
let mut groups: BTreeMap<String, Vec<&OperationInfo>> = BTreeMap::new();
for op in ops {
let tag = op.tags.first().cloned().unwrap_or_else(|| "Server".into());
groups.entry(tag).or_default().push(op);
}
groups
}
fn trait_ident_for_tag(tag: &str) -> syn::Ident {
let pascal = tag.to_pascal_case();
let base = if pascal.is_empty() {
"Server".into()
} else {
pascal
};
format_ident!("{}Api", base)
}
fn status_variant_name(status: &str) -> String {
match status {
"200" => "Ok".into(),
"201" => "Created".into(),
"202" => "Accepted".into(),
"204" => "NoContent".into(),
"301" => "MovedPermanently".into(),
"302" => "Found".into(),
"304" => "NotModified".into(),
"400" => "BadRequest".into(),
"401" => "Unauthorized".into(),
"403" => "Forbidden".into(),
"404" => "NotFound".into(),
"409" => "Conflict".into(),
"410" => "Gone".into(),
"422" => "UnprocessableEntity".into(),
"429" => "TooManyRequests".into(),
"500" => "InternalServerError".into(),
"502" => "BadGateway".into(),
"503" => "ServiceUnavailable".into(),
"default" => "Default".into(),
"2XX" => "Success".into(),
"3XX" => "Redirection".into(),
"4XX" => "ClientError".into(),
"5XX" => "ServerError".into(),
other => format!("Status{}", other.to_ascii_uppercase().replace('X', "x")),
}
}
fn status_token(status: &str) -> TokenStream {
match status {
"200" => quote! { StatusCode::OK },
"201" => quote! { StatusCode::CREATED },
"202" => quote! { StatusCode::ACCEPTED },
"204" => quote! { StatusCode::NO_CONTENT },
"301" => quote! { StatusCode::MOVED_PERMANENTLY },
"302" => quote! { StatusCode::FOUND },
"304" => quote! { StatusCode::NOT_MODIFIED },
"400" => quote! { StatusCode::BAD_REQUEST },
"401" => quote! { StatusCode::UNAUTHORIZED },
"403" => quote! { StatusCode::FORBIDDEN },
"404" => quote! { StatusCode::NOT_FOUND },
"409" => quote! { StatusCode::CONFLICT },
"410" => quote! { StatusCode::GONE },
"422" => quote! { StatusCode::UNPROCESSABLE_ENTITY },
"429" => quote! { StatusCode::TOO_MANY_REQUESTS },
"500" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
"502" => quote! { StatusCode::BAD_GATEWAY },
"503" => quote! { StatusCode::SERVICE_UNAVAILABLE },
"default" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
"2XX" => quote! { StatusCode::OK },
"3XX" => quote! { StatusCode::MOVED_PERMANENTLY },
"4XX" => quote! { StatusCode::BAD_REQUEST },
"5XX" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
other => {
if let Ok(n) = other.parse::<u16>() {
quote! {
StatusCode::from_u16(#n).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
}
} else {
quote! { StatusCode::INTERNAL_SERVER_ERROR }
}
}
}
}
fn parse_type(ty: &str) -> TokenStream {
syn::parse_str::<syn::Type>(ty)
.map(|t| quote! { #t })
.unwrap_or_else(|_| {
let ident = format_ident!("{}", ty);
quote! { #ident }
})
}
fn format_or_raw(ts: TokenStream) -> String {
let raw = ts.to_string();
match syn::parse_file(&raw) {
Ok(parsed) => prettyplease::unparse(&parsed),
Err(_) => raw,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_variant_name_maps_known_codes() {
assert_eq!(status_variant_name("200"), "Ok");
assert_eq!(status_variant_name("4XX"), "ClientError");
assert_eq!(status_variant_name("default"), "Default");
assert_eq!(status_variant_name("418"), "Status418");
}
#[test]
fn trait_ident_for_tag_appends_api() {
let id = trait_ident_for_tag("Responses");
assert_eq!(id.to_string(), "ResponsesApi");
}
#[test]
fn untagged_falls_back_to_server_api() {
let id = trait_ident_for_tag("");
assert_eq!(id.to_string(), "ServerApi");
}
}