Skip to content
Open
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
23 changes: 13 additions & 10 deletions utoipa-gen/src/security_requirement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
token::Comma,
LitStr, Token,
Token,
};

use crate::Array;
use crate::parse_utils;

#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct SecurityRequirementsAttrItem {
pub name: Option<String>,
pub scopes: Option<Vec<String>>,
pub scopes: Option<Vec<parse_utils::LitStrOrExpr>>,
}

#[derive(Default)]
Expand All @@ -30,17 +30,17 @@ impl Parse for SecurityRequirementsAttr {

impl Parse for SecurityRequirementsAttrItem {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name = input.parse::<LitStr>()?.value();
let name = input.parse::<syn::LitStr>()?.value();

input.parse::<Token![=]>()?;

let scopes_stream;
bracketed!(scopes_stream in input);

let scopes = Punctuated::<LitStr, Comma>::parse_terminated(&scopes_stream)?
.iter()
.map(LitStr::value)
.collect::<Vec<_>>();
let scopes =
Punctuated::<parse_utils::LitStrOrExpr, Comma>::parse_terminated(&scopes_stream)?
.into_iter()
.collect::<Vec<_>>();

Ok(Self {
name: Some(name),
Expand All @@ -57,11 +57,14 @@ impl ToTokens for SecurityRequirementsAttr {

for requirement in &self.0 {
if let (Some(name), Some(scopes)) = (&requirement.name, &requirement.scopes) {
let scopes = scopes.iter().collect::<Array<&String>>();
let scopes_tokens = scopes.iter().map(|scope| match scope {
parse_utils::LitStrOrExpr::LitStr(lit) => quote! { #lit.to_string() },
parse_utils::LitStrOrExpr::Expr(expr) => quote! { #expr.to_string() },
});
let scopes_len = scopes.len();

tokens.extend(quote! {
.add::<&str, [&str; #scopes_len], &str>(#name, #scopes)
.add::<&str, [String; #scopes_len], String>(#name, [#(#scopes_tokens),*])
});
}
}
Expand Down
54 changes: 54 additions & 0 deletions utoipa-gen/tests/path_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,60 @@ fn derive_path_with_security_requirements() {
}
}

#[test]
fn derive_path_with_security_requirements_display_types() {
use std::fmt::Display;

#[derive(Debug)]
enum Scope {
Read,
Write,
}

impl Display for Scope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Scope::Read => write!(f, "read:items"),
Scope::Write => write!(f, "write:items"),
}
}
}

const READ_SCOPE: &str = "read:items";

#[utoipa::path(
get,
path = "/items",
responses(
(status = 200, description = "success response")
),
security(
(),
("api_oauth" = [Scope::Read.to_string(), Scope::Write.to_string()]),
("jwt_token" = []),
("mixed" = [READ_SCOPE, Scope::Write.to_string()])
)
)]
#[allow(unused)]
fn get_items() -> String {
"".to_string()
}
let operation = test_api_fn_doc! {
get_items,
operation: get,
path: "/items"
};

assert_value! {operation=>
"security.[0]" = "{}", "Optional security requirement"
"security.[1].api_oauth.[0]" = r###""read:items""###, "api_oauth first scope with Display"
"security.[1].api_oauth.[1]" = r###""write:items""###, "api_oauth second scope with Display"
"security.[2].jwt_token" = "[]", "jwt_token auth scopes"
"security.[3].mixed.[0]" = r###""read:items""###, "mixed first scope literal"
"security.[3].mixed.[1]" = r###""write:items""###, "mixed second scope Display"
}
}

#[test]
fn derive_path_with_extensions() {
#[utoipa::path(
Expand Down
50 changes: 50 additions & 0 deletions utoipa-gen/tests/utoipa_gen_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,53 @@ fn derive_openapi() {

build_foo!(GetFooBody, Foo, FooResources);
}

#[test]
fn derive_openapi_with_security_display_types() {
use std::fmt::Display;

#[derive(Debug)]
enum AuthScope {
Read,
Write,
Admin,
}

impl Display for AuthScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthScope::Read => write!(f, "read:all"),
AuthScope::Write => write!(f, "write:all"),
AuthScope::Admin => write!(f, "admin:all"),
}
}
}

const CUSTOM_SCOPE: &str = "custom:scope";

#[derive(Default, OpenApi)]
#[openapi(
security(
(),
("oauth2" = [AuthScope::Read.to_string(), AuthScope::Write.to_string()]),
("api_key" = []),
("mixed" = [CUSTOM_SCOPE, AuthScope::Admin.to_string()])
)
)]
struct ApiDocWithDisplay;

let api = ApiDocWithDisplay::openapi();
let json = api.to_json().unwrap();
let security = serde_json::from_str::<serde_json::Value>(&json).unwrap()["security"].clone();

assert_eq!(security[0], serde_json::json!({}));
assert_eq!(
security[1]["oauth2"],
serde_json::json!(["read:all", "write:all"])
);
assert_eq!(security[2]["api_key"], serde_json::json!([]));
assert_eq!(
security[3]["mixed"],
serde_json::json!(["custom:scope", "admin:all"])
);
}
Loading