Случалось ли вам налажать во время хотфикса в мастер? Нет?! А вот мне удалось!
Эта история о том, как я забыл обновить документацию. Как в итоге, написал плагин для Swagger (со второго раза). И как увлекся этим так, что забыл про свой больничный и пошел на поправку!
А еще немного про Optional не из Java 8.
Для создания интерактивной документации мы используем Swagger.
Поэтому когда создаешь метод в API, то:
0. Добавляешь необходимые аннотации, типа @RequestMapping и прочее.
1. Добавляешь @ErrorCodes (наша собственная аннотация), и перечисляешь строковые коды ошибок, которые может вернуть этот метод в ответе.
2. Добавляешь @ApiOperation и в поле notes дублируешь информацию об этих ошибках.
3. Добавляешь остальные аннотации…
Выглядело это примерно вот так (убрал лишнее и упростил):
Источником моего провала стал пункт 2, когда я добавил @ErrorCodes, но забыл перечислить строковые коды ошибок в @ApiOperation. Довольный собой, но с лёгким чувством тревоги я отдал свой Pull Request на Code Review. И вот тут мне сообщили, что я забыл про notes! А еще объяснили, что Swagger не подхватывает информацию из @ErrorCodes и именно поэтому приходится ее прописывать вручную. В тот вечер всё закончилось благополучно. Исправил свой недочет и ушёл на больничный.
Возможно, было бы нормально взять и пойти дальше. Поставить себе галочку на полочку к другим таким же, что мол, Макс, будь внимательнее, обращай на это внимание…
Но у меня не получилось. Весь вечер и следующее утро были потрачены на то, чтобы научить Swagger читать нашу аннотацию и самостоятельно дописывать в notes эти самые коды.
Из беглого поиска удалось выяснить, что кто-то уже пытался подружить Swagger со своей аннотацией. Там же была ссылочка на документацию SpringFox в которой говорилось о том, что можно написать плагин!
Счастливое счастье накрыло меня настолько, что даже забыл о простуде и больничном! В своей будущей статье «Как три раза не уволиться из компании» я делюсь тремя историями спасения утопающих. Одна из них о том, как мне удалось написать плагин для Chrome + Firefox, который в несколько раз ускорял работу с Jenkins. Мне было так прикольно его писать! Ведь это микропроект! Мой самый-самый простенький стартап, но с реальными людьми, которые им пользуются. Тогда я вновь выбрался из рутины и обрел вдохновение. Выгорание ушло. Но об этом лучше расскажу в будущей статье. А пока вернемся к плагину для Swagger.
Написать что-то работающее оказалось легко. Взял пример плагина из официальной документации SpringFox, убрал всё лишнее и добавил нужное.
Тестирование я начал с метода в котором не указано значение для notes в @ApiOperation.
Запуск и… Результат! Ура, работает! Строковый код sms.verification.code.fail появился в notes!
Потом я добавил несколько слов в notes и получился вот такой код:
Запустил снова. Результат оказался… неожиданным. SpringFox плагин перезаписал значение notes при генерации документации (О_о)!
Смотрю как устроен context.operationBuilder().notes(String) и вижу там следующее:
Эм… Ок, тогда будем брать текущее значение notes и добавлять error codes. Остается достать аннотацию @ApiOperation, взять нужное значение и добавить к тому, что формирую сам.
Итак, финальная версия (доступная на gist.github.com)
Теперь получилось как надо!
В самом начале работы над плагином не мог понять что не так с тем Optional, который возвращается при поиске аннотации. У этого класса не было стандартных методов, к которым привык работая с java.util.Optional. Например, нет метода ifPresent, зато есть метод orNull.
Выяснилось, что SpringFox использует Optional из Guava.
Написал плагин для SpringFox, который является Spring компонентом и на этапе генерации документации вызывается для чтения значений из нашей аннотации @ErrorCodes.
Плагин доступен на gist.github.com.
Эта история о том, как я забыл обновить документацию. Как в итоге, написал плагин для Swagger (со второго раза). И как увлекся этим так, что забыл про свой больничный и пошел на поправку!
А еще немного про Optional не из Java 8.
Для создания интерактивной документации мы используем Swagger.
Поэтому когда создаешь метод в API, то:
0. Добавляешь необходимые аннотации, типа @RequestMapping и прочее.
1. Добавляешь @ErrorCodes (наша собственная аннотация), и перечисляешь строковые коды ошибок, которые может вернуть этот метод в ответе.
2. Добавляешь @ApiOperation и в поле notes дублируешь информацию об этих ошибках.
3. Добавляешь остальные аннотации…
Выглядело это примерно вот так (убрал лишнее и упростил):
@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) {... }
Источником моего провала стал пункт 2, когда я добавил @ErrorCodes, но забыл перечислить строковые коды ошибок в @ApiOperation. Довольный собой, но с лёгким чувством тревоги я отдал свой Pull Request на Code Review. И вот тут мне сообщили, что я забыл про notes! А еще объяснили, что Swagger не подхватывает информацию из @ErrorCodes и именно поэтому приходится ее прописывать вручную. В тот вечер всё закончилось благополучно. Исправил свой недочет и ушёл на больничный.
Возможно, было бы нормально взять и пойти дальше. Поставить себе галочку на полочку к другим таким же, что мол, Макс, будь внимательнее, обращай на это внимание…
Но у меня не получилось. Весь вечер и следующее утро были потрачены на то, чтобы научить Swagger читать нашу аннотацию и самостоятельно дописывать в notes эти самые коды.
Действие 1. Здесь кто-то был...
Из беглого поиска удалось выяснить, что кто-то уже пытался подружить Swagger со своей аннотацией. Там же была ссылочка на документацию SpringFox в которой говорилось о том, что можно написать плагин!
Счастливое счастье накрыло меня настолько, что даже забыл о простуде и больничном! В своей будущей статье «Как три раза не уволиться из компании» я делюсь тремя историями спасения утопающих. Одна из них о том, как мне удалось написать плагин для Chrome + Firefox, который в несколько раз ускорял работу с Jenkins. Мне было так прикольно его писать! Ведь это микропроект! Мой самый-самый простенький стартап, но с реальными людьми, которые им пользуются. Тогда я вновь выбрался из рутины и обрел вдохновение. Выгорание ушло. Но об этом лучше расскажу в будущей статье. А пока вернемся к плагину для Swagger.
Действие 2. Работает!
Написать что-то работающее оказалось легко. Взял пример плагина из официальной документации SpringFox, убрал всё лишнее и добавил нужное.
Плагин. Версия 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);
}
}
Тестирование я начал с метода в котором не указано значение для notes в @ApiOperation.
@ApiOperation(value = "Some description.")
@PostMapping("/security/confirmation/check")
@ErrorCodes(values = {"sms.verification.code.fail"})
public ResponseDto check(@ApiParam @RequestBody @Valid RequestDto request) { ... }
Запуск и… Результат! Ура, работает! Строковый код sms.verification.code.fail появился в notes!
Действие 3. Работает, но не работает.
Потом я добавил несколько слов в notes и получился вот такой код:
@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) { ... }
Запустил снова. Результат оказался… неожиданным. SpringFox плагин перезаписал значение notes при генерации документации (О_о)!
Смотрю как устроен context.operationBuilder().notes(String) и вижу там следующее:
public OperationBuilder notes(String notes) {
this.notes = (String)BuilderDefaults.defaultIfAbsent(notes, this.notes);
return this;
}
Эм… Ок, тогда будем брать текущее значение notes и добавлять error codes. Остается достать аннотацию @ApiOperation, взять нужное значение и добавить к тому, что формирую сам.
Итак, финальная версия (доступная на gist.github.com)
Плагин. Версия 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);
}
}
Теперь получилось как надо!
Действие 4. Что там про Optional не из Java 8?
В самом начале работы над плагином не мог понять что не так с тем Optional, который возвращается при поиске аннотации. У этого класса не было стандартных методов, к которым привык работая с java.util.Optional. Например, нет метода ifPresent, зато есть метод orNull.
Выяснилось, что SpringFox использует Optional из Guava.
TL;DR
Написал плагин для SpringFox, который является Spring компонентом и на этапе генерации документации вызывается для чтения значений из нашей аннотации @ErrorCodes.
Плагин доступен на gist.github.com.