Skip to content

Commit 977f534

Browse files
authored
Merge pull request #129 from mankdev/response-type-support
Support binary and text responses
2 parents 24437cd + 855c61d commit 977f534

File tree

9 files changed

+306
-8
lines changed

9 files changed

+306
-8
lines changed

src/language/typescript/2.0/serializers/operation-object.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import {
99
SerializedType,
1010
} from '../../common/data/serialized-type';
1111
import { pipe } from 'fp-ts/lib/pipeable';
12-
import { getOrElse, isSome, map, Option } from 'fp-ts/lib/Option';
12+
import { getOrElse, isSome, map, Option, fold } from 'fp-ts/lib/Option';
1313
import { serializeOperationResponses } from './responses-object';
1414
import { fromSerializedType } from '../../common/data/serialized-parameter';
1515
import { getSerializedKindDependency, serializedDependency } from '../../common/data/serialized-dependency';
1616
import { concatIf } from '../../../../utils/array';
1717
import { when } from '../../../../utils/string';
18-
import { getJSDoc, getKindValue, getSafePropertyName, getURL, HTTPMethod } from '../../common/utils';
18+
import { getJSDoc, getKindValue, getSafePropertyName, getURL, HTTPMethod, XHRResponseType } from '../../common/utils';
1919
import { Either, isLeft, left, right } from 'fp-ts/lib/Either';
2020
import { array, either, nonEmptyArray, option, record } from 'fp-ts';
2121
import { combineEither } from '@devexperts/utils/dist/adt/either.utils';
@@ -271,6 +271,22 @@ export const serializeOperationObject = combineReader(
271271
map(() => `@deprecated`),
272272
);
273273

274+
const responseType: XHRResponseType = pipe(
275+
operation.produces,
276+
fold(
277+
() => 'json',
278+
produces => {
279+
if (produces.includes('application/octet-stream')) {
280+
return 'blob';
281+
}
282+
if (produces.includes('text/plain')) {
283+
return 'text';
284+
}
285+
return 'json';
286+
},
287+
),
288+
);
289+
274290
return combineEither(
275291
parameters,
276292
serializedResponses,
@@ -342,6 +358,7 @@ export const serializeOperationObject = combineReader(
342358
e.httpClient.request({
343359
url: ${getURL(url, parameters.serializedPathParameters)},
344360
method: '${method}',
361+
responseType: '${responseType}',
345362
${when(hasQueryParameters, 'query,')}
346363
${when(hasBodyParameters, 'body,')}
347364
${when(hasHeaderParameters, 'headers')}
@@ -351,7 +368,7 @@ export const serializeOperationObject = combineReader(
351368
${serializedResponses.io}.decode(value),
352369
either.mapLeft(ResponseValidationError.create),
353370
either.fold(error => e.httpClient.throwError(error), decoded => e.httpClient.of(decoded)),
354-
),
371+
),
355372
);
356373
},
357374
`;

src/language/typescript/3.0/serializers/operation-object.ts

+39-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { getJSDoc, getKindValue, getSafePropertyName, getTypeName, getURL, HTTPMethod } from '../../common/utils';
1+
import {
2+
getJSDoc,
3+
getKindValue,
4+
getSafePropertyName,
5+
getTypeName,
6+
getURL,
7+
HTTPMethod,
8+
SUCCESSFUL_CODES,
9+
XHRResponseType,
10+
} from '../../common/utils';
211
import {
312
getSerializedPropertyType,
413
getSerializedObjectType,
@@ -34,7 +43,7 @@ import { ResolveRefContext, fromString, getRelativePath, Ref } from '../../../..
3443
import { OperationObject } from '../../../../schema/3.0/operation-object';
3544
import { ParameterObject, ParameterObjectCodec } from '../../../../schema/3.0/parameter-object';
3645
import { RequestBodyObjectCodec } from '../../../../schema/3.0/request-body-object';
37-
import { isSome, none, Option, some } from 'fp-ts/lib/Option';
46+
import { chain, isSome, none, Option, some, map, fromEither, fold } from 'fp-ts/lib/Option';
3847
import { constFalse } from 'fp-ts/lib/function';
3948
import { clientRef } from '../../common/bundled/client';
4049
import { Kind } from '../../../../utils/types';
@@ -49,6 +58,8 @@ import {
4958
SerializedFragment,
5059
} from '../../common/data/serialized-fragment';
5160
import { SchemaObjectCodec } from '../../../../schema/3.0/schema-object';
61+
import { lookup, keys } from 'fp-ts/lib/Record';
62+
import { ResponseObjectCodec } from '../../../../schema/3.0/response-object';
5263
import {
5364
fromSerializedHeaderParameter,
5465
getSerializedHeaderParameterType,
@@ -256,8 +267,9 @@ export const getParameters = combineReader(
256267
);
257268

258269
export const serializeOperationObject = combineReader(
270+
ask<ResolveRefContext>(),
259271
getParameters,
260-
getParameters => (
272+
(e, getParameters) => (
261273
pattern: string,
262274
method: HTTPMethod,
263275
from: Ref,
@@ -274,6 +286,29 @@ export const serializeOperationObject = combineReader(
274286
);
275287

276288
const serializedResponses = serializeResponsesObject(from)(operation.responses);
289+
const responseType: XHRResponseType = pipe(
290+
SUCCESSFUL_CODES,
291+
array.findFirstMap(code => lookup(code, operation.responses)),
292+
chain(response =>
293+
ReferenceObjectCodec.is(response)
294+
? fromEither(e.resolveRef(response.$ref, ResponseObjectCodec))
295+
: some(response),
296+
),
297+
chain(response => response.content),
298+
map(keys),
299+
fold(
300+
() => 'json',
301+
types => {
302+
if (types.includes('application/octet-stream')) {
303+
return 'blob';
304+
}
305+
if (types.includes('text/plain')) {
306+
return 'text';
307+
}
308+
return 'json';
309+
},
310+
),
311+
);
277312

278313
return combineEither(
279314
parameters,
@@ -346,6 +381,7 @@ export const serializeOperationObject = combineReader(
346381
e.httpClient.request({
347382
url: ${getURL(pattern, parameters.serializedPathParameters)},
348383
method: '${method}',
384+
responseType: '${responseType}',
349385
${when(hasQueryParameters, 'query,')}
350386
${when(hasBodyParameter, 'body,')}
351387
${when(hasHeaderParameters, 'headers')}

src/language/typescript/3.0/serializers/response-object.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export const serializeResponseObject = (
1414
): Option<Either<Error, SerializedType>> =>
1515
pipe(
1616
responseObject.content,
17-
option.mapNullable(content => content['application/json']),
17+
option.mapNullable(
18+
content => content['application/json'] || content['text/plain'] || content['application/octet-stream'],
19+
),
1820
option.chain(media => media.schema),
1921
option.map(schema =>
2022
ReferenceObjectCodec.is(schema)

src/language/typescript/common/bundled/client.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const client = `
1616
export interface Request {
1717
readonly method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
1818
readonly url: string;
19+
readonly responseType: 'json' | 'blob' | 'text';
1920
readonly query?: string;
2021
readonly body?: unknown;
2122
readonly headers?: Record<string, unknown>;

src/language/typescript/common/data/serialized-type.ts

+9
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ export const SERIALIZED_DATETIME_TYPE = serializedType(
8282
[serializedDependency('DateFromISOString', 'io-ts-types/lib/DateFromISOString')],
8383
[],
8484
);
85+
export const SERIALIZED_DATE_TYPE = serializedType(
86+
'Date',
87+
'DateFromISODateStringIO',
88+
[serializedDependency('DateFromISODateStringIO', '../utils/utils')],
89+
[],
90+
);
8591
export const SERIALIZED_STRING_TYPE = serializedType('string', 'string', [serializedDependency('string', 'io-ts')], []);
8692
export const getSerializedStringType = (from: Ref, format: Option<string>): Either<Error, SerializedType> => {
8793
return combineEither(utilsRef, utilsRef => {
@@ -103,6 +109,9 @@ export const getSerializedStringType = (from: Ref, format: Option<string>): Eith
103109
),
104110
);
105111
}
112+
case 'binary': {
113+
return some(SERIALIZED_UNKNOWN_TYPE);
114+
}
106115
}
107116
return none;
108117
}),

src/language/typescript/common/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const SUCCESSFUL_CODES = ['200', '201', 'default'];
99
export const CONTROLLERS_DIRECTORY = 'controllers';
1010
export const DEFINITIONS_DIRECTORY = 'definitions';
1111
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
12-
12+
export type XHRResponseType = 'json' | 'blob' | 'text';
1313
const INVALID_NAMES = ['Error', 'Promise', 'PromiseLike', 'Array', 'ArrayLike', 'Function', 'Object'];
1414
const TYPE_NAME_NORMALIZE_REGEX = /\W/g;
1515
const normalize = (name: string): string => name.replace(TYPE_NAME_NORMALIZE_REGEX, '_').replace(/^(\d)/, '_$1');

test/index.spec.ts

+12
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ describe('codegen', () => {
6464
),
6565
);
6666

67+
it(
68+
'specs/3.0/file-and-text.yml',
69+
unsafe(
70+
generate({
71+
spec: path.resolve(__dirname, 'specs/3.0/file-and-text.yml'),
72+
out,
73+
language: serializeOpenAPI3,
74+
decoder: OpenapiObjectCodec,
75+
}),
76+
),
77+
);
78+
6779
it(
6880
'specs/asyncapi-2.0.0/streetlights-api.yml',
6981
unsafe(

test/specs/2.0/yaml/demo.yml

+106
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,107 @@ paths:
8585
type: number
8686
/shared-path-item:
8787
$ref: '../../arbitrary.yml#/SharedPathItem'
88+
/file/{filedId}/version/{version}:
89+
parameters:
90+
- in: path
91+
type: number
92+
required: true
93+
name: filedId
94+
- in: path
95+
type: number
96+
required: true
97+
name: version
98+
get:
99+
tags:
100+
- files
101+
summary: GetSomeFile
102+
operationId: getFile
103+
produces:
104+
- application/octet-stream
105+
responses:
106+
200:
107+
description: succesfull operation
108+
schema:
109+
type: string
110+
format: binary
111+
/fileWithResponseRef:
112+
get:
113+
tags:
114+
- files
115+
summary: GetFileWithResponseRef
116+
operationId: getFileWithResponseRef
117+
produces:
118+
- application/octet-stream
119+
responses:
120+
200:
121+
$ref: '#/responses/SuccessfulFile'
122+
/fileWithSchemaRef:
123+
get:
124+
tags:
125+
- files
126+
summary: GetFileWithSchemaRef
127+
operationId: getFileWithSchemaRef
128+
produces:
129+
- application/octet-stream
130+
responses:
131+
200:
132+
description: succesfull operation
133+
schema:
134+
$ref: '#/definitions/File'
135+
/text/{textId}/version/{version}:
136+
parameters:
137+
- in: path
138+
type: number
139+
required: true
140+
name: textId
141+
- in: path
142+
type: number
143+
required: true
144+
name: version
145+
get:
146+
tags:
147+
- text
148+
summary: GetSomeText
149+
operationId: getText
150+
produces:
151+
- text/plain
152+
responses:
153+
200:
154+
description: succesfull operation
155+
schema:
156+
type: string
157+
/textWithResponseRef:
158+
get:
159+
tags:
160+
- text
161+
summary: GetTextWithResponseRef
162+
operationId: getTextWithResponseRef
163+
produces:
164+
- text/plain
165+
responses:
166+
200:
167+
$ref: '#/responses/SuccessfulText'
168+
/textWithSchemaRef:
169+
get:
170+
tags:
171+
- text
172+
summary: GetTextWithSchemaRef
173+
operationId: getTextWithSchemaRef
174+
produces:
175+
- text/plain
176+
responses:
177+
200:
178+
description: succesfull operation
179+
schema:
180+
$ref: '#/definitions/Text'
88181
definitions:
89182
Id:
90183
type: integer
184+
File:
185+
type: string
186+
format: binary
187+
Text:
188+
type: string
91189
local:
92190
type: object
93191
required:
@@ -150,3 +248,11 @@ responses:
150248
description: General Error
151249
schema:
152250
$ref: '#/definitions/GeneralError'
251+
SuccessfulFile:
252+
description: succesful file data loading
253+
schema:
254+
$ref: '#/definitions/File'
255+
SuccessfulText:
256+
description: succesful text data loading
257+
schema:
258+
$ref: '#/definitions/Text'

0 commit comments

Comments
 (0)