From c5e4fcce20f003b5e5fa2662e24cc8239646e59a Mon Sep 17 00:00:00 2001 From: edanded Date: Mon, 29 Jul 2024 23:55:34 +0200 Subject: [PATCH] Add array query params support (default) Based on https://swagger.io/docs/specification/serialization/#query added support of query params with `style: form` and `explode: true` values --- __tests__/services/UriBuilder.spec.ts | 60 ++++++++++++++++--- package.json | 2 +- .../angular/AngularServicesMethodGenerator.ts | 1 + src/models/method-parameter/IParameter.ts | 1 + .../method-parameter/IQueryParameter.ts | 1 + .../MethodParameterModelBase.ts | 13 ++++ .../QueryMethodParameterModel.ts | 9 ++- src/services/UriBuilder.ts | 5 +- src/swagger/v3/parameter.ts | 3 +- swagger.json | 50 ++++++++++++++++ 10 files changed, 134 insertions(+), 11 deletions(-) diff --git a/__tests__/services/UriBuilder.spec.ts b/__tests__/services/UriBuilder.spec.ts index 8ef0456..baffa0f 100644 --- a/__tests__/services/UriBuilder.spec.ts +++ b/__tests__/services/UriBuilder.spec.ts @@ -22,7 +22,8 @@ describe('UriBuilder tests', () => { dtoType: 'string', name: 'id', place: ParameterPlace.Path, - isModel: false + isModel: false, + isCollection: false, } ], returnType: { @@ -59,7 +60,8 @@ describe('UriBuilder tests', () => { name: 'name', optional: false, place: ParameterPlace.Query, - isModel: false + isModel: false, + isCollection: false, } ], returnType: { @@ -84,6 +86,45 @@ describe('UriBuilder tests', () => { expect(result).toEqual(expected); }); + + test('method with array query param', () => { + // Arrange + const model: IMethodModel = { + kind: MethodKind.Default, + name: 'get', + operation: MethodOperation.GET, + parameters: [ + { + dtoType: 'string', + name: 'name', + optional: false, + place: ParameterPlace.Query, + isModel: false, + isCollection: true, + } + ], + returnType: { + isCollection: false, + isModel: true, + type: { + dtoType: 'IProduct', + kind: PropertyKind.Object, + type: 'Product', + isNullable: true + } + }, + originUri: 'get' + }; + + const expected = '`get?${(name.map(x=> `name=${encodeURIComponent(x)}`).join(\'&\'))}`'; + + // Act + const result = uriBuilder.buildUri(model); + + // Assert + expect(result).toEqual(expected); + }); + test('method with one path and one query params', () => { // Arrange const model: IMethodModel = { @@ -95,14 +136,16 @@ describe('UriBuilder tests', () => { dtoType: 'string', name: 'id', place: ParameterPlace.Path, - isModel: false + isModel: false, + isCollection: false, }, { dtoType: 'string', name: 'name', optional: false, place: ParameterPlace.Query, - isModel: false + isModel: false, + isCollection: false, } ], returnType: { @@ -138,20 +181,23 @@ describe('UriBuilder tests', () => { dtoType: 'string', name: 'customer', place: ParameterPlace.Path, - isModel: false + isModel: false, + isCollection: false, }, { dtoType: 'string', name: 'type', place: ParameterPlace.Path, - isModel: false + isModel: false, + isCollection: false, }, { dtoType: 'string', name: 'date', optional: false, place: ParameterPlace.Query, - isModel: false + isModel: false, + isCollection: false, } ], returnType: { diff --git a/package.json b/package.json index 0307561..70b6d64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@luxbss/gengen", - "version": "1.2.8", + "version": "1.2.9", "description": "Tool for generating models and Angular services based on OpenAPIs and Swagger's JSON", "bin": { "gengen": "./bin/index.js" diff --git a/src/generators/angular/AngularServicesMethodGenerator.ts b/src/generators/angular/AngularServicesMethodGenerator.ts index c766721..fdd5034 100644 --- a/src/generators/angular/AngularServicesMethodGenerator.ts +++ b/src/generators/angular/AngularServicesMethodGenerator.ts @@ -65,6 +65,7 @@ export class AngularServicesMethodGenerator { case ParameterPlace.Query: statement.type = this.getFullTypeName({ type: parameter.dtoType, + isCollection: parameter.isCollection, isModel: parameter.isModel, isOptional: parameter.optional }); diff --git a/src/models/method-parameter/IParameter.ts b/src/models/method-parameter/IParameter.ts index 6d5268c..ad621fd 100644 --- a/src/models/method-parameter/IParameter.ts +++ b/src/models/method-parameter/IParameter.ts @@ -5,4 +5,5 @@ export interface IParameter { dtoType: string; place: ParameterPlace; isModel: boolean; + isCollection: boolean; } diff --git a/src/models/method-parameter/IQueryParameter.ts b/src/models/method-parameter/IQueryParameter.ts index e7a6fb8..3057511 100644 --- a/src/models/method-parameter/IQueryParameter.ts +++ b/src/models/method-parameter/IQueryParameter.ts @@ -4,4 +4,5 @@ import { IParameter } from './IParameter'; export interface IQueryParameter extends IParameter { place: ParameterPlace.Query; optional: boolean; + isCollection: boolean; } diff --git a/src/models/method-parameter/MethodParameterModelBase.ts b/src/models/method-parameter/MethodParameterModelBase.ts index 7616573..7b926c7 100644 --- a/src/models/method-parameter/MethodParameterModelBase.ts +++ b/src/models/method-parameter/MethodParameterModelBase.ts @@ -3,6 +3,7 @@ import { OpenAPIService } from '../../swagger/OpenAPIService'; import { OpenAPITypesGuard } from '../../swagger/OpenAPITypesGuard'; import { IOpenAPI3Parameter } from '../../swagger/v3/parameter'; import { IOpenAPI3Reference } from '../../swagger/v3/reference'; +import { IOpenAPI3ArraySchema } from '../../swagger/v3/schemas/array-schema'; import { OpenAPI3SimpleSchema } from '../../swagger/v3/schemas/schema'; import { lowerFirst } from '../../utils'; import { ParameterPlace } from '../kinds/ParameterPlace'; @@ -12,6 +13,7 @@ export abstract class MethodParameterModelBase implements IParameter { public name: string; public dtoType!: string; public isModel!: boolean; + public isCollection: boolean = false; public abstract place: ParameterPlace; constructor( @@ -34,6 +36,11 @@ export abstract class MethodParameterModelBase implements IParameter { return; } + if (this.typesGuard.isCollection(this.model.schema)) { + this.setupCollection(this.model.schema); + return; + } + if (this.typesGuard.isEnum(this.openAPIService.getRefSchema(this.model.schema))) { this.setupRef(this.model.schema); return; @@ -47,6 +54,12 @@ export abstract class MethodParameterModelBase implements IParameter { this.isModel = true; } + private setupCollection(schema: IOpenAPI3ArraySchema): void { + this.dtoType = this.typesService.getSimpleType(schema.items as OpenAPI3SimpleSchema).dtoType; + this.isModel = false; + this.isCollection = true; + } + private setupSimple(schema: OpenAPI3SimpleSchema): void { this.dtoType = this.typesService.getSimpleType(schema).dtoType; this.isModel = false; diff --git a/src/models/method-parameter/QueryMethodParameterModel.ts b/src/models/method-parameter/QueryMethodParameterModel.ts index a9d7e05..a901d8a 100644 --- a/src/models/method-parameter/QueryMethodParameterModel.ts +++ b/src/models/method-parameter/QueryMethodParameterModel.ts @@ -2,6 +2,7 @@ import { TypesService } from '../../services/TypesService'; import { OpenAPIService } from '../../swagger/OpenAPIService'; import { OpenAPITypesGuard } from '../../swagger/OpenAPITypesGuard'; import { IOpenAPI3Parameter } from '../../swagger/v3/parameter'; +import { IOpenAPI3ArraySchema } from '../../swagger/v3/schemas/array-schema'; import { ParameterPlace } from '../kinds/ParameterPlace'; import { IQueryParameter } from './IQueryParameter'; import { MethodParameterModelBase } from './MethodParameterModelBase'; @@ -9,13 +10,19 @@ import { MethodParameterModelBase } from './MethodParameterModelBase'; export class QueryMethodParameterModel extends MethodParameterModelBase implements IQueryParameter { public place: ParameterPlace.Query; public optional: boolean; + isCollection: boolean; - constructor(model: IOpenAPI3Parameter, typesGuard: OpenAPITypesGuard, typesService: TypesService, openAPIService: OpenAPIService) { + constructor(model: IOpenAPI3Parameter, typesGuard: OpenAPITypesGuard, typesService: TypesService, openAPIService: OpenAPIService, isCollection: boolean = false) { super(typesService, model, typesGuard, openAPIService); this.optional = this.getOptional(model); + this.isCollection = this.getIsCollection(model); this.place = ParameterPlace.Query; } + private getIsCollection(model: IOpenAPI3Parameter): boolean { + return (model.schema as IOpenAPI3ArraySchema)?.type === 'array'; + } + private getOptional(model: IOpenAPI3Parameter): boolean { return model.required === undefined ? false : !model.required; } diff --git a/src/services/UriBuilder.ts b/src/services/UriBuilder.ts index 9c75a1a..6bcc090 100644 --- a/src/services/UriBuilder.ts +++ b/src/services/UriBuilder.ts @@ -16,7 +16,10 @@ export class UriBuilder { } private getQueryParams(params: IParameter[]): string { - const pairs = params.filter((z) => z.place === ParameterPlace.Query).map((z) => `${z.name}=\${encodeURIComponent(${z.name})}`); + const pairs = params.filter((z) => z.place === ParameterPlace.Query) + .map((z) => z.isCollection ? + `\${(${z.name}.map(x=> \`${z.name}=\${encodeURIComponent(x)}\`).join('&'))}` : + `${z.name}=\${encodeURIComponent(${z.name})}`); if (!pairs.length) { return ''; diff --git a/src/swagger/v3/parameter.ts b/src/swagger/v3/parameter.ts index 988d039..01467e8 100644 --- a/src/swagger/v3/parameter.ts +++ b/src/swagger/v3/parameter.ts @@ -1,9 +1,10 @@ import { IOpenAPI3Reference } from './reference'; +import { IOpenAPI3ArraySchema } from './schemas/array-schema'; import { OpenAPI3SimpleSchema } from './schemas/schema'; export interface IOpenAPI3Parameter { name: string; in: 'query' | 'header' | 'path' | 'cookie'; required?: boolean; - schema?: OpenAPI3SimpleSchema | IOpenAPI3Reference; + schema?: OpenAPI3SimpleSchema | IOpenAPI3Reference | IOpenAPI3ArraySchema; } diff --git a/swagger.json b/swagger.json index 4cf805e..70e677c 100644 --- a/swagger.json +++ b/swagger.json @@ -53,6 +53,56 @@ } } }, + "/api/v1/Product/Items/GetByIdsQuery": { + "post": { + "tags": [ + "Product" + ], + "parameters": [ + { + "name": "ids", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + }, "/api/v1/Product/Items/GetByIds": { "post": { "tags": [