Вітання! Сьогодні будемо додавати передплату на групу статей у JavaRush. Цьому відповідає issue JRTB-5 на GitHub. Поясню: у JavaRush є розділ Статті , а в ньому - Групи статей. Ідея полягає в тому, щоб через телеграм-бота отримувати повідомлення про нову статтю з однієї чи кількох груп.
Насамперед потрібно додати Unitrest — бібліотеку для створення http запитів до JavaRush API:
забивав цвяхи мікроскопом, отримував відповідь з http-запиту через Unirest, перекладав його в рядок і потім цей рядок парсив через Jackson… Це було страшно, нудно і вимагало багатьох додаткових речей. У цій бібліотеці можна побачити, як це виглядає. Як дійдуть руки — відрефактор все.
Тепер питання: а чи працює наш код так, як ми очікуємо? Відповісти нескладно: потрібно просто написати тести для них. Як я вже неодноразово говорив, розробники повинні вміти писати тести. Тому, користуючись нашим Swagger UI, відправлятимемо запити, дивитися відповіді та підставлятимемо їх у тести як очікуваний результат. Ви могли відразу помітити, що кількість груп не статична і може змінюватись. І ви маєте рацію. Питання лише в тому, як часто ця кількість змінюється? Дуже рідко, тож у розрізі кількох місяців можна стверджувати, що це значення буде статичним. А якщо щось зміниться, оновимо випробування. Зустрічайте - CodeGymGroupClientTest:
лайк, передплата, дзвіночок , став зірку нашому проекту , пиши коментарі та оцінюй статтю!
Додаємо JRTB-5
Скажімо, мені цікаві статті із групи Історії успіху . Тому я хочу підписатися на оновлення цієї групи та отримувати щоразу посилання на нову публікацію. В рамках реалізації цього завдання нам потрібно навчитися користуватися відкритим API для роботи з групами JavaRush. Саме до цього моменту така річ наспіла. Ось посилання на опис відкритого API .Друзі! Хочете відразу дізнаватися, коли вийде новий код проекту чи нова стаття? Приєднуйтесь до мого каналу . Там збираю свою статтю, думки та open-source розробку воєдино. |
Що таке swagger? Зараз розберемося
Про сваггера ми ще не говорабо. Для тих, хто не знає, коротко поясню: це місце, де можна відкрито подивитися на API якогось сервера і спробувати виконати якісь запити до нього. Зазвичай у сваггері згруповано можливі запити. У нашому випадку є три групи: forum-question , group , post. У кожній групі буде один і більше запитів із зазначенням усіх необхідних даних, щоб цей запит побудувати (тобто які додаткові параметри можна передати, що з ними робити, який http метод, і так далі). Раджу почитати і подивитися більше з цієї теми, тому це частина розробки, з якою зіткнеться майже кожен з вас. Щоб розібратися, дізнаємося, скільки груп є в JavaRush. Для цього розкриємо групу group-controller та виберемо Get запит /api/1.0/rest/groups/count . Він поверне нам кількість груп у JavaRush. Дивимося: На зображенні видно, що запит підтримує кілька параметрів (query, type, filter). Щоб спробувати цей запит на смак, потрібно знайти кнопку Try it out , після чого ці параметри можна буде налаштувати:Також там можна налаштувати і тип, і фільтр, і кверю (тут взагалі цікаво: це буде пошук за текстом у групі). Але поки що запустимо без будь-яких обмежень і подивимося, скільки всього груп у JavaRush. Для цього натисніть на Execute. Трохи нижче буде відповідь (в секції Server Response) на цей запит: Ми бачимо, що всього груп 30 , цей запит виконався за 95ms і є набір деяких хедерів у відповіді. Далі спробуємо налаштувати якісь параметри. Виберемо параметр type рівним значенню COMPANY і подивимося, як зміниться результат: 4. Як це перевірити? Легко: можна піти на сайт, знайти секцію статті, вибрати всі групи і там додати відповідний фільтр ( https://javarush.com/groups/all?type=COMPANY ). І так, дійсно, їх всього 4. Хоча реально три :DПоки що сходиться. До речі, якщо ми перевіримо університети, їх поки що немає. Заради інтересу подивіться, що буде, якщо поставити filter = MY у браузері, де ви залогінені в Javarush і не залогінені. Більше про сваггер — у цій статті на Хабрі .Пишемо клієнта до Javarush API для груп
Тепер на основі відкритого API напишемо Java клієнта, який вміє робити запити, отримувати відповіді та знає, які саме об'єкти будуть надходити. Об'єкти також візьмемо із сваггера, із секції Models (у самому низу сторінки). Створимо новий пакет і назвемо його codegymclient поряд з service, repository. У майбутньому ми це винесемо до окремої бібліотеки в рамках організації Javarush Community і будемо використовувати виключно як залежність.Про створення Java клієнтів я вже писав у статті “Гайд зі створення клієнта для Skyscanner API та його публікації у jCenter та Maven Central” . |
<dependency>
<groupId>com.konghq</groupId>
<artifactId>unirest-java</artifactId>
<version>${unirest.version}</version>
</dependency>
І виносимо версію до блоку properties:
<unirest.version>3.11.01</unirest.version>
Коли з'явиться залежність, можна починати додавати код. Створимо клієнт для груп CodeGymGroupClient та реалізацію в клас CodeGymGroupClientImpl. Але спочатку потрібно створити DTO-шки (data transfer object) - тобто, класи, об'єкти яких будуть носити всі необхідні для клієнта дані. Всі моделі можна подивитися в сваггері, У самому низу є секція Models , в якій їх можна вважати. Ось як виглядає GroupDiscussionInfo у сваггері: У пакеті codegymclient створимо пакет dto , до якого додамо, на основі даних із сваггера, наступні класи:
-
MeGroupInfoStatus :
package com.github.codegymcommunity.jrtb.codegymclient.dto; /** * Member group status. */ public enum MeGroupInfoStatus { UNKNOWN, CANDIDATE, INVITEE, MEMBER, EDITOR, MODERATOR, ADMINISTRATOR, BANNED }
-
MeGroupInfo :
package com.github.codegymcommunity.jrtb.codegymclient.dto; import lombok.Data; /** * Group information related to authorized user. If there is no user - will be null. */ @Data public class MeGroupInfo { private MeGroupInfoStatus status; private Integer userGroupId; }
-
GroupInfoType :
package com.github.codegymcommunity.jrtb.codegymclient.dto; /** * Group Info type; */ public enum GroupInfoType { UNKNOWN, CITY, COMPANY, COLLEGE, TECH, SPECIAL, COUNTRY }
-
UserDiscussionInfo :
package com.github.codegymcommunity.jrtb.codegymclient.dto; import lombok.Data; /** * DTO for User discussion info. */ @Data public class UserDiscussionInfo { private Boolean isBookmarked; private Integer lastTime; private Integer newCommentsCount; }
-
GroupVisibilitysdtatus :
package com.github.codegymcommunity.jrtb.codegymclient.dto; /** * Group Visibility status. */ public enum GroupVisibilityStatus { UNKNOWN, RESTRICTED, PUBLIC, PROTECTED, PRIVATE, DISABLED, DELETED }
-
Потім GroupInfo :
package com.github.codegymcommunity.jrtb.codegymclient.dto; import lombok.Data; import lombok.ToString; /** * Group Info DTO class. */ @Data @ToString public class GroupInfo { private Integer id; private String avatarUrl; private String createTime; private String description; private String key; private Integer levelToEditor; private MeGroupInfo meGroupInfo; private String pictureUrl; private String title; private GroupInfoType type; private Integer userCount; private GroupVisibilityStatus visibilityStatus; }
package com.github.codegymcommunity.jrtb.codegymclient.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* Group discussion info class.
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ToString(callSuper = true)
public class GroupDiscussionInfo extends GroupInfo {
private UserDiscussionInfo userDiscussionInfo;
private Integer commentsCount;
}
Також нам знадобиться фільтр для запиту GroupFilter :
package com.github.codegymcommunity.jrtb.codegymclient.dto;
/**
* Filters for group requests.
*/
public enum GroupFilter {
UNKNOWN, MY, ALL
}
У запиті на отримання ID-шника видає GroupDiscussionInfo, а в запиті на колекцію груп можна отримати як GroupInfo, так і GroupDiscussionInfo. Оскільки запити можуть мати type, query, filter, offset і limit, створимо окремий клас GroupRequestArgs і зробимо його класом-білдером (почитайте, що таке патерн builder):
package com.github.codegymcommunity.jrtb.codegymclient.dto;
import lombok.*;
import java.util.HashMap;
import java.util.Map;
import static java.util.Objects.nonNull;
/**
* Request arguments for group requests.
*/
@Builder
@Getter
public class GroupRequestArgs {
private final String query;
private final GroupInfoType type;
private final GroupFilter filter;
/**
* specified where to start getting groups
*/
private final Integer offset;
/**
* Limited number of groups.
*/
private final Integer limit;
public Map populateQueries() {
Map queries = new HashMap<>();
if(nonNull(query)) {
queries.put("query", query);
}
if(nonNull(type)) {
queries.put("type", type);
}
if(nonNull(filter)) {
queries.put("filter", filter);
}
if(nonNull(offset)) {
queries.put("offset", offset);
}
if(nonNull(limit)) {
queries.put("limit", limit);
}
return queries;
}
}
Для пошуку кількості груп він дещо відрізняється. У ньому є лише query, type та filter. І начебто, не хочеться дублювати код. Разом з тим, якщо почати об'єднувати їх, виходить некрасиво в роботі з білдерами. Тому я вирішив їх розділити та зробити повторення коду. Ось як виглядає GroupCountRequestArgs :
package com.github.codegymcommunity.jrtb.codegymclient.dto;
import lombok.Builder;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
import static java.util.Objects.nonNull;
/**
* Request arguments for group count requests.
*/
@Builder
@Getter
public class GroupsCountRequestArgs {
private final String query;
private final GroupInfoType type;
private final GroupFilter filter;
public Map populateQueries() {
Map queries = new HashMap<>();
if (nonNull(query)) {
queries.put("query", query);
}
if (nonNull(type)) {
queries.put("type", type);
}
if (nonNull(filter)) {
queries.put("filter", filter);
}
return queries;
}
}
Так, я не згадав, що в останніх двох класах є метод populateQueries, який підготує карту для створення запиту (побачте його далі). Грунтуючись на описаних вище класах, створимо інтерфейс для CodeGymGroupClient :
package com.github.codegymcommunity.jrtb.codegymclient;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupDiscussionInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupRequestArgs;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupsCountRequestArgs;
import java.util.List;
/**
* Client for Javarush Open API corresponds to Groups.
*/
public interface CodeGymGroupClient {
/**
* Get all the {@link GroupInfo} filtered by provided {@link GroupRequestArgs}.
*
* @param requestArgs provided {@link GroupRequestArgs}.
* @return the collection of the {@link GroupInfo} objects.
*/
List<GroupInfo> getGroupList(GroupRequestArgs requestArgs);
/**
* Get all the {@link GroupDiscussionInfo} filtered by provided {@link GroupRequestArgs}.
*
* @param requestArgs provided {@link GroupRequestArgs}
* @return the collection of the {@link GroupDiscussionInfo} objects.
*/
List<GroupDiscussionInfo> getGroupDiscussionList(GroupRequestArgs requestArgs);
/**
* Get count of groups filtered by provided {@link GroupRequestArgs}.
*
* @param countRequestArgs provided {@link GroupsCountRequestArgs}.
* @return the count of the groups.
*/
Integer getGroupCount(GroupsCountRequestArgs countRequestArgs);
/**
* Get {@link GroupDiscussionInfo} by provided ID.
*
* @param id provided ID.
* @return {@link GroupDiscussionInfo} object.
*/
GroupDiscussionInfo getGroupById(Integer id);
}
Два різні запити на випадок, коли ми хочемо отримати інформацію GroupInfo або GroupDiscussionInfo додав. В іншому ці запити тотожні, і різниця буде лише в тому, що в одному прапорі includeDiscussion буде true, а в іншому - false. Тому вийшло 4 методи, а не три. Тепер приступимо до реалізації:
package com.github.codegymcommunity.jrtb.codegymclient;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupDiscussionInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupRequestArgs;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupsCountRequestArgs;
import kong.unirest.GenericType;
import kong.unirest.Unirest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Implementation of the {@link CodeGymGroupClient} interface.
*/
@Component
public class CodeGymGroupClientImpl implements CodeGymGroupClient {
private final String codegymApiGroupPath;
public CodeGymGroupClientImpl(@Value("${codegym.api.path}") String codegymApi) {
this.codegymApiGroupPath = codegymApi + "/groups";
}
@Override
public List<GroupInfo> getGroupList(GroupRequestArgs requestArgs) {
return Unirest.get(codegymApiGroupPath)
.queryString(requestArgs.populateQueries())
.asObject(new GenericType<list<GroupInfo>>() {
})
.getBody();
}
@Override
public List<GroupDiscussionInfo> getGroupDiscussionList(GroupRequestArgs requestArgs) {
return Unirest.get(codegymApiGroupPath)
.queryString(requestArgs.populateQueries())
.asObject(new GenericType<list<GroupDiscussionInfo>>() {
})
.getBody();
}
@Override
public Integer getGroupCount(GroupsCountRequestArgs countRequestArgs) {
return Integer.valueOf(
Unirest.get(String.format("%s/count", codegymApiGroupPath))
.queryString(countRequestArgs.populateQueries())
.asString()
.getBody()
);
}
@Override
public GroupDiscussionInfo getGroupById(Integer id) {
return Unirest.get(String.format("%s/group%s", codegymApiGroupPath, id.toString()))
.asObject(GroupDiscussionInfo.class)
.getBody();
}
}
Шлях до АПИшке я додаю в конструкторі, використовуючи вже знайому нам інструкцію Value. Вона має на увазі, що значення всередині інструкції відповідає полю у файлі з пропертями. Тому додамо до application.properties новий рядок:
codegym.api.path=https://codegym.com/api/1.0/rest
Тепер це значення буде в одному місці для всіх клієнтів API, і якщо шлях до API зміниться, ми його швидко оновимо. Раніше я У кого буде бажання спробувати оновити цю бібліотеку — додати одержання об'єктів лише за допомогою інструментів бібліотеки unirest — пишіть у личку або як нове issue у самій бібліотеці. Вам це буде реальний досвід роботи, а мені не шкода. Проведу повноцінний code review та допоможу, якщо потрібно буде. |
package com.github.codegymcommunity.jrtb.codegymclient;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupDiscussionInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupRequestArgs;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupsCountRequestArgs;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static com.github.codegymcommunity.jrtb.codegymclient.dto.GroupInfoType.TECH;
@DisplayName("Integration-level testing for CodeGymGroupClientImplTest")
class CodeGymGroupClientTest {
private final CodeGymGroupClient groupClient = new CodeGymGroupClientImpl("https://javarush.com/api/1.0/rest");
@Test
public void shouldProperlyGetGroupsWithEmptyArgs() {
//given
GroupRequestArgs args = GroupRequestArgs.builder().build();
//when
List<GroupInfo> groupList = groupClient.getGroupList(args);
//then
Assertions.assertNotNull(groupList);
Assertions.assertFalse(groupList.isEmpty());
}
@Test
public void shouldProperlyGetWithOffSetAndLimit() {
//given
GroupRequestArgs args = GroupRequestArgs.builder()
.offset(1)
.limit(3)
.build();
//when
List<GroupInfo> groupList = groupClient.getGroupList(args);
//then
Assertions.assertNotNull(groupList);
Assertions.assertEquals(3, groupList.size());
}
@Test
public void shouldProperlyGetGroupsDiscWithEmptyArgs() {
//given
GroupRequestArgs args = GroupRequestArgs.builder().build();
//when
List<GroupDiscussionInfo> groupList = groupClient.getGroupDiscussionList(args);
//then
Assertions.assertNotNull(groupList);
Assertions.assertFalse(groupList.isEmpty());
}
@Test
public void shouldProperlyGetGroupDiscWithOffSetAndLimit() {
//given
GroupRequestArgs args = GroupRequestArgs.builder()
.offset(1)
.limit(3)
.build();
//when
List<GroupDiscussionInfo> groupList = groupClient.getGroupDiscussionList(args);
//then
Assertions.assertNotNull(groupList);
Assertions.assertEquals(3, groupList.size());
}
@Test
public void shouldProperlyGetGroupCount() {
//given
GroupsCountRequestArgs args = GroupsCountRequestArgs.builder().build();
//when
Integer groupCount = groupClient.getGroupCount(args);
//then
Assertions.assertEquals(30, groupCount);
}
@Test
public void shouldProperlyGetGroupTECHCount() {
//given
GroupsCountRequestArgs args = GroupsCountRequestArgs.builder()
.type(TECH)
.build();
//when
Integer groupCount = groupClient.getGroupCount(args);
//then
Assertions.assertEquals(7, groupCount);
}
@Test
public void shouldProperlyGetGroupById() {
//given
Integer androidGroupId = 16;
//when
GroupDiscussionInfo groupById = groupClient.getGroupById(androidGroupId);
//then
Assertions.assertNotNull(groupById);
Assertions.assertEquals(16, groupById.getId());
Assertions.assertEquals(TECH, groupById.getType());
Assertions.assertEquals("android", groupById.getKey());
}
}
Тести написані в такому ж стилі, як раніше. Є по кілька тестів на кожен запит. Тестувати все не має сенсу, тому що я думаю, що цей АПІ і так уже відтестовано найкращим чином.
Висновок
У рамках цієї статті ми додали клієнта Java для груп до JavaRush API. Як то кажуть, вік живи — вік навчайся. Поки писав цього клієнта, скористався їхньою документацією і зручно використовував роботу з об'єктами, яку вони надають. Звертаю увагу на завдання, яке я запропонував. Кому цікаво — пишіть особистим повідомленням, я більш ніж впевнений, що це буде цікавий досвід. То була перша частина. У другій ми реалізуємо безпосередньо команду з додавання та (якщо вкладемося в одну статтю) додамо отримання списку груп, на яких користувач підписаний. Далі, хто має бажання і талант до написання текстів для бота, прошу написати мені в ЛЗ. Я не фахівець у цій справі, і будь-яка допомога мені буде дуже доречною. Оформимо все це як розробку в open source, буде цікаво! Ну і як завжди -Корисні посилання |
---|
|
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ