diff --git a/src/language/typescript/3.0/serializers/operation-object.ts b/src/language/typescript/3.0/serializers/operation-object.ts index ceb82ed..79d5c58 100644 --- a/src/language/typescript/3.0/serializers/operation-object.ts +++ b/src/language/typescript/3.0/serializers/operation-object.ts @@ -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, @@ -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'; @@ -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)}`), @@ -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}',`, ), ); @@ -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( @@ -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(() => ''), ); @@ -353,17 +334,34 @@ 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}(${argsTypeWithAccept}): ${getKindValue( + kind, + `MapToResponse${operationName}[A]`, + )};`, + ), + ); const argsIO = concatIf( hasParameters, @@ -371,24 +369,82 @@ export const serializeOperationObject = combineReader( ['parameters'], ).join(','); + const methodTypeIO = pipe( + serializedResponses, + either.fold( + () => `(${argsIO})`, + () => ` + (${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: A, b: unknown) => + (mapToIO[a].decode(b) as unknown) as Either;`, + ), + ); + + 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)), ), @@ -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([ diff --git a/src/language/typescript/3.0/serializers/paths-object.ts b/src/language/typescript/3.0/serializers/paths-object.ts index 0d81dbd..dc913f2 100644 --- a/src/language/typescript/3.0/serializers/paths-object.ts +++ b/src/language/typescript/3.0/serializers/paths-object.ts @@ -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, @@ -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, @@ -56,6 +63,8 @@ const serializeGrouppedPaths = combineReader( `${from.name}.ts`, ` ${dependencies} + + ${serializedMaps.type} export interface ${from.name} { ${serializedHKT.type} diff --git a/src/language/typescript/3.0/serializers/request-body-object.ts b/src/language/typescript/3.0/serializers/request-body-object.ts index 3993170..db69d7f 100644 --- a/src/language/typescript/3.0/serializers/request-body-object.ts +++ b/src/language/typescript/3.0/serializers/request-body-object.ts @@ -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) => + getKeyMatchValue(content, requestMediaRegexp); export const serializeRequestBodyObject = (from: Ref, body: RequestBodyObject): Either => 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 => { + 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 => - 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); + } +}; diff --git a/src/language/typescript/3.0/serializers/response-maps.ts b/src/language/typescript/3.0/serializers/response-maps.ts new file mode 100644 index 0000000..39de6c9 --- /dev/null +++ b/src/language/typescript/3.0/serializers/response-maps.ts @@ -0,0 +1,67 @@ +import { array, either, option } from 'fp-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { flow } from 'fp-ts/lib/function'; +import { Either } from 'fp-ts/lib/Either'; +import { Option } from 'fp-ts/lib/Option'; +import { sequenceEither } from '@devexperts/utils/dist/adt/either.utils'; +import { getOperationName } from './operation-object'; +import { serializeResponsesObject } from './responses-object'; +import { HTTPMethod } from '../../common/utils'; +import { foldSerializedTypes, serializedType, SerializedType } from '../../common/data/serialized-type'; +import { Ref } from '../../../../utils/ref'; +import { PathItemObject } from '../../../../schema/3.0/path-item-object'; +import { OperationObject } from '../../../../schema/3.0/operation-object'; + +const serializeResponseMap = ( + pattern: string, + method: HTTPMethod, + from: Ref, + operation: OperationObject, +): Either => { + const operationName = getOperationName(pattern, operation, method); + const serializedResponses = serializeResponsesObject(from)(operation.responses); + return pipe( + serializedResponses, + either.map( + flow( + either.fold( + () => serializedType('', '', [], []), + sr => { + const rows = sr.map(s => `'${s.mediaType}': ${s.schema.type};`); + const type = `type MapToResponse${operationName} = {${rows.join('')}};`; + return serializedType(type, '', [], []); // dependecies in serializeOperationObject serializedResponses + }, + ), + ), + ), + ); +}; + +export const serializeResponseMaps = ( + pattern: string, + item: PathItemObject, + from: Ref, +): Either => { + const methods: [HTTPMethod, Option][] = [ + ['GET', item.get], + ['POST', item.post], + ['PUT', item.put], + ['DELETE', item.delete], + ['PATCH', item.patch], + ['HEAD', item.head], + ['OPTIONS', item.options], + ]; + + return pipe( + methods, + array.map(([method, opObject]) => + pipe( + opObject, + option.map(operation => serializeResponseMap(pattern, method, from, operation)), + ), + ), + array.compact, + sequenceEither, + either.map(foldSerializedTypes), + ); +}; diff --git a/src/language/typescript/3.0/serializers/response-object.ts b/src/language/typescript/3.0/serializers/response-object.ts index 8de777b..d90b377 100644 --- a/src/language/typescript/3.0/serializers/response-object.ts +++ b/src/language/typescript/3.0/serializers/response-object.ts @@ -1,12 +1,24 @@ -import { SerializedType, getSerializedRefType } from '../../common/data/serialized-type'; +import { + SerializedType, + getSerializedRefType, + SERIALIZED_STRING_TYPE, + getSerializedBlobType, +} from '../../common/data/serialized-type'; import { pipe } from 'fp-ts/lib/pipeable'; import { serializeSchemaObject } from './schema-object'; import { Either } from 'fp-ts/lib/Either'; import { fromString, Ref } from '../../../../utils/ref'; -import { either, option } from 'fp-ts'; +import { either, option, array, nonEmptyArray } from 'fp-ts'; import { ResponseObject } from '../../../../schema/3.0/response-object'; import { Option } from 'fp-ts/lib/Option'; -import { ReferenceObjectCodec } from '../../../../schema/3.0/reference-object'; +import { ReferenceObject, ReferenceObjectCodec } from '../../../../schema/3.0/reference-object'; +import { getKeyMatchValue, getKeyMatchValues, getResponseTypeFromMediaType, XHRResponseType } from '../../common/utils'; +import { SchemaObject } from '../../../../schema/3.0/schema-object'; +import { sequenceEither } from '@devexperts/utils/dist/adt/either.utils'; + +const requestMediaRegexp = /^(video|audio|image|application|text|\*)\/(\w+|\*)/; + +export type SerializedResponse = { mediaType: string; schema: SerializedType }; export const serializeResponseObject = ( from: Ref, @@ -14,13 +26,70 @@ export const serializeResponseObject = ( ): Option> => pipe( responseObject.content, - option.mapNullable( - content => content['application/json'] || content['text/plain'] || content['application/octet-stream'], + option.chain(content => getKeyMatchValue(content, requestMediaRegexp)), + option.chain(({ key: mediaType, value: { schema } }) => + pipe( + schema, + option.map(schema => ({ mediaType, schema })), + ), ), - option.chain(media => media.schema), - option.map(schema => - ReferenceObjectCodec.is(schema) - ? pipe(fromString(schema.$ref), either.map(getSerializedRefType(from))) - : serializeSchemaObject(from)(schema), + option.map(({ mediaType, schema }) => { + const resType = getResponseTypeFromMediaType(mediaType); + return serializeResponseSchema(resType, schema, from); + }), + ); + +export const serializeResponseObjectWithMediaType = ( + from: Ref, + responseObject: ResponseObject, +): Option> => + pipe( + responseObject.content, + option.chain(content => getKeyMatchValues(content, requestMediaRegexp)), + option.chain(arr => + pipe( + arr, + array.map(({ key: mediaType, value: { schema } }) => + pipe( + schema, + option.map(schema => ({ mediaType, schema })), + ), + ), + array.filterMap(a => a), + nonEmptyArray.fromArray, + ), + ), + option.map(arr => + pipe( + arr, + array.map(({ mediaType, schema }) => { + const resType = getResponseTypeFromMediaType(mediaType); + return pipe( + serializeResponseSchema(resType, schema, from), + either.map(schema => ({ + mediaType, + schema, + })), + ); + }), + sequenceEither, + ), ), ); + +const serializeResponseSchema = ( + responseType: XHRResponseType, + schema: ReferenceObject | SchemaObject, + from: Ref, +): Either => { + switch (responseType) { + case 'json': + return ReferenceObjectCodec.is(schema) + ? pipe(fromString(schema.$ref), either.map(getSerializedRefType(from))) + : serializeSchemaObject(from)(schema); + case 'text': + return either.right(SERIALIZED_STRING_TYPE); + case 'blob': + return getSerializedBlobType(from); + } +}; diff --git a/src/language/typescript/3.0/serializers/responses-object.ts b/src/language/typescript/3.0/serializers/responses-object.ts index 7536679..40d7355 100644 --- a/src/language/typescript/3.0/serializers/responses-object.ts +++ b/src/language/typescript/3.0/serializers/responses-object.ts @@ -1,27 +1,53 @@ import { - intercalateSerializedTypes, - serializedType, - SerializedType, - uniqSerializedTypesByTypeAndIO, - SERIALIZED_VOID_TYPE, getSerializedRefType, + SERIALIZED_VOID_TYPE, + uniqSerializedTypesByTypeAndIO, + serializedType, + intercalateSerializedTypes, } from '../../common/data/serialized-type'; -import { SUCCESSFUL_CODES } from '../../common/utils'; +import { DEFAULT_MEDIA_TYPE, SUCCESSFUL_CODES } from '../../common/utils'; import { pipe } from 'fp-ts/lib/pipeable'; -import { serializeResponseObject } from './response-object'; -import { serializedDependency } from '../../common/data/serialized-dependency'; -import { concatIfL } from '../../../../utils/array'; +import { SerializedResponse, serializeResponseObjectWithMediaType } from './response-object'; import { sequenceEither } from '@devexperts/utils/dist/adt/either.utils'; import { array, either, option, record } from 'fp-ts'; import { Either } from 'fp-ts/lib/Either'; import { fromString, Ref } from '../../../../utils/ref'; import { ResponsesObject } from '../../../../schema/3.0/responses-object'; -import { some } from 'fp-ts/lib/Option'; +import { flow } from 'fp-ts/lib/function'; import { ReferenceObjectCodec } from '../../../../schema/3.0/reference-object'; +import { some } from 'fp-ts/lib/Option'; +import { eqString } from 'fp-ts/lib/Eq'; +import { serializedDependency } from '../../common/data/serialized-dependency'; + +const concatNonUniqResonses = (responses: SerializedResponse[]): SerializedResponse[] => + pipe( + responses, + array.map(({ mediaType }) => mediaType), + array.uniq(eqString), + array.map(mediaType => { + const schemes = pipe( + responses, + array.filter(a => a.mediaType === mediaType), + array.map(a => a.schema), + uniqSerializedTypesByTypeAndIO, + ); + if (schemes.length > 1) { + const combined = intercalateSerializedTypes(serializedType('|', ',', [], []), schemes); + const scheme = serializedType( + combined.type, + `union([${combined.io}])`, + combined.dependencies.concat([serializedDependency('union', 'io-ts')]), + [], + ); + return { mediaType, schema: scheme }; + } + return { mediaType, schema: schemes[0] }; + }), + ); export const serializeResponsesObject = (from: Ref) => ( responsesObject: ResponsesObject, -): Either => { +): Either> => { const serializedResponses = pipe( SUCCESSFUL_CODES, array.map(code => @@ -35,30 +61,27 @@ export const serializeResponsesObject = (from: Ref) => ( () => new Error(`Invalid ${r.$ref} for ResponsesObject'c code "${code}"`), ), either.map(getSerializedRefType(from)), + either.map(type => [{ mediaType: DEFAULT_MEDIA_TYPE, schema: type }]), some, ) - : serializeResponseObject(from, r), + : serializeResponseObjectWithMediaType(from, r), ), ), ), array.compact, sequenceEither, - either.map(uniqSerializedTypesByTypeAndIO), + either.map(flow(array.flatten, concatNonUniqResonses)), ); return pipe( serializedResponses, either.map(serializedResponses => { if (serializedResponses.length === 0) { - return SERIALIZED_VOID_TYPE; + return either.left({ mediaType: DEFAULT_MEDIA_TYPE, schema: SERIALIZED_VOID_TYPE }); + } else if (serializedResponses.length === 1) { + return either.left(serializedResponses[0]); + } else { + return either.right(serializedResponses); } - const combined = intercalateSerializedTypes(serializedType('|', ',', [], []), serializedResponses); - const isUnion = serializedResponses.length > 1; - return serializedType( - combined.type, - isUnion ? `union([${combined.io}])` : combined.io, - concatIfL(isUnion, combined.dependencies, () => [serializedDependency('union', 'io-ts')]), - [], - ); }), ); }; diff --git a/src/language/typescript/common/bundled/utils.ts b/src/language/typescript/common/bundled/utils.ts index 5f8f1c3..0a13e24 100644 --- a/src/language/typescript/common/bundled/utils.ts +++ b/src/language/typescript/common/bundled/utils.ts @@ -6,8 +6,19 @@ import { fromRef } from '../../../../utils/fs'; export const utilsRef = fromString('#/utils/utils'); const utils = ` - import { either } from 'fp-ts/lib/Either'; - import { Type, failure, success, string as tstring } from 'io-ts'; + import { either, left, right } from 'fp-ts/lib/Either'; + import { + Type, + type, + TypeOf, + failure, + success, + string as tstring, + literal, + Validate, + Context, + getValidationError, + } from 'io-ts'; export const DateFromISODateStringIO = new Type( 'DateFromISODateString', @@ -24,6 +35,60 @@ const utils = ` .toString() .padStart(2, '0')}\`, ); + + export type Base64 = TypeOf; + + export const Base64IO = type({ + string: tstring, + format: literal('base64'), + }); + + export const Base64FromStringIO = new Type( + 'Base64FromString', + (u): u is Base64 => Base64IO.is(u), + (u, c) => either.chain(tstring.validate(u, c), string => success({ string, format: 'base64' })), + a => a.string, + ); + + export type Binary = TypeOf; + + export const BinaryIO = type({ + string: tstring, + format: literal('binary'), + }); + + export const BinaryFromStringIO = new Type( + 'BinaryFromString', + (u): u is Binary => BinaryIO.is(u), + (u, c) => either.chain(tstring.validate(u, c), string => success({ string, format: 'binary' })), + a => a.string, + ); + + const validateBlob: Validate = (u: unknown, c: Context) => + u instanceof Blob ? right(u) : left([getValidationError(u, c)]); + + export const BlobToBlobIO = new Type( + 'Base64FromString', + (u): u is Blob => u instanceof Blob, + validateBlob, + a => a, + ); + + const blobMediaRegexp = /^(video|audio|image|application)/; + const textMediaRegexp = /^text/; + export const getResponseTypeFromMediaType = (mediaType: string) => { + if (mediaType === 'application/json') { + return 'json'; + } + if (blobMediaRegexp.test(mediaType)) { + return 'blob'; + } + if (textMediaRegexp.test(mediaType)) { + return 'text'; + } + return 'json'; + }; + `; export const utilsFile = pipe( diff --git a/src/language/typescript/common/data/serialized-type.ts b/src/language/typescript/common/data/serialized-type.ts index 89660bf..258bc6c 100644 --- a/src/language/typescript/common/data/serialized-type.ts +++ b/src/language/typescript/common/data/serialized-type.ts @@ -68,6 +68,16 @@ export const SERIALIZED_UNKNOWN_TYPE = serializedType( [serializedDependency('unknown', 'io-ts')], [], ); +export const getSerializedBlobType = (from: Ref): Either => { + return combineEither(utilsRef, utilsRef => + serializedType( + 'Blob', + 'BlobToBlobIO', + [serializedDependency('BlobToBlobIO', getRelativePath(from, utilsRef))], + [], + ), + ); +}; export const SERIALIZED_BOOLEAN_TYPE = serializedType( 'boolean', 'boolean', @@ -109,8 +119,32 @@ export const getSerializedStringType = (from: Ref, format: Option): Eith ), ); } + case 'byte': + case 'base64': { + return some( + serializedType( + 'Base64', + 'Base64FromStringIO', + [ + serializedDependency('Base64FromStringIO', getRelativePath(from, utilsRef)), + serializedDependency('Base64', getRelativePath(from, utilsRef)), + ], + [], + ), + ); + } case 'binary': { - return some(SERIALIZED_UNKNOWN_TYPE); + return some( + serializedType( + 'Binary', + 'BinaryFromStringIO', + [ + serializedDependency('BinaryFromStringIO', getRelativePath(from, utilsRef)), + serializedDependency('Binary', getRelativePath(from, utilsRef)), + ], + [], + ), + ); } } return none; diff --git a/src/language/typescript/common/utils.ts b/src/language/typescript/common/utils.ts index ed055bf..728258a 100644 --- a/src/language/typescript/common/utils.ts +++ b/src/language/typescript/common/utils.ts @@ -4,6 +4,9 @@ import { Options } from 'prettier'; import { fromString, ResolveRefContext } from '../../../utils/ref'; import { Kind } from '../../../utils/types'; import { ask } from 'fp-ts/lib/Reader'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { keys } from 'fp-ts/lib/Record'; +import { array, option, nonEmptyArray } from 'fp-ts'; export const SUCCESSFUL_CODES = ['200', '201', 'default']; export const CONTROLLERS_DIRECTORY = 'controllers'; @@ -77,3 +80,36 @@ export const getSafePropertyName = (value: string): string => value.replace(REPLACE_PATTERN, '_').replace(/^(\d)/, '_$1') || '_'; export const context = ask(); + +export const getKeyMatchValue = (record: Record, regexp: RegExp) => + pipe( + record, + keys, + array.findFirst(s => regexp.test(s)), + option.map(key => ({ key, value: record[key] })), + ); + +export const getKeyMatchValues = (record: Record, regexp: RegExp) => + pipe( + record, + keys, + array.filter(s => regexp.test(s)), + nonEmptyArray.fromArray, + option.map(nonEmptyArray.map(key => ({ key, value: record[key] }))), + ); + +const blobMediaRegexp = /^(video|audio|image|application)/; +const textMediaRegexp = /^text/; +export const DEFAULT_MEDIA_TYPE = 'application/json'; +export const getResponseTypeFromMediaType = (mediaType: string): XHRResponseType => { + if (mediaType === 'application/json') { + return 'json'; + } + if (blobMediaRegexp.test(mediaType)) { + return 'blob'; + } + if (textMediaRegexp.test(mediaType)) { + return 'text'; + } + return 'json'; +}; diff --git a/test/specs/3.0/file-and-text.yml b/test/specs/3.0/file-and-text.yml index 2f340d0..831d01c 100644 --- a/test/specs/3.0/file-and-text.yml +++ b/test/specs/3.0/file-and-text.yml @@ -91,6 +91,102 @@ paths: text/plain: schema: $ref: '#/components/schemas/Text' + /table: + get: + tags: + - media + summary: get table with selected media + operationId: loadTable + parameters: + - in: header + required: true + name: version + schema: + type: number + responses: + 200: + description: succesfull operation + content: + application/json: + schema: + $ref: '#/components/schemas/TableObject' + text/csv: + schema: + type: string + post: + tags: + - media + summary: post table + operationId: loadTable2 + responses: + 200: + description: succesfull operation + content: + application/json: + schema: + $ref: '#/components/schemas/TableObject' + text/csv: + schema: + type: string + 201: + description: succesfull operation + content: + application/json: + schema: + $ref: '#/components/schemas/Text' + text/csv: + schema: + type: string + /inline-image: + get: + tags: + - media + summary: load image in base64 + operationId: loadInlineImage + responses: + 200: + description: succesfull operation + content: + application/json: + schema: + type: string + format: base64' + /image: + get: + tags: + - media + summary: get image + operationId: loadImage + responses: + 200: + description: succesfull operation + content: + image/png: + schema: + type: string + format: binary + image/jpeg: + schema: + type: string + format: binary + image/gif: + schema: + type: string + format: binary + post: + tags: + - media + summary: upload image + operationId: uploadImage + requestBody: + content: + image/png: + schema: + type: string + format: byte + responses: + 200: + description: succesfull operation components: schemas: File: @@ -98,6 +194,26 @@ components: format: binary Text: type: string + TableObject: + type: object + required: + - tableName + - rows + properties: + tableName: + type: string + rows: + type: array + items: + type: object + required: + - id + - name + properties: + id: + type: integer + name: + type: string responses: Successful: description: succesful operation