Geração de código do OpenAPI v3 (aka Swagger 3) para TypeScript e não apenas

Há dois anos, comecei o desenvolvimento mais um um gerador de código gratuito da OpenAPI Specification v3 para TypeScript ( está disponível no Github ). Inicialmente, decidi fazer uma geração eficiente de tipos de dados primitivos e complexos no TypeScript, levando em consideração vários recursos do esquema JSON , como oneOf / anyOf / allOf , etc. (A solução nativa do Swagger tinha alguns problemas com isso). Outra idéia foi usar esquemas de especificações para validação na parte frontal, traseira e outras partes do sistema.



Agora, o gerador de código está relativamente pronto - está no estágio MVP . Ele tem muito do que é necessário em termos de geração de tipos de dados, além de uma biblioteca experimental para gerar serviços de front-end (até agora para Angular). Neste artigo, quero mostrar os desenvolvimentos e dizer como eles podem ajudar se você usar o TypeScript e o OpenAPI v3. Ao longo do caminho, quero compartilhar algumas idéias e considerações que surgiram no meu processo de trabalho. Bem, se você estiver interessado, pode ler a história que eu escondi no spoiler para não complicar a leitura da parte técnica.


Conteúdo


  1. Antecedentes
  2. Descrição do produto
  3. Instalação e uso
  4. Pratique usando um gerador de código
  5. Usando tipos de dados gerados em aplicativos
  6. Decomposição de circuitos dentro da especificação da OEA
  7. Decomposição aninhada
  8. Serviços gerados automaticamente para trabalhar com a API REST
    1. Por que isso é necessário?
    2. Geração de serviços
    3. Usando serviços gerados
  9. Em vez de um posfácio


Antecedentes


Expandir para ler (pular)

Tudo começou há dois anos - então eu trabalhei em uma empresa que desenvolvia uma plataforma de mineração de dados e fui responsável pelo frontend (principalmente TypeScript + Angular). Os recursos do projeto eram estruturas de dados complexas com um grande número de parâmetros (30 ou mais) e nem sempre relações comerciais óbvias entre eles. A empresa estava crescendo e o ambiente de software passava por mudanças bastante frequentes. O front-end tinha que ter conhecimento das nuances, porque alguns cálculos foram duplicados na frente e no back-end. Ou seja, esse foi o caso ao usar o OpenAPI é mais do que apropriado. Encontrei um período na empresa em que, em questão de meses, a equipe de desenvolvimento adquiriu uma única especificação, que se tornou uma base de conhecimento comum para os departamentos de back, front e até mesmo o Core, que estavam escondidos atrás da ampla parte de trás do back-end da web. A versão OpenAPI foi escolhida “para crescimento” - e ainda muito jovem v3.0


Isso não era mais uma especificação em um ou mais arquivos YML / JSON estáticos, e não o resultado de anotadores , mas toda uma biblioteca de componentes, métodos, modelos e propriedades, organizados de acordo com o conceito DDD da plataforma. A biblioteca foi dividida em diretórios e arquivos, e um colecionador especialmente organizado produziu documentos da OEA para cada área de estudo. Uma maneira experimental foi o fluxo de trabalho construído, que poderia ser descrito como Design-First.


Há um bom artigo no blog da empresa Yandex.Money, que falou sobre Design First

O Design First e a especificação geral ajudaram a descralizar o conhecimento, mas um novo problema se tornou aparente - mantendo a relevância do código. A especificação descreveu várias dezenas de métodos e dezenas (e depois centenas) de entidades. Mas o código teve que ser escrito manualmente: tipos de dados, serviços para trabalhar com REST, etc. Um ou dois sprints com histórias paralelas mudaram bastante a imagem; adicione complexidade à fusão de várias histórias e ao fator humano. A rotina ameaçava ser significativa e a solução parecia óbvia - você precisa da geração de código. Afinal, as especificações da OEA já continham todo o necessário para não redigitá-lo manualmente. Mas não foi tão simples.


O frontend está no final do ciclo de produção, então senti mudanças mais dolorosas do que colegas de outros departamentos. Ao projetar a API REST, o ambiente de back-end estava decidindo e, mesmo após a aprovação do “Design First”, a inércia permaneceu; para o front-end, tudo parecia menos óbvio. De fato, eu entendi isso desde o início e comecei a sondar o solo com antecedência - quando a conversa sobre uma especificação "universal" estava apenas começando. Não se falava em escrever seu próprio gerador de código; Eu só queria encontrar algo pronto.


Fiquei desapontado Havia dois problemas: a versão 3.0 da OEA, com o apoio de quem parecia não haver pressa e a qualidade das soluções em si - naquela época (lembro-me há dois anos), consegui encontrar duas soluções relativamente prontas: da Swagger e da Microsoft (parece que ). No primeiro, o apoio à OEA 3.0 estava na versão beta profunda. O segundo funcionou apenas com a versão 2.x, mas não havia previsões inequívocas. A propósito, não consegui iniciar o gerador de código da Microsoft nem em um documento de teste no formato Swagger 2.0. A solução da Swagger funcionou, mas um esquema mais ou menos complicado com links $ ref se transformou em um incompreensível "ERRO!", E dependências recursivas enviaram-no para um loop infinito. Houve problemas com tipos primitivos . Além disso, eu não entendia muito bem como trabalhar com serviços gerados automaticamente - eles pareciam ser feitos para exibição, e seu uso real criava mais problemas do que eles resolviam (na minha opinião). E, finalmente, a integração do arquivo JAR em um CI / CD orientado ao NPM foi inconveniente: tive que baixar manualmente o instantâneo necessário , que parecia pesar 13 megabytes, e fazer algo com ele. De um modo geral, fiz uma pausa e decidi assistir o que acontece a seguir.


Após cerca de cinco meses, o problema da geração de código surgiu novamente. Eu tive que reescrever e expandir parte do aplicativo Web e, ao mesmo tempo, queria refatorar serviços antigos para trabalhar com a API REST e tipos de dados. Mas a avaliação da complexidade não foi otimista: de uma semana para duas - e isso é apenas para serviços REST e descrições de tipo. Não direi que isso me deprimiu muito, mas ainda assim. Por outro lado, nunca encontrei uma solução para geração de código e não esperei, e sua implementação dificilmente levaria menos tempo. Ou seja, não havia dúvida: o benefício é duvidoso, os riscos são grandes. Ninguém apoiaria essa idéia, e eu não a propus. Enquanto isso, as férias de maio se aproximavam e a empresa me "devia" vários dias por trabalhar no fim de semana. Durante duas semanas, fugi de todas as experiências de trabalho para a Geórgia, onde morei por quase um ano.


Entre festas e festas, eu precisava fazer alguma coisa e decidi escrever minha decisão. Trabalhar em cafés de verão perto de Vake Park foi surpreendentemente produtivo e voltei a Peter com um gerador de código pronto para tipos de dados. Então, por mais um mês, "terminei" os serviços nos finais de semana antes de ele estar pronto para o trabalho.


Desde o início, abri o gerador de código, trabalhando nele no meu tempo livre. Embora, de fato, ele tenha escrito um rascunho de trabalho. Não direi que a revisão / adaptação ocorreu sem problemas; e não vou dizer que eles foram significativos. Mas, em algum momento, notei que parei de usar a documentação Redoc / Swagger: navegar pelo código era mais conveniente, desde que o código estivesse sempre atualizado e com comentários. Logo, "pontuei" minhas realizações, sem desenvolvê-las, até que um colega (agora há seis meses que eu parti para outra empresa) me aconselhou a levá-las mais a sério (ele também veio com o nome).


Eu não tinha tempo livre suficiente e vários meses levaram para finalizar em segundo plano: playground , aplicação de teste, reorganização do projeto. Agora estou pronto para receber feedback.


Descrição do produto


No momento, a solução para geração de código inclui três bibliotecas NPM integradas no oscro @codegena e localizadas em um repositório comum:


A bibliotecaDescrição do produto
@ codegena / oapi3tsA biblioteca base é um conversor do OAS3 para descrições de tipo de dados (agora suporta apenas o TypeScript)
@ codegena / ng-api-serviceExtensão para Serviços Angulares
@ codegena / oapi3ts-cliShell para uso conveniente em scripts CLI


Instalação e uso


A opção mais prática é usar nos scripts do NodeJS executados a partir da CLI. Primeiro você precisa instalar as dependências:


 npm i @codegena/oapi3ts, @codegena/ng-api-service, @codegena/oapi3ts-cli 

Em seguida, crie um arquivo js (por exemplo, update-typings.js ) com o código:


 "use strict"; var cliLib = require('@codegena/oapi3ts-cli'); var cliApp = new cliLib.CliApplication; cliApp.createTypings(); // cliApp.createServices('angular'); // optional 

E comece passando três parâmetros:


 node ./update-typings.js --srcPath ./specs/todo-app-spec.json --destPath ./src/lib --separatedFiles true 

No destPath serão gerados arquivos e, de fato, o conteúdo desse diretório no repositório do projeto é criado da mesma maneira. Aqui está o script de geração , e é assim que ele é executado nos scripts do NPM. No entanto, se desejar, você pode usá-lo mesmo no navegador, como é feito no Playground .



Pratique usando um gerador de código


Em seguida, quero falar sobre o que obteremos como resultado: qual é a ideia de como isso nos ajudará. Um auxílio visual será o código do aplicativo de demonstração. Consiste em duas partes: um back-end (na estrutura NestJS ) e um front-end (no Angular ). Se desejar, você pode até executá-lo localmente .


Mesmo que você não esteja familiarizado com Angular e / ou NestJS, isso não deve causar problemas: os exemplos de código que serão fornecidos devem ser entendidos pela maioria dos desenvolvedores de TypeScript.

Embora o aplicativo seja o mais simplificado possível (por exemplo, o back-end armazena dados em uma sessão, não no banco de dados), tentei recriar o fluxo de dados e a hierarquia de tipos de dados inerentes ao aplicativo real. Está cerca de 80-85% pronto, mas o "acabamento" pode ser adiado, mas por enquanto é mais importante falar sobre o que já está lá.



Usando tipos de dados gerados em aplicativos


Suponha que tenhamos uma especificação OpenAPI (por exemplo, esta ) com a qual devemos trabalhar. Não importa se criamos algo do zero ou apoiamos, existe uma coisa importante com a qual provavelmente começaremos - digitando. Começaremos a descrever os tipos de dados básicos ou a fazer alterações neles. A maioria dos programadores faz isso para facilitar seu desenvolvimento futuro. Portanto, você não precisa examinar a documentação mais uma vez, lembre-se das listagens de parâmetros; e você pode ter certeza de que o IDE e / ou o compilador perceberão um erro de digitação.


Nossa especificação pode ou não incluir a seção components.schem . Mas, de qualquer forma, descreverá conjuntos de parâmetros, solicitações e respostas - e podemos usá-lo. Considere um exemplo:


 @Controller('group') export class AppController { // ... @Put(':groupId') rewriteGroup( @Param(ParseQueryPipe) { groupId }: RewriteGroupParameters, @Body() body: RewriteGroupRequest, @Session() session ): RewriteGroupResponse<HttpStatus.OK> { return this.appService .setSession(session) .rewriteGroup(groupId, body); } // ... } 

Este é um fragmento de controlador para a estrutura NestJS com os parâmetros ( RewriteGroupParameters ), corpo da solicitação ( RewriteGroupRequest ) e corpo da resposta ( RewriteGroupResponse<T> ) RewriteGroupResponse<T> . Já neste fragmento de código, podemos ver os benefícios da digitação:


  • Se confundirmos o nome do parâmetro destruído groupId , especificando groupId , obteremos imediatamente um erro no editor.
  • Se o método this.appService.rewriteGroup (groupId, body) tiver digitado parâmetros, poderemos controlar a correção do parâmetro do body passado. E se o formato dos dados de entrada do método do controlador ou do método de serviço mudar, nós o saberemos imediatamente. No futuro, observo que o método de entrada do método de serviço possui um tipo de dados diferente de RewriteGroupRequest , mas, no nosso caso, eles serão idênticos. No entanto, se de repente o método de serviço for alterado e começar a aceitar o ToDoGroup vez do ToDoGroupBlank , o IDE e o compilador mostrarão imediatamente os locais das discrepâncias:
  • Da mesma maneira, podemos controlar a conformidade do resultado retornado. Se a especificação do status de uma resposta bem-sucedida for alterada e se tornar 202 vez de 200 , também descobriremos isso, porque RewriteGroupResponse é um genérico com um tipo enumerado :

Agora, vamos ver um exemplo do aplicativo front-end que funciona com outro método da API :


 protected initSelectedGroupData(truth: ComponentTruth): Observable<ComponentTruth> { return this.getGroupsService.request(null, { isComplete: null, withItems: false }).pipe( pickResponseBody<GetGroupsResponse<200>>(200, null, true), switchMap<ToDoGroup[], Observable<ComponentTruth>>( groups => this.loadItemsOfSelectedGroups({ ...truth, groups }) ) ); } 

Não vamos nos antecipar e analisar o operador RxJS customizado pickResponseBody , mas vamos nos concentrar no refinamento do tipo GetGroupsResponse . Nós o usamos em uma cadeia de operadores RxJS, e o operador a seguir possui um refinamento de entrada de ToDoGroup[] . Se esse código funcionar, os tipos de dados indicados correspondem um ao outro. Aqui também podemos controlar a correspondência de tipos e, se o formato da resposta em nossa API mudar repentinamente, isso não escapará à nossa atenção:



E, é claro, os parâmetros de chamada this.getGroupsService.request também this.getGroupsService.request digitados. Mas este é o tópico dos serviços gerados.


Nos exemplos acima, vemos que a digitação de solicitações, respostas e parâmetros pode ser usada em várias partes do sistema - front-end, back-end etc. Se o back-end e o front-end estiverem no mesmo mono-repositório e tiverem um ambiente ecológico compatível, eles poderão usar a mesma biblioteca compartilhada com o código gerado. Porém, mesmo que o back-end e o front-end sejam suportados por equipes diferentes e não tenham nada em comum, exceto a especificação pública da OEA, ainda será mais fácil sincronizar seu código.


Decomposição de circuitos dentro da especificação da OEA


Provavelmente, nos exemplos anteriores, você prestou atenção nas ToDoGroup , ToDoGroup , com as quais RewriteGroupResponse e GetGroupsResponse . Na verdade, RewriteGroupResponse é apenas um alias genérico para ToDoGroup , HttpErrorBadRequest , etc. É fácil adivinhar que ToDoGroup e HttpErrorBadRequest são os esquemas da seção de especificação components.schem referenciada pelo ponto de extremidade rewriteGroup (diretamente ou por meio de intermediários ):


 "responses": { "200": { "description": "Todo group saved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ToDoGroup" } } } }, "400": { "$ref": "#/components/responses/errorBadRequest" }, "404": { "$ref": "#/components/responses/errorGroupNotFound" }, "409": { "$ref": "#/components/responses/errorConflict" }, "500": { "$ref": "#/components/responses/errorServer" } } 

Essa é a decomposição usual das estruturas de dados e seu princípio é o mesmo que em outras linguagens de programação. Os componentes, por sua vez, também podem ser decompostos: consulte outros componentes (incluindo recursivamente), use a combinação e outros recursos do Esquema JSON. Mas, independentemente da complexidade, eles devem ser convertidos corretamente em descrições de tipos de dados. Quero mostrar como você pode usar a decomposição no OpenAPI e como será o código gerado.


Os componentes em uma especificação da OEA bem projetada se sobrepõem ao modelo DDD dos aplicativos que o utilizam. Mas mesmo que a especificação seja imperfeita, você pode confiar nela, construindo seu próprio modelo de dados. Isso lhe dará mais controle sobre a correspondência dos seus tipos de dados com os tipos de subsistemas integráveis.

Como nosso aplicativo é uma lista de tarefas, a principal essência é a tarefa. É lógico colocá-lo nos componentes em primeiro lugar, porque outras entidades e terminais estarão de alguma forma conectados a ele. Mas antes disso você precisa entender duas coisas:


  • Descrevemos não apenas a abstração, mas também as regras de validação, e quanto mais precisas e inequívocas forem, melhor.
  • Como qualquer entidade armazenada em um banco de dados, uma tarefa possui dois tipos de propriedades: serviço e entrada do usuário.

Acontece que, dependendo do cenário de uso, temos duas estruturas de dados: a Tarefa que o usuário acabou de criar e a Tarefa que já está armazenada no banco de dados. No segundo caso, ele possui um UID exclusivo, data de criação, alteração etc., e esses dados devem ser atribuídos no back-end. Descrevi duas entidades ( ToDoTaskBlank e ToDoTask ) de tal maneira que a primeira é um subconjunto da segunda:


 "components": { "ToDoTaskBlank": { "title": "Base part of data of item in todo's group", "description": "Data about group item needed for creation of it", "properties": { "groupUid": { "description": "An unique id of group that item belongs to", "$ref": "#/components/schemas/Uid" }, "title": { "description": "Short brief of task to be done", "type": "string", "minLength": 3, "maxLength": 64 }, "description": { "description": "Detailed description and context of the task. Allowed using of Common Markdown.", "type": ["string", "null"], "minLength": 10, "maxLength": 1024 }, "isDone": { "description": "Status of task: is done or not", "type": "boolean", "default": "false", "example": false }, "position": { "description": "Position of a task in group. Allows to track changing of state of a concrete item, including changing od position.", "type": "number", "min": 0, "max": 4096, "example": 0 }, "attachments": { "type": "array", "description": "Any material attached to the task: may be screenshots, photos, pdf- or doc- documents on something else", "items": { "$ref": "#/components/schemas/AttachmentMeta" }, "maxItems": 16, "example": [] } }, "required": [ "isDone", "title" ], "example": { "isDone": false, "title": "Book soccer field", "description": "The complainant agreed and recruited more members to play soccer." } }, "ToDoTask": { "title": "Item in todo's group", "description": "Describe data structure of an item in group of tasks", "allOf": [ { "$ref": "#/components/schemas/ToDoTaskBlank" }, { "type": "object", "properties": { "uid": { "description": "An unique id of task", "$ref": "#/components/schemas/Uid", "readOnly": true }, "dateCreated": { "description": "Date/time (ISO) when task was created", "type": "string", "format": "date-time", "readOnly": true, "example": "2019-11-17T11:20:51.555Z" }, "dateChanged": { "description": "Date/time (ISO) when task was changed last time", "type": "string", "format": "date-time", "readOnly": true, "example": "2019-11-17T11:20:51.555Z" } }, "required": [ "dateChanged", "dateCreated", "position", "uid" ] } ] } } 

Na saída, temos duas interfaces TypeScript, e a primeira será herdada pela segunda :


 /** * ## Base part of data of item in todo's group * Data about group item needed for creation of it */ export interface ToDoTaskBlank { // ... imagine there are ToDoTaskBlank properties } /** * ## Item in todo's group * Describe data structure of an item in group of tasks */ export interface ToDoTask extends ToDoTaskBlank { /** * ## UID of element * An unique id of task */ readonly uid: string; /** * Date/time (ISO) when task was created */ readonly dateCreated: string; /** * Date/time (ISO) when task was changed last time */ readonly dateChanged: string; // ... imagine there are ToDoTaskBlank properties } 

Agora, temos as descrições básicas da entidade Tarefa e as referimos a elas no código de nosso aplicativo, como foi feito no aplicativo de demonstração :


 import { ToDoTask, ToDoTaskBlank, } from '@our-npm-scope/our-generated-lib'; export interface ToDoTaskTeaser extends ToDoTask { isInvalid?: boolean; /** * Means this task just created, has temporary uid * and not saved yet. */ isJustCreated?: boolean; /** * Means this task is saving now. */ isPending?: boolean; /** * Previous uid of task temporary assigned until * it gets saved and gets new UID from backend. */ prevTempUid?: string; } 

Neste exemplo, descrevemos uma nova entidade, adicionando ao ToDoTask as propriedades que nos faltam no lado do aplicativo front-end. Na verdade, expandimos o modelo de dados resultante, levando em consideração as especificidades locais. Em torno desse modelo, um conjunto de ferramentas locais e algo como um DTO primitivo aumentam gradualmente:


 export function downgradeTeaserToTask( taskTeaser: ToDoTaskTeaser ): ToDoTask { const task = { ...taskTeaser }; if (!task.description || !task.description.trim()) { delete task.description; } else { task.description = task.description.trim(); } delete task.isJustCreated; delete task.isPending; delete task.prevTempUid; return task; } export function downgradeTeaserToTaskBlank( taskTeaser: ToDoTaskTeaser ): ToDoTaskBlank { const task = downgradeTeaserToTask(taskTeaser) as any; delete task.dateChanged; delete task.dateCreated; delete task.uid; return task; } 

Alguém prefere tornar o modelo de dados mais integral e usar classes.
 export class ToDoTaskTeaser implements ToDoTask { // … imagine, definitions from ToDoTask are here constructor( task: ToDoTask, public isInvalid?: boolean, public isJustCreated?: boolean, public isPending?: boolean, public prevTempUid?: string ) { Object.assign(this, task); } downgradeTeaserToTask(): ToDoTask { const task = {...this}; if (!task.description || !task.description.trim()) { delete task.description; } else { task.description = task.description.trim(); } delete task.isJustCreated; delete task.isPending; delete task.prevTempUid; return task; } downgradeTeaserToTaskBlank(): ToDoTaskBlank { // … some code } } 

Mas isso é uma questão de estilo, adequação e como a arquitetura do aplicativo se desenvolverá. Em geral, independentemente da abordagem, podemos confiar em um modelo de dados básico e ter mais controle sobre a conformidade da digitação. Portanto, se por algum motivo o uid do ToDoTask se tornar um número, conheceremos todas as partes do código que exigem atualização:




Decomposição aninhada


Então agora temos a interface ToDoTask e podemos fazer referência a ela. Da mesma forma, descreveremos o ToDoTaskGroup e o ToDoTaskGroupBlank , e eles conterão propriedades dos tipos ToDoTask e ToDoTaskBlank , respectivamente. Mas agora vamos dividir o “Grupo de Tarefas” em dois, e não em três componentes: para maior clareza, descreveremos o delta em ToDoGroupExtendedData . Então, eu quero demonstrar uma abordagem na qual um componente é criado a partir dos outros dois:


 "ToDoGroup": { "allOf": [ { "$ref": "#/components/schemas/ToDoGroupBlank" }, { "$ref": "#/components/schemas/ToDoGroupExtendedData" } ] } 

Após iniciar a geração do código, obtemos uma construção TypeScript ligeiramente diferente:


 export type ToDoGroup = ToDoGroupBlank & // Data needed for group creation ToDoGroupExtendedData; // Extended data has to be obtained after first save 

Como o ToDoGroup não possui seu próprio "corpo", o gerador de código preferiu transformá-lo em uma união de interfaces. No entanto, se você adicionar a terceira parte com seu próprio esquema (anônimo), o resultado será uma interface com dois ancestrais (mas é melhor não fazer isso). E vamos observar que a propriedade de items da interface ToDoGroupBlank digitada como uma matriz do ToDoTaskBlank e redefinida no ToDoGroupBlank no ToDoTask . Assim, o gerador de código é capaz de transferir as nuances bastante complexas de decomposição do esquema JSON para o TypeScipt.


 /* tslint:disable */ import { ToDoTaskBlank } from './to-do-task-blank'; /** * ## Base part of data of group * Data needed for group creation */ export interface ToDoGroupBlank { // ... items?: Array<ToDoTaskBlank>; // ... } 

 /* tslint:disable */ import { ToDoTask } from './to-do-task'; /** * ## Extended data of group * Extended data has to be obtained after first save */ export interface ToDoGroupExtendedData { // ... items: Array<ToDoTask>; } 

Bem, é claro, no ToDoTask / ToDoTaskBlank também podemos usar a decomposição. Você deve ter notado que a propriedade attachments é descrita como uma matriz de elementos do tipo AttachmentMeta . E esse componente é descrito da seguinte maneira:


 "AttachmentMeta": { "description": "Common meta data model of any type of attachment", "oneOf": [ {"$ref": "#/components/schemas/AttachmentMetaImage"}, {"$ref": "#/components/schemas/AttachmentMetaDocument"}, {"$ref": "#/components/schemas/ExternalResource"} ] } 

Ou seja, esse componente se refere a outros componentes. Como ele não possui seu próprio esquema, o gerador de código não o transforma em um tipo de dados separado para não multiplicar entidades, mas transforma uma descrição anônima do tipo enumerado:


 /** * Any material attached to the task: may be screenshots, photos, pdf- or doc- * documents on something else */ attachments?: Array< | AttachmentMetaImage // Meta data of image attached to task | AttachmentMetaDocument // Meta data of document attached to task | string // Link to any external resource >; 

Ao mesmo tempo, para os componentes AttachmentMetaDocument e AttachmentMetaDocument , são descritas interfaces não anônimas importadas nos arquivos que as utilizam:


 import { AttachmentMetaDocument } from './attachment-meta-document'; import { AttachmentMetaImage } from './attachment-meta-image'; 

Mas mesmo em AttachmentMetaImage , podemos encontrar um link para outra interface ImageOptions renderizada, usada duas vezes, inclusive dentro de uma interface anônima (o resultado da conversão de AdditionalProperties ):


 /* tslint:disable */ import { ImageOptions } from './image-options'; /** * Meta data of image attached to task */ export interface AttachmentMetaImage { // ... /** * Possible thumbnails of uploaded image */ thumbs?: { [key: string]: { /** * Link to any external resource */ url?: string; imageOptions?: ImageOptions; }; }; // ... imageOptions: ImageOptions; } 

Assim, com base nas ToDoGroup ou ToDoGroup , na verdade integramos várias entidades ao código e a uma cadeia de suas conexões comerciais, o que nos dá mais controle sobre as alterações no sistema excessivo que vão além do nosso código. Obviamente, isso não faz sentido em todos os casos. Mas se você usa o OpenAPI, poderá ter mais um pequeno bônus, além da documentação real.



Serviços gerados automaticamente para trabalhar com a API REST



Por que isso é necessário?


Se usarmos um aplicativo front-end estatístico médio que funcione com uma API REST mais ou menos complexa, uma parte considerável de seu código será de serviços (ou apenas funções) para acessar a API. Eles incluem:


  • Mapeamentos de URL e Parâmetro
  • Validação de parâmetros, solicitação e resposta
  • Extração de dados e manipulação de emergência

É desagradável que, em muitos aspectos, isso seja típico e não contenha nenhuma lógica única. Vamos supor um exemplo - como um esboço geral, o trabalho com a API pode ser construído:


Um exemplo esquemático simplificado de trabalho com a API REST
 import _ from 'lodash'; import { Observable, fromFetch, throwError } from 'rxjs'; import { switchMap } from 'rxjs/operators'; // Definitions const URLS = { 'getTasksOfGroup': `${env.REST_API_BASE_URL}/tasks/\${groupId}`, // ... other urls ... }; const URL_TEMPLATES = _.mapValues(urls, url => _.template(url)); interface GetTaskConditions { isDone?: true | false; offset?: number; limit?: number; } interface ErrorReponse { error: boolean; message?: string; } // Helpers // I taken this snippet from StackOverflow only for example function encodeData(data) { return Object.keys(data).map(function(key) { return [key, data[key]].map(encodeURIComponent).join("="); }).join("&"); } // REST API functions // our REST API working function example function getTasksFromServer(groupUid: string, conditions: GetTaskConditions = {}): Observable<Response> { if (!groupUid) { return throwError(new Error('You should specify "groupUid"!')); } if (!_.isString(groupUid)) { return throwError(new Error('`groupUid` should be string!')); } if (_.isBoolean(conditions.isDone)) { // ... applying of conditions.isDone } else if (conditions.isDone !== undefined) { return throwError(new Error('`isDone` should be "true", "false" or should\'t be set!'!)); } if (offset) { // ... check of `offset` and applying or error throwing } if (limit) { // ... check of `limit` and applying or error throwing } const url = [ URL_TEMPLATES['getTasksOfGroup']({groupUid}), ...(conditions ? [encodeData(conditions)] : []) ]; return fromFetch(url); } // Using of REST API working functions function getRemainedTasks(groupUid: number): Observable<ToDoTask[] | ErrorReponse> { return getTasksFromServer(groupUid, {isDone: false}).pipe( switchMap(response => { if (response.ok) { // OK return data return response.json(); } else { // Server is returning a status requiring the client to try something else. return of({ error: true, message: `Error ${response.status}` }); } }), catchError(err => { // Network or other error, handle appropriately console.error(err); return of({ error: true, message: err.message }) }) ); } 

Você pode usar uma abstração de alto nível para trabalhar com o REST - dependendo da pilha usada, pode ser: Axios , Angular HttpClient ou qualquer outra solução semelhante. Mas o mais provável é que, basicamente, seu código coincida com este exemplo. Quase certamente, incluirá:


  • Serviços ou funções para acessar pontos de extremidade específicos (função getTasksFromServer em nosso exemplo)
  • Partes de código que processam o resultado (função getRemainedTasks )

Em um aplicativo do mundo real, esse código será mais complicado: a especificação do aplicativo de demonstração descreve 5-6 opções de resposta . Freqüentemente, a API REST é projetada de forma que cada status de resposta do servidor seja tratado adequadamente. Mas mesmo a verificação dos dados de entrada tende a se tornar mais difícil no processo de desenvolvimento do aplicativo: quanto mais tempo leva para dar suporte e processar análises de erros, mais você deseja saber sobre os gargalos na circulação de dados no aplicativo.


Em cada nó do encaixe das peças do software podem ocorrer erros, a detecção prematura dos quais (bem como a pesquisa de problemas difíceis de diagnosticar) pode ser muito cara para os negócios. Portanto, haverá verificações adicionais de esclarecimento. À medida que a base de código cresce, e o número de casos cobertos, o mesmo acontece com a complexidade de fazer alterações. Mas os negócios são uma mudança constante, e não há como contornar isso. Portanto, devemos nos preocupar com como faremos alterações antecipadamente.

Voltando ao tópico OpenAPI, observamos que nas especificações da OEA pode haver informações suficientes para:


  • Descreva todos os terminais necessários na forma de funções ou serviços
  • URL

. , , / — 5, 10 200, . , , : , , , RxJS- pickResponseBody , , - ; tapResponse , side-effect (tap) HTTP-. , - . , , .


, — -, . , , , "" / API "-" "" . - , "" ( ), .

, REST API Angular. , , /. . , , . , , .. .




" " . Angular-, update-typings.js :


 "use strict"; var cliLib = require('@codegena/oapi3ts-cli'); var cliApp = new cliLib.CliApplication; cliApp.createTypings(); cliApp.createServices('angular'); 

, Angular- API . , - - , . , RewriteGroupService . ApiService , , , -:


-
 // Typings for this API method import { RewriteGroupParameters, RewriteGroupResponse, RewriteGroupRequest } from '../typings'; // Schemas import { schema as domainSchema } from './schema.b4c655ec1635af1be28bd6'; /** * Service for angular based on ApiAgent solution. * Provides assured request to API method with implicit * validation and common errors handling scheme. */ @Injectable() export class RewriteGroupService extends ApiService< RewriteGroupResponse, RewriteGroupRequest, RewriteGroupParameters > { protected get method(): 'PUT' { return 'PUT'; } /** * Path template, example: `/some/path/{id}`. */ protected get pathTemplate(): string { return '/group/{groupId}'; } /** * Parameters in a query. */ protected get queryParams(): string[] { return ['forceSave']; } // ... } 

, JSON Schema , . , , :


 import { schema as domainSchema } from './schema.b4c655ec1635af1be28bd6'; 

, schema.b4c655ec1635af1be28bd6.ts , , .



, Angular-.


Angular-

ApiModule :


 import { ApiModule, API_ERROR_HANDLER } from '@codegena/ng-api-service'; import { CreateGroupItemService, GetGroupsService, GetGroupItemsService, UpdateFewItemsService } from '@codegena/todo-app-scheme'; @NgModule({ imports: [ ApiModule, // ... ], providers: [ RewriteGroupService, { provide: API_ERROR_HANDLER, useClass: ApiErrorHandlerService }, // ... ], // ... }) export class TodoAppModule { } 

, [])( https://angular.io/guide/dependency-injection ):


 @Injectable() export class TodoTasksStore { constructor( protected createGroupItemService: CreateGroupItemService, protected getGroupsService: GetGroupsService, protected getGroupItemsService: GetGroupItemsService, protected updateFewItemsService: UpdateFewItemsService ) {} } 

— , request , :


 return this.getGroupsService.request(null, { isComplete: null, withItems: false }).pipe( pickResponseBody<GetGroupsResponse<200>>(200, null, true), switchMap<ToDoGroup[], Observable<ComponentTruth>>( groups => this.loadItemsOfSelectedGroups({ ...truth, groups }) ) ); 

request Observable<HttpResponse<R> | HttpEvent<R>> , , . , , . , , , . RxJS- pickResponseBody .


, , , . API, . . , :



. JSON Schema . , "" - . , Sentry Kibana , . . , , .


, . , :)


Em vez de um posfácio


, . -, " " — . , , , .


— , - / ( ). , — .


.

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


All Articles