diff --git a/src/language/typescript/2.0/serializers/operation-object.ts b/src/language/typescript/2.0/serializers/operation-object.ts index aac0b73..b2b0ff7 100644 --- a/src/language/typescript/2.0/serializers/operation-object.ts +++ b/src/language/typescript/2.0/serializers/operation-object.ts @@ -9,13 +9,13 @@ import { SerializedType, } from '../../common/data/serialized-type'; import { pipe } from 'fp-ts/lib/pipeable'; -import { getOrElse, isSome, map, Option } from 'fp-ts/lib/Option'; +import { getOrElse, isSome, map, Option, fold } from 'fp-ts/lib/Option'; import { serializeOperationResponses } from './responses-object'; import { fromSerializedType } from '../../common/data/serialized-parameter'; import { getSerializedKindDependency, serializedDependency } from '../../common/data/serialized-dependency'; import { concatIf } from '../../../../utils/array'; import { when } from '../../../../utils/string'; -import { getJSDoc, getKindValue, getSafePropertyName, getURL, HTTPMethod } from '../../common/utils'; +import { getJSDoc, getKindValue, getSafePropertyName, getURL, HTTPMethod, XHRResponseType } from '../../common/utils'; import { Either, isLeft, left, right } from 'fp-ts/lib/Either'; import { array, either, nonEmptyArray, option, record } from 'fp-ts'; import { combineEither } from '@devexperts/utils/dist/adt/either.utils'; @@ -271,6 +271,22 @@ export const serializeOperationObject = combineReader( map(() => `@deprecated`), ); + const responseType: XHRResponseType = pipe( + operation.produces, + fold( + () => 'json', + produces => { + if (produces.includes('application/octet-stream')) { + return 'blob'; + } + if (produces.includes('text/plain')) { + return 'text'; + } + return 'json'; + }, + ), + ); + return combineEither( parameters, serializedResponses, @@ -342,6 +358,7 @@ export const serializeOperationObject = combineReader( e.httpClient.request({ url: ${getURL(url, parameters.serializedPathParameters)}, method: '${method}', + responseType: '${responseType}', ${when(hasQueryParameters, 'query,')} ${when(hasBodyParameters, 'body,')} ${when(hasHeaderParameters, 'headers')} @@ -351,7 +368,7 @@ export const serializeOperationObject = combineReader( ${serializedResponses.io}.decode(value), either.mapLeft(ResponseValidationError.create), either.fold(error => e.httpClient.throwError(error), decoded => e.httpClient.of(decoded)), - ), + ), ); }, `; diff --git a/src/language/typescript/3.0/serializers/operation-object.ts b/src/language/typescript/3.0/serializers/operation-object.ts index 17eb0ba..ceb82ed 100644 --- a/src/language/typescript/3.0/serializers/operation-object.ts +++ b/src/language/typescript/3.0/serializers/operation-object.ts @@ -1,4 +1,13 @@ -import { getJSDoc, getKindValue, getSafePropertyName, getTypeName, getURL, HTTPMethod } from '../../common/utils'; +import { + getJSDoc, + getKindValue, + getSafePropertyName, + getTypeName, + getURL, + HTTPMethod, + SUCCESSFUL_CODES, + XHRResponseType, +} from '../../common/utils'; import { getSerializedPropertyType, getSerializedObjectType, @@ -34,7 +43,7 @@ import { ResolveRefContext, fromString, getRelativePath, Ref } from '../../../.. import { OperationObject } from '../../../../schema/3.0/operation-object'; import { ParameterObject, ParameterObjectCodec } from '../../../../schema/3.0/parameter-object'; import { RequestBodyObjectCodec } from '../../../../schema/3.0/request-body-object'; -import { isSome, none, Option, some } from 'fp-ts/lib/Option'; +import { chain, isSome, none, Option, some, map, fromEither, fold } from 'fp-ts/lib/Option'; import { constFalse } from 'fp-ts/lib/function'; import { clientRef } from '../../common/bundled/client'; import { Kind } from '../../../../utils/types'; @@ -49,6 +58,8 @@ import { SerializedFragment, } from '../../common/data/serialized-fragment'; import { SchemaObjectCodec } from '../../../../schema/3.0/schema-object'; +import { lookup, keys } from 'fp-ts/lib/Record'; +import { ResponseObjectCodec } from '../../../../schema/3.0/response-object'; import { fromSerializedHeaderParameter, getSerializedHeaderParameterType, @@ -256,8 +267,9 @@ export const getParameters = combineReader( ); export const serializeOperationObject = combineReader( + ask(), getParameters, - getParameters => ( + (e, getParameters) => ( pattern: string, method: HTTPMethod, from: Ref, @@ -274,6 +286,29 @@ export const serializeOperationObject = combineReader( ); const serializedResponses = serializeResponsesObject(from)(operation.responses); + const responseType: XHRResponseType = pipe( + SUCCESSFUL_CODES, + array.findFirstMap(code => lookup(code, operation.responses)), + chain(response => + ReferenceObjectCodec.is(response) + ? fromEither(e.resolveRef(response.$ref, ResponseObjectCodec)) + : some(response), + ), + chain(response => response.content), + map(keys), + fold( + () => 'json', + types => { + if (types.includes('application/octet-stream')) { + return 'blob'; + } + if (types.includes('text/plain')) { + return 'text'; + } + return 'json'; + }, + ), + ); return combineEither( parameters, @@ -346,6 +381,7 @@ export const serializeOperationObject = combineReader( e.httpClient.request({ url: ${getURL(pattern, parameters.serializedPathParameters)}, method: '${method}', + responseType: '${responseType}', ${when(hasQueryParameters, 'query,')} ${when(hasBodyParameter, 'body,')} ${when(hasHeaderParameters, 'headers')} diff --git a/src/language/typescript/3.0/serializers/response-object.ts b/src/language/typescript/3.0/serializers/response-object.ts index 902a7e1..8de777b 100644 --- a/src/language/typescript/3.0/serializers/response-object.ts +++ b/src/language/typescript/3.0/serializers/response-object.ts @@ -14,7 +14,9 @@ export const serializeResponseObject = ( ): Option> => pipe( responseObject.content, - option.mapNullable(content => content['application/json']), + option.mapNullable( + content => content['application/json'] || content['text/plain'] || content['application/octet-stream'], + ), option.chain(media => media.schema), option.map(schema => ReferenceObjectCodec.is(schema) diff --git a/src/language/typescript/common/bundled/client.ts b/src/language/typescript/common/bundled/client.ts index 1d3e9c8..c8e7602 100644 --- a/src/language/typescript/common/bundled/client.ts +++ b/src/language/typescript/common/bundled/client.ts @@ -16,6 +16,7 @@ export const client = ` export interface Request { readonly method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'; readonly url: string; + readonly responseType: 'json' | 'blob' | 'text'; readonly query?: string; readonly body?: unknown; readonly headers?: Record; diff --git a/src/language/typescript/common/data/serialized-type.ts b/src/language/typescript/common/data/serialized-type.ts index 82e0476..9725ce6 100644 --- a/src/language/typescript/common/data/serialized-type.ts +++ b/src/language/typescript/common/data/serialized-type.ts @@ -82,6 +82,12 @@ export const SERIALIZED_DATETIME_TYPE = serializedType( [serializedDependency('DateFromISOString', 'io-ts-types/lib/DateFromISOString')], [], ); +export const SERIALIZED_DATE_TYPE = serializedType( + 'Date', + 'DateFromISODateStringIO', + [serializedDependency('DateFromISODateStringIO', '../utils/utils')], + [], +); export const SERIALIZED_STRING_TYPE = serializedType('string', 'string', [serializedDependency('string', 'io-ts')], []); export const getSerializedStringType = (from: Ref, format: Option): Either => { return combineEither(utilsRef, utilsRef => { @@ -103,6 +109,9 @@ export const getSerializedStringType = (from: Ref, format: Option): Eith ), ); } + case 'binary': { + return some(SERIALIZED_UNKNOWN_TYPE); + } } return none; }), diff --git a/src/language/typescript/common/utils.ts b/src/language/typescript/common/utils.ts index dbe7ba0..ed055bf 100644 --- a/src/language/typescript/common/utils.ts +++ b/src/language/typescript/common/utils.ts @@ -9,7 +9,7 @@ export const SUCCESSFUL_CODES = ['200', '201', 'default']; export const CONTROLLERS_DIRECTORY = 'controllers'; export const DEFINITIONS_DIRECTORY = 'definitions'; export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'; - +export type XHRResponseType = 'json' | 'blob' | 'text'; const INVALID_NAMES = ['Error', 'Promise', 'PromiseLike', 'Array', 'ArrayLike', 'Function', 'Object']; const TYPE_NAME_NORMALIZE_REGEX = /\W/g; const normalize = (name: string): string => name.replace(TYPE_NAME_NORMALIZE_REGEX, '_').replace(/^(\d)/, '_$1'); diff --git a/test/index.spec.ts b/test/index.spec.ts index 2cde297..cfb1a58 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -64,6 +64,18 @@ describe('codegen', () => { ), ); + it( + 'specs/3.0/file-and-text.yml', + unsafe( + generate({ + spec: path.resolve(__dirname, 'specs/3.0/file-and-text.yml'), + out, + language: serializeOpenAPI3, + decoder: OpenapiObjectCodec, + }), + ), + ); + it( 'specs/asyncapi-2.0.0/streetlights-api.yml', unsafe( diff --git a/test/specs/2.0/yaml/demo.yml b/test/specs/2.0/yaml/demo.yml index 6aa3512..36da9f2 100644 --- a/test/specs/2.0/yaml/demo.yml +++ b/test/specs/2.0/yaml/demo.yml @@ -85,9 +85,107 @@ paths: type: number /shared-path-item: $ref: '../../arbitrary.yml#/SharedPathItem' + /file/{filedId}/version/{version}: + parameters: + - in: path + type: number + required: true + name: filedId + - in: path + type: number + required: true + name: version + get: + tags: + - files + summary: GetSomeFile + operationId: getFile + produces: + - application/octet-stream + responses: + 200: + description: succesfull operation + schema: + type: string + format: binary + /fileWithResponseRef: + get: + tags: + - files + summary: GetFileWithResponseRef + operationId: getFileWithResponseRef + produces: + - application/octet-stream + responses: + 200: + $ref: '#/responses/SuccessfulFile' + /fileWithSchemaRef: + get: + tags: + - files + summary: GetFileWithSchemaRef + operationId: getFileWithSchemaRef + produces: + - application/octet-stream + responses: + 200: + description: succesfull operation + schema: + $ref: '#/definitions/File' + /text/{textId}/version/{version}: + parameters: + - in: path + type: number + required: true + name: textId + - in: path + type: number + required: true + name: version + get: + tags: + - text + summary: GetSomeText + operationId: getText + produces: + - text/plain + responses: + 200: + description: succesfull operation + schema: + type: string + /textWithResponseRef: + get: + tags: + - text + summary: GetTextWithResponseRef + operationId: getTextWithResponseRef + produces: + - text/plain + responses: + 200: + $ref: '#/responses/SuccessfulText' + /textWithSchemaRef: + get: + tags: + - text + summary: GetTextWithSchemaRef + operationId: getTextWithSchemaRef + produces: + - text/plain + responses: + 200: + description: succesfull operation + schema: + $ref: '#/definitions/Text' definitions: Id: type: integer + File: + type: string + format: binary + Text: + type: string local: type: object required: @@ -150,3 +248,11 @@ responses: description: General Error schema: $ref: '#/definitions/GeneralError' + SuccessfulFile: + description: succesful file data loading + schema: + $ref: '#/definitions/File' + SuccessfulText: + description: succesful text data loading + schema: + $ref: '#/definitions/Text' diff --git a/test/specs/3.0/file-and-text.yml b/test/specs/3.0/file-and-text.yml new file mode 100644 index 0000000..2f340d0 --- /dev/null +++ b/test/specs/3.0/file-and-text.yml @@ -0,0 +1,115 @@ +openapi: '3.0.2' +info: + version: 1.0.0 + title: Swagger Petstore +paths: + /file/{filedId}/version/{version}: + parameters: + - in: path + required: true + name: filedId + schema: + type: number + - in: path + required: true + name: version + schema: + type: number + get: + tags: + - files + summary: GetSomeFile + operationId: getFile + responses: + 200: + description: succesfull operation + content: + application/octet-stream: + schema: + type: string + format: binary + + + /fileWithResponseRef: + get: + tags: + - files + summary: GetFileWithResponseRef + operationId: getFileWithResponseRef + responses: + 200: + $ref: '#/components/responses/SuccessfulFile' + /fileWithSchemaRef: + get: + tags: + - files + summary: GetFileWithSchemaRef + operationId: getFileWithSchemaRef + responses: + 200: + description: succesfull operation + content: + application/octet-stream: + schema: + $ref: '#/components/schemas/File' + /text/1/version/1: + get: + tags: + - text + summary: GetSomeText + operationId: getText + responses: + 200: + description: succesfull operation + content: + text/plain: + schema: + type: string + /textWithResponseRef: + get: + tags: + - text + summary: GetTextWithResponseRef + operationId: getTextWithResponseRef + responses: + 200: + description: ok + content: + text/plain: + schema: + $ref: '#/components/responses/SuccessfulText' + /textWithSchemaRef: + get: + tags: + - text + summary: GetTextWithSchemaRef + operationId: getTextWithSchemaRef + responses: + 200: + description: succesfull operation + content: + text/plain: + schema: + $ref: '#/components/schemas/Text' +components: + schemas: + File: + type: string + format: binary + Text: + type: string + responses: + Successful: + description: succesful operation + SuccessfulFile: + description: succesfull operation + content: + application/octet-stream: + schema: + $ref: '#/components/schemas/File' + SuccessfulText: + description: succesful text data loading + content: + text/plain: + schema: + $ref: '#/components/schemas/Text'