Привет! Сегодня будем добавлять подписку на группу статей в JavaRush. Этому соответствует
issue JRTB-5 на GitHub.
Поясню: в JavaRush есть раздел
Статьи, а в нем —
Группы статей. Идея состоит в том, чтобы через телеграм-бота получать уведомления о новой статье из одной или нескольких групп.
![“Java-проект от А до Я”: Добавляем возможность подписаться на группу статей. Часть 1 - 1]()
Добавляем 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.
Смотрим:
![“Java-проект от А до Я”: Добавляем возможность подписаться на группу статей. Часть 1 - 2]()
На изображении видно, что этот запрос поддерживает несколько параметров (query, type, filter).
Чтобы попробовать этот запрос на вкус, нужно найти кнопку
Try it out, после чего эти параметры можно будет настроить:
![“Java-проект от А до Я”: Добавляем возможность подписаться на группу статей. Часть 1 - 3]()
Также там можно настроить и тип, и фильтр, и кверю (здесь вообще интересно: это будет поиск по тексту в группе). Но пока запустим без каких-либо ограничений и посмотрим, сколько всего групп в JavaRush. Для этого нажмем на Execute. Чуть ниже будет ответ (в секции Server Response) на этот запрос:
![“Java-проект от А до Я”: Добавляем возможность подписаться на группу статей. Часть 1 - 4]()
Мы видим, что всего групп
30, запрос этот выполнился за 95ms и есть набор некоторых хедеров в ответе.
Далее попробуем настроить какие-то параметры. Выберем параметр type равным значению COMPANY и посмотрим, как изменится результат:
![“Java-проект от А до Я”: Добавляем возможность подписаться на группу статей. Часть 1 - 5]()
Их 4. Как это проверить? Легко: можно пойти на сайт, найти секцию статьи, выбрать все группы и там добавить соответствующий фильтр (
https://javarush.com/groups/all?type=COMPANY). И да, действительно, их всего 4. Хотя реально три :D
![“Java-проект от А до Я”: Добавляем возможность подписаться на группу статей. Часть 1 - 6]()
Пока что сходится. К слову, если мы проверим университеты, то их пока что нет.
Ради интереса посмотрите, что будет, если поставить filter = MY в браузере, где вы залогинены в Javarush и не залогинены.
Больше о сваггере — в
этой статье на Хабре.
Пишем клиента к Javarush API для групп
Теперь на основе открытого API напишем Java клиента, который умеет делать запросы, получать ответы и знает, какие именно объекты будут приходить.
Объекты также возьмем из сваггера, из секции
Models (в самом низу страницы).
Создадим новый пакет и назовем его javarushclient рядом с service, repository. В будущем мы это вынесем в отдельную библиотеку в рамках организации Javarush Community и будем использовать исключительно как зависимость.
В первую очередь нужно добавить Unitrest — библиотеку для создания http запросов к JavaRush API:
<dependency>
<groupId>com.konghq</groupId>
<artifactId>unirest-java</artifactId>
<version>${unirest.version}</version>
</dependency>
И выносим версию в блок properties:
<unirest.version>3.11.01</unirest.version>
Когда у нас появится зависимость, можно начинать добавлять код.
Создадим клиент для групп JavaRushGroupClient и реализацию в класс JavaRushGroupClientImpl.
Но сперва нужно создать DTO-шки (data transfer object) — то есть, классы, объекты которых будут носить все нужные для клиента данные.
Все модели можно посмотреть в сваггере, В самом низу есть секция
Models, в которой можно их считать.
Вот как выглядит
GroupDiscussionInfo в сваггере:
![“Java-проект от А до Я”: Добавляем возможность подписаться на группу статей. Часть 1 - 7]()
В пакете javarushclient создадим пакет
dto, в который добавим, на основе данных из сваггера, следующие классы:
MeGroupInfoStatus:
package com.github.javarushcommunity.jrtb.javarushclient.dto;
/**
* Member group status.
*/
public enum MeGroupInfoStatus {
UNKNOWN, CANDIDATE, INVITEE, MEMBER, EDITOR, MODERATOR, ADMINISTRATOR, BANNED
}
MeGroupInfo:
package com.github.javarushcommunity.jrtb.javarushclient.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.javarushcommunity.jrtb.javarushclient.dto;
/**
* Group Info type;
*/
public enum GroupInfoType {
UNKNOWN, CITY, COMPANY, COLLEGE, TECH, SPECIAL, COUNTRY
}
UserDiscussionInfo:
package com.github.javarushcommunity.jrtb.javarushclient.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.javarushcommunity.jrtb.javarushclient.dto;
/**
* Group Visibility status.
*/
public enum GroupVisibilityStatus {
UNKNOWN, RESTRICTED, PUBLIC, PROTECTED, PRIVATE, DISABLED, DELETED
}
Потом — GroupInfo:
package com.github.javarushcommunity.jrtb.javarushclient.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;
}
Так как
GroupInfo и
GroupDiscussionInfo почти полностью совпадают, свяжем их в наследовании —
GroupDiscusionInfo:
package com.github.javarushcommunity.jrtb.javarushclient.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.javarushcommunity.jrtb.javarushclient.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.javarushcommunity.jrtb.javarushclient.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.javarushcommunity.jrtb.javarushclient.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, который подготовит мапу для создания запроса (увидите его далее).
Основываясь на описанных выше классах, создадим интерфейс для
JavaRushGroupClient:
package com.github.javarushcommunity.jrtb.javarushclient;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupsCountRequestArgs;
import java.util.List;
/**
* Client for Javarush Open API corresponds to Groups.
*/
public interface JavaRushGroupClient {
/**
* 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.javarushcommunity.jrtb.javarushclient;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.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 JavaRushGroupClient} interface.
*/
@Component
public class JavaRushGroupClientImpl implements JavaRushGroupClient {
private final String javarushApiGroupPath;
public JavaRushGroupClientImpl(@Value("${javarush.api.path}") String javarushApi) {
this.javarushApiGroupPath = javarushApi + "/groups";
}
@Override
public List<GroupInfo> getGroupList(GroupRequestArgs requestArgs) {
return Unirest.get(javarushApiGroupPath)
.queryString(requestArgs.populateQueries())
.asObject(new GenericType<list<GroupInfo>>() {
})
.getBody();
}
@Override
public List<GroupDiscussionInfo> getGroupDiscussionList(GroupRequestArgs requestArgs) {
return Unirest.get(javarushApiGroupPath)
.queryString(requestArgs.populateQueries())
.asObject(new GenericType<list<GroupDiscussionInfo>>() {
})
.getBody();
}
@Override
public Integer getGroupCount(GroupsCountRequestArgs countRequestArgs) {
return Integer.valueOf(
Unirest.get(String.format("%s/count", javarushApiGroupPath))
.queryString(countRequestArgs.populateQueries())
.asString()
.getBody()
);
}
@Override
public GroupDiscussionInfo getGroupById(Integer id) {
return Unirest.get(String.format("%s/group%s", javarushApiGroupPath, id.toString()))
.asObject(GroupDiscussionInfo.class)
.getBody();
}
}
Путь к АПИшке я добавляю в конструкторе, используя уже знакомую нам аннотацию Value. Она подразумевает, что значение внутри аннотации соответствует полю в файле с пропертями. Поэтому добавим в application.properties новую строку:
javarush.api.path=https://javarush.com/api/1.0/rest
Теперь это значение будет находиться в одном месте для всех клиентов АПИ, и если путь к АПИ изменится, мы его быстро обновим.
Раньше я
забивал гвозди микроскопом получал ответ из http-запроса через Unirest, переводил его в строку и потом эту строку парсил через Jackson… Это было страшно, муторно и требовало многих дополнительных вещей. В этой
библиотеке можно посмотреть, как это выглядит. Как дойдут руки — отрефакторю все.
У кого будет желание попробовать обновить эту библиотеку — добавить получение объектов только при помощи инструментов библиотеки unirest — пишите в личку или как новое issue в самой библиотеке. Для вас это будет реальный опыт работы, а мне не жалко. Проведу полноценный code review и помогу, если нужно будет. |
Теперь вопрос: а работает ли наш код так, как мы ожидаем? Ответить несложно: нужно просто написать тесты для них. Как я уже не раз говорил, разработчики должны уметь писать тесты.
Поэтому, пользуясь нашим Swagger UI, будем отправлять запросы, смотреть ответы и подставлять их в тесты как ожидаемый результат.
Вы могли сразу заметить, что количество групп не статическое и может меняться. И вы правы. Вопрос лишь в том, насколько часто это количество меняется? Очень редко, так что в разрезе нескольких месяцев можно утверждать, что это значение будет статично. А если что-то изменится, обновим тесты.
Встречайте — JavaRushGroupClientTest:
package com.github.javarushcommunity.jrtb.javarushclient;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.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.javarushcommunity.jrtb.javarushclient.dto.GroupInfoType.TECH;
@DisplayName("Integration-level testing for JavaRushGroupClientImplTest")
class JavaRushGroupClientTest {
private final JavaRushGroupClient groupClient = new JavaRushGroupClientImpl("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, будет интересно!
Ну и как обычно —
лайк, подписка, колокольчик, ставь звезду
нашему проекту, пиши комментарии и оценивай статью!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
ебатьсебе мозги и просто подписаться по rss ?