From 763cc68275c28935ecdeb5375bc8f1ed7725af62 Mon Sep 17 00:00:00 2001 From: raveclassic Date: Mon, 9 Dec 2019 19:51:41 +0300 Subject: [PATCH] refactor: simplify resolveRef BREAKING CHANGE: language is now a Reader of ResolveRefContext BREAKING CHANGE: resolveRef now requires decoder as a second argument --- src/index.ts | 20 ++- .../2.0/serializers/operation-object.ts | 10 +- .../2.0/serializers/path-item-object.ts | 122 +++++++++--------- .../3.0/serializers/components-object.ts | 36 ++---- .../3.0/serializers/operation-object.ts | 14 +- .../serializers/components-object.ts | 12 +- src/language/typescript/common/utils.ts | 10 +- src/utils/ref.ts | 9 ++ test/index.ts | 24 +--- 9 files changed, 117 insertions(+), 140 deletions(-) diff --git a/src/index.ts b/src/index.ts index 87147f9..42a48c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,20 +4,22 @@ import * as path from 'path'; import * as $RefParser from 'json-schema-ref-parser'; import { pipe } from 'fp-ts/lib/pipeable'; import { array, either, taskEither } from 'fp-ts'; -import { Either, isLeft } from 'fp-ts/lib/Either'; +import { Either, isLeft, toError } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { reportIfFailed } from './utils/io-ts'; import { TaskEither } from 'fp-ts/lib/TaskEither'; import { sketchParser121 } from './parsers/sketch-121'; +import { ResolveRef, ResolveRefContext } from './utils/ref'; +import { Reader } from 'fp-ts/lib/Reader'; +export interface Language { + (documents: Record): Either; +} export interface GenerateOptions { readonly out: string; readonly spec: string; readonly decoder: Decoder; - readonly language: ( - documents: Record, - resolveRef: (ref: string) => Either, - ) => Either; + readonly language: Reader>; } const log = (...args: unknown[]) => console.log('[SWAGGER-CODEGEN-TS]:', ...args); @@ -64,7 +66,13 @@ export const generate = (options: GenerateOptions): TaskEither either.tryCatch(() => $refs.get(ref), identity)))); + const resolveRef: ResolveRef = ($ref, decoder) => + pipe( + either.tryCatch(() => $refs.get($ref), toError), + either.chain(resolved => reportIfFailed(decoder.decode(resolved))), + ); + + await write(out, getUnsafe(options.language({ resolveRef })(specs))); log('Done'); }, identity); diff --git a/src/language/typescript/2.0/serializers/operation-object.ts b/src/language/typescript/2.0/serializers/operation-object.ts index b0306f6..57e9327 100644 --- a/src/language/typescript/2.0/serializers/operation-object.ts +++ b/src/language/typescript/2.0/serializers/operation-object.ts @@ -15,11 +15,11 @@ 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 { Context, getJSDoc, getKindValue, getURL, HTTPMethod } from '../../common/utils'; +import { getJSDoc, getKindValue, getURL, HTTPMethod } from '../../common/utils'; import { Either, isLeft, left, right } from 'fp-ts/lib/Either'; import { array, either, nonEmptyArray, option } from 'fp-ts'; import { combineEither } from '@devexperts/utils/dist/adt/either.utils'; -import { fromString, getRelativePath, Ref } from '../../../../utils/ref'; +import { ResolveRefContext, fromString, getRelativePath, Ref } from '../../../../utils/ref'; import { clientRef } from '../../common/bundled/client'; import { ArrayParameterObjectCollectionFormat, @@ -71,7 +71,7 @@ const contains = array.elem( ); const getParameters = combineReader( - ask(), + ask(), e => (from: Ref, operation: OperationObject, pathItem: PathItemObject): Either => { const processedParameters: ParameterObject[] = []; const pathParameters: PathParameterObject[] = []; @@ -93,9 +93,7 @@ const getParameters = combineReader( for (const parameter of parameters) { const resolved = ReferenceObjectCodec.is(parameter) ? pipe( - parameter, - e.resolveRef, - ParameterObjectCodec.decode, + e.resolveRef(parameter.$ref, ParameterObjectCodec), either.mapLeft(() => new Error(`Unable to resolve parameter with $ref "${parameter.$ref}"`)), ) : right(parameter); diff --git a/src/language/typescript/2.0/serializers/path-item-object.ts b/src/language/typescript/2.0/serializers/path-item-object.ts index 7afb23b..9de1006 100644 --- a/src/language/typescript/2.0/serializers/path-item-object.ts +++ b/src/language/typescript/2.0/serializers/path-item-object.ts @@ -11,65 +11,67 @@ import { decapitalize } from '@devexperts/utils/dist/string'; import { Either } from 'fp-ts/lib/Either'; import { combineEither, sequenceEither } from '@devexperts/utils/dist/adt/either.utils'; import { either, nonEmptyArray, option, record } from 'fp-ts'; -import { getRelativePath, Ref } from '../../../../utils/ref'; +import { ResolveRefContext, getRelativePath, Ref } from '../../../../utils/ref'; import { clientRef } from '../../common/bundled/client'; import { OperationObject } from '../../../../schema/2.0/operation-object'; import { tuple } from 'fp-ts/lib/function'; import { uniqString } from '../../../../utils/array'; import { combineReader } from '@devexperts/utils/dist/adt/reader.utils'; import { ask } from 'fp-ts/lib/Reader'; -import { Context } from '../../common/utils'; -const serializePath = combineReader(ask(), serializeOperationObject, (e, serializeOperationObject) => { - const run = (from: Ref, url: string, kind: Kind, item: PathItemObject): Either => { - if (isSome(item.$ref)) { - const $ref = item.$ref.value; - return pipe( - e.resolveRef({ $ref }), - PathItemObjectCodec.decode, - either.mapLeft(() => new Error(`Unable to resolve PathItem $ref: "${$ref}"`)), - either.chain(resolved => run(from, url, kind, resolved)), - ); - } else { - const get = pipe( - item.get, - map(operation => serializeOperationObject(from, url, 'GET', kind, operation, item)), - ); - const put = pipe( - item.put, - map(operation => serializeOperationObject(from, url, 'PUT', kind, operation, item)), - ); - const post = pipe( - item.post, - map(operation => serializeOperationObject(from, url, 'POST', kind, operation, item)), - ); - const remove = pipe( - item.delete, - map(operation => serializeOperationObject(from, url, 'DELETE', kind, operation, item)), - ); - const options = pipe( - item.options, - map(operation => serializeOperationObject(from, url, 'OPTIONS', kind, operation, item)), - ); - const head = pipe( - item.head, - map(operation => serializeOperationObject(from, url, 'HEAD', kind, operation, item)), - ); - const patch = pipe( - item.patch, - map(operation => serializeOperationObject(from, url, 'PATCH', kind, operation, item)), - ); - const operations = [get, put, post, remove, options, head, patch]; - return pipe( - operations, - array.compact, - sequenceEither, - either.map(foldSerializedTypes), - ); - } - }; - return run; -}); +const serializePath = combineReader( + ask(), + serializeOperationObject, + (e, serializeOperationObject) => { + const run = (from: Ref, url: string, kind: Kind, item: PathItemObject): Either => { + if (isSome(item.$ref)) { + const $ref = item.$ref.value; + return pipe( + e.resolveRef($ref, PathItemObjectCodec), + either.mapLeft(() => new Error(`Unable to resolve PathItem $ref: "${$ref}"`)), + either.chain(resolved => run(from, url, kind, resolved)), + ); + } else { + const get = pipe( + item.get, + map(operation => serializeOperationObject(from, url, 'GET', kind, operation, item)), + ); + const put = pipe( + item.put, + map(operation => serializeOperationObject(from, url, 'PUT', kind, operation, item)), + ); + const post = pipe( + item.post, + map(operation => serializeOperationObject(from, url, 'POST', kind, operation, item)), + ); + const remove = pipe( + item.delete, + map(operation => serializeOperationObject(from, url, 'DELETE', kind, operation, item)), + ); + const options = pipe( + item.options, + map(operation => serializeOperationObject(from, url, 'OPTIONS', kind, operation, item)), + ); + const head = pipe( + item.head, + map(operation => serializeOperationObject(from, url, 'HEAD', kind, operation, item)), + ); + const patch = pipe( + item.patch, + map(operation => serializeOperationObject(from, url, 'PATCH', kind, operation, item)), + ); + const operations = [get, put, post, remove, options, head, patch]; + return pipe( + operations, + array.compact, + sequenceEither, + either.map(foldSerializedTypes), + ); + } + }; + return run; + }, +); export const serializePathGroup = combineReader( serializePath, @@ -113,21 +115,21 @@ export const serializePathGroup = combineReader( ]); return file( `${from.name}.ts`, - ` - ${dependencies} - + ` + ${dependencies} + export interface ${from.name} { ${serializedHKT.type} } - + export interface ${from.name}1 { - ${serializedKind.type} + ${serializedKind.type} } - + export interface ${from.name}2 { - ${serializedKind2.type} + ${serializedKind2.type} } - + export function ${decapitalize(from.name)}(e: { httpClient: HTTPClient2 }): ${from.name}2 export function ${decapitalize(from.name)}(e: { httpClient: HTTPClient1 }): ${from.name}1 export function ${decapitalize(from.name)}(e: { httpClient: HTTPClient }): ${from.name}; diff --git a/src/language/typescript/3.0/serializers/components-object.ts b/src/language/typescript/3.0/serializers/components-object.ts index a8bc625..6bbfe82 100644 --- a/src/language/typescript/3.0/serializers/components-object.ts +++ b/src/language/typescript/3.0/serializers/components-object.ts @@ -36,7 +36,7 @@ const serializeSchema = (from: Ref, schema: SchemaObject): Either = `${from.name}.ts`, ` ${dependencies} - + export type ${typeName} = ${serialized.type}; export const ${ioName} = ${serialized.io}; `, @@ -51,12 +51,7 @@ const serializeSchemas = combineReader( schemas, record.collect((name, schema) => { const resolved = ReferenceObjectCodec.is(schema) - ? pipe( - schema, - e.resolveRef, - SchemaObjectCodec.decode, - reportIfFailed, - ) + ? e.resolveRef(schema.$ref, SchemaObjectCodec) : right(schema); const ref = pipe( from, @@ -82,7 +77,7 @@ const serializeParameter = (from: Ref, parameterObject: ParameterObject): Either `${from.name}.ts`, ` ${dependencies} - + export type ${getTypeName(from.name)} = ${serialized.type}; export const ${getIOName(from.name)} = ${serialized.io}; `, @@ -97,12 +92,7 @@ const serializeParameters = combineReader( parameters, record.collect((name, parameter) => { const resolved = ReferenceObjectCodec.is(parameter) - ? pipe( - parameter, - e.resolveRef, - ParameterObjectCodec.decode, - reportIfFailed, - ) + ? e.resolveRef(parameter.$ref, ParameterObjectCodec) : right(parameter); const ref = pipe( from, @@ -129,7 +119,7 @@ const serializeResponse = (from: Ref, responseObject: ResponseObject): Either { const resolved = ReferenceObjectCodec.is(response) - ? pipe( - response, - e.resolveRef, - ResponseObjectCodec.decode, - reportIfFailed, - ) + ? e.resolveRef(response.$ref, ResponseObjectCodec) : right(response); const ref = pipe( from, @@ -173,7 +158,7 @@ const serializeRequestBody = (from: Ref, requestBody: RequestBodyObject): Either `${from.name}.ts`, ` ${serializeDependencies(serialized.dependencies)} - + export type ${getTypeName(from.name)} = ${serialized.type}; export const ${getIOName(from.name)} = ${serialized.io}; `, @@ -188,12 +173,7 @@ const serializeRequestBodies = combineReader( requestBodies, record.collect((name, requestBody) => { const resolved = ReferenceObjectCodec.is(requestBody) - ? pipe( - requestBody, - e.resolveRef, - RequestBodyObjectCodec.decode, - reportIfFailed, - ) + ? e.resolveRef(requestBody.$ref, RequestBodyObjectCodec) : right(requestBody); const ref = pipe( from, diff --git a/src/language/typescript/3.0/serializers/operation-object.ts b/src/language/typescript/3.0/serializers/operation-object.ts index 89c875e..ef1fd2b 100644 --- a/src/language/typescript/3.0/serializers/operation-object.ts +++ b/src/language/typescript/3.0/serializers/operation-object.ts @@ -1,4 +1,4 @@ -import { Context, getJSDoc, getKindValue, getURL, HTTPMethod } from '../../common/utils'; +import { getJSDoc, getKindValue, getURL, HTTPMethod } from '../../common/utils'; import { getSerializedPropertyType, getSerializedObjectType, @@ -30,7 +30,7 @@ import { import { concatIf } from '../../../../utils/array'; import { when } from '../../../../utils/string'; import { serializeRequestBodyObject } from './request-body-object'; -import { fromString, getRelativePath, Ref } from '../../../../utils/ref'; +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'; @@ -72,7 +72,7 @@ const eqParameterByNameAndIn: Eq = getStructEq({ const contains = array.elem(eqParameterByNameAndIn); const getParameters = combineReader( - ask(), + ask(), e => (from: Ref, operation: OperationObject, pathItem: PathItemObject): Either => { const processedParameters: ParameterObject[] = []; const pathParameters: ParameterObject[] = []; @@ -87,7 +87,7 @@ const getParameters = combineReader( for (const parameter of parameters) { const resolved = ReferenceObjectCodec.is(parameter) - ? reportIfFailed(ParameterObjectCodec.decode(e.resolveRef(parameter))) + ? e.resolveRef(parameter.$ref, ParameterObjectCodec) : right(parameter); if (isLeft(resolved)) { @@ -134,7 +134,7 @@ const getParameters = combineReader( schema, either.chain(schema => ReferenceObjectCodec.is(schema) - ? reportIfFailed(SchemaObjectCodec.decode(e.resolveRef(schema))) + ? e.resolveRef(schema.$ref, SchemaObjectCodec) : right(schema), ), ); @@ -172,7 +172,7 @@ const getParameters = combineReader( if (isLeft(reference)) { return left(new Error(`Invalid RequestBodyObject.$ref "${requestBody.$ref}"`)); } - const resolved = option.fromEither(RequestBodyObjectCodec.decode(e.resolveRef(requestBody))); + const resolved = option.fromEither(e.resolveRef(requestBody.$ref, RequestBodyObjectCodec)); if (!isSome(resolved)) { return left(new Error(`Unable to resolve RequestBodyObject with ref ${requestBody.$ref}`)); @@ -308,7 +308,7 @@ export const serializeOperationObject = combineReader( ${operationName}: (${argsIO}) => { ${bodyIO} ${queryIO} - + return e.httpClient.chain( e.httpClient.request({ url: ${getURL(pattern, parameters.serializedPathParameters)}, diff --git a/src/language/typescript/asyncapi-2.0.0/serializers/components-object.ts b/src/language/typescript/asyncapi-2.0.0/serializers/components-object.ts index 22d09b1..844a35d 100644 --- a/src/language/typescript/asyncapi-2.0.0/serializers/components-object.ts +++ b/src/language/typescript/asyncapi-2.0.0/serializers/components-object.ts @@ -31,7 +31,7 @@ const serializeMessage = (from: Ref, messageObject: MessageObject): Either { const resolved = ReferenceObjectCodec.is(message) - ? reportIfFailed(MessageObjectCodec.decode(context.resolveRef(message))) + ? context.resolveRef(message.$ref, MessageObjectCodec) : right(message); const ref = pipe( from, @@ -76,7 +76,7 @@ const serializeSchema = (from: Ref, schema: SchemaObject): Either { const resolved = ReferenceObjectCodec.is(schema) - ? pipe( - e.resolveRef(schema), - SchemaObjectCodec.decode, - reportIfFailed, - ) + ? e.resolveRef(schema.$ref, SchemaObjectCodec) : right(schema); const ref = pipe( from, diff --git a/src/language/typescript/common/utils.ts b/src/language/typescript/common/utils.ts index ec08291..6494f91 100644 --- a/src/language/typescript/common/utils.ts +++ b/src/language/typescript/common/utils.ts @@ -1,8 +1,7 @@ import { SerializedPathParameter } from './data/serialized-path-parameter'; import { unless } from '../../../utils/string'; import { Options } from 'prettier'; -import { fromString } from '../../../utils/ref'; -import { ReferenceObject } from '../../../schema/3.0/reference-object'; +import { fromString, ResolveRefContext } from '../../../utils/ref'; import { Kind } from '../../../utils/types'; import { ask } from 'fp-ts/lib/Reader'; @@ -44,11 +43,6 @@ export interface SerializeOptions { export const pathsRef = fromString('#/paths'); -export interface Context { - readonly resolveRef: (referenceObject: ReferenceObject) => unknown; -} -export const context = ask(); - export const getKindValue = (kind: Kind, value: string): string => { switch (kind) { case 'HKT': { @@ -69,3 +63,5 @@ export const UNSAFE_PROPERTY_PATTERN = /[^a-zA-Z_0-9]/; const REPLACE_PATTERN = new RegExp(UNSAFE_PROPERTY_PATTERN, 'g'); export const getSafePropertyName = (value: string): string => value.replace(REPLACE_PATTERN, '_').replace(/^(\d)/, '_$1') || '_'; + +export const context = ask(); diff --git a/src/utils/ref.ts b/src/utils/ref.ts index 241969f..1c79d91 100644 --- a/src/utils/ref.ts +++ b/src/utils/ref.ts @@ -3,6 +3,7 @@ import { isNonEmpty, uniq } from 'fp-ts/lib/Array'; import { last, NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; import { Either, left, right } from 'fp-ts/lib/Either'; import { Eq, eqString, getStructEq } from 'fp-ts/lib/Eq'; +import { Decoder } from 'io-ts'; export interface Ref { readonly $ref: string; @@ -92,3 +93,11 @@ export const getRelativePath = (from: Ref, to: Ref): string => { }; export const getFullPath = (ref: Ref): string => path.join(ref.target, ref.path); + +export interface ResolveRef { + ($ref: string, decoder: Decoder): Either; +} + +export interface ResolveRefContext { + readonly resolveRef: ResolveRef; +} diff --git a/test/index.ts b/test/index.ts index 76fb921..0d154a9 100644 --- a/test/index.ts +++ b/test/index.ts @@ -9,7 +9,7 @@ import { serialize as serializeSketch } from '../src/language/typescript/sketch- import { OpenapiObjectCodec } from '../src/schema/3.0/openapi-object'; import * as del from 'del'; import { pipe } from 'fp-ts/lib/pipeable'; -import { either, option, taskEither } from 'fp-ts'; +import { either, taskEither } from 'fp-ts'; import { identity } from 'fp-ts/lib/function'; import { TaskEither } from 'fp-ts/lib/TaskEither'; import { FileFormatCodec } from '../src/schema/sketch-121/file-format'; @@ -22,47 +22,35 @@ const out = path.resolve(cwd, 'out'); const test1 = generate({ spec: path.resolve(__dirname, 'specs/2.0/json/swagger.json'), out, - language: (documents, resolveRef) => - serializeSwagger2({ - resolveRef: referenceObject => option.toUndefined(option.fromEither(resolveRef(referenceObject.$ref))), - })(documents), + language: serializeSwagger2, decoder: SwaggerObject, }); const test2 = generate({ spec: path.resolve(__dirname, 'specs/2.0/yaml/demo.yml'), out, - language: (documents, resolveRef) => - serializeSwagger2({ - resolveRef: referenceObject => option.toUndefined(option.fromEither(resolveRef(referenceObject.$ref))), - })(documents), + language: serializeSwagger2, decoder: SwaggerObject, }); const test3 = generate({ spec: path.resolve(__dirname, 'specs/3.0/demo.yml'), out, - language: (documents, resolveRef) => - serializeOpenAPI3({ - resolveRef: referenceObject => option.toUndefined(option.fromEither(resolveRef(referenceObject.$ref))), - })(documents), + language: serializeOpenAPI3, decoder: OpenapiObjectCodec, }); const test4 = generate({ spec: path.resolve(__dirname, 'specs/asyncapi-2.0.0/streetlights-api.yml'), out, - language: (documents, resolveRef) => - serializeAsyncAPI({ - resolveRef: referenceObject => option.toUndefined(option.fromEither(resolveRef(referenceObject.$ref))), - })(documents), + language: serializeAsyncAPI, decoder: AsyncAPIObjectCodec, }); const test5 = generate({ spec: path.resolve(__dirname, 'specs/sketch/demo.sketch'), out, - language: documents => serializeSketch({ nameStorage: createNameStorage() })(documents), + language: () => serializeSketch({ nameStorage: createNameStorage() }), decoder: FileFormatCodec, });