gotham_restful_derive/
endpoint.rs

1use crate::{
2	util::{CollectToResult, ExpectLit, IntoIdent},
3	AttributeArgs
4};
5use lazy_regex::regex_is_match;
6use paste::paste;
7use proc_macro2::{Ident, Span, TokenStream};
8use quote::{format_ident, quote, quote_spanned, ToTokens};
9use std::str::FromStr;
10use syn::{
11	parse::Parse, spanned::Spanned, Attribute, Error, Expr, FnArg, ItemFn, LitBool, LitStr, Meta,
12	PatType, Result, ReturnType, Type
13};
14use unindent::Unindent;
15
16#[allow(clippy::large_enum_variant)]
17pub enum EndpointType {
18	ReadAll,
19	Read,
20	Search,
21	Create,
22	UpdateAll,
23	Update,
24	DeleteAll,
25	Delete,
26	Custom {
27		method: Option<Expr>,
28		uri: Option<LitStr>,
29		params: Option<LitBool>,
30		body: Option<LitBool>
31	}
32}
33
34impl EndpointType {
35	pub fn custom() -> Self {
36		Self::Custom {
37			method: None,
38			uri: None,
39			params: None,
40			body: None
41		}
42	}
43}
44
45macro_rules! endpoint_type_setter {
46	($name:ident : $ty:ty) => {
47		impl EndpointType {
48			paste! {
49				fn [<set_ $name>](&mut self, span: Span, [<new_ $name>]: $ty) -> Result<()> {
50					match self {
51						Self::Custom { $name, .. } if $name.is_some() => {
52							Err(Error::new(span, concat!("`", stringify!($name), "` must not appear more than once")))
53						},
54						Self::Custom { $name, .. } => {
55							*$name = Some([<new_ $name>]);
56							Ok(())
57						},
58						_ => Err(Error::new(span, concat!("`", stringify!($name), "` can only be used on custom endpoints")))
59					}
60				}
61			}
62		}
63	};
64}
65
66endpoint_type_setter!(method: Expr);
67endpoint_type_setter!(uri: LitStr);
68endpoint_type_setter!(params: LitBool);
69endpoint_type_setter!(body: LitBool);
70
71impl FromStr for EndpointType {
72	type Err = Error;
73
74	fn from_str(str: &str) -> Result<Self> {
75		match str {
76			"ReadAll" | "read_all" => Ok(Self::ReadAll),
77			"Read" | "read" => Ok(Self::Read),
78			"Search" | "search" => Ok(Self::Search),
79			"Create" | "create" => Ok(Self::Create),
80			"ChangeAll" | "change_all" => Ok(Self::UpdateAll),
81			"Change" | "change" => Ok(Self::Update),
82			"RemoveAll" | "remove_all" => Ok(Self::DeleteAll),
83			"Remove" | "remove" => Ok(Self::Delete),
84			_ => Err(Error::new(
85				Span::call_site(),
86				format!("Unknown method: `{str}'")
87			))
88		}
89	}
90}
91
92impl EndpointType {
93	fn http_method(&self) -> Option<TokenStream> {
94		let hyper_method = quote!(::gotham_restful::gotham::hyper::Method);
95		match self {
96			Self::ReadAll | Self::Read | Self::Search => Some(quote!(#hyper_method::GET)),
97			Self::Create => Some(quote!(#hyper_method::POST)),
98			Self::UpdateAll | Self::Update => Some(quote!(#hyper_method::PUT)),
99			Self::DeleteAll | Self::Delete => Some(quote!(#hyper_method::DELETE)),
100			Self::Custom { method, .. } => method.as_ref().map(ToTokens::to_token_stream)
101		}
102	}
103
104	fn uri(&self) -> Option<TokenStream> {
105		match self {
106			Self::ReadAll | Self::Create | Self::UpdateAll | Self::DeleteAll => Some(quote!("")),
107			Self::Read | Self::Update | Self::Delete => Some(quote!(":id")),
108			Self::Search => Some(quote!("search")),
109			Self::Custom { uri, .. } => uri.as_ref().map(ToTokens::to_token_stream)
110		}
111	}
112
113	fn operation_verb(&self) -> TokenStream {
114		let some = quote!(::core::option::Option::Some);
115		match self {
116			Self::ReadAll => quote!(#some("read_all")),
117			Self::Read => quote!(#some("read")),
118			Self::Search => quote!(#some("search")),
119			Self::Create => quote!(#some("create")),
120			Self::UpdateAll => quote!(#some("update_all")),
121			Self::Update => quote!(#some("update")),
122			Self::DeleteAll => quote!(#some("delete_all")),
123			Self::Delete => quote!(#some("delete")),
124			Self::Custom { .. } => quote!(::core::option::Option::None)
125		}
126	}
127
128	fn has_placeholders(&self) -> LitBool {
129		match self {
130			Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => {
131				LitBool {
132					value: false,
133					span: Span::call_site()
134				}
135			},
136			Self::Read | Self::Update | Self::Delete => LitBool {
137				value: true,
138				span: Span::call_site()
139			},
140			Self::Custom { uri, .. } => LitBool {
141				value: uri
142					.as_ref()
143					.map(|uri| regex_is_match!(r#"(^|/):[^/]+(/|$)"#, &uri.value()))
144					.unwrap_or(false),
145				span: Span::call_site()
146			}
147		}
148	}
149
150	fn placeholders_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
151		match self {
152			Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => {
153				quote!(::gotham_restful::NoopExtractor)
154			},
155			Self::Read | Self::Update | Self::Delete => {
156				quote!(::gotham_restful::private::IdPlaceholder::<#arg_ty>)
157			},
158			Self::Custom { .. } => {
159				if self.has_placeholders().value {
160					arg_ty.to_token_stream()
161				} else {
162					quote!(::gotham_restful::NoopExtractor)
163				}
164			},
165		}
166	}
167
168	fn needs_params(&self) -> LitBool {
169		match self {
170			Self::ReadAll
171			| Self::Read
172			| Self::Create
173			| Self::UpdateAll
174			| Self::Update
175			| Self::DeleteAll
176			| Self::Delete => LitBool {
177				value: false,
178				span: Span::call_site()
179			},
180			Self::Search => LitBool {
181				value: true,
182				span: Span::call_site()
183			},
184			Self::Custom { params, .. } => params.clone().unwrap_or_else(|| LitBool {
185				value: false,
186				span: Span::call_site()
187			})
188		}
189	}
190
191	fn params_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
192		match self {
193			Self::ReadAll
194			| Self::Read
195			| Self::Create
196			| Self::UpdateAll
197			| Self::Update
198			| Self::DeleteAll
199			| Self::Delete => {
200				quote!(::gotham_restful::NoopExtractor)
201			},
202			Self::Search => quote!(#arg_ty),
203			Self::Custom { .. } => {
204				if self.needs_params().value {
205					arg_ty.to_token_stream()
206				} else {
207					quote!(::gotham_restful::NoopExtractor)
208				}
209			},
210		}
211	}
212
213	fn needs_body(&self) -> LitBool {
214		match self {
215			Self::ReadAll | Self::Read | Self::Search | Self::DeleteAll | Self::Delete => LitBool {
216				value: false,
217				span: Span::call_site()
218			},
219			Self::Create | Self::UpdateAll | Self::Update => LitBool {
220				value: true,
221				span: Span::call_site()
222			},
223			Self::Custom { body, .. } => body.clone().unwrap_or_else(|| LitBool {
224				value: false,
225				span: Span::call_site()
226			})
227		}
228	}
229
230	fn body_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
231		match self {
232			Self::ReadAll | Self::Read | Self::Search | Self::DeleteAll | Self::Delete => {
233				quote!(())
234			},
235			Self::Create | Self::UpdateAll | Self::Update => quote!(#arg_ty),
236			Self::Custom { .. } => {
237				if self.needs_body().value {
238					arg_ty.to_token_stream()
239				} else {
240					quote!(())
241				}
242			},
243		}
244	}
245}
246
247#[allow(clippy::large_enum_variant)]
248enum HandlerArgType {
249	StateRef,
250	StateMutRef,
251	MethodArg(Type),
252	DatabaseConnection(Type),
253	AuthStatus(Type),
254	AuthStatusRef(Type)
255}
256
257impl HandlerArgType {
258	fn is_method_arg(&self) -> bool {
259		matches!(self, Self::MethodArg(_))
260	}
261
262	fn is_database_conn(&self) -> bool {
263		matches!(self, Self::DatabaseConnection(_))
264	}
265
266	fn is_auth_status(&self) -> bool {
267		matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_))
268	}
269
270	fn ty(&self) -> Option<&Type> {
271		match self {
272			Self::MethodArg(ty)
273			| Self::DatabaseConnection(ty)
274			| Self::AuthStatus(ty)
275			| Self::AuthStatusRef(ty) => Some(ty),
276			_ => None
277		}
278	}
279
280	fn quote_ty(&self) -> Option<TokenStream> {
281		self.ty().map(|ty| quote!(#ty))
282	}
283}
284
285struct HandlerArg {
286	ident_span: Span,
287	ty: HandlerArgType
288}
289
290impl HandlerArg {
291	fn span(&self) -> Span {
292		self.ident_span
293	}
294}
295
296fn interpret_arg_ty(attrs: &[Attribute], name: &str, ty: Type) -> Result<HandlerArgType> {
297	let attr = attrs
298		.iter()
299		.filter_map(|attr| {
300			match &attr.meta {
301				Meta::List(meta) if meta.path.is_ident("rest_arg") => {
302					// TODO shouldn't this be parsed properly?
303					Some(meta.tokens.to_string())
304				},
305				_ => None
306			}
307		})
308		.next();
309
310	// TODO issue a warning for _state usage once diagnostics become stable
311	if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state"))
312	{
313		return match ty {
314			Type::Reference(ty) => Ok(if ty.mutability.is_none() {
315				HandlerArgType::StateRef
316			} else {
317				HandlerArgType::StateMutRef
318			}),
319			_ => Err(Error::new(
320				ty.span(),
321				"The state parameter has to be a (mutable) reference to gotham_restful::State"
322			))
323		};
324	}
325
326	if cfg!(feature = "auth")
327		&& (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth"))
328	{
329		return Ok(match ty {
330			Type::Reference(ty) => HandlerArgType::AuthStatusRef(*ty.elem),
331			ty => HandlerArgType::AuthStatus(ty)
332		});
333	}
334
335	if cfg!(feature = "database")
336		&& (attr.as_deref() == Some("connection")
337			|| attr.as_deref() == Some("conn")
338			|| (attr.is_none() && name == "conn"))
339	{
340		return Ok(HandlerArgType::DatabaseConnection(match ty {
341			Type::Reference(ty) => *ty.elem,
342			ty => ty
343		}));
344	}
345
346	Ok(HandlerArgType::MethodArg(ty))
347}
348
349fn interpret_arg(_index: usize, arg: &PatType) -> Result<HandlerArg> {
350	let pat = &arg.pat;
351	let orig_name = quote!(#pat);
352	let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?;
353
354	Ok(HandlerArg {
355		ident_span: arg.pat.span(),
356		ty
357	})
358}
359
360#[cfg(feature = "openapi")]
361fn expand_operation_verb(operation_verb: TokenStream) -> Option<TokenStream> {
362	Some(quote! {
363		fn operation_verb() -> ::core::option::Option<&'static ::core::primitive::str> {
364			#operation_verb
365		}
366	})
367}
368
369#[cfg(not(feature = "openapi"))]
370fn expand_operation_verb(_: TokenStream) -> Option<TokenStream> {
371	None
372}
373
374#[cfg(feature = "openapi")]
375fn expand_operation_id(fun_ident: &Ident, operation_id: Option<LitStr>) -> Option<TokenStream> {
376	let op_id = match operation_id {
377		Some(operation_id) => quote! {
378			::gotham_restful::OperationId::Manual(
379				::std::string::String::from(#operation_id)
380			)
381		},
382		None => {
383			let verb = fun_ident.to_string();
384			quote! {
385				::gotham_restful::OperationId::SemiAuto(
386					::std::borrow::Cow::Borrowed(#verb)
387				)
388			}
389		}
390	};
391	Some(quote! {
392		fn operation_id() -> ::gotham_restful::OperationId {
393			#op_id
394		}
395	})
396}
397
398#[cfg(not(feature = "openapi"))]
399fn expand_operation_id(_: &Ident, _: Option<LitStr>) -> Option<TokenStream> {
400	None
401}
402
403fn expand_wants_auth(wants_auth: Option<LitBool>, default: bool) -> TokenStream {
404	let wants_auth = wants_auth.unwrap_or_else(|| LitBool {
405		value: default,
406		span: Span::call_site()
407	});
408
409	quote! {
410		fn wants_auth() -> ::core::primitive::bool {
411			#wants_auth
412		}
413	}
414}
415
416pub fn endpoint_ident(fn_ident: &Ident) -> Ident {
417	format_ident!("{}___gotham_restful_endpoint", fn_ident)
418}
419
420macro_rules! error_if_not_openapi {
421	($($ident:ident),*) => {
422		$(
423			#[cfg(not(feature = "openapi"))]
424			if let Some($ident) = $ident {
425				return Err(Error::new(
426					$ident.span(),
427					concat!("`", stringify!($ident), "` is only supported with the openapi feature")
428				));
429			}
430		)*
431	};
432}
433
434// clippy doesn't realize that vectors can be used in closures
435#[allow(clippy::needless_collect)]
436fn expand_endpoint_type(
437	mut ty: EndpointType,
438	AttributeArgs(attrs): AttributeArgs,
439	fun: &ItemFn
440) -> Result<TokenStream> {
441	// reject unsafe functions
442	if let Some(unsafety) = fun.sig.unsafety {
443		return Err(Error::new(
444			unsafety.span(),
445			"Endpoint handler methods must not be unsafe"
446		));
447	}
448
449	// parse arguments
450	let mut debug: bool = false;
451	let mut operation_id: Option<LitStr> = None;
452	let mut schema: Option<Ident> = None;
453	let mut status_codes: Option<Ident> = None;
454	let mut wants_auth: Option<LitBool> = None;
455	for meta in attrs {
456		match meta {
457			Meta::NameValue(kv) => {
458				if kv.path.is_ident("debug") {
459					debug = kv.value.expect_bool()?.value;
460				} else if kv.path.is_ident("operation_id") {
461					operation_id = Some(kv.value.expect_str()?);
462				} else if kv.path.is_ident("schema") {
463					schema = Some(kv.value.expect_str()?.into_ident())
464				} else if kv.path.is_ident("status_codes") {
465					status_codes = Some(kv.value.expect_str()?.into_ident())
466				} else if kv.path.is_ident("wants_auth") {
467					wants_auth = Some(kv.value.expect_bool()?);
468				} else if kv.path.is_ident("method") {
469					ty.set_method(
470						kv.path.span(),
471						kv.value.expect_str()?.parse_with(Expr::parse)?
472					)?;
473				} else if kv.path.is_ident("uri") {
474					ty.set_uri(kv.path.span(), kv.value.expect_str()?)?;
475				} else if kv.path.is_ident("params") {
476					ty.set_params(kv.path.span(), kv.value.expect_bool()?)?;
477				} else if kv.path.is_ident("body") {
478					ty.set_body(kv.path.span(), kv.value.expect_bool()?)?;
479				} else {
480					return Err(Error::new(kv.path.span(), "Unknown attribute"));
481				}
482			},
483			_ => return Err(Error::new(meta.span(), "Invalid attribute syntax"))
484		}
485	}
486	error_if_not_openapi!(operation_id, schema, status_codes);
487	if schema.is_some() != status_codes.is_some() {
488		return Err(Error::new(
489			schema
490				.map(|s| s.span())
491				.unwrap_or_else(|| status_codes.unwrap().span()),
492			"`schema` and `status_codes` may only be used together"
493		));
494	}
495
496	// extract the documentation
497	let mut doc: Vec<String> = vec![String::new()];
498	for attr in &fun.attrs {
499		match &attr.meta {
500			Meta::NameValue(kv) if kv.path.is_ident("doc") => {
501				doc.push(kv.value.clone().expect_str()?.value())
502			},
503			_ => {}
504		}
505	}
506	let doc = doc.join("\n").unindent();
507	#[cfg_attr(not(feature = "openapi"), allow(unused_variables))]
508	let doc = doc.trim_end();
509
510	#[allow(unused_mut)]
511	let mut description: Option<TokenStream> = None;
512	#[cfg(feature = "openapi")]
513	if !doc.is_empty() {
514		description = Some(quote! {
515			fn description() -> ::core::option::Option<::std::string::String> {
516				::core::option::Option::Some(::std::string::String::from(#doc))
517			}
518		});
519	}
520
521	// extract arguments into pattern, ident and type
522	let args = fun
523		.sig
524		.inputs
525		.iter()
526		.enumerate()
527		.map(|(i, arg)| match arg {
528			FnArg::Typed(arg) => interpret_arg(i, arg),
529			FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter"))
530		})
531		.collect_to_result()?;
532
533	let fun_vis = &fun.vis;
534	let fun_ident = &fun.sig.ident;
535	let fun_is_async = fun.sig.asyncness.is_some();
536
537	let ident = endpoint_ident(fun_ident);
538	let (output_ty, is_no_content) = match &fun.sig.output {
539		ReturnType::Default => (quote!(::gotham_restful::NoContent), true),
540		ReturnType::Type(_, ty) => (quote!(#ty), false)
541	};
542	let output_struct_ident = schema.as_ref().map(|schema_fn| {
543		Ident::new(
544			&format!("{schema_fn}_gotham_restful_ResponseSchema"),
545			Span::call_site()
546		)
547	});
548	let output_struct = schema.map(|schema_fn| {
549		let output_struct_ident = output_struct_ident
550			.as_ref()
551			.unwrap_or_else(|| unreachable!());
552		let status_codes_fn = status_codes.unwrap_or_else(|| unreachable!());
553
554		let schema_call = quote_spanned! { schema_fn.span() =>
555			let schema = ::gotham_restful::private::CustomSchema::schema(#schema_fn, code);
556		};
557		let status_codes_call = quote_spanned! { status_codes_fn.span() =>
558			let status_codes = ::gotham_restful::private::CustomStatusCodes::status_codes(#status_codes_fn);
559		};
560
561		quote! {
562			#[allow(non_camel_case_types)]
563			struct #output_struct_ident(#output_ty);
564
565			impl ::core::convert::From<#output_ty> for #output_struct_ident {
566				fn from(o: #output_ty) -> Self {
567					Self(o)
568				}
569			}
570
571			impl ::gotham_restful::IntoResponse for #output_struct_ident
572			where
573				#output_ty: ::gotham_restful::IntoResponse
574			{
575				type Err = <#output_ty as ::gotham_restful::IntoResponse>::Err;
576
577				fn into_response(self) -> ::gotham_restful::private::BoxFuture<'static,
578						::core::result::Result<::gotham_restful::Response, Self::Err>>
579				{
580					::gotham_restful::IntoResponse::into_response(self.0)
581				}
582
583				fn accepted_types() -> ::core::option::Option<
584						::std::vec::Vec<::gotham_restful::gotham::mime::Mime>>
585				{
586					<#output_ty as ::gotham_restful::IntoResponse>::accepted_types()
587				}
588			}
589
590			impl ::gotham_restful::ResponseSchema for #output_struct_ident {
591				fn schema(
592					code: ::gotham_restful::gotham::hyper::StatusCode
593				) -> ::gotham_restful::private::OpenapiSchema
594				{
595					#schema_call
596					schema
597				}
598
599				fn status_codes() -> ::std::vec::Vec<::gotham_restful::gotham::hyper::StatusCode> {
600					#status_codes_call
601					status_codes
602				}
603			}
604		}
605	});
606	let (output_typedef, final_return_ty) = match output_struct_ident {
607		Some(output_struct_ident) => (
608			quote!(type Output = #output_struct_ident;),
609			quote!(#output_struct_ident)
610		),
611		None => (
612			quote_spanned!(output_ty.span() => type Output = #output_ty;),
613			quote!(#output_ty)
614		)
615	};
616
617	let arg_tys = args
618		.iter()
619		.filter(|arg| arg.ty.is_method_arg())
620		.collect::<Vec<_>>();
621	let mut arg_ty_idx = 0;
622	let mut next_arg_ty = |return_none: bool| {
623		if return_none {
624			return Ok(None);
625		}
626		if arg_ty_idx >= arg_tys.len() {
627			return Err(Error::new(
628				fun_ident.span(),
629				"Too few arguments for this endpoint handler"
630			));
631		}
632		let ty = arg_tys[arg_ty_idx].ty.ty().unwrap();
633		arg_ty_idx += 1;
634		Ok(Some(ty))
635	};
636
637	let http_method = ty.http_method().ok_or_else(|| {
638		Error::new(
639			Span::call_site(),
640			"Missing `method` attribute (e.g. `#[endpoint(method = \"gotham_restful::gotham::hyper::Method::GET\")]`)"
641		)
642	})?;
643	let uri = ty.uri().ok_or_else(|| {
644		Error::new(
645			Span::call_site(),
646			"Missing `uri` attribute (e.g. `#[endpoint(uri = \"custom_endpoint\")]`)"
647		)
648	})?;
649	let has_placeholders = ty.has_placeholders();
650	let placeholder_ty = ty.placeholders_ty(next_arg_ty(!has_placeholders.value)?);
651	let placeholder_typedef =
652		quote_spanned!(placeholder_ty.span() => type Placeholders = #placeholder_ty;);
653	let needs_params = ty.needs_params();
654	let params_ty = ty.params_ty(next_arg_ty(!needs_params.value)?);
655	let params_typedef = quote_spanned!(params_ty.span() => type Params = #params_ty;);
656	let needs_body = ty.needs_body();
657	let body_ty = ty.body_ty(next_arg_ty(!needs_body.value)?);
658	let body_typedef = quote_spanned!(body_ty.span() => type Body = #body_ty;);
659
660	if arg_ty_idx < arg_tys.len() {
661		return Err(Error::new(
662			fun_ident.span(),
663			"Extra arguments for this endpoint handler"
664		));
665	}
666
667	let mut handle_args: Vec<TokenStream> = Vec::new();
668	if has_placeholders.value {
669		if matches!(ty, EndpointType::Custom { .. }) {
670			handle_args.push(quote!(placeholders));
671		} else {
672			handle_args.push(quote!(placeholders.id));
673		}
674	}
675	if needs_params.value {
676		handle_args.push(quote!(params));
677	}
678	if needs_body.value {
679		handle_args.push(quote!(body.unwrap()));
680	}
681	let handle_args = args.iter().map(|arg| match arg.ty {
682		HandlerArgType::StateRef | HandlerArgType::StateMutRef => quote!(state),
683		HandlerArgType::MethodArg(_) => handle_args.remove(0),
684		HandlerArgType::DatabaseConnection(_) => quote!(&mut conn),
685		HandlerArgType::AuthStatus(_) => quote!(auth),
686		HandlerArgType::AuthStatusRef(_) => quote!(&auth)
687	});
688
689	let expand_handle_content = || {
690		let mut state_block = quote!();
691		if let Some(arg) = args.iter().find(|arg| arg.ty.is_auth_status()) {
692			let auth_ty = arg.ty.quote_ty();
693			let auth_borrow = quote! {
694				::gotham_restful::private::clone_from_state::<#auth_ty>(state)
695			};
696			state_block = quote! {
697				#state_block
698				let auth: #auth_ty = #auth_borrow;
699			}
700		}
701
702		let mut handle_content = quote!(#fun_ident(#(#handle_args),*));
703		if fun_is_async {
704			if let Some(arg) = args
705				.iter()
706				.find(|arg| matches!(arg.ty, HandlerArgType::StateRef))
707			{
708				return Err(Error::new(
709					arg.span(),
710					"Endpoint handler functions that are async must not take `&State` as an argument, consider taking `&mut State`"
711				));
712			}
713			handle_content = quote!(#handle_content.await);
714		}
715		if is_no_content {
716			handle_content = quote!(#handle_content; <::gotham_restful::NoContent as ::std::default::Default>::default())
717		}
718
719		if let Some(arg) = args.iter().find(|arg| arg.ty.is_database_conn()) {
720			let conn_ty = arg.ty.quote_ty();
721			state_block = quote! {
722				#state_block
723				let repo = <::gotham_restful::private::Repo<#conn_ty>>::borrow_from(state).clone();
724			};
725			handle_content = quote! {
726				repo.run::<_, _, ()>(move |mut conn| {
727					Ok({ #handle_content })
728				}).await.unwrap()
729			};
730		}
731
732		Ok(quote! {
733			use ::gotham_restful::private::FutureExt as _;
734			use ::gotham_restful::gotham::state::FromState as _;
735			#state_block
736			async move {
737				#handle_content
738			}.map(<#final_return_ty>::from).boxed()
739		})
740	};
741	let handle_content = match expand_handle_content() {
742		Ok(content) => content,
743		Err(err) => err.to_compile_error()
744	};
745
746	let tr8 = if cfg!(feature = "openapi") {
747		quote!(::gotham_restful::EndpointWithSchema)
748	} else {
749		quote!(::gotham_restful::Endpoint)
750	};
751	let operation_verb = expand_operation_verb(ty.operation_verb());
752	let operation_id = expand_operation_id(fun_ident, operation_id);
753	let wants_auth = expand_wants_auth(wants_auth, args.iter().any(|arg| arg.ty.is_auth_status()));
754	let code = quote! {
755		#[doc(hidden)]
756		/// `gotham_restful` implementation detail
757		#[allow(non_camel_case_types)]
758		#fun_vis struct #ident;
759
760		const _: () = {
761			#output_struct
762
763			impl #tr8 for #ident {
764				fn http_method() -> ::gotham_restful::gotham::hyper::Method {
765					#http_method
766				}
767
768				fn uri() -> ::std::borrow::Cow<'static, ::core::primitive::str> {
769					{ #uri }.into()
770				}
771
772				#operation_verb
773
774				#output_typedef
775
776				fn has_placeholders() -> ::core::primitive::bool {
777					#has_placeholders
778				}
779				#placeholder_typedef
780
781				fn needs_params() -> ::core::primitive::bool {
782					#needs_params
783				}
784				#params_typedef
785
786				fn needs_body() -> ::core::primitive::bool {
787					#needs_body
788				}
789				#body_typedef
790
791				fn handle<'a>(
792					state: &'a mut ::gotham_restful::gotham::state::State,
793					placeholders: Self::Placeholders,
794					params: Self::Params,
795					body: ::core::option::Option<Self::Body>
796				) -> ::gotham_restful::private::BoxFuture<'a, Self::Output> {
797					#handle_content
798				}
799
800				#operation_id
801				#description
802				#wants_auth
803			}
804		};
805	};
806	if debug {
807		eprintln!("{code}");
808	}
809	Ok(code)
810}
811
812pub(crate) fn expand_endpoint(
813	ty: EndpointType,
814	attrs: AttributeArgs,
815	fun: ItemFn
816) -> Result<TokenStream> {
817	let endpoint_type = match expand_endpoint_type(ty, attrs, &fun) {
818		Ok(code) => code,
819		Err(err) => err.to_compile_error()
820	};
821	Ok(quote! {
822		#fun
823		#endpoint_type
824	})
825}