openapi_trait_shared/codegen/
security.rs1use heck::{ToPascalCase, ToSnakeCase};
16use openapiv3::{APIKeyLocation, OpenAPI, Operation, ReferenceOr, SecurityScheme};
17use proc_macro2::TokenStream;
18use quote::{format_ident, quote};
19
20#[derive(Debug, Clone, Copy)]
22pub enum ApiKeyIn {
23 Header,
24 Query,
25 Cookie,
26}
27
28#[derive(Debug, Clone)]
30pub enum SchemeKind {
31 ApiKey { key: String, location: ApiKeyIn },
32 HttpBearer,
33 HttpBasic,
34}
35
36#[derive(Debug, Clone)]
38pub struct SchemeInfo {
39 pub name: String,
41 pub ident: syn::Ident,
43 pub snake: String,
45 pub kind: SchemeKind,
46}
47
48#[derive(Debug, Clone, Default)]
50pub struct OpSecurity {
51 pub alternatives: Vec<String>,
53 pub had_unsupported_and: bool,
55}
56
57#[must_use]
59pub fn collect_schemes(openapi: &OpenAPI) -> Vec<SchemeInfo> {
60 let Some(components) = openapi.components.as_ref() else {
61 return Vec::new();
62 };
63 let mut out = Vec::new();
64 for (name, ref_or) in &components.security_schemes {
65 let scheme = match ref_or {
66 ReferenceOr::Item(s) => s,
67 ReferenceOr::Reference { .. } => continue,
68 };
69 let kind = match scheme {
70 SecurityScheme::APIKey { location, name, .. } => {
71 let loc = match location {
72 APIKeyLocation::Header => ApiKeyIn::Header,
73 APIKeyLocation::Query => ApiKeyIn::Query,
74 APIKeyLocation::Cookie => ApiKeyIn::Cookie,
75 };
76 SchemeKind::ApiKey {
77 key: name.clone(),
78 location: loc,
79 }
80 }
81 SecurityScheme::HTTP { scheme, .. } => match scheme.to_ascii_lowercase().as_str() {
82 "bearer" => SchemeKind::HttpBearer,
83 "basic" => SchemeKind::HttpBasic,
84 _ => continue,
85 },
86 SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => continue,
87 };
88 let ident = format_ident!("{}", name.to_pascal_case());
89 let snake = name.to_snake_case();
90 out.push(SchemeInfo {
91 name: name.clone(),
92 ident,
93 snake,
94 kind,
95 });
96 }
97 out
98}
99
100#[must_use]
111pub fn resolve_op_security(
112 op: &Operation,
113 openapi: &OpenAPI,
114 schemes: &[SchemeInfo],
115) -> OpSecurity {
116 let requirements = match op.security.as_ref() {
117 Some(v) => v,
118 None => match openapi.security.as_ref() {
119 Some(v) => v,
120 None => return OpSecurity::default(),
121 },
122 };
123
124 let mut out = OpSecurity::default();
125 for req in requirements {
126 if req.len() > 1 {
127 out.had_unsupported_and = true;
128 continue;
129 }
130 let Some((name, _scopes)) = req.iter().next() else {
131 continue;
132 };
133 if scheme_by_name(schemes, name).is_some() {
134 out.alternatives.push(name.clone());
135 }
136 }
137 out
138}
139
140#[must_use]
142pub fn scheme_by_name<'a>(schemes: &'a [SchemeInfo], name: &str) -> Option<&'a SchemeInfo> {
143 schemes.iter().find(|s| s.name == name)
144}
145
146#[must_use]
148pub fn generate_scheme_types(schemes: &[SchemeInfo]) -> TokenStream {
149 let items: Vec<TokenStream> = schemes
150 .iter()
151 .map(|s| {
152 let ident = &s.ident;
153 match &s.kind {
154 SchemeKind::ApiKey { .. } | SchemeKind::HttpBearer => quote! {
155 #[derive(::core::fmt::Debug, ::core::clone::Clone)]
156 pub struct #ident(pub ::std::string::String);
157 },
158 SchemeKind::HttpBasic => quote! {
159 #[derive(::core::fmt::Debug, ::core::clone::Clone)]
160 pub struct #ident {
161 pub username: ::std::string::String,
162 pub password: ::std::string::String,
163 }
164 },
165 }
166 })
167 .collect();
168 quote! { #(#items)* }
169}
170
171#[must_use]
173pub fn generate_op_auth_enum(op_id: &str, alternatives: &[&SchemeInfo]) -> Option<TokenStream> {
174 if alternatives.len() < 2 {
175 return None;
176 }
177 let ident = auth_enum_ident(op_id);
178 let variants: Vec<TokenStream> = alternatives
179 .iter()
180 .map(|s| {
181 let variant = &s.ident;
182 let ty = &s.ident;
183 quote! { #variant(#ty), }
184 })
185 .collect();
186 Some(quote! {
187 #[derive(::core::fmt::Debug, ::core::clone::Clone)]
188 pub enum #ident {
189 #(#variants)*
190 }
191 })
192}
193
194#[must_use]
196pub fn auth_enum_ident(op_id: &str) -> syn::Ident {
197 format_ident!("{}Auth", op_id.to_pascal_case())
198}
199
200#[must_use]
204pub fn auth_param_type(op_id: &str, op_security: &OpSecurity) -> Option<TokenStream> {
205 if op_security.alternatives.is_empty() {
206 return None;
207 }
208 if op_security.alternatives.len() == 1 {
209 return Some(TokenStream::new()); }
211 let ident = auth_enum_ident(op_id);
212 Some(quote! { #ident })
213}
214
215#[must_use]
217pub fn resolve_alternatives<'a>(
218 op_security: &'a OpSecurity,
219 schemes: &'a [SchemeInfo],
220) -> Vec<&'a SchemeInfo> {
221 op_security
222 .alternatives
223 .iter()
224 .filter_map(|name| scheme_by_name(schemes, name))
225 .collect()
226}
227
228#[must_use]
230pub fn auth_state_ident(mod_ident: &syn::Ident) -> syn::Ident {
231 format_ident!("{}AuthState", mod_ident.to_string().to_pascal_case())
232}
233
234#[must_use]
236pub fn client_auth_trait_ident(mod_ident: &syn::Ident) -> syn::Ident {
237 format_ident!("{}ClientAuth", mod_ident.to_string().to_pascal_case())
238}
239
240#[must_use]
242pub fn scheme_field_ident(scheme: &SchemeInfo) -> syn::Ident {
243 format_ident!("{}", scheme.snake)
244}
245
246#[must_use]
249pub fn generate_unsupported_and_errors(ops_with_and: &[String]) -> TokenStream {
250 if ops_with_and.is_empty() {
251 return TokenStream::new();
252 }
253 let msgs: Vec<TokenStream> = ops_with_and
254 .iter()
255 .map(|op_id| {
256 let msg = format!(
257 "openapi-trait: operation `{op_id}` requires multiple security schemes simultaneously (AND); v0.1 only supports OR of single-scheme alternatives"
258 );
259 quote! { ::core::compile_error!(#msg); }
260 })
261 .collect();
262 quote! { #(#msgs)* }
263}