Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support arbitrary exprs in operation_id #472

Merged
merged 3 commits into from
Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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