Панель управления услугами. Часть 2. На пути к фронтенду

    Вступление. Еще немного про api.


    image


    Итак, в прошлый раз мы остановились на описание процесса сборки api, с тех пор некоторые вещи успели измениться. А именно — Grunt была заменен на Gulp. Главная причина такой перестановки — скорость работы.


    После перехода разница стала заметна невооруженным глазом (тем более, gulp выводит время, затраченное на каждую задачу). Достигается такой результат за счет того, что все задачи выполняются параллельно по умолчанию. Такое решение отлично нам подошло. Некоторые части работы, которую выполнял Grunt были независимы друг от друга, а значит их можно было выполнять одновременно. Например, добавление отступов для файлов definitions и paths.
    К сожалению, без минусов не обошлось. Задачи, требующие выполнения предыдущих, остались. Но gulp имеет обширную базу пакетов на все случаи жизни, поэтому решение было найдено достаточно быстро — пакет runSequence


    gulp.task('client', (callback) => {
        runSequence(
            'client:indent',
            'client:concat',
            'client:replace',
            'client:validate',
            'client:toJson',
            'client:clean',
            callback
        );
    });

    То есть вместо стандартного объявления задачи для gulp, аргументом передается callback, в котором задачи выполняются по указанному порядке. В нашем случае порядок выполнения был важен только для 4 задач из 40, поэтому прирост в скорости по сравнению с Grunt ощутим.


    Также, gulp позволил отказаться от coffeescript в пользу ES6. Код изменился минимально, но отпала необходимость при нечастых изменениях в конфигурации сборки api вспоминать как писать на coffeescript, так как нигде более он не использовался.
    Пример части конфигураций для сравнения:


    Gulp


    gulp.task('admin:indent_misc', () => {
        return gulp.src(`${root}/projects.yml`)
            .pipe(indent({
                tabs: false,
                amount: 2
                }))
            .pipe(gulp.dest(`${interimDir}`))
    });

    Grunt


    indent: 
      admin_misc:
        src: [
          '<%= admin_root %>/authorization.yml'
          '<%= admin_root %>/projects.yml'
        ]
        dest: '<%= admin_interim_dir %>/'
        options:
          style: 'space'
          size: 2
          change: 1 


    Также, стоит упомянуть о небольших граблях, на которые нам удалось наступить.
    Они заключались в следующем: после генерации файлов api и попытки запуска angular-приложение выводилась ошибка повторного экспорта ResourceService. Отправной точкой для поиска стал файл api/model/models.ts. Он содержит экспорты всех интерфейсов и сервисов, которые используются в дальнейшем.


    Здесь следует добавить небольшое отступление и рассказать как swagger-codegen присваивает имена интерфейсам и сервисам.


    Небольшое отступление

    Интерфейс
    Исходя из шаблона интерфейса, если у свойства сущности указан тип object, то для него создается отдельные интерфейс, который именуется %Имя_сущностиИмя_свойства%.


    Сервис
    Исходя из шаблона сервиса имя сервиса состоит из имени тега и слова Service, например, OrderService. Поэтому, если указать у пути в спецификации несколько тегов, то этот метод попадет в несколько сервисов. Такой подход позволяет в одном случае импортировать только необходимый сервис и избежать импорта нескольких сервисов в другом.


    Итак, в файле models.ts действительно присутствовало два экспорта ResourceService, один представлял сервиса для доступа к методам сущности resource, а второй — интерфейс для свойства service у сущности resource. Поэтому и возник такой конфликт. Решением стало переименование свойства.


    От API к фронтенду.


    image


    Как я уже говорил, спецификация swagger позволяет сформировать необходимые файлы работы с api как для бекенда, так и для фронтенда. В нашем случае, генерация кода api для Angular2 выполняется с помощью простой команды:


    java -jar ./swagger-codegen-cli.jar generate \ 
    -i client_swagger.json \
    -l typescript-angular \
    -o ../src/app/api \
    -c ./typescript_config.json

    Разбор параметров:


    • java -jar ./swagger-codegen-cli.jar generate — запуск jar-файла swagger-codegen
    • -i client_swagger.json – файл спецификации, полученный в итоге работы Gulp
    • -l typescript-angular – язык, для которого выполняется генерация кода
    • -o ../src/app/api — целевая директория для файлов api
    • -c ./typescript_config.json – дополнительная конфигурация (для устранения проблема именования переменных, о которой я рассказывал в первой части)

    Учитывая, что количество языков, а соответственно шаблонов и кода для генерации, огромно, периодически в голове появляется мысль пересобрать codegen только под наши нужды и оставить только Typescript-Angular. Тем более, сами разработчики предоставляют инструкции по добавлению собственных шаблонов.


    Таким нехитрым образом мы получаем все необходимые модули, интерфейсы, классы и сервисы для работы с api.
    Пример одного из интерфейсов, полученных с помощью codegen:


    Входной файл спецификации service_definition.yaml
    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'

    На выходе получаем интерфейс, понятный angular’у
    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'
        }
    }

    Выдержка из файла спецификации service_path.yml
    /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)

    Выдержка из готового сервиса для Angular
    /* 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);
        }
    }

    Таким образом, для того, чтобы сделать, например, post-запрос на создание услуги с помощью соответствующего сервиса необходимо:


    • Добавить в компонент Service.service
    • Вызвать метод service.create с параметром в соответствии с интерфейсом appControllersServiceCreate
    • Подписаться для получения результата

    Сразу хочу пояснить, почему параметр носит имя в стиле Java. Причина в том, что это имя формируется из спецификации, а точнее из поля name:


    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

    Мы решили использовать такое громоздкое название, чтобы имена не пересекались и были уникальными. Если указать в качестве имени, например, data, то codegen будет добавлять к data счетчик и это выльется в 10 интерфейсов с именем Data_0, Data_1 и так далее. Найти нужный интерфейс при импорте становится проблематично).


    Также, стоит знать, что codegen создает модули, которые необходимо импортировать, а их имена формируются исходя из тега метода. Таким образом, вышеуказанный метод будет присутствовать в модулях Dedicated и Service. Это удобно, так как позволяет не импортировать api целиком и не блуждать среди методов, а использовать только то, что требовалось для компонента.


    Как известно, в Angular 4.4 заменили HttpModule на HttpClientModule, который добавил удобства (почитать о разнице можно например тут. Но, к сожалению, текущая стабильная версия codegen работает с HttpModule. Поэтому остаются подобные конструкции:


    HttpClientModule вернул был json по умолчанию:


    .map((response: Response) => {
        if (response.status === 204) {
            return undefined;
        } else {
            return response.json() || {};
        }

    Добавление заголовка для авторизации ложится на плечи HttpInterceptor:


    if (this.configuration.accessToken) {
                let accessToken = typeof this.configuration.accessToken === 'function'
                    ? this.configuration.accessToken()
                    : this.configuration.accessToken;
                headers.set('Authorization', 'Bearer ' + accessToken);
            }

    С нетерпением ждем обновления, а пока работаем с тем, что есть.


    В следующей части я начну рассказ уже непосредственно про Angular и api буду касаться уже со стороны фронтенда.

    Дата-центр «Миран»
    23,00
    Компания
    Поделиться публикацией

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое