use quote::{ToTokens, format_ident, quote};
use serde::{Deserialize, Serialize};
use serde_tokenstream::{Error, from_tokenstream};
use syn::spanned::Spanned;
use syn_parsing::ItemFnForSignature;
mod syn_parsing;
#[derive(Deserialize, Debug)]
#[serde(rename_all = "UPPERCASE")]
enum MethodType {
Delete,
Get,
Head,
Patch,
Post,
Put,
Options,
}
impl MethodType {
fn as_str(&self) -> &'static str {
match self {
MethodType::Delete => "DELETE",
MethodType::Get => "GET",
MethodType::Head => "HEAD",
MethodType::Patch => "PATCH",
MethodType::Post => "POST",
MethodType::Put => "PUT",
MethodType::Options => "OPTIONS",
}
}
}
#[derive(Deserialize, Debug)]
struct EndpointMetadata {
method: MethodType,
path: String,
http_response: Option<String>,
#[serde(default)]
tags: Vec<String>,
#[serde(default)]
unpublished: bool,
#[serde(default)]
deprecated: bool,
content_type: Option<String>,
#[serde(default)]
trace_level: Option<String>,
#[serde(default)]
no_common: bool,
#[serde(default)]
status_code_response: Option<String>,
#[serde(default)]
extra_tracing_fields: Vec<String>,
}
#[derive(Deserialize, Debug, Serialize)]
#[serde(rename_all = "UPPERCASE")]
enum ChannelProtocol {
Websockets,
}
impl std::fmt::Display for ChannelProtocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChannelProtocol::Websockets => write!(f, "WEBSOCKETS"),
}
}
}
#[derive(Deserialize, Debug)]
struct ChannelMetadata {
protocol: ChannelProtocol,
path: String,
#[serde(default)]
tags: Vec<String>,
#[serde(default)]
unpublished: bool,
#[serde(default)]
deprecated: bool,
#[serde(default)]
no_common: bool,
#[serde(default)]
trace_level: Option<String>,
#[serde(default)]
extra_tracing_fields: Vec<String>,
}
const USAGE: &str = "Endpoint handlers must have the following signature:
async fn(
rqctx: dropshot::RequestContext<MyContext>,
[query_params: Query<Q>,]
[path_params: Path<P>,]
[body_param: TypedBody<J>,]
[body_param: UntypedBody,]
[body_param: StreamingBody,]
[raw_request: RawRequest,]
) -> Result<HttpResponse*, HttpError>";
#[proc_macro_attribute]
pub fn zoo_endpoint(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
do_output(do_endpoint(attr.into(), item.into()))
}
fn do_endpoint(
attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
let metadata = from_tokenstream(&attr)?;
do_endpoint_inner(metadata, attr, item, None)
}
#[proc_macro_attribute]
pub fn zoo_channel(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
do_output(do_channel(attr.into(), item.into()))
}
fn do_channel(
attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
let ChannelMetadata {
protocol,
path,
tags,
unpublished,
deprecated,
no_common,
trace_level,
extra_tracing_fields,
} = from_tokenstream(&attr)?;
match protocol {
ChannelProtocol::Websockets => {
let metadata = EndpointMetadata {
method: MethodType::Get,
path,
tags,
unpublished,
deprecated,
http_response: None,
content_type: Some("application/json".to_string()),
no_common,
status_code_response: None,
trace_level,
extra_tracing_fields,
};
do_endpoint_inner(metadata, attr, item, Some(protocol))
}
}
}
fn do_output(res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>) -> proc_macro::TokenStream {
match res {
Err(err) => err.to_compile_error().into(),
Ok((endpoint, errors)) => {
let compiler_errors = errors.iter().map(|err| err.to_compile_error());
let output = quote! {
#endpoint
#( #compiler_errors )*
};
output.into()
}
}
}
fn do_endpoint_inner(
metadata: EndpointMetadata,
attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
protocol: Option<ChannelProtocol>,
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
let ast: ItemFnForSignature = syn::parse2(item.clone())?;
let method = metadata.method.as_str();
let path = metadata.path;
let content_type = metadata.content_type.unwrap_or_else(|| "application/json".to_string());
if !matches!(
content_type.as_str(),
"application/json" | "application/x-www-form-urlencoded" | "multipart/form-data"
) {
return Err(Error::new_spanned(&attr, "invalid content type for endpoint"));
}
let mut errors = Vec::new();
if ast.sig.constness.is_some() {
errors.push(Error::new_spanned(
ast.sig.constness,
"endpoint handlers may not be const functions",
));
}
if ast.sig.asyncness.is_none() {
errors.push(Error::new_spanned(
ast.sig.fn_token,
"endpoint handler functions must be async",
));
}
if ast.sig.unsafety.is_some() {
errors.push(Error::new_spanned(
ast.sig.unsafety,
"endpoint handlers may not be unsafe",
));
}
if ast.sig.abi.is_some() {
errors.push(Error::new_spanned(
&ast.sig.abi,
"endpoint handler may not use an alternate ABI",
));
}
if !ast.sig.generics.params.is_empty() {
errors.push(Error::new_spanned(
&ast.sig.generics,
"generics are not permitted for endpoint handlers",
));
}
if ast.sig.variadic.is_some() {
errors.push(Error::new_spanned(&ast.sig.variadic, "no language C here"));
}
let name = &ast.sig.ident;
let mod_name = format_ident!("zoo_endpoint_{}", name.to_string());
let method_ident = format_ident!("{}", method);
let visibility = &ast.vis;
let docs = ast
.attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("doc") {
Some(attr.to_token_stream())
} else {
None
}
})
.collect::<Vec<_>>();
let description_doc_comment = if docs.is_empty() {
quote! {}
} else {
quote! {
#(#docs)*
}
};
let tags = metadata
.tags
.iter()
.map(|tag| {
quote! { #tag }
})
.collect::<Vec<_>>();
let unpublished = metadata.unpublished;
let deprecated = metadata.deprecated;
let no_common = metadata.no_common;
let trace_level = if let Some(trace_level) = metadata.trace_level {
quote! { level = #trace_level, }
} else {
quote! {}
};
let http_status = if let Some(http_response) = metadata.http_response {
let http_response: syn::Type = syn::parse_str(&http_response).map_err(|e| Error::new_spanned(&attr, e))?;
let http_response_str = http_response.to_token_stream().to_string();
if http_response_str.starts_with("dropshot") {
Some(quote! { #http_response })
} else if http_response_str.contains("HttpResponseGenericNoContent")
|| http_response_str.contains("HttpResponseRedirect")
{
Some(quote! { common::server::http_response::#http_response })
} else {
Some(quote! { dropshot::#http_response })
}
} else {
None
};
let status_code_response = if let Some(status_code_response) = metadata.status_code_response {
let http_response: syn::Type =
syn::parse_str(&status_code_response).map_err(|e| Error::new_spanned(&attr, e))?;
let http_response_str = http_response.to_token_stream().to_string();
if http_response_str.starts_with("dropshot") {
Some(quote! { #http_response })
} else if http_response_str.contains("HttpResponseGenericNoContent")
|| http_response_str.contains("HttpResponseRedirect")
{
Some(quote! { common::server::http_response::#http_response })
} else {
Some(quote! { dropshot::#http_response })
}
} else {
None
};
let (first_arg, context) = match ast.sig.inputs.first() {
Some(syn::FnArg::Typed(syn::PatType {
attrs: _,
pat: _,
colon_token: _,
ty,
})) => {
if ty.to_token_stream().to_string().contains("RequestContext") {
(ty.to_token_stream(), quote! { rqctx })
} else {
(
quote! {
dropshot::RequestContext<#ty>
},
quote! { rqctx.context().clone() },
)
}
}
Some(first_arg @ syn::FnArg::Receiver(_)) => {
errors.push(Error::new(first_arg.span(), "Expected a non-receiver argument"));
(quote! { () }, quote! { () })
}
None => {
errors.push(Error::new(
ast.sig.paren_token.span.join(),
"Endpoint requires arguments",
));
(quote! { () }, quote! { () })
}
};
let return_type_inner = match &ast.sig.output {
syn::ReturnType::Default => quote! { () },
syn::ReturnType::Type(_, ty) => {
match &**ty {
syn::Type::Path(syn::TypePath { path, .. }) => {
let path = &path.segments;
if path.len() == 1 {
let seg = &path[0];
if seg.ident == "Result" {
if let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
args,
..
}) = &seg.arguments
{
if args.len() == 2 || args.len() == 1 {
let mut args = args.iter();
let ok = args.next().unwrap();
if let syn::GenericArgument::Type(ty) = ok {
quote! { #ty }
} else {
errors.push(Error::new_spanned(
ok,
"Expected a type argument for the Ok variant of the result",
));
quote! { () }
}
} else {
errors.push(Error::new_spanned(seg, "Expected two type arguments for the Result"));
quote! { () }
}
} else {
errors.push(Error::new_spanned(
seg,
"Expected angle-bracketed arguments for the Result",
));
quote! { () }
}
} else {
errors.push(Error::new_spanned(seg, "Expected a Result type for the return type"));
quote! { () }
}
} else {
if protocol.is_none() {
errors.push(Error::new_spanned(
path,
"Expected a single segment for the return type",
));
}
quote! { () }
}
}
_ => {
errors.push(Error::new_spanned(ty, "Expected a path type for the return type"));
quote! { () }
}
}
}
};
let return_type = if let Some(ref http_status) = http_status {
if http_status.to_string().contains("HttpResponseDeleted")
|| http_status.to_string().contains("HttpResponseGenericNoContent")
|| http_status.to_string().contains("HttpResponseUpdatedNoContent")
{
quote! { Result<#http_status, dropshot::HttpError> }
} else {
quote! { Result<#http_status<#return_type_inner>, dropshot::HttpError> }
}
} else if let Some(ref protocol) = protocol {
match protocol {
ChannelProtocol::Websockets => quote! { dropshot::WebsocketChannelResult },
}
} else {
quote! { Result<#return_type_inner, dropshot::HttpError> }
};
let arg_types = ast
.sig
.inputs
.iter()
.enumerate()
.flat_map(|(index, arg)| {
if index == 0 {
return None;
}
match arg {
syn::FnArg::Typed(syn::PatType {
attrs: _,
pat: _,
colon_token: _,
ty,
}) => Some(ty),
_ => None,
}
})
.collect::<Vec<_>>();
let arg_names = (0..arg_types.len())
.map(|i| {
let argname = format_ident!("arg{}", i);
quote! { #argname }
})
.collect::<Vec<_>>();
let inner = if let Some(ref http_status) = http_status {
if http_status.to_string().contains("HttpResponseDeleted")
|| http_status.to_string().contains("HttpResponseGenericNoContent")
|| http_status.to_string().contains("HttpResponseUpdatedNoContent")
|| http_status.to_string().contains("HttpResponseRedirect")
{
quote! {
let status_code = #http_status::STATUS_CODE;
root_span.record("http.response.status_code", status_code.as_u16());
Ok(#http_status)
}
} else {
quote! {
let status_code = #http_status::<#return_type_inner>::STATUS_CODE;
root_span.record("http.response.status_code", status_code.as_u16());
Ok(#http_status::<#return_type_inner>(r))
}
}
} else if let Some(ref status_code_response) = status_code_response {
if status_code_response.to_string().contains("HttpResponseDeleted")
|| status_code_response
.to_string()
.contains("HttpResponseGenericNoContent")
|| status_code_response
.to_string()
.contains("HttpResponseUpdatedNoContent")
|| status_code_response.to_string().contains("HttpResponseRedirect")
{
quote! {
let status_code = #status_code_response::STATUS_CODE;
root_span.record("http.response.status_code", status_code.as_u16());
Ok(r)
}
} else {
quote! {
let status_code = #status_code_response::<()>::STATUS_CODE;
root_span.record("http.response.status_code", status_code.as_u16());
Ok(r)
}
}
} else {
let inner_return_type = return_type_inner.to_string().replace(' ', "");
if inner_return_type.starts_with("hyper::Response<")
|| inner_return_type.starts_with("http::Response<")
|| inner_return_type.starts_with("http::response::Response<")
{
quote! {
let status_code = r.status().as_u16();
root_span.record("http.response.status_code", status_code);
Ok(r)
}
} else {
quote! {
root_span.record("http.response.status_code", 200);
Ok(r)
}
}
};
let tracing_skip_inner = if protocol.is_some() {
let found = arg_types.iter().enumerate().find_map(|(i, ty)| {
if ty.to_token_stream().to_string().contains("WebsocketConnection") {
Some(format_ident!("arg{}", i))
} else {
None
}
});
if let Some(found) = found {
quote! { #found }
} else {
quote! {}
}
} else {
quote! {}
};
let dropshot_macro = if let Some(ref protocol) = protocol {
let ident = format_ident!("{}", protocol.to_string());
quote! {
#[dropshot::channel {
protocol = #ident,
path = #path,
tags = [ #(#tags),* ],
deprecated = #deprecated,
unpublished = #unpublished,
}]
}
} else {
quote! {
#[dropshot::endpoint {
method = #method_ident,
path = #path,
tags = [ #(#tags),* ],
deprecated = #deprecated,
unpublished = #unpublished,
content_type = #content_type,
}]
}
};
let handle_error = if no_common {
quote! {
{
root_span.record("exception.original_error", e.to_string());
if e.is::<dropshot::HttpError>() {
let dropshot_error = e.downcast::<dropshot::HttpError>().unwrap();
root_span.record("http.response.status_code", dropshot_error.status_code.as_u16());
Err(dropshot_error.into())
} else {
root_span.record("http.response.status_code", 500);
Err(dropshot::HttpError::for_internal_error(format!("{e:?}")).into())
}
}
}
} else {
quote! {
{
root_span.record("exception.original_error", e.to_string());
let http_err = common::server::handle_anyhow_err_as_http_err(e.into());
root_span.record("http.response.status_code", http_err.status_code.as_u16());
Err(http_err.into())
}
}
};
let tracing_fields = metadata
.extra_tracing_fields
.iter()
.map(|field| {
let unquoted_field: syn::Expr = syn::parse_str(field).expect("Unable to parse tracing field");
quote! {
#unquoted_field = tracing::field::Empty
}
})
.collect::<Vec<_>>();
let stream = quote! {
#description_doc_comment
#[tracing::instrument(
skip(rqctx, #tracing_skip_inner),
parent = None,
fields(
http.request.method = #method,
http.route = #path,
http.response.status_code = 0,
trace.trace_id = tracing::field::Empty,
trace.parent_id = tracing::field::Empty,
api_call.id = tracing::field::Empty,
exception.original_error = tracing::field::Empty,
#(#tracing_fields),*
),
err,
#trace_level
)]
#dropshot_macro
#visibility async fn #name(
rqctx: #first_arg,
#(#arg_names: #arg_types),*
) -> #return_type
{
let root_span = tracing::Span::current();
root_span.record("api_call.id", rqctx.request_id.to_string());
use std::str::FromStr;
use dropshot::HttpCodedResponse;
use tracing_opentelemetry::OpenTelemetrySpanExt;
use opentelemetry::trace::TraceContextExt;
if let Some(traceparent) = rqctx.request.headers().get("traceparent") {
if let Ok(tps) = traceparent.to_str() {
if tps.len() == 55 {
let segs: Vec<&str> = tps.split('-').collect();
if segs.len() == 4 {
if let Ok(version) = u8::from_str_radix(segs[0], 16) {
if let Ok(trace_id) = u128::from_str_radix(segs[1], 16) {
if let Ok(parent_id) = u64::from_str_radix(segs[2], 16) {
if let Ok(flags) = u8::from_str_radix(segs[3], 16) {
let trace_flags = opentelemetry::trace::TraceFlags::new(flags) & opentelemetry::trace::TraceFlags::SAMPLED;
let trace_state = match rqctx.request.headers().get("tracestate") {
Some(trace_state) => {
opentelemetry::trace::TraceState::from_str(trace_state.to_str().unwrap_or_default()).unwrap_or_else(|_| opentelemetry::trace::TraceState::default())
}
None => opentelemetry::trace::TraceState::default(),
};
let span_context = opentelemetry::trace::SpanContext::new(
trace_id.into(),
parent_id.into(),
trace_flags,
true, trace_state,
);
if span_context.is_valid() {
let context = root_span.context().with_remote_span_context(span_context);
root_span.set_parent(context);
}
}
}
}
}
}
}
}
}
match #mod_name::#name(#context, #(#arg_names),*).await {
Ok(r) => {
#inner
}
Err(e) => #handle_error,
}
}
mod #mod_name {
use super::*;
#item
}
};
if !errors.is_empty() {
errors.insert(0, Error::new_spanned(&ast.sig, USAGE));
}
if path.contains(":.*}") && !metadata.unpublished {
errors.push(Error::new_spanned(
&attr,
"paths that contain a wildcard match must include 'unpublished = \
true'",
));
}
Ok((stream, errors))
}
#[cfg(test)]
fn clean_text(s: &str) -> String {
if cfg!(not(windows)) {
let regex = regex::Regex::new(r"(})(\n\s{0,8}[^} ])").unwrap();
regex.replace_all(s, "$1\n$2").to_string()
} else {
let regex = regex::Regex::new(r"(})(\r\n\s{0,8}[^} ])").unwrap();
regex.replace_all(s, "$1\r\n$2").to_string()
}
}
#[cfg(test)]
fn get_text_fmt(output: &proc_macro2::TokenStream) -> anyhow::Result<String> {
let content = rustfmt_wrapper::rustfmt(output).unwrap();
Ok(clean_text(&content))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_do_gen_basic_get() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
http_response = "dropshot::HttpResponseOk",
},
quote! {
async fn handler_xyz(
_rqctx: Arc<Context>,
) -> Result<()> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/basic-get.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_basic_deleted() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
http_response = "dropshot::HttpResponseDeleted",
},
quote! {
async fn handler_xyz(
_rqctx: Arc<Context>,
) -> Result<()> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/basic-deleted.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_more_params() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
http_response = "dropshot::HttpResponseOk",
},
quote! {
async fn handler_xyz(
_rqctx: Arc<Context>,
path_params: Path<UserPathParams>,
query_params: Query<UserQueryParams>,
body_param: TypedBody<UserBody>,
) -> Result<()> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/more-params.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_return_type() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
http_response = "dropshot::HttpResponseOk",
},
quote! {
async fn handler_xyz(
_rqctx: Arc<Context>,
path_params: Path<UserPathParams>,
query_params: Query<UserQueryParams>,
body_param: TypedBody<UserBody>,
) -> Result<serde_json::Value> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/return-type.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_raw_request_context() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
http_response = "dropshot::HttpResponseOk",
},
quote! {
async fn handler_xyz(
_rqctx: RequestContext<Arc<Context>>,
path_params: Path<UserPathParams>,
query_params: Query<UserQueryParams>,
body_param: TypedBody<UserBody>,
) -> Result<serde_json::Value> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/raw-request-context.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_raw_response() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
},
quote! {
async fn handler_xyz(
_rqctx: RequestContext<Arc<Context>>,
) -> Result<hyper::Response<hyper::Body>> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/raw-response.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_channel() {
let (item, errors) = do_channel(
quote! {
protocol = WEBSOCKETS,
path = "/term",
tags = ["term"]
},
quote! {
async fn create_term(
rqctx: dropshot::RequestContext<Arc<Context>>,
upgraded: dropshot::WebsocketConnection,
) -> Result<()> {
let token = rqctx.context().require_auth(&rqctx).await?;
match crate::server::handlers::create_term(&rqctx, upgraded, &token).await {
Ok(_) => Ok(()),
Err(err) => Err(common::error::Error::InternalError {
internal_message: format!("term stopped with error: {}", err),
}
.into()),
}
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/channel.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_channel_more_args() {
let (item, errors) = do_channel(
quote! {
protocol = WEBSOCKETS,
path = "/term",
tags = ["term"]
},
quote! {
async fn create_term(
rqctx: dropshot::RequestContext<Arc<Context>>,
query_params: Query<Thing>,
upgraded: dropshot::WebsocketConnection,
) -> Result<()> {
let token = rqctx.context().require_auth(&rqctx).await?;
match crate::server::handlers::create_term(&rqctx, upgraded, &token).await {
Ok(_) => Ok(()),
Err(err) => Err(common::error::Error::InternalError {
internal_message: format!("term stopped with error: {}", err),
}
.into()),
}
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/channel-more-args.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_basic_status_code_response() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
status_code_response = "dropshot::HttpResponseOk",
},
quote! {
async fn handler_xyz(
_rqctx: Arc<Context>,
) -> Result<()> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/basic-status-code-response.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_basic_no_common() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
http_response = "dropshot::HttpResponseOk",
no_common = true,
},
quote! {
async fn handler_xyz(
_rqctx: Arc<Context>,
) -> Result<()> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/basic-no-common.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_basic_trace_level() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
http_response = "dropshot::HttpResponseOk",
trace_level = "trace",
},
quote! {
async fn handler_xyz(
_rqctx: Arc<Context>,
) -> Result<()> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/basic-trace-level.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_basic_hyper_response() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
},
quote! {
async fn handler_xyz(
_rqctx: Arc<Context>,
) -> Result<hyper::Response<hyper::Body>> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/basic-hyper-response.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_basic_http_response() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
},
quote! {
async fn handler_xyz(
_rqctx: Arc<Context>,
) -> Result<http::Response<hyper::Body>> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/basic-http-response.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_basic_http_response_with_extra_fields() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
extra_tracing_fields = ["foo", "foo.bar"]
},
quote! {
async fn handler_xyz(
_rqctx: Arc<Context>,
) -> Result<http::Response<hyper::Body>> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/extra-tracing-fields.rs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_do_gen_basic_http_response_diff_path() {
let (item, errors) = do_endpoint(
quote! {
method = GET,
path = "/a/b/c",
tags = ["tag1", "tag2"],
},
quote! {
async fn handler_xyz(
_rqctx: Arc<Context>,
) -> Result<http::response::Response<hyper::Body>> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents(
"tests/basic-http-response-diff-path.rs.gen",
&get_text_fmt(&item).unwrap(),
);
}
#[test]
fn test_do_gen_basic_no_common_websocket() {
let (item, errors) = do_channel(
quote! {
protocol = WEBSOCKETS,
path = "/a/b/c",
tags = ["tag1", "tag2"],
no_common = true,
},
quote! {
async fn handler_xyz(
_rqctx: Arc<Context>,
) -> Result<http::response::Response<hyper::Body>> {
Ok(())
}
},
)
.unwrap();
if !errors.is_empty() {
for e in &errors {
eprintln!("{e}");
}
}
assert!(errors.is_empty());
expectorate::assert_contents("tests/basic-no-common-websocket.rs.gen", &get_text_fmt(&item).unwrap());
}
}