From 12418971f04ebd56089a433485e6f572eb590a34 Mon Sep 17 00:00:00 2001 From: raveclassic Date: Tue, 29 Oct 2019 20:22:50 +0300 Subject: [PATCH] feat: abstract from data structure in client BREAKING CHANGE: apiClient dependency was renamed to httpClient BREAKING CHANGE: APIClient was renamed to HTTPClient and now extends MonadThrow BREAKING CHANGE: FullAPIRequest/APIRequest were joined and renamed to Request closes #48 --- .../2.0-rx/serializers/operation-object.ts | 45 ++--- .../2.0-rx/serializers/path-item-object.ts | 94 ++++++--- .../2.0-rx/serializers/paths-object.ts | 104 ++++++++-- .../3.0-rx/serializers/operation-object.ts | 46 ++--- .../3.0-rx/serializers/path-item-object.ts | 22 +- .../3.0-rx/serializers/paths-object.ts | 188 ++++++++++++++---- src/language/typescript/common/client.ts | 22 +- .../common/data/serialized-dependency.ts | 15 ++ src/language/typescript/common/utils.ts | 17 ++ src/utils/types.ts | 2 +- 10 files changed, 393 insertions(+), 162 deletions(-) diff --git a/src/language/typescript/2.0-rx/serializers/operation-object.ts b/src/language/typescript/2.0-rx/serializers/operation-object.ts index fa6bd5a..e16b38b 100644 --- a/src/language/typescript/2.0-rx/serializers/operation-object.ts +++ b/src/language/typescript/2.0-rx/serializers/operation-object.ts @@ -17,11 +17,15 @@ import { SerializedParameter, serializedParameter, } from '../../common/data/serialized-parameter'; -import { EMPTY_DEPENDENCIES, serializedDependency } from '../../common/data/serialized-dependency'; +import { + EMPTY_DEPENDENCIES, + getSerializedKindDependency, + serializedDependency, +} from '../../common/data/serialized-dependency'; import { identity } from 'fp-ts/lib/function'; import { concatIf, concatIfL } from '../../../../utils/array'; import { unless, when } from '../../../../utils/string'; -import { Context, getJSDoc, getURL, HTTPMethod } from '../../common/utils'; +import { Context, getJSDoc, getURL, HTTPMethod, getKindValue } from '../../common/utils'; import { Either, isLeft, right } from 'fp-ts/lib/Either'; import { array, either, nonEmptyArray, option } from 'fp-ts'; import { combineEither, sequenceEither } from '@devexperts/utils/dist/adt/either.utils'; @@ -49,6 +53,7 @@ import { serializeSchemaObject } from './schema-object'; import { traverseArrayEither } from '../../../../utils/either'; import { PathItemObject } from '../../../../schema/2.0/path-item-object'; import { Eq, eqString, getStructEq } from 'fp-ts/lib/Eq'; +import { Kind } from '../../../../utils/types'; interface Parameters { readonly pathParameters: PathParameterObject[]; @@ -221,6 +226,7 @@ export const serializeOperationObject = combineReader( from: Ref, url: string, method: HTTPMethod, + kind: Kind, operation: OperationObject, pathItem: PathItemObject, ): Either => { @@ -283,7 +289,7 @@ export const serializeOperationObject = combineReader( const type = ` ${getJSDoc(array.compact([deprecated, operation.summary, paramsSummary]))} - readonly ${operationName}: (${argsType}) => LiveData; + readonly ${operationName}: (${argsType}) => ${getKindValue(kind, serializedResponses.type)}; `; const serializedUrl = getURL(url, parameters.serializedPathParameters); @@ -292,41 +298,30 @@ export const serializeOperationObject = combineReader( ${operationName}: (${argsName}) => { ${when(hasParameters, `const encoded = partial({ ${serializedParameters.io} }).encode(parameters);`)} - return e.apiClient - .request({ + return e.httpClient.chain( + e.httpClient.request({ url: ${serializedUrl}, method: '${method}', ${when(hasQueryParameters, 'query: encoded.query,')} ${when(hasBodyParameters, 'body: encoded.body,')} - }) - .pipe( - map(data => - pipe( - data, - chain(value => - fromEither( - pipe( - ${serializedResponses.io}.decode(value), - mapLeft(ResponseValidationError.create), - ), - ), - ), - ), + }), + value => + pipe( + ${serializedResponses.io}.decode(value), + either.mapLeft(ResponseValidationError.create), + either.fold(error => e.httpClient.throwError(error), decoded => e.httpClient.of(decoded)), ), - ); + ); }, `; const dependencies = concatIfL( hasParameters, [ - serializedDependency('map', 'rxjs/operators'), - serializedDependency('fromEither', '@devexperts/remote-data-ts'), - serializedDependency('chain', '@devexperts/remote-data-ts'), serializedDependency('ResponseValidationError', getRelativePath(from, clientRef)), - serializedDependency('LiveData', '@devexperts/rx-utils/dist/rd/live-data.utils'), serializedDependency('pipe', 'fp-ts/lib/pipeable'), - serializedDependency('mapLeft', 'fp-ts/lib/Either'), + serializedDependency('either', 'fp-ts'), + getSerializedKindDependency(kind), ...flatten(parameters.serializedPathParameters.map(parameter => parameter.dependencies)), ...serializedResponses.dependencies, ...serializedParameters.dependencies, diff --git a/src/language/typescript/2.0-rx/serializers/path-item-object.ts b/src/language/typescript/2.0-rx/serializers/path-item-object.ts index 0c85815..4518661 100644 --- a/src/language/typescript/2.0-rx/serializers/path-item-object.ts +++ b/src/language/typescript/2.0-rx/serializers/path-item-object.ts @@ -4,7 +4,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { isSome, map, Option } from 'fp-ts/lib/Option'; import { serializeOperationObject } from './operation-object'; import { array, flatten } from 'fp-ts/lib/Array'; -import { Dictionary } from '../../../../utils/types'; +import { Dictionary, Kind } from '../../../../utils/types'; import { file, File } from '../../../../utils/fs'; import { serializedDependency, serializeDependencies } from '../../common/data/serialized-dependency'; import { decapitalize } from '@devexperts/utils/dist/string'; @@ -21,43 +21,43 @@ 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, item: PathItemObject): Either => { + 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, resolved)), + either.chain(resolved => run(from, url, kind, resolved)), ); } else { const get = pipe( item.get, - map(operation => serializeOperationObject(from, url, 'GET', operation, item)), + map(operation => serializeOperationObject(from, url, 'GET', kind, operation, item)), ); const put = pipe( item.put, - map(operation => serializeOperationObject(from, url, 'PUT', operation, item)), + map(operation => serializeOperationObject(from, url, 'PUT', kind, operation, item)), ); const post = pipe( item.post, - map(operation => serializeOperationObject(from, url, 'POST', operation, item)), + map(operation => serializeOperationObject(from, url, 'POST', kind, operation, item)), ); const remove = pipe( item.delete, - map(operation => serializeOperationObject(from, url, 'DELETE', operation, item)), + map(operation => serializeOperationObject(from, url, 'DELETE', kind, operation, item)), ); const options = pipe( item.options, - map(operation => serializeOperationObject(from, url, 'OPTIONS', operation, item)), + map(operation => serializeOperationObject(from, url, 'OPTIONS', kind, operation, item)), ); const head = pipe( item.head, - map(operation => serializeOperationObject(from, url, 'HEAD', operation, item)), + map(operation => serializeOperationObject(from, url, 'HEAD', kind, operation, item)), ); const patch = pipe( item.patch, - map(operation => serializeOperationObject(from, url, 'PATCH', operation, item)), + map(operation => serializeOperationObject(from, url, 'PATCH', kind, operation, item)), ); const operations = [get, put, post, remove, options, head, patch]; return pipe( @@ -74,34 +74,72 @@ const serializePath = combineReader(ask(), serializeOperationObject, (e export const serializePathGroup = combineReader( serializePath, serializePath => (from: Ref, name: string, group: Dictionary): Either => { - const serialized = pipe( + const serializedHKT = pipe( group, - record.collect((url, item) => serializePath(from, url, item)), + record.collect((url, item) => serializePath(from, url, 'HKT', item)), sequenceEither, either.map(foldSerializedTypes), ); - return combineEither(serialized, clientRef, (serialized, clientRef) => { - const dependencies = serializeDependencies([ - ...serialized.dependencies, - serializedDependency('asks', 'fp-ts/lib/Reader'), - serializedDependency('APIClient', getRelativePath(from, clientRef)), - ]); - return file( - `${from.name}.ts`, - ` + const serializedKind = pipe( + group, + record.collect((url, item) => serializePath(from, url, '*', item)), + sequenceEither, + either.map(foldSerializedTypes), + ); + + const serializedKind2 = pipe( + group, + record.collect((url, item) => serializePath(from, url, '* -> *', item)), + sequenceEither, + either.map(foldSerializedTypes), + ); + + return combineEither( + serializedHKT, + serializedKind, + serializedKind2, + clientRef, + (serializedHKT, serializedKind, serializedKind2, clientRef) => { + const dependencies = serializeDependencies([ + ...serializedHKT.dependencies, + ...serializedKind.dependencies, + ...serializedKind2.dependencies, + serializedDependency('HTTPClient', getRelativePath(from, clientRef)), + serializedDependency('HTTPClient1', getRelativePath(from, clientRef)), + serializedDependency('HTTPClient2', getRelativePath(from, clientRef)), + serializedDependency('URIS', 'fp-ts/lib/HKT'), + serializedDependency('URIS2', 'fp-ts/lib/HKT'), + ]); + return file( + `${from.name}.ts`, + ` ${dependencies} - export interface ${from.name} { - ${serialized.type} + export interface ${from.name} { + ${serializedHKT.type} + } + + export interface ${from.name}1 { + ${serializedKind.type} + } + + export interface ${from.name}2 { + ${serializedKind2.type} } - export const ${decapitalize(from.name)} = asks((e: { apiClient: APIClient }): ${from.name} => ({ - ${serialized.io} - })); + 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}; + export function ${decapitalize(from.name)}(e: { httpClient: HTTPClient }): ${from.name}; { + return { + ${serializedHKT.io} + } + } `, - ); - }); + ); + }, + ); }, ); diff --git a/src/language/typescript/2.0-rx/serializers/paths-object.ts b/src/language/typescript/2.0-rx/serializers/paths-object.ts index 1c7092e..8d7a407 100644 --- a/src/language/typescript/2.0-rx/serializers/paths-object.ts +++ b/src/language/typescript/2.0-rx/serializers/paths-object.ts @@ -1,34 +1,99 @@ import { PathsObject } from '../../../../schema/2.0/paths-object'; -import { directory, Directory } from '../../../../utils/fs'; +import { directory, Directory, File, file } from '../../../../utils/fs'; import { serializePathGroup, serializePathItemObjectTags } from './path-item-object'; -import { CONTROLLERS_DIRECTORY } from '../../common/utils'; +import { CONTROLLERS_DIRECTORY, getControllerName } from '../../common/utils'; import { pipe } from 'fp-ts/lib/pipeable'; -import { either, option, record } from 'fp-ts'; -import { sequenceEither } from '@devexperts/utils/dist/adt/either.utils'; -import { Either } from 'fp-ts/lib/Either'; -import { addPathParts, Ref } from '../../../../utils/ref'; +import { array, either, option, record } from 'fp-ts'; +import { combineEither, sequenceEither } from '@devexperts/utils/dist/adt/either.utils'; +import { Either, isLeft, right } from 'fp-ts/lib/Either'; +import { addPathParts, getRelativePath, Ref } from '../../../../utils/ref'; import { Dictionary } from '../../../../utils/types'; import { PathItemObject } from '../../../../schema/2.0/path-item-object'; -import { camelize } from '@devexperts/utils/dist/string'; +import { camelize, decapitalize } from '@devexperts/utils/dist/string'; import { combineReader } from '@devexperts/utils/dist/adt/reader.utils'; +import { serializedDependency, serializeDependencies } from '../../common/data/serialized-dependency'; +import { clientRef } from '../../common/client'; export const serializePaths = combineReader( serializePathGroup, - serializePathGroup => (from: Ref, paths: PathsObject): Either => - pipe( - groupPathsByTag(paths), - record.collect((name, group) => { - return pipe( + serializePathGroup => (from: Ref, paths: PathsObject): Either => { + const groupped = groupPathsByTag(paths); + const files = pipe( + groupped, + record.collect((name, group) => + pipe( from, - addPathParts(`${name}Controller`), + addPathParts(name), either.chain(from => serializePathGroup(from, name, group)), - ); - }), + ), + ), sequenceEither, - either.map(serialized => directory(CONTROLLERS_DIRECTORY, serialized)), - ), + ); + const index = pipe( + from, + addPathParts(CONTROLLERS_DIRECTORY), + either.chain(from => serializePathsIndex(from, record.keys(groupped))), + ); + return combineEither(files, index, (files, index) => directory(CONTROLLERS_DIRECTORY, [...files, index])); + }, ); +const serializePathsIndex = (from: Ref, pathNames: string[]): Either => { + if (isLeft(clientRef)) { + return clientRef; + } + const pathToClient = getRelativePath(from, clientRef.right); + const dependencies = serializeDependencies([ + ...array.flatten( + pathNames.map(name => { + const p = `./${name}`; + return [ + serializedDependency(decapitalize(name), p), + serializedDependency(name, p), + serializedDependency(`${name}1`, p), + serializedDependency(`${name}2`, p), + ]; + }), + ), + serializedDependency('URIS', 'fp-ts/lib/HKT'), + serializedDependency('URIS2', 'fp-ts/lib/HKT'), + serializedDependency('URIS2', 'fp-ts/lib/HKT'), + serializedDependency('HTTPClient', pathToClient), + serializedDependency('HTTPClient1', pathToClient), + serializedDependency('HTTPClient2', pathToClient), + ]); + const content = ` + export interface Controllers { + ${pathNames.map(name => `${decapitalize(name)}: ${name};`).join('\n')} + } + export interface Controllers1 { + ${pathNames.map(name => `${decapitalize(name)}: ${name}1;`).join('\n')} + } + export interface Controllers2 { + ${pathNames.map(name => `${decapitalize(name)}: ${name}2;`).join('\n')} + } + + export function controllers(e: { httpClient: HTTPClient2 }): Controllers2 + export function controllers(e: { httpClient: HTTPClient1 }): Controllers1 + export function controllers(e: { httpClient: HTTPClient }): Controllers; + export function controllers(e: { httpClient: HTTPClient }): Controllers { + return { + ${pathNames.map(name => `${decapitalize(name)}: ${decapitalize(name)}(e),`).join('\n')} + } + } + `; + return right( + file( + `${from.name}.ts`, + ` + ${dependencies} + + ${content} + `, + ), + ); +}; + const groupPathsByTag = (pathsObject: PathsObject): Dictionary> => { const keys = Object.keys(pathsObject); const result: Record = {}; @@ -36,8 +101,8 @@ const groupPathsByTag = (pathsObject: PathsObject): Dictionary camelize(p, false)), - option.getOrElse(() => 'Unknown'), + option.map(p => getControllerName(camelize(p, false))), + option.getOrElse(() => getControllerName('Unknown')), ); result[tag] = { ...(result[tag] || {}), @@ -46,3 +111,4 @@ const groupPathsByTag = (pathsObject: PathsObject): Dictionary pipe( @@ -213,7 +214,7 @@ const getParameters = combineReader( export const serializeOperationObject = combineReader( getParameters, - getParameters => (pattern: string, method: HTTPMethod, from: Ref) => ( + getParameters => (pattern: string, method: HTTPMethod, from: Ref, kind: Kind) => ( operation: OperationObject, ): Either => { const parameters = getParameters(from)(operation); @@ -264,49 +265,38 @@ export const serializeOperationObject = combineReader( ).join(','); const type = ` - ${getJSDoc(array.compact([deprecated, operation.summary]))} - readonly ${operationName}: (${argsType}) => LiveData; - `; + ${getJSDoc(array.compact([deprecated, operation.summary]))} + readonly ${operationName}: (${argsType}) => ${getKindValue(kind, serializedResponses.type)}; + `; const io = ` ${operationName}: (${argsName}) => { ${when(hasParameters, `const encoded = partial({ ${serializedParameters.io} }).encode(parameters);`)} - return e.apiClient - .request({ + return e.httpClient.chain( + e.httpClient.request({ url: ${serializedUrl}, method: '${method}', ${when(hasQueryParameters, 'query: encoded.query,')} ${when(hasBodyParameter, 'body: encoded.body,')} - }) - .pipe( - map(data => - pipe( - data, - chain(value => - fromEither( - pipe( - ${serializedResponses.io}.decode(value), - mapLeft(ResponseValidationError.create), - ), - ), - ), - ), + }), + value => + pipe( + ${serializedResponses.io}.decode(value), + either.mapLeft(ResponseValidationError.create), + either.fold(error => e.httpClient.throwError(error), decoded => e.httpClient.of(decoded)), ), - ); + ); }, `; const dependencies = concatIfL( hasParameters, [ - serializedDependency('map', 'rxjs/operators'), - serializedDependency('fromEither', '@devexperts/remote-data-ts'), - serializedDependency('chain', '@devexperts/remote-data-ts'), serializedDependency('ResponseValidationError', getRelativePath(from, clientRef)), - serializedDependency('LiveData', '@devexperts/rx-utils/dist/rd/live-data.utils'), serializedDependency('pipe', 'fp-ts/lib/pipeable'), - serializedDependency('mapLeft', 'fp-ts/lib/Either'), + serializedDependency('either', 'fp-ts'), + getSerializedKindDependency(kind), ...pipe( serializedPathParameters, array.map(p => p.dependencies), diff --git a/src/language/typescript/3.0-rx/serializers/path-item-object.ts b/src/language/typescript/3.0-rx/serializers/path-item-object.ts index 2473f51..c18b3f0 100644 --- a/src/language/typescript/3.0-rx/serializers/path-item-object.ts +++ b/src/language/typescript/3.0-rx/serializers/path-item-object.ts @@ -10,37 +10,43 @@ import { array, either, nonEmptyArray, option } from 'fp-ts'; import { Ref } from '../../../../utils/ref'; import { PathItemObject } from '../../../../schema/3.0/path-item-object'; import { Option } from 'fp-ts/lib/Option'; +import { Kind } from '../../../../utils/types'; export const serializePathItemObject = combineReader( serializeOperationObject, - serializeOperationObject => (pattern: string, item: PathItemObject, from: Ref): Either => { + serializeOperationObject => ( + pattern: string, + item: PathItemObject, + from: Ref, + kind: Kind, + ): Either => { const get = pipe( item.get, - option.map(serializeOperationObject(pattern, 'GET', from)), + option.map(serializeOperationObject(pattern, 'GET', from, kind)), ); const post = pipe( item.post, - option.map(serializeOperationObject(pattern, 'POST', from)), + option.map(serializeOperationObject(pattern, 'POST', from, kind)), ); const put = pipe( item.put, - option.map(serializeOperationObject(pattern, 'PUT', from)), + option.map(serializeOperationObject(pattern, 'PUT', from, kind)), ); const remove = pipe( item.delete, - option.map(serializeOperationObject(pattern, 'DELETE', from)), + option.map(serializeOperationObject(pattern, 'DELETE', from, kind)), ); const patch = pipe( item.patch, - option.map(serializeOperationObject(pattern, 'PATCH', from)), + option.map(serializeOperationObject(pattern, 'PATCH', from, kind)), ); const head = pipe( item.head, - option.map(serializeOperationObject(pattern, 'HEAD', from)), + option.map(serializeOperationObject(pattern, 'HEAD', from, kind)), ); const options = pipe( item.options, - option.map(serializeOperationObject(pattern, 'OPTIONS', from)), + option.map(serializeOperationObject(pattern, 'OPTIONS', from, kind)), ); return pipe( array.compact([get, post, put, remove, patch, head, options]), diff --git a/src/language/typescript/3.0-rx/serializers/paths-object.ts b/src/language/typescript/3.0-rx/serializers/paths-object.ts index c8f7c49..2823b37 100644 --- a/src/language/typescript/3.0-rx/serializers/paths-object.ts +++ b/src/language/typescript/3.0-rx/serializers/paths-object.ts @@ -6,80 +6,180 @@ import { Dictionary, serializeDictionary } from '../../../../utils/types'; import { foldSerializedTypes } from '../../common/data/serialized-type'; import { serializedDependency, serializeDependencies } from '../../common/data/serialized-dependency'; import { decapitalize, camelize } from '@devexperts/utils/dist/string'; -import { Either } from 'fp-ts/lib/Either'; +import { Either, isLeft, right } from 'fp-ts/lib/Either'; import { sequenceEither } from '@devexperts/utils/dist/adt/either.utils'; import { combineReader } from '@devexperts/utils/dist/adt/reader.utils'; -import { either, option } from 'fp-ts'; +import { array, either, option, record } from 'fp-ts'; import { addPathParts, getRelativePath, Ref } from '../../../../utils/ref'; import { combineEither } from '@devexperts/utils/dist/adt/either.utils'; import { applyTo } from '../../../../utils/function'; import { PathsObject } from '../../../../schema/3.0/paths-object'; import { clientRef } from '../../common/client'; - -const groupPathsByTag = (pathsObject: PathsObject): Dictionary => { - const keys = Object.keys(pathsObject); - const result: Record = {}; - for (const key of keys) { - const path = pathsObject[key]; - const tag = pipe( - serializePathItemObjectTags(path), - option.map(p => camelize(p, false)), - option.getOrElse(() => 'Unknown'), - ); - result[tag] = { - ...(result[tag] || {}), - [key]: path, - }; - } - return result; -}; +import { getControllerName } from '../../common/utils'; const serializeGrouppedPaths = combineReader( serializePathItemObject, serializePathItemObject => (from: Ref) => (groupped: PathsObject): Either => { - const serialized = pipe( - serializeDictionary(groupped, (pattern, item) => serializePathItemObject(pattern, item, from)), + const serializedHKT = pipe( + serializeDictionary(groupped, (pattern, item) => serializePathItemObject(pattern, item, from, 'HKT')), sequenceEither, either.map(foldSerializedTypes), ); - return combineEither(serialized, clientRef, (serialized, clientRef) => { - const dependencies = serializeDependencies([ - ...serialized.dependencies, - serializedDependency('Reader', 'fp-ts/lib/Reader'), - serializedDependency('APIClient', getRelativePath(from, clientRef)), - ]); - return file( - `${from.name}.ts`, - ` + const serializedKind = pipe( + serializeDictionary(groupped, (pattern, item) => serializePathItemObject(pattern, item, from, '*')), + sequenceEither, + either.map(foldSerializedTypes), + ); + const serializedKind2 = pipe( + serializeDictionary(groupped, (pattern, item) => serializePathItemObject(pattern, item, from, '* -> *')), + sequenceEither, + either.map(foldSerializedTypes), + ); + + return combineEither( + serializedHKT, + serializedKind, + serializedKind2, + clientRef, + (serializedHKT, serializedKind, serializedKind2, clientRef) => { + const dependencies = serializeDependencies([ + ...serializedHKT.dependencies, + ...serializedKind.dependencies, + ...serializedKind2.dependencies, + serializedDependency('Reader', 'fp-ts/lib/Reader'), + serializedDependency('HTTPClient', getRelativePath(from, clientRef)), + serializedDependency('HTTPClient1', getRelativePath(from, clientRef)), + serializedDependency('HTTPClient2', getRelativePath(from, clientRef)), + serializedDependency('URIS', 'fp-ts/lib/HKT'), + serializedDependency('URIS2', 'fp-ts/lib/HKT'), + ]); + return file( + `${from.name}.ts`, + ` ${dependencies} - export interface ${from.name} { - ${serialized.type} + export interface ${from.name} { + ${serializedHKT.type} + } + + export interface ${from.name}1 { + ${serializedKind.type} + } + + export interface ${from.name}2 { + ${serializedKind2.type} } - export const ${decapitalize(from.name)}: Reader<{ apiClient: APIClient }, ${from.name}> = e => ({ - ${serialized.io} - }) + 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}; + export function ${decapitalize(from.name)}(e: { httpClient: HTTPClient }): ${from.name} { + return { + ${serializedHKT.io} + } + } `, - ); - }); + ); + }, + ); }, ); export const serializePathsObject = combineReader( serializeGrouppedPaths, - serializeGrouppedPaths => (from: Ref) => (pathsObject: PathsObject): Either => - pipe( - groupPathsByTag(pathsObject), + serializeGrouppedPaths => (from: Ref) => (pathsObject: PathsObject): Either => { + const groupped = groupPathsByTag(pathsObject); + const files = pipe( + groupped, collect((name, groupped) => pipe( from, - addPathParts(`${name}Controller`), + addPathParts(name), either.map(serializeGrouppedPaths), either.chain(applyTo(groupped)), ), ), sequenceEither, - either.map(children => directory('paths', children)), - ), + ); + const index = pipe( + from, + addPathParts('paths'), + either.chain(from => serializePathsIndex(from, record.keys(groupped))), + ); + return combineEither(files, index, (files, index) => directory('paths', [...files, index])); + }, ); + +const groupPathsByTag = (pathsObject: PathsObject): Dictionary => { + const keys = Object.keys(pathsObject); + const result: Record = {}; + for (const key of keys) { + const path = pathsObject[key]; + const tag = pipe( + serializePathItemObjectTags(path), + option.map(p => getControllerName(camelize(p, false))), + option.getOrElse(() => getControllerName('Unknown')), + ); + result[tag] = { + ...(result[tag] || {}), + [key]: path, + }; + } + return result; +}; + +const serializePathsIndex = (from: Ref, pathNames: string[]): Either => { + if (isLeft(clientRef)) { + return clientRef; + } + const pathToClient = getRelativePath(from, clientRef.right); + const dependencies = serializeDependencies([ + ...array.flatten( + pathNames.map(name => { + const p = `./${name}`; + return [ + serializedDependency(decapitalize(name), p), + serializedDependency(name, p), + serializedDependency(`${name}1`, p), + serializedDependency(`${name}2`, p), + ]; + }), + ), + serializedDependency('URIS', 'fp-ts/lib/HKT'), + serializedDependency('URIS2', 'fp-ts/lib/HKT'), + serializedDependency('URIS2', 'fp-ts/lib/HKT'), + serializedDependency('HTTPClient', pathToClient), + serializedDependency('HTTPClient1', pathToClient), + serializedDependency('HTTPClient2', pathToClient), + ]); + const content = ` + export interface Controllers { + ${pathNames.map(name => `${decapitalize(name)}: ${name};`).join('\n')} + } + export interface Controllers1 { + ${pathNames.map(name => `${decapitalize(name)}: ${name}1;`).join('\n')} + } + export interface Controllers2 { + ${pathNames.map(name => `${decapitalize(name)}: ${name}2;`).join('\n')} + } + + export function controllers(e: { httpClient: HTTPClient2 }): Controllers2 + export function controllers(e: { httpClient: HTTPClient1 }): Controllers1 + export function controllers(e: { httpClient: HTTPClient }): Controllers; + export function controllers(e: { httpClient: HTTPClient }): Controllers { + return { + ${pathNames.map(name => `${decapitalize(name)}: ${decapitalize(name)}(e),`).join('\n')} + } + } + `; + return right( + file( + `${from.name}.ts`, + ` + ${dependencies} + + ${content} + `, + ), + ); +}; diff --git a/src/language/typescript/common/client.ts b/src/language/typescript/common/client.ts index 1045b08..305f631 100644 --- a/src/language/typescript/common/client.ts +++ b/src/language/typescript/common/client.ts @@ -6,23 +6,27 @@ import { pipe } from 'fp-ts/lib/pipeable'; export const clientRef = fromString('#/client/client'); const client = ` - import { left } from 'fp-ts/lib/Either'; + import { HKT, Kind, Kind2, URIS, URIS2 } from 'fp-ts/lib/HKT'; + import { MonadThrow, MonadThrow1, MonadThrow2 } from 'fp-ts/lib/MonadThrow'; import { Errors } from 'io-ts'; import { PathReporter } from 'io-ts/lib/PathReporter'; - import { LiveData } from '@devexperts/rx-utils/dist/rd/live-data.utils'; + import { left } from 'fp-ts/lib/Either'; - export interface APIRequest { + export interface Request { + readonly method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'; readonly url: string; readonly query?: object; readonly body?: unknown; } - export interface FullAPIRequest extends APIRequest { - readonly method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'; + export interface HTTPClient extends MonadThrow { + readonly request: (request: Request) => HKT; } - - export interface APIClient { - readonly request: (request: FullAPIRequest) => LiveData + export interface HTTPClient1 extends MonadThrow1 { + readonly request: (request: Request) => Kind; + } + export interface HTTPClient2 extends MonadThrow2 { + readonly request: (request: Request) => Kind2; } export class ResponseValidationError extends Error { @@ -31,7 +35,7 @@ const client = ` } constructor(readonly errors: Errors) { - super(PathReporter.report(left(errors)).join('\\n\\n')); + super(PathReporter.report(left(errors)).join('\\n\\n')); this.name = 'ResponseValidationError'; Object.setPrototypeOf(this, ResponseValidationError); } diff --git a/src/language/typescript/common/data/serialized-dependency.ts b/src/language/typescript/common/data/serialized-dependency.ts index abf89bc..2028c0a 100644 --- a/src/language/typescript/common/data/serialized-dependency.ts +++ b/src/language/typescript/common/data/serialized-dependency.ts @@ -3,6 +3,7 @@ import { groupBy, head } from 'fp-ts/lib/NonEmptyArray'; import { collect } from 'fp-ts/lib/Record'; import { uniqString } from '../../../../utils/array'; import { getMonoid as getArrayMonoid } from 'fp-ts/lib/Array'; +import { Kind } from '../../../../utils/types'; export const EMPTY_DEPENDENCIES: SerializedDependency[] = []; @@ -32,3 +33,17 @@ export const monoidDependencies = getArrayMonoid(); const dependencyOption = serializedDependency('Option', 'fp-ts/lib/Option'); const dependencyOptionFromNullable = serializedDependency('optionFromNullable', 'io-ts-types/lib/optionFromNullable'); export const OPTION_DEPENDENCIES: SerializedDependency[] = [dependencyOption, dependencyOptionFromNullable]; + +export const getSerializedKindDependency = (kind: Kind): SerializedDependency => { + switch (kind) { + case 'HKT': { + return serializedDependency('HKT', 'fp-ts/lib/HKT'); + } + case '*': { + return serializedDependency('Kind', 'fp-ts/lib/HKT'); + } + case '* -> *': { + return serializedDependency('Kind2', 'fp-ts/lib/HKT'); + } + } +}; diff --git a/src/language/typescript/common/utils.ts b/src/language/typescript/common/utils.ts index 9b611f4..83fcc90 100644 --- a/src/language/typescript/common/utils.ts +++ b/src/language/typescript/common/utils.ts @@ -3,6 +3,7 @@ import { unless } from '../../../utils/string'; import { Options } from 'prettier'; import { fromString } from '../../../utils/ref'; import { ReferenceObject } from '../../../schema/3.0/reference-object'; +import { Kind } from '../../../utils/types'; export const SUCCESSFUL_CODES = ['200', '201', 'default']; export const CONTROLLERS_DIRECTORY = 'controllers'; @@ -45,3 +46,19 @@ export const pathsRef = fromString('#/paths'); export interface Context { readonly resolveRef: (referenceObject: ReferenceObject) => unknown; } + +export const getKindValue = (kind: Kind, value: string): string => { + switch (kind) { + case 'HKT': { + return `HKT`; + } + case '*': { + return `Kind`; + } + case '* -> *': { + return `Kind2`; + } + } +}; + +export const getControllerName = (name: string): string => `${name}Controller`; diff --git a/src/utils/types.ts b/src/utils/types.ts index 9b02296..169cc58 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -5,4 +5,4 @@ export const serializeDictionary = ( serializeValue: (name: string, value: A) => B, ): B[] => Object.keys(dictionary).map(name => serializeValue(name, dictionary[name])); -export type Kind = 'HKT' | 'Kind' | 'Kind2'; +export type Kind = 'HKT' | '*' | '* -> *';