Skip to content

[Bugfix] Response type does not de-reference $refs #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@
"version": "0.2.0",
"configurations": [
{
"name": "Launch via npm",
"name": "Debug generate",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "generate:debug"],
"port": 9229
},
{
"name": "Debug CLI",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "cli:debug"],
"port": 9229
}
]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "Create typescript api module from swagger schema",
"scripts": {
"cli": "node index.js -p ./tests/schemas/v3.0/personal-api-example.json -n swagger-test-cli.ts",
"cli:debug": "node --nolazy --inspect-brk=9229 index.js -p ./tests/schemas/v2.0/adafruit.yaml -n swagger-test-cli.ts",
"cli:help": "node index.js -h",
"test:all": "npm-run-all generate validate test:routeTypes test:noClient --continue-on-error",
"generate": "node tests/generate.js",
Expand Down
60 changes: 60 additions & 0 deletions src/components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const _ = require("lodash");
const { parseSchema } = require("./schema");
const { addToConfig } = require("./config");

/**
*
* @typedef TypeInfo
* {
* typeName: "Foo",
* componentName: "schemas",
* rawTypeData: {...},
* typeData: {...} (result parseSchema())
* }
*/

/**
* @returns {{ "#/components/schemas/Foo": TypeInfo, ... }}
*/
const createComponentsMap = components => {
const componentsMap = _.reduce(components, (map, component, componentName) => {
_.each(component, (rawTypeData, typeName) => {
// only map data for now
map[`#/components/${componentName}/${typeName}`] = {
typeName,
rawTypeData,
componentName,
typeData: null,
}
})
return map;
}, {})

addToConfig({ componentsMap })

return componentsMap;
}



/**
* @returns {TypeInfo[]}
*/
const filterComponentsMap = (componentsMap, componentName) =>
_.filter(componentsMap, (v, ref) => _.startsWith(ref, `#/components/${componentName}`))


/** @returns {{ type, typeIdentifier, name, description, content }} */
const getTypeData = typeInfo => {
if (!typeInfo.typeData) {
typeInfo.typeData = parseSchema(typeInfo.rawTypeData, typeInfo.typeName)
}

return typeInfo.typeData;
}

module.exports = {
getTypeData,
createComponentsMap,
filterComponentsMap,
}
17 changes: 17 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

const config = {
/** CLI flag */
generateRouteTypes: false,
/** CLI flag */
generateClient: true,
/** parsed swagger schema from getSwaggerObject() */
swaggerSchema: null,
/** { "#/components/schemas/Foo": @TypeInfo, ... } */
componentsMap: {},
}

/** needs to use data everywhere in project */
module.exports = {
addToConfig: configParts => Object.assign(config, configParts),
config,
}
22 changes: 22 additions & 0 deletions src/files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const _ = require("lodash");
const fs = require("fs");
const { resolve } = require("path");

const getFileContent = path =>
fs.readFileSync(path, { encoding: 'UTF-8' })

const pathIsExist = path =>
path && fs.existsSync(path)

const createFile = (pathTo, fileName, content) =>
fs.writeFileSync(resolve(__dirname, pathTo, `./${fileName}`), content, _.noop)

const getTemplate = templateName =>
getFileContent(resolve(__dirname, `./templates/${templateName}.mustache`))

module.exports = {
getTemplate,
createFile,
pathIsExist,
getFileContent,
}
38 changes: 24 additions & 14 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
const mustache = require("mustache");
const _ = require("lodash");
const fs = require("fs");
const path = require("path");
const { parseSchema } = require('./schema');
const { parseSchemas } = require('./schema');
const { parseRoutes, groupRoutes } = require('./routes');
const { createApiConfig } = require('./apiConfig');
const { getModelType } = require('./modelTypes');
const { getSwaggerObject } = require('./swagger');
const { createComponentsMap, filterComponentsMap } = require("./components");
const { getTemplate, createFile, pathIsExist } = require('./files');
const { addToConfig, config: defaults } = require("./config");

mustache.escape = value => value

Expand All @@ -16,25 +17,34 @@ module.exports = {
output,
url,
name,
generateRouteTypes = true,
generateClient = true,
generateRouteTypes = defaults.generateRouteTypes,
generateClient = defaults.generateClient,
}) => new Promise((resolve, reject) => {
getSwaggerObject(input, url).then(({ info, paths, servers, components }) => {
addToConfig({
generateRouteTypes,
generateClient,
})
getSwaggerObject(input, url).then(swaggerSchema => {
addToConfig({ swaggerSchema });
const { info, paths, servers, components } = swaggerSchema;
console.log('☄️ start generating your typescript api')

const apiTemplate = fs.readFileSync(path.resolve(__dirname, './templates/api.mustache'), 'utf-8');
const clientTemplate = fs.readFileSync(path.resolve(__dirname, './templates/client.mustache'), 'utf-8');
const routeTypesTemplate = fs.readFileSync(path.resolve(__dirname, './templates/route-types.mustache'), 'utf-8');
const apiTemplate = getTemplate('api');
const clientTemplate = getTemplate('client');
const routeTypesTemplate = getTemplate('route-types');

const componentsMap = createComponentsMap(components);
const schemasMap = filterComponentsMap(componentsMap, "schemas")

const parsedSchemas = _.map(_.get(components, "schemas"), parseSchema)
const routes = parseRoutes(paths, parsedSchemas, components);
const parsedSchemas = parseSchemas(components);
const routes = parseRoutes(swaggerSchema, parsedSchemas, componentsMap, components);
const hasSecurityRoutes = routes.some(route => route.security);
const hasQueryRoutes = routes.some(route => route.hasQuery);
const apiConfig = createApiConfig({ info, servers }, hasSecurityRoutes);

const configuration = {
apiConfig,
modelTypes: _.map(parsedSchemas, getModelType),
modelTypes: _.map(schemasMap, getModelType),
hasSecurityRoutes,
hasQueryRoutes,
routes: groupRoutes(routes),
Expand All @@ -46,8 +56,8 @@ module.exports = {
generateClient ? mustache.render(clientTemplate, configuration) : '',
].join('');

if (output && fs.existsSync(output)) {
fs.writeFileSync(path.resolve(__dirname, output, `./${name}`), sourceFile, _.noop)
if (pathIsExist(output)) {
createFile(output, name, sourceFile);
console.log(`✔️ your typescript api file created in "${output}"`)
}

Expand Down
6 changes: 4 additions & 2 deletions src/modelTypes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const _ = require('lodash');
const { formatters } = require("./typeFormatters");
const { checkAndRenameModelName } = require("./modelNames");
const { getTypeData } = require('./components');

const CONTENT_KEYWORD = '__CONTENT__';

Expand All @@ -10,8 +11,9 @@ const contentWrapersByTypeIdentifier = {
'type': `= ${CONTENT_KEYWORD}`,
}

// { typeIdentifier, name, content, type }
const getModelType = ({ typeIdentifier, name: originalName, content, type, description }) => {
const getModelType = typeInfo => {
const { typeIdentifier, name: originalName, content, type, description } = getTypeData(typeInfo);

if (!contentWrapersByTypeIdentifier[typeIdentifier]) {
throw new Error(`${typeIdentifier} - type identifier is unknown for this utility`)
}
Expand Down
54 changes: 38 additions & 16 deletions src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const _ = require("lodash");
const { collect } = require("./utils");
const { parseSchema, getRefType } = require("./schema");
const { checkAndRenameModelName } = require("./modelNames");
const { getTypeData, typeInfoIsIn } = require("./components");
const { inlineExtraFormatters } = require("./typeFormatters");

const methodAliases = {
Expand All @@ -12,10 +13,21 @@ const methodAliases = {
delete: (pathName, hasPathInserts) => _.camelCase(`${pathName}_delete`)
}

const getSchemaFromRequestType = requestType => {
const content = _.get(requestType, "content")

if (!content) return null;

const contentByType = _.find(content, contentByType => contentByType.schema);

return contentByType && contentByType.schema;
}

const getTypeFromRequestInfo = (requestInfo, parsedSchemas, operationId, contentType) => {
// TODO: make more flexible pick schema without content type
const schema = _.get(requestInfo, `content["${contentType}"].schema`);
const refType = getRefType(requestInfo);
const schema = getSchemaFromRequestType(requestInfo);
// const refType = getRefTypeName(requestInfo);
const refTypeInfo = getRefType(requestInfo);

if (schema) {
const extractedSchema = _.get(schema, 'additionalProperties', schema);
Expand All @@ -28,11 +40,24 @@ const getTypeFromRequestInfo = (requestInfo, parsedSchemas, operationId, content
return checkAndRenameModelName(foundSchema ? foundSchema.name : content);
}

if (refType) {
// TODO: its temp solution because sometimes `swagger2openapi` create refs as operationId + name
const refTypeWithoutOpId = refType.replace(operationId, '');
const foundedSchemaByName = _.find(parsedSchemas, ({ name }) => name === refType || name === refTypeWithoutOpId)
return foundedSchemaByName && foundedSchemaByName.name ? checkAndRenameModelName(foundedSchemaByName.name) : 'any'
if (refTypeInfo) {
// const refTypeWithoutOpId = refType.replace(operationId, '');
// const foundedSchemaByName = _.find(parsedSchemas, ({ name }) => name === refType || name === refTypeWithoutOpId)

// TODO:HACK fix problem of swagger2opeanpi
const typeNameWithoutOpId = _.replace(refTypeInfo.typeName, operationId, '')
if (_.find(parsedSchemas, schema => schema.name === typeNameWithoutOpId))
return checkAndRenameModelName(typeNameWithoutOpId);

switch (refTypeInfo.componentName) {
case "schemas":
return checkAndRenameModelName(refTypeInfo.typeName);
case "responses":
case "requestBodies":
return parseSchema(getSchemaFromRequestType(refTypeInfo.rawTypeData), 'none', inlineExtraFormatters).content
default:
return parseSchema(refTypeInfo.rawTypeData, 'none', inlineExtraFormatters).content
}
}

return 'any';
Expand All @@ -55,10 +80,9 @@ const getRouteName = (operationId, method, route, moduleName) => {
return createCustomOperationId(method, route, moduleName);
}

const parseRoutes = (routes, parsedSchemas, components) =>
_.entries(routes)
const parseRoutes = ({ paths }, parsedSchemas) =>
_.entries(paths)
.reduce((routes, [route, requestInfoByMethodsMap]) => {
const globalParametersMap = _.get(components, "parameters", {});
parameters = _.get(requestInfoByMethodsMap, 'parameters');

// TODO: refactor that hell
Expand Down Expand Up @@ -90,16 +114,14 @@ const parseRoutes = (routes, parsedSchemas, components) =>
const pathParams = collect(parameters, parameter => {
if (parameter.in === 'path') return parameter;

const refTypeName = getRefType(parameter);
const globalParam = refTypeName && globalParametersMap[refTypeName]
return globalParam && globalParametersMap[refTypeName].in === "path" && globalParam
const refTypeInfo = getRefType(parameter);
return refTypeInfo && refTypeInfo.rawTypeData.in === "path" && refTypeInfo.rawTypeData
})
const queryParams = collect(parameters, parameter => {
if (parameter.in === 'query') return parameter;

const refTypeName = getRefType(parameter);
const globalParam = refTypeName && globalParametersMap[refTypeName]
return globalParam && globalParametersMap[refTypeName].in === "query" && globalParam;
const refTypeInfo = getRefType(parameter);
return refTypeInfo && refTypeInfo.rawTypeData.in === "query" && refTypeInfo.rawTypeData
})
const moduleName = _.camelCase(route.split('/').filter(Boolean)[0]);

Expand Down
Loading