1717import org .eclipse .microprofile .openapi .models .Operation ;
1818import org .eclipse .microprofile .openapi .models .PathItem ;
1919import org .eclipse .microprofile .openapi .models .Paths ;
20+ import org .eclipse .microprofile .openapi .models .media .Content ;
21+ import org .eclipse .microprofile .openapi .models .media .MediaType ;
22+ import org .eclipse .microprofile .openapi .models .media .Schema ;
2023import org .eclipse .microprofile .openapi .models .responses .APIResponse ;
2124import org .eclipse .microprofile .openapi .models .responses .APIResponses ;
2225import org .eclipse .microprofile .openapi .models .security .SecurityRequirement ;
2326import org .eclipse .microprofile .openapi .models .security .SecurityScheme ;
2427
2528/**
26- * This filter replaces the former AutoTagFilter and AutoRolesAllowedFilter and has three functions:
29+ * This filter has the following functions:
2730 * <ul>
28- * <li>Add operation descriptions based on the associated Java method name handling the operation
29- * <li>Add operation tags based on the associated Java class of the operation
31+ * <li>Add operation descriptions based on the associated Java method name handling the operation</li>
32+ * <li>Add operation tags based on the associated Java class of the operation</li>
3033 * <li>Add security requirements based on discovered {@link jakarta.annotation.security.RolesAllowed},
3134 * {@link io.quarkus.security.PermissionsAllowed}, and {@link io.quarkus.security.Authenticated}
32- * annotations.
35+ * annotations. Also add the expected security responses if needed.</li>
36+ * <li>Add Bad Request (400) response for invalid input (if none is provided)</li>
3337 * </ul>
3438 */
3539public class OperationFilter implements OASFilter {
@@ -42,20 +46,22 @@ public class OperationFilter implements OASFilter {
4246 private final String defaultSecuritySchemeName ;
4347 private final boolean doAutoTag ;
4448 private final boolean doAutoOperation ;
49+ private final boolean doAutoBadRequest ;
4550 private final boolean alwaysIncludeScopesValidForScheme ;
4651
4752 public OperationFilter (Map <String , ClassAndMethod > classNameMap ,
4853 Map <String , List <String >> rolesAllowedMethodReferences ,
4954 List <String > authenticatedMethodReferences ,
5055 String defaultSecuritySchemeName ,
51- boolean doAutoTag , boolean doAutoOperation , boolean alwaysIncludeScopesValidForScheme ) {
56+ boolean doAutoTag , boolean doAutoOperation , boolean doAutoBadRequest , boolean alwaysIncludeScopesValidForScheme ) {
5257
5358 this .classNameMap = Objects .requireNonNull (classNameMap );
5459 this .rolesAllowedMethodReferences = Objects .requireNonNull (rolesAllowedMethodReferences );
5560 this .authenticatedMethodReferences = Objects .requireNonNull (authenticatedMethodReferences );
5661 this .defaultSecuritySchemeName = Objects .requireNonNull (defaultSecuritySchemeName );
5762 this .doAutoTag = doAutoTag ;
5863 this .doAutoOperation = doAutoOperation ;
64+ this .doAutoBadRequest = doAutoBadRequest ;
5965 this .alwaysIncludeScopesValidForScheme = alwaysIncludeScopesValidForScheme ;
6066 }
6167
@@ -77,18 +83,18 @@ public void filterOpenAPI(OpenAPI openAPI) {
7783 .map (Map .Entry ::getValue )
7884 .map (PathItem ::getOperations )
7985 .filter (Objects ::nonNull )
80- .map (Map ::values )
81- .flatMap (Collection ::stream )
86+ .flatMap (operations -> operations .entrySet ().stream ())
8287 .forEach (operation -> {
83- final String methodRef = methodRef (operation );
88+ final String methodRef = methodRef (operation . getValue () );
8489
8590 if (methodRef != null ) {
86- maybeSetSummaryAndTag (operation , methodRef );
87- maybeAddSecurityRequirement (operation , methodRef , schemeName , scopesValidForScheme ,
91+ maybeSetSummaryAndTag (operation . getValue () , methodRef );
92+ maybeAddSecurityRequirement (operation . getValue () , methodRef , schemeName , scopesValidForScheme ,
8893 defaultSecurityErrors );
94+ maybeAddBadRequestResponse (openAPI , operation , methodRef );
8995 }
9096
91- operation .removeExtension (EXT_METHOD_REF );
97+ operation .getValue (). removeExtension (EXT_METHOD_REF );
9298 });
9399 }
94100
@@ -97,6 +103,131 @@ private String methodRef(Operation operation) {
97103 return (String ) (extensions != null ? extensions .get (EXT_METHOD_REF ) : null );
98104 }
99105
106+ private void maybeAddBadRequestResponse (OpenAPI openAPI , Map .Entry <PathItem .HttpMethod , Operation > operation ,
107+ String methodRef ) {
108+ if (!classNameMap .containsKey (methodRef )) {
109+ return ;
110+ }
111+
112+ if (doAutoBadRequest
113+ && isPOSTorPUT (operation ) // Only applies to PUT and POST
114+ && hasBody (operation ) // Only applies to input
115+ && !isStringOrNumberOrBoolean (operation , openAPI ) // Except String, Number and boolean
116+ && !isFileUpload (operation , openAPI )) { // and file
117+ if (!operation .getValue ().getResponses ().hasAPIResponse ("400" )) { // Only when the user has not already added one
118+ operation .getValue ().getResponses ().addAPIResponse ("400" ,
119+ OASFactory .createAPIResponse ().description ("Bad Request" ));
120+ }
121+ }
122+ }
123+
124+ private boolean isPOSTorPUT (Map .Entry <PathItem .HttpMethod , Operation > operation ) {
125+ return operation .getKey ().equals (PathItem .HttpMethod .POST )
126+ || operation .getKey ().equals (PathItem .HttpMethod .PUT );
127+ }
128+
129+ private boolean hasBody (Map .Entry <PathItem .HttpMethod , Operation > operation ) {
130+ return operation .getValue ().getRequestBody () != null ;
131+ }
132+
133+ private boolean isStringOrNumberOrBoolean (Map .Entry <PathItem .HttpMethod , Operation > operation , OpenAPI openAPI ) {
134+ boolean isStringOrNumberOrBoolean = false ;
135+ Content content = operation .getValue ().getRequestBody ().getContent ();
136+ if (content != null ) {
137+ for (MediaType mediaType : content .getMediaTypes ().values ()) {
138+ if (mediaType != null && mediaType .getSchema () != null ) {
139+ Schema schema = mediaType .getSchema ();
140+
141+ if (schema .getRef () != null
142+ || (schema .getContentSchema () != null && schema .getContentSchema ().getRef () != null ))
143+ schema = resolveSchema (schema , openAPI .getComponents ());
144+ if (isString (schema ) || isNumber (schema ) || isBoolean (schema )) {
145+ isStringOrNumberOrBoolean = true ;
146+ }
147+ }
148+ }
149+ }
150+ return isStringOrNumberOrBoolean ;
151+ }
152+
153+ private Schema resolveSchema (Schema schema , Components components ) {
154+ while (schema != null ) {
155+ // Resolve `$ref` schema
156+ if (schema .getRef () != null && components != null ) {
157+ String refName = schema .getRef ().replace ("#/components/schemas/" , "" );
158+ schema = components .getSchemas ().get (refName );
159+ if (schema == null )
160+ break ;
161+ } else if (schema .getContentSchema () != null ) {
162+ schema = schema .getContentSchema ();
163+ continue ;
164+ }
165+
166+ break ;
167+ }
168+ return schema ;
169+ }
170+
171+ private boolean isFileUpload (Map .Entry <PathItem .HttpMethod , Operation > operation , OpenAPI openAPI ) {
172+ boolean isFile = false ;
173+ Content content = operation .getValue ().getRequestBody ().getContent ();
174+ if (content != null ) {
175+ for (Map .Entry <String , MediaType > kv : content .getMediaTypes ().entrySet ()) {
176+ String mediaTypeKey = kv .getKey ();
177+ if ("multipart/form-data" .equals (mediaTypeKey ) || "application/octet-stream" .equals (mediaTypeKey )) {
178+ MediaType mediaType = kv .getValue ();
179+ if (mediaType != null && mediaType .getSchema () != null ) {
180+ if (isFileSchema (mediaType .getSchema (), openAPI .getComponents ())) {
181+ isFile = true ;
182+ }
183+ }
184+ }
185+ }
186+ }
187+ return isFile ;
188+ }
189+
190+ private boolean isFileSchema (Schema schema , Components components ) {
191+ if (isString (schema ) && isBinaryFormat (schema )) {
192+ return true ; // Direct file schema
193+ }
194+ if (isObject (schema ) && schema .getProperties () != null ) {
195+ // Check if it has a "file" property with type "string" and format "binary"
196+ return schema .getProperties ().values ().stream ()
197+ .anyMatch (prop -> isString (prop ) && isBinaryFormat (prop ));
198+ }
199+ if (schema .getRef () != null && components != null ) {
200+ // Resolve reference and check recursively
201+ String refName = schema .getRef ().replace ("#/components/schemas/" , "" );
202+ Schema referencedSchema = components .getSchemas ().get (refName );
203+ if (referencedSchema != null ) {
204+ return isFileSchema (referencedSchema , components );
205+ }
206+ }
207+ return false ;
208+ }
209+
210+ private boolean isString (Schema schema ) {
211+ return schema != null && schema .getType () != null && schema .getType ().contains (Schema .SchemaType .STRING );
212+ }
213+
214+ private boolean isNumber (Schema schema ) {
215+ return schema != null && schema .getType () != null && (schema .getType ().contains (Schema .SchemaType .INTEGER )
216+ || schema .getType ().contains (Schema .SchemaType .NUMBER ));
217+ }
218+
219+ private boolean isBoolean (Schema schema ) {
220+ return schema != null && schema .getType () != null && schema .getType ().contains (Schema .SchemaType .BOOLEAN );
221+ }
222+
223+ private boolean isObject (Schema schema ) {
224+ return schema != null && schema .getType () != null && schema .getType ().contains (Schema .SchemaType .OBJECT );
225+ }
226+
227+ private boolean isBinaryFormat (Schema schema ) {
228+ return "binary" .equals (schema .getFormat ());
229+ }
230+
100231 private void maybeSetSummaryAndTag (Operation operation , String methodRef ) {
101232 if (!classNameMap .containsKey (methodRef )) {
102233 return ;
0 commit comments