So, last time we stopped at a description of the api build process, since then some things have changed. Namely - Grunt was replaced by Gulp. The main reason for this change is the speed of work.
After the transition, the difference became noticeable to the naked eye (especially since gulp displays the time spent on each task). This result is achieved due to the fact that all tasks are performed in parallel by default. Such a solution suited us perfectly. Some parts of the work that Grunt performed were independent of each other, which means they could be performed simultaneously. For example, indenting the definitions and paths files.
Unfortunately, it was not without drawbacks. Tasks requiring the previous ones remain. But gulp has an extensive database of packages for all occasions, so the solution was found fairly quickly - runSequence package
gulp.task('client', (callback) => { runSequence( 'client:indent', 'client:concat', 'client:replace', 'client:validate', 'client:toJson', 'client:clean', callback ); });
That is, instead of the standard task declaration for gulp, the argument is a callback in which tasks are executed in the specified order. In our case, the order of execution was important only for 4 tasks out of 40, so the increase in speed compared with Grunt is noticeable.
Also, gulp allowed to refuse coffeescript in favor of ES6. The code has changed minimally, but there is no need for infrequent changes in the configuration of the api assembly to remember how to write on coffeescript, since it has not been used anywhere else.
Example of a part of configurations for comparison
gulp.task('admin:indent_misc', () => { return gulp.src(`${root}/projects.yml`) .pipe(indent({ tabs: false, amount: 2 })) .pipe(gulp.dest(`${interimDir}`)) });
indent: admin_misc: src: [ '<%= admin_root %>/authorization.yml' '<%= admin_root %>/projects.yml' ] dest: '<%= admin_interim_dir %>/' options: style: 'space' size: 2 change: 1
Also, it is worth mentioning the small rake, which we managed to step on.
They consisted in the following: after generating api files and attempting to launch an angular application, an error was displayed to re-export the ResourceService. The starting point for the search was the file api / model / models.ts. It contains exports of all interfaces and services that are used in the future.
Here you should add a small digression and tell how swagger-codegen assigns names to interfaces and services.
Interface
Based on the interface template , if the entity property has an object type, then an individual interface is created for it, which is called% EntityName_PropertyName%.
Service
Based on the service template, the service name consists of the tag name and the word Service, for example, OrderService. Therefore, if you specify several tags on the path in the specification, then this method will fall into several services. This approach allows in one case to import only the required service and to avoid importing several services in another.
So, in the models.ts file there were actually two ResourceService exports, one representing the service for accessing the resource entity methods, and the other for the service property of the resource entity. Therefore, there was such a conflict. The solution was to rename the property.
As I said before, the swagger specification allows you to create the necessary files for working with api for both the backend and the frontend. In our case, the generation of the api code for Angular2 is performed using a simple command:
java -jar ./swagger-codegen-cli.jar generate \ -i client_swagger.json \ -l typescript-angular \ -o ../src/app/api \ -c ./typescript_config.json
Analysis of parameters:
Considering that the number of languages, and accordingly templates and code for generation, is enormous, from time to time there appears in my head the idea to reassemble the codegen only for our needs and leave only Typescript-Angular. Moreover, the developers themselves provide instructions on how to add your own templates.
In this simple way we get all the necessary modules, interfaces, classes and services for working with api.
An example of one of the interfaces obtained using codegen:
Service: type: object required: - id properties: id: type: integer description: Unique service identifier format: 'int32' readOnly: true date: type: string description: Registration date format: date readOnly: true start_date: type: string description: Start service date format: date readOnly: true expire_date: type: string description: End service date format: date readOnly: true status: type: string description: Service status enum: - 'empty' - 'allocated' - 'launched' - 'stalled' - 'stopped' - 'deallocated' is_primary: type: boolean description: Service primary state priority: type: integer description: Service priority format: 'int32' readOnly: true attributes: type: array description: Service attributes items: type: string primary_service: type: integer description: Unique service identifier format: 'int32' readOnly: true example: 138 options: type: array items: type: string order: type: integer description: Unique order identifier format: 'int32' readOnly: true proposal: type: integer description: Unique proposal identifier format: 'int32' readOnly: true resources: type: array items: type: object properties: url: type: string description: Resources for this service Services: type: array items: $ref: '#/definitions/Service'
import { ServiceOptions } from './serviceOptions'; import { ServiceOrder } from './serviceOrder'; import { ServicePrimaryService } from './servicePrimaryService'; import { ServiceProposal } from './serviceProposal'; import { ServiceResources } from './serviceResources'; /** * Service entry reflects fact of obtaining some resources within order (technical part). In other hand service points to proposal that was used for ordering (commercial part). Service can be primary (ordered using tariff proposal) and non-primary (ordered using option proposal). */ export interface Service { /** * Record id */ id: number; /** * Service order date */ date?: string; /** * Service will only be launched after this date (if nonempty) */ start_date?: string; /** * Service will be stopped after this date (if nonempty) */ expire_date?: string; /** * Service current status. Meaning: * empty - initial status, not allocated * allocated - all option services and current service are allocated and ready to launch * launched - all option services and current one launched and works * stalled - service can be stalled in any time. Options also goes to the same status * stopped - service and option services terminates their activity but still stay allocated * deallocated - resources of service and option ones are released and service became piece of history */ status?: number; /** * Whether this service is primary in its order. Otherwise it is option service */ is_primary?: boolean; /** * Optional priority in order allocating process. The less number the earlier service will be allocated */ priority?: number; primary_service?: ServicePrimaryService; order?: ServiceOrder; proposal?: ServiceProposal; /** * Comment for service */ comment?: string; /** * Service's cost (see also pay_type, pay_period, onetime_cost) */ cost?: number; /** * Service's one time payment amount */ onetime_cost?: number; /** * Bill amount calculation type depending on service consuming */ pay_type?: Service.PayTypeEnum; /** * Service bill payment period */ pay_period?: Service.PayPeriodEnum; options?: ServiceOptions; resources?: ServiceResources; } export namespace Service { export enum PayTypeEnum { Fixed = <any> 'fixed', Proportional = <any> 'proportional' } export enum PayPeriodEnum { Daily = <any> 'daily', Monthly = <any> 'monthly', Halfyearly = <any> 'halfyearly', Yearly = <any> 'yearly' } }
/dedic/services: get: tags: [Dedicated, Service] x-swagger-router-controller: app.controllers.service operationId: get_list security: - oauth: [] summary: Get services list parameters: - $ref: '#/parameters/limit' - $ref: '#/parameters/offset' responses: 200: description: Returns services schema: $ref: '#/definitions/Services' examples: application/json: objects: - id: 3 date: '2016-11-01' start_date: '2016-11-02' expire_date: '2017-11-01' status: 'allocated' is_primary: true priority: 3 primary_service: null options: url: "https://doc.miran.ru/api/v1/dedic/services/3/options" order: url: 'https://doc.miran.ru/api/v1/orders/3' comment: 'Test comment for service id3' cost: 2100.00 onetime_cost: 1000.00 pay_type: 'fixed' pay_period: 'daily' proposal: url: 'https://doc.miran.ru/api/v1/dedic/proposals/12' agreement: url: 'https://doc.miran.ru/api/v1/agreements/5' resorces: url: "https://doc.miran.ru/api/v1/dedic/services/3/resources" - id: 7 date: '2016-02-12' start_date: '2016-02-12' expire_date: '2016-02-12' status: 'stopped' is_primary: true priority: 2 primary_service: null options: url: "https://doc.miran.ru/api/v1/dedic/services/7/options" order: url: 'https://doc.miran.ru/api/v1/orders/7' comment: 'Test comment for service id 7' cost: 2100.00 onetime_cost: 1000.00 pay_type: 'fixed' pay_period: 'daily' proposal: url: 'https://doc.miran.ru/api/v1/dedic/proposals/12' agreement: url: 'https://doc.miran.ru/api/v1/agreements/2' resorces: url: "https://doc.miran.ru/api/v1/dedic/services/7/resources" total_count: 2 500: $ref: "#/responses/Standard500" post: tags: [Dedicated, Service] x-swagger-router-controller: app.controllers.service operationId: create security: - oauth: [] summary: Create service in order parameters: - name: app_controllers_service_create in: body schema: type: object additionalProperties: false required: - order - proposal properties: order: type: integer description: Service will be attached to this preliminary created order format: 'int32' minimum: 0 proposal: type: integer format: 'int32' description: Proposal to be used for service. Tariff will create primary service, not tariff - option one minimum: 0 responses: 201: description: Service successfully created 400: description: Incorrect order id (deleted or not found) or proposal id (expired or not found)
/* tslint:disable:no-unused-variable member-ordering */ import { Inject, Injectable, Optional } from '@angular/core'; import { Http, Headers, URLSearchParams } from '@angular/http'; import { RequestMethod, RequestOptions, RequestOptionsArgs } from '@angular/http'; import { Response, ResponseContentType } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import '../rxjs-operators'; import { AppControllersServiceCreate } from '../model/appControllersServiceCreate'; import { AppControllersServiceUpdate } from '../model/appControllersServiceUpdate'; import { InlineResponse2006 } from '../model/inlineResponse2006'; import { InlineResponse2007 } from '../model/inlineResponse2007'; import { InlineResponse2008 } from '../model/inlineResponse2008'; import { InlineResponse2009 } from '../model/inlineResponse2009'; import { InlineResponse401 } from '../model/inlineResponse401'; import { Service } from '../model/service'; import { Services } from '../model/services'; import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; import { Configuration } from '../configuration'; @Injectable() export class ServiceService { protected basePath = ''; public defaultHeaders: Headers = new Headers(); public configuration: Configuration = new Configuration(); constructor( protected http: Http, @Optional()@Inject(BASE_PATH) basePath: string, @Optional() configuration: Configuration) { if (basePath) { this.basePath = basePath; } if (configuration) { this.configuration = configuration; this.basePath = basePath || configuration.basePath || this.basePath; } } /** * * Extends object by coping non-existing properties. * @param objA object to be extended * @param objB source object */ private extendObj<T1,T2>(objA: T1, objB: T2) { for(let key in objB){ if(objB.hasOwnProperty(key)){ (objA as any)[key] = (objB as any)[key]; } } return <T1&T2>objA; } /** * @param consumes string[] mime-types * @return true: consumes contains 'multipart/form-data', false: otherwise */ private canConsumeForm(consumes: string[]): boolean { const form = 'multipart/form-data'; for (let consume of consumes) { if (form === consume) { return true; } } return false; } /** * * @summary Delete service * @param id Unique entity identifier */ public _delete(id: number, extraHttpRequestParams?: any): Observable<{}> { return this._deleteWithHttpInfo(id, extraHttpRequestParams) .map((response: Response) => { if (response.status === 204) { return undefined; } else { return response.json() || {}; } }); } /** * * @summary Create service in order * @param appControllersServiceCreate */ public create(appControllersServiceCreate?: AppControllersServiceCreate, extraHttpRequestParams?: any): Observable<{}> { return this.createWithHttpInfo(appControllersServiceCreate, extraHttpRequestParams) .map((response: Response) => { if (response.status === 204) { return undefined; } else { return response.json() || {}; } }); } /** * Create service in order * * @param appControllersServiceCreate */ public createWithHttpInfo appControllersServiceCreate?: AppControllersServiceCreate, extraHttpRequestParams?: any): Observable<Response> { const path = this.basePath + '/dedic/services'; let queryParameters = new URLSearchParams(); // https://github.com/angular/angular/issues/6845 let headers = new Headers(this.defaultHeaders.toJSON()); // to determine the Accept header let produces: string[] = [ 'application/json' ]; // authentication (oauth) required // oauth required if (this.configuration.accessToken) { let accessToken = typeof this.configuration.accessToken === 'function' ? this.configuration.accessToken() : this.configuration.accessToken; headers.set('Authorization', 'Bearer ' + accessToken); } headers.set('Content-Type', 'application/json'); let requestOptions: RequestOptionsArgs = new RequestOptions({ method: RequestMethod.Post, headers: headers, // https://github.com/angular/angular/issues/10612 body: appControllersServiceCreate == null ? '' : JSON.stringify(appControllersServiceCreate), search: queryParameters, withCredentials:this.configuration.withCredentials }); // https://github.com/swagger-api/swagger-codegen/issues/4037 if (extraHttpRequestParams) { requestOptions = (<any>Object).assign(requestOptions, extraHttpRequestParams); } return this.http.request(path, requestOptions); } }
Thus, in order to make, for example, a post-request for creating a service using the corresponding service, you must:
Just want to explain why the parameter is named in the style of Java. The reason is that this name is formed from the specification, or rather from the name field:
post: tags: [Dedicated, Service] x-swagger-router-controller: app.controllers.service operationId: create security: - oauth: [] summary: Create service in order parameters: - name: app_controllers_service_create in: body
We decided to use such a bulky name so that the names do not overlap and are unique. If you specify as a name, for example, data, the codegen will add a counter to the data and this will result in 10 interfaces named Data_0, Data_1, and so on. Finding the right interface when importing becomes problematic).
Also, it is worth knowing that codegen creates modules that need to be imported, and their names are formed based on the method tag. Thus, the above method will be present in the modules Dedicated and Service. This is convenient because it allows you not to import api entirely and not wander among the methods, but to use only what was required for the component.
As you know, in Angular 4.4, HttpModule was replaced by HttpClientModule, which added convenience (you can read about the difference here, for example. But unfortunately, the current stable version of codegen works with HttpModule. Therefore, such constructions remain:
HttpClientModule returned was json by default:
.map((response: Response) => { if (response.status === 204) { return undefined; } else { return response.json() || {}; }
Adding a header for authorization falls on the shoulders of the HttpInterceptor :
if (this.configuration.accessToken) { let accessToken = typeof this.configuration.accessToken === 'function' ? this.configuration.accessToken() : this.configuration.accessToken; headers.set('Authorization', 'Bearer ' + accessToken); }
We look forward to the update, but for now we are working with what is.
In the next part I’ll start the story directly about Angular and I’ll touch on api from the front end.
Source: https://habr.com/ru/post/347154/
All Articles