From 24b70fac3d645e868190761b43246d7c5eb07394 Mon Sep 17 00:00:00 2001 From: Alexander Kuzmin Date: Thu, 24 Dec 2020 20:20:48 +0300 Subject: [PATCH] feat: Support headers (#133) partially closes #57 --- .../2.0/serializers/operation-object.ts | 53 ++++++++++++++++--- .../3.0/serializers/operation-object.ts | 51 +++++++++++++++++- .../typescript/common/bundled/client.ts | 1 + .../data/serialized-header-parameters.ts | 50 +++++++++++++++++ test/specs/2.0/json/swagger.json | 24 +++++++-- test/specs/2.0/yaml/demo.yml | 14 +++++ test/specs/2.0/yaml/swagger.yml | 5 ++ test/specs/3.0/demo.yml | 17 ++++++ test/specs/3.0/petstore-expanded.yaml | 5 ++ 9 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 src/language/typescript/common/data/serialized-header-parameters.ts diff --git a/src/language/typescript/2.0/serializers/operation-object.ts b/src/language/typescript/2.0/serializers/operation-object.ts index f7e6088..aac0b73 100644 --- a/src/language/typescript/2.0/serializers/operation-object.ts +++ b/src/language/typescript/2.0/serializers/operation-object.ts @@ -25,7 +25,6 @@ import { ArrayParameterObjectCollectionFormat, BodyParameterObject, FormDataParameterObject, - HeaderParameterObject, ParameterObject, ParameterObjectCodec, PathParameterObject, @@ -53,13 +52,18 @@ import { } from '../../common/data/serialized-fragment'; import { sequenceOptionEither } from '../../../../utils/option'; import { identity } from 'fp-ts/lib/function'; +import { + fromSerializedHeaderParameter, + getSerializedHeaderParameterType, + SerializedHeaderParameter, +} from '../../common/data/serialized-header-parameters'; interface Parameters { readonly pathParameters: PathParameterObject[]; readonly serializedPathParameters: SerializedPathParameter[]; readonly serializedQueryParameter: Option; readonly serializedBodyParameter: Option; - readonly headerParameters: HeaderParameterObject[]; + readonly serializedHeadersParameter: Option; readonly formDataParameters: FormDataParameterObject[]; readonly queryString: Option; } @@ -79,7 +83,7 @@ const getParameters = combineReader( const serializedPathParameters: SerializedPathParameter[] = []; const serializedQueryParameters: SerializedType[] = []; const bodyParameters: BodyParameterObject[] = []; - const headerParameters: HeaderParameterObject[] = []; + const serializedHeaderParameters: SerializedHeaderParameter[] = []; const formDataParameters: FormDataParameterObject[] = []; const queryStringFragments: SerializedFragment[] = []; @@ -129,7 +133,13 @@ const getParameters = combineReader( break; } case 'header': { - headerParameters.push(resolved.right); + const serializedParameter = pipe( + serialized.right, + fromSerializedHeaderParameter(resolved.right.name), + getSerializedHeaderParameterType, + ); + + serializedHeaderParameters.push(serializedParameter); break; } case 'formData': { @@ -195,6 +205,16 @@ const getParameters = combineReader( sequenceOptionEither, ); + const serializedHeadersParameter = pipe( + nonEmptyArray.fromArray(serializedHeaderParameters), + option.map(parameters => + pipe( + intercalateSerializedTypes(serializedType(';', ',', [], []), parameters), + getSerializedObjectType(), + ), + ), + ); + const queryString = pipe( nonEmptyArray.fromArray(queryStringFragments), option.map(queryStringFragments => @@ -215,7 +235,7 @@ const getParameters = combineReader( serializedPathParameters, serializedQueryParameter, serializedBodyParameter, - headerParameters, + serializedHeadersParameter, formDataParameters, queryString, })); @@ -258,7 +278,8 @@ export const serializeOperationObject = combineReader( (parameters, serializedResponses, clientRef) => { const hasQueryParameters = isSome(parameters.serializedQueryParameter); const hasBodyParameters = isSome(parameters.serializedBodyParameter); - const hasParameters = hasQueryParameters || hasBodyParameters; + const hasHeaderParameters = isSome(parameters.serializedHeadersParameter); + const hasParameters = hasQueryParameters || hasBodyParameters || hasHeaderParameters; const bodyType = pipe( parameters.serializedBodyParameter, @@ -282,10 +303,22 @@ export const serializeOperationObject = combineReader( option.getOrElse(() => ''), ); + const headersType = pipe( + parameters.serializedHeadersParameter, + option.map(headers => `headers: ${headers.type}`), + option.getOrElse(() => ''), + ); + + const headersIO = pipe( + parameters.serializedHeadersParameter, + option.map(headers => `const headers = ${headers.io}.encode(parameters.headers)`), + option.getOrElse(() => ''), + ); + const argsType = concatIf( hasParameters, parameters.serializedPathParameters.map(p => p.type), - [`parameters: { ${queryType}${bodyType} }`], + [`parameters: { ${queryType}${bodyType}${headersType} }`], ).join(','); const type = ` @@ -303,6 +336,7 @@ export const serializeOperationObject = combineReader( ${operationName}: (${argsIO}) => { ${bodyIO} ${queryIO} + ${headersIO} return e.httpClient.chain( e.httpClient.request({ @@ -310,6 +344,7 @@ export const serializeOperationObject = combineReader( method: '${method}', ${when(hasQueryParameters, 'query,')} ${when(hasBodyParameters, 'body,')} + ${when(hasHeaderParameters, 'headers')} }), value => pipe( @@ -342,6 +377,10 @@ export const serializeOperationObject = combineReader( parameters.queryString, option.map(p => p.dependencies), ), + pipe( + parameters.serializedHeadersParameter, + option.map(p => p.dependencies), + ), ]), ]), ]; diff --git a/src/language/typescript/3.0/serializers/operation-object.ts b/src/language/typescript/3.0/serializers/operation-object.ts index ff02e07..17eb0ba 100644 --- a/src/language/typescript/3.0/serializers/operation-object.ts +++ b/src/language/typescript/3.0/serializers/operation-object.ts @@ -49,6 +49,11 @@ import { SerializedFragment, } from '../../common/data/serialized-fragment'; import { SchemaObjectCodec } from '../../../../schema/3.0/schema-object'; +import { + fromSerializedHeaderParameter, + getSerializedHeaderParameterType, + SerializedHeaderParameter, +} from '../../common/data/serialized-header-parameters'; const getOperationName = (pattern: string, operation: OperationObject, method: HTTPMethod): string => pipe( @@ -61,6 +66,7 @@ interface Parameters { readonly serializedPathParameters: SerializedPathParameter[]; readonly serializedQueryParameter: Option; readonly serializedBodyParameter: Option; + readonly serializedHeadersParameter: Option; readonly serializedQueryString: Option; } @@ -79,6 +85,7 @@ export const getParameters = combineReader( const serializedQueryParameters: SerializedType[] = []; let serializedBodyParameter: Option = none; const queryStringFragments: SerializedFragment[] = []; + const serializedHeaderParameters: SerializedHeaderParameter[] = []; // note that PathItem parameters should go after OperationObject parameters because they have lower priority // this means that OperationObject can override PathItemObject parameters @@ -126,6 +133,16 @@ export const getParameters = combineReader( serializedPathParameters.push(serializedParameter); break; } + case 'header': { + const serializedParameter = pipe( + serialized.right, + fromSerializedHeaderParameter(resolved.right.name), + getSerializedHeaderParameterType, + ); + + serializedHeaderParameters.push(serializedParameter); + break; + } case 'query': { const serializedParameter = getSerializedOptionalType(required, serialized.right); const schema = getParameterObjectSchema(resolved.right); @@ -217,10 +234,21 @@ export const getParameters = combineReader( ), ); + const serializedHeadersParameter = pipe( + nonEmptyArray.fromArray(serializedHeaderParameters), + option.map(parameters => + pipe( + intercalateSerializedTypes(serializedType(';', ',', [], []), parameters), + getSerializedObjectType(), + ), + ), + ); + return right({ pathParameters, serializedPathParameters, serializedQueryParameter, + serializedHeadersParameter, serializedBodyParameter, serializedQueryString, }); @@ -254,7 +282,8 @@ export const serializeOperationObject = combineReader( (parameters, serializedResponses, clientRef) => { const hasQueryParameters = isSome(parameters.serializedQueryParameter); const hasBodyParameter = isSome(parameters.serializedBodyParameter); - const hasParameters = hasQueryParameters || hasBodyParameter; + const hasHeaderParameters = isSome(parameters.serializedHeadersParameter); + const hasParameters = hasQueryParameters || hasBodyParameter || hasHeaderParameters; const bodyType = pipe( parameters.serializedBodyParameter, @@ -278,10 +307,22 @@ export const serializeOperationObject = combineReader( option.getOrElse(() => ''), ); + const headersType = pipe( + parameters.serializedHeadersParameter, + option.map(headers => `headers: ${headers.type}`), + option.getOrElse(() => ''), + ); + + const headersIO = pipe( + parameters.serializedHeadersParameter, + option.map(headers => `const headers = ${headers.io}.encode(parameters.headers)`), + option.getOrElse(() => ''), + ); + const argsType = concatIf( hasParameters, parameters.serializedPathParameters.map(p => p.type), - [`parameters: { ${queryType}${bodyType} }`], + [`parameters: { ${queryType}${bodyType}${headersType} }`], ).join(','); const type = ` @@ -299,6 +340,7 @@ export const serializeOperationObject = combineReader( ${operationName}: (${argsIO}) => { ${bodyIO} ${queryIO} + ${headersIO} return e.httpClient.chain( e.httpClient.request({ @@ -306,6 +348,7 @@ export const serializeOperationObject = combineReader( method: '${method}', ${when(hasQueryParameters, 'query,')} ${when(hasBodyParameter, 'body,')} + ${when(hasHeaderParameters, 'headers')} }), value => pipe( @@ -338,6 +381,10 @@ export const serializeOperationObject = combineReader( parameters.serializedQueryString, option.map(p => p.dependencies), ), + pipe( + parameters.serializedHeadersParameter, + option.map(p => p.dependencies), + ), ]), ]), ]; diff --git a/src/language/typescript/common/bundled/client.ts b/src/language/typescript/common/bundled/client.ts index 9645b29..1d3e9c8 100644 --- a/src/language/typescript/common/bundled/client.ts +++ b/src/language/typescript/common/bundled/client.ts @@ -18,6 +18,7 @@ export const client = ` readonly url: string; readonly query?: string; readonly body?: unknown; + readonly headers?: Record; } export interface HTTPClient extends MonadThrow { diff --git a/src/language/typescript/common/data/serialized-header-parameters.ts b/src/language/typescript/common/data/serialized-header-parameters.ts new file mode 100644 index 0000000..ea59b7a --- /dev/null +++ b/src/language/typescript/common/data/serialized-header-parameters.ts @@ -0,0 +1,50 @@ +import { Ref, uniqRefs } from '../../../../utils/ref'; +import { getTypeName } from '../utils'; +import { serializedDependency, SerializedDependency, uniqSerializedDependencies } from './serialized-dependency'; +import { SerializedParameter } from './serialized-parameter'; + +export interface SerializedHeaderParameter extends SerializedParameter { + readonly name: string; +} + +export const serializedHeaderParameter = ( + name: string, + type: string, + io: string, + isRequired: boolean, + dependencies: SerializedDependency[], + refs: Ref[], +): SerializedHeaderParameter => ({ + name, + type, + io, + isRequired, + dependencies: uniqSerializedDependencies(dependencies), + refs: uniqRefs(refs), +}); + +export const fromSerializedHeaderParameter = (name: string) => ( + serialized: SerializedParameter, +): SerializedHeaderParameter => ({ + ...serialized, + name, +}); + +export const getSerializedHeaderParameterType = (serialized: SerializedHeaderParameter): SerializedHeaderParameter => { + const name = getTypeName(serialized.name); + return serializedHeaderParameter( + name, + `${name}: ${serialized.isRequired ? serialized.type : `option.Option<${serialized.type}>`}`, + `${name}: ${serialized.isRequired ? serialized.io : `optionFromNullable(${serialized.io})`}`, + serialized.isRequired, + serialized.dependencies.concat( + serialized.isRequired + ? [] + : [ + serializedDependency('optionFromNullable', 'io-ts-types/lib/optionFromNullable'), + serializedDependency('option', 'fp-ts'), + ], + ), + serialized.refs, + ); +}; diff --git a/test/specs/2.0/json/swagger.json b/test/specs/2.0/json/swagger.json index c24eb2e..06e4c59 100644 --- a/test/specs/2.0/json/swagger.json +++ b/test/specs/2.0/json/swagger.json @@ -41,8 +41,7 @@ "https", "http" ], - "parameters": { - }, + "parameters": {}, "paths": { "/pet": { "post": { @@ -70,6 +69,21 @@ "schema": { "$ref": "./common.json#/definitions/Pet" } + }, + { + "in": "header", + "name": "Custom-Header", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "Custom-Header-Two", + "required": true, + "type": "array", + "items": { + "type": "string" + } } ], "responses": { @@ -122,9 +136,9 @@ } } }, - "400":{ - "description":"Bad Request", - "schema":{ + "400": { + "description": "Bad Request", + "schema": { "$ref": "./common.json#/definitions/ErrorResponse" } }, diff --git a/test/specs/2.0/yaml/demo.yml b/test/specs/2.0/yaml/demo.yml index f4a1252..6aa3512 100644 --- a/test/specs/2.0/yaml/demo.yml +++ b/test/specs/2.0/yaml/demo.yml @@ -37,6 +37,20 @@ paths: items: type: integer collectionFormat: multi + - in: header + name: Custom-Header + required: true + type: string + - in: header + name: Custom-Header-Two + required: false + type: string + - in: header + name: Custom-Header-Three + required: true + type: array + items: + type: string operationId: test responses: 200: diff --git a/test/specs/2.0/yaml/swagger.yml b/test/specs/2.0/yaml/swagger.yml index d401f10..1f1aa43 100644 --- a/test/specs/2.0/yaml/swagger.yml +++ b/test/specs/2.0/yaml/swagger.yml @@ -50,6 +50,11 @@ paths: required: true schema: "$ref": "./common.yml#/definitions/Pet" + - in: header + name: Custom-Header + required: true + schema: + type: string responses: '200': description: default diff --git a/test/specs/3.0/demo.yml b/test/specs/3.0/demo.yml index 37c40b0..262a79d 100644 --- a/test/specs/3.0/demo.yml +++ b/test/specs/3.0/demo.yml @@ -28,6 +28,23 @@ paths: required: true schema: type: number + - in: header + name: Custom-Header + required: true + schema: + type: string + - in: header + name: Custom-Header-Two + required: false + schema: + type: string + - in: header + name: Custom-Header-Three + required: true + schema: + type: array + items: + type: string requestBody: description: inline request body content: diff --git a/test/specs/3.0/petstore-expanded.yaml b/test/specs/3.0/petstore-expanded.yaml index 28d53cf..1af0791 100644 --- a/test/specs/3.0/petstore-expanded.yaml +++ b/test/specs/3.0/petstore-expanded.yaml @@ -39,6 +39,11 @@ paths: schema: type: integer format: int32 + - in: header + name: Custom-Header + required: true + schema: + type: string responses: '200': description: pet response