📜 ⬆️ ⬇️

Pre-hospital hotfix or “Hey, Swagger! Where are my mistakes? ”

Have you ever fix during hotfix in the master? Not?! But I managed!

This story is about how I forgot to update the documentation. As a result, I wrote a plugin for Swagger (from the second time). And how I got carried away with this so that I forgot about my sick-list and went on the mend!


And a little more about Optional not from Java 8.

We use Swagger to create online documentation.
')
Therefore, when you create a method in the API, then:

0. Add necessary annotations, such as @RequestMapping and so on.

1. Add @ErrorCodes (our own annotation), and list the string error codes that this method can return in the response.

2. You add @ApiOperation and in the notes field you duplicate information about these errors.

3. Add the rest of the annotations ...

It looked like this (removed too much and simplified):

@ApiOperation( value = "Some description.", notes = "List of possible error codes:" + "<ul>" + " <li>sms.verification.code.fail</li>" + "</ul>") @PostMapping("/security/confirmation/check") @ErrorCodes(values = {"sms.verification.code.fail"}) public ResponseDto check(@ApiParam @RequestBody @Valid RequestDto request) {... } 

Point 2 was the source of my failure when I added @ErrorCodes, but forgot to list the string error codes in @ApiOperation. Satisfied with myself, but with a slight feeling of anxiety, I gave my pull request to the Code Review. And here I was informed that I forgot about the notes! They also explained that Swagger does not pick up information from @ErrorCodes and that is why it is necessary to prescribe it manually. That evening everything ended well. Corrected his defect and went to the hospital.

Perhaps it would be normal to take and go further. Put a tick on the shelf to others the same, that they say, Max, be careful, pay attention to it ...

But I did not succeed. The whole evening and the next morning were spent on teaching Swagger to read our annotation and independently write these codes into notes.

Action 1. Someone was here ...


From a cursory search, we managed to find out that someone had already tried to make friends with Swagger with their annotation. There was also a reference to the SpringFox documentation in which it was said that you can write a plugin!

A happy happiness came over me so much that I even forgot about a cold and sick leave! In my future article “How not to quit a company three times,” I share three stories of saving drowning people. One of them is about how I managed to write a plugin for Chrome + Firefox, which speeds up work with Jenkins several times. I was so cool to write it! After all, this is a microproject! My most simple startup, but with real people who use it. Then I got out of the routine again and found inspiration. Burnout is gone. But this is better to tell in a future article. In the meantime, back to the plugin for Swagger.

Action 2. Works!


Writing something working turned out to be easy. He took the example of a plug-in from the official SpringFox documentation, removed all unnecessary and added the right one.

Plugin. Version 1
 @Component @Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1) public class SwaggerErrorCodesConfiguration implements OperationBuilderPlugin { /** * Appends note of operation. Adds error codes to note of operation. * <code> * <h3>List of possible errors:</h3> * <ul> * <li>error.code.1</li> * <li>error.code.2</li> * <li>error.code.3</li> * </ul> * </code> * @param context operation context */ @Override public void apply(OperationContext context) { Method operationMethod = context.getHandlerMethod().getMethod(); // Check method is annotated by @ErrorCodes ErrorCodes errorCodes = findAnnotation(operationMethod, ErrorCodes.class); if (errorCodes == null) { return; } StringBuilder errorCodesNote = new StringBuilder(); errorCodesNote.append("<h3>List of possible errors:</h3>"); errorCodesNote.append("<ul>"); for (String errorCode: errorCodes.values()) { errorCodesNote.append("<li>").append(errorCode).append("</li>"); } errorCodesNote.append("</ul>"); // Write new version of notes. context.operationBuilder().notes(errorCodesNote.toString()).build(); } @Override public boolean supports(DocumentationType delimiter) { return SwaggerPluginSupport.pluginDoesApply(delimiter); } } 


I started testing with a method in which no value is specified for notes in @ApiOperation.

 @ApiOperation(value = "Some description.") @PostMapping("/security/confirmation/check") @ErrorCodes(values = {"sms.verification.code.fail"}) public ResponseDto check(@ApiParam @RequestBody @Valid RequestDto request) { ... } 

Run and ... Result! Hurray, it works! The string code sms.verification.code.fail appeared in the notes!



Step 3: Works, but does not work.


Then I added a few words to notes and I got this code:

 @ApiOperation(value = "Some description.", notes = "Some initial note.") @PostMapping("/security/confirmation/check") @ErrorCodes(values = {"sms.verification.code.fail"}) public ResponseDto check(@ApiParam @RequestBody @Valid RequestDto request) { ... } 

Launched again. The result was ... unexpected. SpringFox plugin rewritten the value of notes when generating documentation (O_o)!

I look how context.operationBuilder (). Notes (String) is arranged and I see the following there:

 public OperationBuilder notes(String notes) { this.notes = (String)BuilderDefaults.defaultIfAbsent(notes, this.notes); return this; } 

Um ... Ok, then we take the current value of the notes and add error codes. It remains to get the @ApiOperation annotation, take the desired value and add to what I am creating myself.

So, the final version ( available at gist.github.com )

Plugin. Version 2
 @Component @Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1) public class SwaggerErrorCodesConfiguration implements OperationBuilderPlugin { /** * Appends note of operation. Adds error codes to note of operation. * <code> * <h3>List of possible errors:</h3> * <ul> * <li>error.code.1</li> * <li>error.code.2</li> * <li>error.code.3</li> * </ul> * </code> * @param context operation context */ @Override public void apply(OperationContext context) { Method operationMethod = context.getHandlerMethod().getMethod(); // Check method is annotated by @ApiOperation ApiOperation apiOperation = findApiOperationAnnotation(operationMethod).orNull(); if (apiOperation == null) { return; } // Check method is annotated by @ErrorCodes ErrorCodes errorCodes = findAnnotation(operationMethod, ErrorCodes.class); if (errorCodes == null) { return; } // Prepend notes by using current value of notes in @ApiOperation StringBuilder errorCodesNote = new StringBuilder(apiOperation.notes()); errorCodesNote.append("<h3>List of possible errors:</h3>"); errorCodesNote.append("<ul>"); for (String errorCode: errorCodes.values()) { errorCodesNote.append("<li>").append(errorCode).append("</li>"); } errorCodesNote.append("</ul>"); // Write new version of notes. context.operationBuilder().notes(errorCodesNote.toString()).build(); } @Override public boolean supports(DocumentationType delimiter) { return SwaggerPluginSupport.pluginDoesApply(delimiter); } } 


Now it turned out right!



Step 4. What is there about Optional not from Java 8?


At the very beginning of work on the plugin could not understand what is wrong with the Optional, which is returned when searching for annotations. This class did not have standard methods to which got used working with java.util.Optional . For example, there is no ifPresent method, but there is an orNull method.
It turned out that SpringFox uses Optional from Guava .

TL; DR


I wrote a plugin for SpringFox, which is a Spring component and at the documentation generation stage it is called to read the values ​​from our @ErrorCodes annotation.

The plugin is available at gist.github.com .

Source: https://habr.com/ru/post/420841/


All Articles