Skip to content

Commit

Permalink
Support arbitrary exprs in operation_id (#472)
Browse files Browse the repository at this point in the history
Add support for arbitrary expressions in `operation_id` of `utoipa::path` attribute macro.
Also update docs and add a test.
  • Loading branch information
djrenren committed Feb 2, 2023
1 parent 96acebf commit 9124559
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 12 deletions.
4 changes: 3 additions & 1 deletion utoipa-gen/src/component/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ impl SerdeValue {
let mut rest = *cursor;
while let Some((tt, next)) = rest.token_tree() {
match tt {
TokenTree::Ident(ident) if ident == "skip" || ident == "skip_serializing" => value.skip = true,
TokenTree::Ident(ident) if ident == "skip" || ident == "skip_serializing" => {
value.skip = true
}
TokenTree::Ident(ident) if ident == "flatten" => value.flatten = true,
TokenTree::Ident(ident) if ident == "rename" => {
if let Some((literal, _)) = parse_next_lit_str(next) {
Expand Down
4 changes: 3 additions & 1 deletion utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,9 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
///
/// * `path = "..."` Must be OpenAPI format compatible str with arguments withing curly braces. E.g _`{id}`_
///
/// * `operation_id = "..."` Unique operation id for the endpoint. By default this is mapped to function name.
/// * `operation_id = ...` Unique operation id for the endpoint. By default this is mapped to function name.
/// The operation_id can be any valid expression (e.g. string literals, macro invocations, variables) so long
/// as its result can be converted to a `String` using `String::from`.
///
/// * `context_path = "..."` Can add optional scope for **path**. The **context_path** will be prepended to beginning of **path**.
/// This is particularly useful when **path** does not contain the full path to the endpoint. For example if web framework
Expand Down
25 changes: 15 additions & 10 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use std::{io::Error, str::FromStr};

use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use proc_macro_error::abort;
use quote::{format_ident, quote, ToTokens};
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::Paren;
use syn::{parenthesized, parse::Parse, Token};
use syn::{LitStr, Type};
use syn::{Expr, ExprLit, Lit, LitStr, Type};

use crate::component::{GenericType, TypeTree};
use crate::{parse_utils, Deprecated};
Expand Down Expand Up @@ -77,7 +78,7 @@ pub struct PathAttr<'p> {
request_body: Option<RequestBodyAttr<'p>>,
responses: Vec<Response<'p>>,
pub(super) path: Option<String>,
operation_id: Option<String>,
operation_id: Option<Expr>,
tag: Option<String>,
params: Vec<Parameter<'p>>,
security: Option<Array<'p, SecurityRequirementAttr>>,
Expand Down Expand Up @@ -183,7 +184,8 @@ impl Parse for PathAttr<'_> {

match attribute_name {
"operation_id" => {
path_attr.operation_id = Some(parse_utils::parse_next_literal_str(input)?);
path_attr.operation_id =
Some(parse_utils::parse_next(input, || Expr::parse(input))?);
}
"path" => {
path_attr.path = Some(parse_utils::parse_next_literal_str(input)?);
Expand Down Expand Up @@ -362,11 +364,14 @@ impl<'p> Path<'p> {
impl<'p> ToTokens for Path<'p> {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let path_struct = format_ident!("{}{}", PATH_STRUCT_PREFIX, self.fn_name);
let operation_id: &String = self
let operation_id = self
.path_attr
.operation_id
.as_ref()
.or(Some(&self.fn_name))
.clone()
.or(Some(ExprLit {
attrs: vec![],
lit: Lit::Str(LitStr::new(&self.fn_name, Span::call_site()))
}.into()))
.unwrap_or_else(|| {
abort! {
Span::call_site(), "operation id is not defined for path";
Expand Down Expand Up @@ -469,7 +474,7 @@ impl<'p> ToTokens for Path<'p> {

#[cfg_attr(feature = "debug", derive(Debug))]
struct Operation<'a> {
operation_id: &'a String,
operation_id: Expr,
summary: Option<&'a String>,
description: Option<&'a Vec<String>>,
deprecated: &'a Option<bool>,
Expand Down Expand Up @@ -498,8 +503,8 @@ impl ToTokens for Operation<'_> {
.securities(Some(#security_requirements))
})
}
let operation_id = self.operation_id;
tokens.extend(quote! {
let operation_id = &self.operation_id;
tokens.extend(quote_spanned! { operation_id.span() =>
.operation_id(Some(#operation_id))
});

Expand Down
23 changes: 23 additions & 0 deletions utoipa-gen/tests/path_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,29 @@ fn derive_path_params_into_params_with_raw_identifier() {
)
}

#[test]
fn arbitrary_expr_in_operation_id() {
#[utoipa::path(
get,
path = "foo",
operation_id=format!("{}", 3+5),
responses(
(status = 200, description = "success response")
),
)]
#[allow(unused)]
fn get_foo() {}

#[derive(OpenApi, Default)]
#[openapi(paths(get_foo))]
struct ApiDoc;

let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
let operation_id = doc.pointer("/paths/foo/get/operationId").unwrap();

assert_json_eq!(operation_id, json!("8"))
}

#[test]
fn derive_path_with_validation_attributes() {
#[derive(IntoParams)]
Expand Down
3 changes: 3 additions & 0 deletions utoipa-gen/tests/utoipa_gen_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,14 @@ struct Pet {
mod pet_api {
use super::*;

const ID: &str = "get_pet";

/// Get pet by id
///
/// Get pet from database by pet database id
#[utoipa::path(
get,
operation_id = ID,
path = "/pets/{id}",
responses(
(status = 200, description = "Pet found successfully", body = Pet),
Expand Down

0 comments on commit 9124559

Please sign in to comment.