JavaRush /Blogue Java /Random-PT /Estamos adicionando a capacidade de assinar um grupo de a...
Roman Beekeeper
Nível 35

Estamos adicionando a capacidade de assinar um grupo de artigos. (Parte 1) - "Projeto Java de A a Z"

Publicado no grupo Random-PT
Olá! Hoje adicionaremos uma assinatura a um grupo de artigos no JavaRush. Isso corresponde ao problema JRTB-5 no GitHub. Deixa eu explicar: JavaRush tem uma seção chamada Artigos , e nela existem Grupos de Artigos. A ideia é receber notificações sobre um novo artigo de um ou mais grupos por meio de um bot de telegram.“Projeto Java de A a Z”: Adicionando a capacidade de assinar um grupo de artigos.  Parte 1 - 1

Adicionar JRTB-5

Digamos que estou interessado em artigos do grupo Histórias de Sucesso . Portanto, quero me inscrever nas atualizações deste grupo e sempre receber um link para uma nova publicação. Como parte desta tarefa, precisamos aprender como usar a API aberta para trabalhar com grupos no JavaRush. Justo neste momento tal coisa chegou. Aqui está um link para uma descrição da API aberta .
Amigos! Quer saber imediatamente quando um novo código para um projeto ou um novo artigo for lançado? Entre no meu canal tg . Lá eu reúno meus artigos, pensamentos e desenvolvimento de código aberto.

O que é arrogância? Vamos descobrir agora

Ainda não falamos sobre a arrogância. Para quem não sabe, vou explicar brevemente: este é um lugar onde você pode olhar abertamente a API de um servidor e tentar fazer algumas requisições a ele. Normalmente, um swagger agrupa possíveis solicitações. No nosso caso, existem três grupos: forum-question , group , post . Em cada grupo haverá uma ou mais solicitações indicando todos os dados necessários para construir esta solicitação (ou seja, quais parâmetros adicionais podem ser passados, o que fazer com eles, qual método http e assim por diante). Aconselho vocês a lerem e observarem mais sobre esse assunto, pois essa é a parte do desenvolvimento que quase todos vocês encontrarão. Para descobrir, vamos descobrir quantos grupos existem no JavaRush. Para fazer isso, expanda o grupo group-controller e selecione Get request /api/1.0/rest/groups/count . Ele nos retornará o número de grupos no JavaRush. Vejamos: “Projeto Java de A a Z”: Adicionando a capacidade de assinar um grupo de artigos.  Parte 1 - 2A imagem mostra que esta consulta suporta vários parâmetros (consulta, tipo, filtro). Para testar esta solicitação, você precisa encontrar o botão Experimente , após o qual estes parâmetros podem ser configurados: “Projeto Java de A a Z”: Adicionando a capacidade de assinar um grupo de artigos.  Parte 1 - 3Você também pode configurar o tipo, o filtro e a consulta (na verdade é interessante aqui: esta será uma pesquisa de texto em um grupo). Mas, por enquanto, vamos executá-lo sem quaisquer restrições e ver quantos grupos existem no JavaRush. Para fazer isso, clique em Executar. Logo abaixo haverá uma resposta (na seção Resposta do Servidor) para esta solicitação: “Projeto Java de A a Z”: Adicionando a capacidade de assinar um grupo de artigos.  Parte 1 - 4Vemos que existem 30 grupos no total , esta solicitação foi concluída em 95ms e há um conjunto de alguns cabeçalhos na resposta. A seguir, vamos tentar configurar alguns parâmetros. Vamos selecionar o parâmetro de tipo igual ao valor COMPANY e ver como o resultado muda: “Projeto Java de A a Z”: Adicionando a capacidade de assinar um grupo de artigos.  Parte 1 - 5São 4. Como verificar isso? É fácil: você pode acessar o site, encontrar a seção de artigos, selecionar todos os grupos e adicionar o filtro apropriado lá ( https://javarush.com/groups/all?type=COMPANY ). E sim, de fato, existem apenas 4. Embora na verdade existam três :D “Projeto Java de A a Z”: Adicionando a capacidade de assinar um grupo de artigos.  Parte 1 - 6Até agora cabe. Aliás, se verificarmos as universidades, ainda não há nenhuma. Apenas por diversão, veja o que acontece se você definir filter = MY em um navegador onde você está logado no Javarush e não logado. Mais sobre a arrogância - neste artigo sobre Habré .

Escrevendo um cliente para a API Javarush para grupos

Agora, com base na API aberta, escreveremos um cliente Java que pode fazer solicitações, receber respostas e saber exatamente quais objetos chegarão. Também pegaremos objetos do swagger, da seção Modelos (no final da página). Vamos criar um novo pacote e chamá-lo de javarushclient próximo ao serviço, repositório. No futuro, moveremos isso para uma biblioteca separada dentro da organização da comunidade Javarush e a usaremos exclusivamente como uma dependência. Primeiro de tudo, você precisa adicionar Unitrest, uma biblioteca para criar solicitações http para a API JavaRush:
<dependency>
  <groupId>com.konghq</groupId>
  <artifactId>unirest-java</artifactId>
  <version>${unirest.version}</version>
</dependency>
E coloque a versão no bloco de propriedades:
<unirest.version>3.11.01</unirest.version>
Assim que tivermos uma dependência, podemos começar a adicionar código. Vamos criar um cliente para grupos JavaRushGroupClient e uma implementação na classe JavaRushGroupClientImpl. Mas primeiro você precisa criar DTOs (objetos de transferência de dados) - ou seja, classes cujos objetos transportarão todos os dados necessários para o cliente. Todos os modelos podem ser visualizados no swagger.Na parte inferior há uma seção Modelos , na qual você pode contá-los. Esta é a aparência de GroupDiscussionInfo no swagger: No pacote javarushclient, criaremos um “Projeto Java de A a Z”: Adicionando a capacidade de assinar um grupo de artigos.  Parte 1 - 7pacote dto , ao qual adicionaremos, com base nos dados do swagger, as seguintes classes:
  • MeGroupInfoStatus :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Member group status.
    */
    public enum MeGroupInfoStatus {
       UNKNOWN, CANDIDATE, INVITEE, MEMBER, EDITOR, MODERATOR, ADMINISTRATOR, BANNED
    }

  • Informações do meu grupo :

    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
    }

  • Informações sobre discussão do usuário :

    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;
    }

  • Status de visibilidade do grupo :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Group Visibility status.
    */
    public enum GroupVisibilityStatus {
       UNKNOWN, RESTRICTED, PUBLIC, PROTECTED, PRIVATE, DISABLED, DELETED
    }

  • Então - 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;
    }

Como GroupInfo e GroupDiscussionInfo são quase completamente iguais, vamos vinculá-los por herança - 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;
}
Também precisaremos de um filtro para a solicitação GroupFilter :
package com.github.javarushcommunity.jrtb.javarushclient.dto;

/**
* Filters for group requests.
*/
public enum GroupFilter {

   UNKNOWN, MY, ALL
}
Em uma solicitação para obter por ID, ele retorna GroupDiscussionInfo, e em uma solicitação para uma coleção de grupos, você pode obter GroupInfo e GroupDiscussionInfo. Como as solicitações podem ter tipo, consulta, filtro, deslocamento e limite, vamos criar uma classe GroupRequestArgs separada e torná-la uma classe construtora (leia qual é o padrão do construtor):
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;
   }
}
Para pesquisar o número de grupos, é um pouco diferente. Possui apenas consulta, tipo e filtro. E parece que você não deseja duplicar o código. Ao mesmo tempo, se você começar a combiná-los, ficará feio ao trabalhar com construtores. Então resolvi separá-los e repetir o código. Esta é a aparência de 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;
   }
}
Sim, não mencionei que as duas últimas classes possuem um método populateQueries, que preparará o mapa para a criação de uma consulta (você verá mais tarde). Com base nas classes descritas acima, vamos criar uma interface para 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);
}
Duas solicitações diferentes para o caso em que desejamos adicionar informações de GroupInfo ou GroupDiscussionInfo. Caso contrário, essas consultas serão idênticas, e a única diferença será que em uma o sinalizador includeDiscussion será verdadeiro e na outra será falso. Portanto, havia 4 métodos, e não três. Agora vamos começar a implementar:
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();
   }


}
Eu adiciono o caminho para a API no construtor usando a já familiar anotação Value. Implica que o valor dentro da anotação corresponde a um campo no arquivo de propriedades. Portanto, vamos adicionar uma nova linha em application.properties:
javarush.api.path=https://javarush.com/api/1.0/rest
Este valor agora estará em um só lugar para todos os clientes da API e, se o caminho da API mudar, iremos atualizá-lo rapidamente. Anteriormente, eu martelava pregos com um microscópio, recebia uma resposta de uma solicitação http via Unirest, traduzia em uma string e depois analisava essa string por meio de Jackson... Era assustador, tedioso e exigia muitas coisas adicionais. Nesta biblioteca você pode ver como é. Assim que eu colocar as mãos nele, refatorarei tudo.
Quem quiser tentar atualizar esta biblioteca - adicionar objetos receptores apenas utilizando as ferramentas da biblioteca unirest - escreva em mensagem pessoal ou como novo fascículo na própria biblioteca. Esta será uma experiência de trabalho real para você, mas não me importo. Conduzirei uma revisão completa do código e ajudarei se necessário.
Agora a questão é: nosso código funciona como esperamos? A resposta é fácil: você só precisa escrever testes para eles. Como já disse mais de uma vez, os desenvolvedores devem ser capazes de escrever testes. Portanto, utilizando nossa UI Swagger, enviaremos solicitações, analisaremos as respostas e as substituiremos nos testes conforme o resultado esperado. Você deve ter notado imediatamente que o número de grupos não é estático e pode mudar. E você está certo. A única questão é com que frequência esse número muda? Muito raramente, pelo que ao longo de vários meses podemos dizer que este valor será estático. E se algo mudar, atualizaremos os testes. Conheça - 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());
   }
}
Os testes são escritos no mesmo estilo de antes. Existem vários testes para cada solicitação. Não adianta testar tudo, pois acho que essa API já foi testada da melhor forma.

Conclusão

Como parte deste artigo, adicionamos um cliente Java para grupos à API JavaRush. Como se costuma dizer, viva e aprenda. Enquanto escrevia este cliente, aproveitei sua documentação e usei convenientemente o trabalho com os objetos que eles fornecem. Chamo sua atenção para a tarefa que propus. Se alguém estiver interessado, escreva-me uma mensagem privada, tenho a certeza que será uma experiência muito interessante. Esta foi a primeira parte. No segundo, implementaremos diretamente o comando de adição e (se cabermos em um artigo) adicionaremos obtendo uma lista de grupos nos quais o usuário está inscrito. A seguir, quem tiver vontade e talento para escrever textos para o bot, por favor me escreva por PM. Não sou especialista no assunto e qualquer ajuda seria muito útil. Vamos formalizar tudo isso como desenvolvimento open source, vai ser interessante! Bem, como sempre - curta, inscreva-se, ligue , dê uma estrela ao nosso projeto , escreva comentários e avalie o artigo!
Links Úteis

Uma lista de todos os materiais da série está no início deste artigo.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION