Skip to content

Commit

Permalink
Merge pull request #144 from Geksanit/feature/arbitrary-file-download…
Browse files Browse the repository at this point in the history
…s-for-oas-3

Feature/arbitrary file downloads for oas 3
  • Loading branch information
kokovtsev authored Sep 10, 2021
2 parents 8fa2b05 + 27bc575 commit 411a4a3
Show file tree
Hide file tree
Showing 10 changed files with 616 additions and 100 deletions.
165 changes: 118 additions & 47 deletions src/language/typescript/3.0/serializers/operation-object.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
import {
getJSDoc,
getKindValue,
getSafePropertyName,
getTypeName,
getURL,
HTTPMethod,
SUCCESSFUL_CODES,
XHRResponseType,
} from '../../common/utils';
import { getJSDoc, getKindValue, getSafePropertyName, getTypeName, getURL, HTTPMethod } from '../../common/utils';
import {
getSerializedPropertyType,
getSerializedObjectType,
Expand Down Expand Up @@ -38,13 +29,13 @@ import {
} from '../../common/data/serialized-path-parameter';
import { concatIf } from '../../../../utils/array';
import { when } from '../../../../utils/string';
import { serializeRequestBodyObject } from './request-body-object';
import { getRequestMedia, serializeRequestBodyObject } from './request-body-object';
import { ResolveRefContext, fromString, getRelativePath, Ref } from '../../../../utils/ref';
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 { chain, isSome, none, Option, some, map, fromEither, fold } from 'fp-ts/lib/Option';
import { constFalse } from 'fp-ts/lib/function';
import { constFalse, flow } from 'fp-ts/lib/function';
import { clientRef } from '../../common/bundled/client';
import { Kind } from '../../../../utils/types';
import { ReferenceObjectCodec } from '../../../../schema/3.0/reference-object';
Expand All @@ -58,15 +49,13 @@ 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,
SerializedHeaderParameter,
} from '../../common/data/serialized-header-parameters';

const getOperationName = (pattern: string, operation: OperationObject, method: HTTPMethod): string =>
export const getOperationName = (pattern: string, operation: OperationObject, method: HTTPMethod): string =>
pipe(
operation.operationId,
option.getOrElse(() => `${method}_${getSafePropertyName(pattern)}`),
Expand Down Expand Up @@ -286,27 +275,19 @@ 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),
const serializedContentType = pipe(
operation.requestBody,
chain(requestBody =>
ReferenceObjectCodec.is(requestBody)
? fromEither(e.resolveRef(requestBody.$ref, RequestBodyObjectCodec))
: some(requestBody),
),
chain(response => response.content),
map(keys),
map(request => request.content),
chain(getRequestMedia),
map(({ key }) => key),
fold(
() => 'json',
types => {
if (types.includes('application/octet-stream')) {
return 'blob';
}
if (types.includes('text/plain')) {
return 'text';
}
return 'json';
},
() => '',
contentType => `'Content-type': '${contentType}',`,
),
);

Expand All @@ -333,7 +314,7 @@ export const serializeOperationObject = combineReader(

const queryType = pipe(
parameters.serializedQueryParameter,
option.map(query => `query: ${query.type},`),
option.map(query => `query: ${query.type};`),
option.getOrElse(() => ''),
);
const queryIO = pipe(
Expand All @@ -344,7 +325,7 @@ export const serializeOperationObject = combineReader(

const headersType = pipe(
parameters.serializedHeadersParameter,
option.map(headers => `headers: ${headers.type}`),
option.map(headers => `headers: ${headers.type};`),
option.getOrElse(() => ''),
);

Expand All @@ -353,42 +334,117 @@ export const serializeOperationObject = combineReader(
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}${headersType} }`],
[`parameters${hasParameters ? '' : '?'}: { ${queryType}${bodyType}${headersType} }`],
).join(',');

const type = `
${getJSDoc(array.compact([deprecated, operation.summary]))}
readonly ${operationName}: (${argsType}) => ${getKindValue(kind, serializedResponses.type)};
`;
const argsTypeWithAccept = concatIf(
true,
parameters.serializedPathParameters.map(p => p.type),
[`parameters${hasParameters ? '' : '?'}: { ${queryType}${bodyType}${headersType} accept: A; }`],
).join(',');

const type = pipe(
serializedResponses,
either.fold(
sr => `
${getJSDoc(array.compact([deprecated, operation.summary]))}
readonly ${operationName}: (${argsType}) => ${getKindValue(kind, sr.schema.type)};
`,
sr => `
${getJSDoc(array.compact([deprecated, operation.summary]))}
${operationName}(${argsType}): ${getKindValue(kind, `MapToResponse${operationName}['${sr[0].mediaType}']`)};
${operationName}<A extends keyof MapToResponse${operationName}>(${argsTypeWithAccept}): ${getKindValue(
kind,
`MapToResponse${operationName}[A]`,
)};`,
),
);

const argsIO = concatIf(
hasParameters,
parameters.pathParameters.map(p => p.name),
['parameters'],
).join(',');

const methodTypeIO = pipe(
serializedResponses,
either.fold(
() => `(${argsIO})`,
() => `
<A extends keyof MapToResponse${operationName}>(${argsTypeWithAccept}): ${getKindValue(
kind,
`MapToResponse${operationName}[A]`,
)}`,
),
);

const decode = pipe(
serializedResponses,
either.fold(
sr => `${sr.schema.io}.decode(value)`,
() => `decode(accept, value)`,
),
);
const acceptIO = pipe(
serializedResponses,
either.fold(
sr => `const accept = '${sr.mediaType}';`,
sr => `const accept = (parameters && parameters.accept || '${sr[0].mediaType}') as A`,
),
);

const mapToIO = pipe(
serializedResponses,
either.fold(
() => '',
sr => {
const rows = sr.map(s => `'${s.mediaType}': ${s.schema.io}`);
return `const mapToIO = { ${rows.join()} };`;
},
),
);

const decodeIO = pipe(
serializedResponses,
either.fold(
() => '',
() =>
`const decode = <A extends keyof MapToResponse${operationName}>(a: A, b: unknown) =>
(mapToIO[a].decode(b) as unknown) as Either<Errors, MapToResponse${operationName}[A]>;`,
),
);

const requestHeaders = `{
Accept: accept,
${serializedContentType}
}`;

const io = `
${operationName}: (${argsIO}) => {
${operationName}: ${methodTypeIO} => {
${bodyIO}
${queryIO}
${headersIO}
${acceptIO}
${mapToIO}
${decodeIO}
const responseType = getResponseTypeFromMediaType(accept);
const requestHeaders = ${requestHeaders}
return e.httpClient.chain(
e.httpClient.request({
url: ${getURL(pattern, parameters.serializedPathParameters)},
method: '${method}',
responseType: '${responseType}',
responseType,
${when(hasQueryParameters, 'query,')}
${when(hasBodyParameter, 'body,')}
${when(hasHeaderParameters, 'headers')}
headers: {${hasHeaderParameters ? '...headers,' : ''} ...requestHeaders}
}),
value =>
pipe(
${serializedResponses.io}.decode(value),
${decode},
either.mapLeft(ResponseValidationError.create),
either.fold(error => e.httpClient.throwError(error), decoded => e.httpClient.of(decoded)),
),
Expand All @@ -397,11 +453,26 @@ export const serializeOperationObject = combineReader(
`;

const dependencies = [
serializedDependency('getResponseTypeFromMediaType', '../utils/utils'),
serializedDependency('ResponseValidationError', getRelativePath(from, clientRef)),
serializedDependency('pipe', 'fp-ts/lib/pipeable'),
serializedDependency('either', 'fp-ts'),
getSerializedKindDependency(kind),
...serializedResponses.dependencies,
...pipe(
serializedResponses,
either.fold(
s => s.schema.dependencies,
flow(
array.map(s => s.schema.dependencies),
array.flatten,
arr => [
...arr,
serializedDependency('Errors', 'io-ts'),
serializedDependency('Either', 'fp-ts/lib/Either'),
],
),
),
),
...array.flatten([
...parameters.serializedPathParameters.map(p => p.dependencies),
...array.compact([
Expand Down
11 changes: 10 additions & 1 deletion src/language/typescript/3.0/serializers/paths-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { applyTo } from '../../../../utils/function';
import { PathsObject } from '../../../../schema/3.0/paths-object';
import { clientRef } from '../../common/bundled/client';
import { getControllerName } from '../../common/utils';
import { serializeResponseMaps } from './response-maps';

const serializeGrouppedPaths = combineReader(
serializePathItemObject,
Expand All @@ -35,13 +36,19 @@ const serializeGrouppedPaths = combineReader(
sequenceEither,
either.map(foldSerializedTypes),
);
const serializedResponseMaps = pipe(
serializeDictionary(groupped, (pattern, item) => serializeResponseMaps(pattern, item, from)),
sequenceEither,
either.map(foldSerializedTypes),
);

return combineEither(
serializedHKT,
serializedKind,
serializedKind2,
clientRef,
(serializedHKT, serializedKind, serializedKind2, clientRef) => {
serializedResponseMaps,
(serializedHKT, serializedKind, serializedKind2, clientRef, serializedMaps) => {
const dependencies = serializeDependencies([
...serializedHKT.dependencies,
...serializedKind.dependencies,
Expand All @@ -56,6 +63,8 @@ const serializeGrouppedPaths = combineReader(
`${from.name}.ts`,
`
${dependencies}
${serializedMaps.type}
export interface ${from.name}<F> {
${serializedHKT.type}
Expand Down
60 changes: 43 additions & 17 deletions src/language/typescript/3.0/serializers/request-body-object.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,59 @@
import { serializeSchemaObject } from './schema-object';
import { getSerializedRefType, SerializedType } from '../../common/data/serialized-type';
import {
getSerializedBlobType,
getSerializedRefType,
SerializedType,
SERIALIZED_STRING_TYPE,
} from '../../common/data/serialized-type';
import { Either, mapLeft } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import { either, option, record } from 'fp-ts';
import { either, option } from 'fp-ts';
import { fromString, Ref } from '../../../../utils/ref';
import { RequestBodyObject } from '../../../../schema/3.0/request-body-object';
import { ReferenceObjectCodec, ReferenceObject } from '../../../../schema/3.0/reference-object';
import { SchemaObject } from '../../../../schema/3.0/schema-object';
import { getKeyMatchValue, getResponseTypeFromMediaType, XHRResponseType } from '../../common/utils';
import { MediaTypeObject } from '../../../../schema/3.0/media-type-object';

const requestMediaRegexp = /^(video|audio|image|application|text|multipart|\*)\/(\w+|\*)/;
export const getRequestMedia = (content: Record<string, MediaTypeObject>) =>
getKeyMatchValue(content, requestMediaRegexp);

export const serializeRequestBodyObject = (from: Ref, body: RequestBodyObject): Either<Error, SerializedType> =>
pipe(
getSchema(body),
either.chain(schema =>
ReferenceObjectCodec.is(schema)
getRequestMedia(body.content),
option.chain(({ key: mediaType, value: { schema } }) =>
pipe(
schema,
option.map(schema => ({ mediaType, schema })),
),
),
either.fromOption(() => new Error('No schema found for ReqeustBodyObject')),
either.chain(({ mediaType, schema }) => {
const resType = getResponseTypeFromMediaType(mediaType);
return serializeRequestSchema(resType, schema, from);
}),
);

const serializeRequestSchema = (
responseType: XHRResponseType,
schema: ReferenceObject | SchemaObject,
from: Ref,
): Either<Error, SerializedType> => {
switch (responseType) {
case 'json':
return ReferenceObjectCodec.is(schema)
? pipe(
schema.$ref,
fromString,
fromString(schema.$ref),
mapLeft(
() => new Error(`Invalid MediaObject.content.$ref "${schema.$ref}" for RequestBodyObject`),
),
either.map(getSerializedRefType(from)),
)
: serializeSchemaObject(from)(schema),
),
);

const getSchema = (requestBodyObject: RequestBodyObject): Either<Error, ReferenceObject | SchemaObject> =>
pipe(
record.lookup('application/json', requestBodyObject.content),
option.chain(media => media.schema),
either.fromOption(() => new Error('No schema found for ReqeustBodyObject')),
);
: serializeSchemaObject(from)(schema);
case 'text':
return either.right(SERIALIZED_STRING_TYPE);
case 'blob':
return getSerializedBlobType(from);
}
};
Loading

0 comments on commit 411a4a3

Please sign in to comment.