Skip to content

Commit 0fd0fd4

Browse files
authored
[Bugfix] Response type does not de-reference $refs (#19)
* chore: add common test schemas * refactor: encapsulate nodejs.fs module usage; feat: dereference components responses + improve types
1 parent efb34f5 commit 0fd0fd4

File tree

21 files changed

+520
-2879
lines changed

21 files changed

+520
-2879
lines changed

.vscode/launch.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,22 @@
55
"version": "0.2.0",
66
"configurations": [
77
{
8-
"name": "Launch via npm",
8+
"name": "Debug generate",
99
"type": "node",
1010
"request": "launch",
1111
"cwd": "${workspaceFolder}",
1212
"runtimeExecutable": "npm",
1313
"runtimeArgs": ["run-script", "generate:debug"],
1414
"port": 9229
15+
},
16+
{
17+
"name": "Debug CLI",
18+
"type": "node",
19+
"request": "launch",
20+
"cwd": "${workspaceFolder}",
21+
"runtimeExecutable": "npm",
22+
"runtimeArgs": ["run-script", "cli:debug"],
23+
"port": 9229
1524
}
1625
]
1726
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"description": "Create typescript api module from swagger schema",
55
"scripts": {
66
"cli": "node index.js -p ./tests/schemas/v3.0/personal-api-example.json -n swagger-test-cli.ts",
7+
"cli:debug": "node --nolazy --inspect-brk=9229 index.js -p ./tests/schemas/v2.0/adafruit.yaml -n swagger-test-cli.ts",
78
"cli:help": "node index.js -h",
89
"test:all": "npm-run-all generate validate test:routeTypes test:noClient --continue-on-error",
910
"generate": "node tests/generate.js",

src/components.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const _ = require("lodash");
2+
const { parseSchema } = require("./schema");
3+
const { addToConfig } = require("./config");
4+
5+
/**
6+
*
7+
* @typedef TypeInfo
8+
* {
9+
* typeName: "Foo",
10+
* componentName: "schemas",
11+
* rawTypeData: {...},
12+
* typeData: {...} (result parseSchema())
13+
* }
14+
*/
15+
16+
/**
17+
* @returns {{ "#/components/schemas/Foo": TypeInfo, ... }}
18+
*/
19+
const createComponentsMap = components => {
20+
const componentsMap = _.reduce(components, (map, component, componentName) => {
21+
_.each(component, (rawTypeData, typeName) => {
22+
// only map data for now
23+
map[`#/components/${componentName}/${typeName}`] = {
24+
typeName,
25+
rawTypeData,
26+
componentName,
27+
typeData: null,
28+
}
29+
})
30+
return map;
31+
}, {})
32+
33+
addToConfig({ componentsMap })
34+
35+
return componentsMap;
36+
}
37+
38+
39+
40+
/**
41+
* @returns {TypeInfo[]}
42+
*/
43+
const filterComponentsMap = (componentsMap, componentName) =>
44+
_.filter(componentsMap, (v, ref) => _.startsWith(ref, `#/components/${componentName}`))
45+
46+
47+
/** @returns {{ type, typeIdentifier, name, description, content }} */
48+
const getTypeData = typeInfo => {
49+
if (!typeInfo.typeData) {
50+
typeInfo.typeData = parseSchema(typeInfo.rawTypeData, typeInfo.typeName)
51+
}
52+
53+
return typeInfo.typeData;
54+
}
55+
56+
module.exports = {
57+
getTypeData,
58+
createComponentsMap,
59+
filterComponentsMap,
60+
}

src/config.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
const config = {
3+
/** CLI flag */
4+
generateRouteTypes: false,
5+
/** CLI flag */
6+
generateClient: true,
7+
/** parsed swagger schema from getSwaggerObject() */
8+
swaggerSchema: null,
9+
/** { "#/components/schemas/Foo": @TypeInfo, ... } */
10+
componentsMap: {},
11+
}
12+
13+
/** needs to use data everywhere in project */
14+
module.exports = {
15+
addToConfig: configParts => Object.assign(config, configParts),
16+
config,
17+
}

src/files.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const _ = require("lodash");
2+
const fs = require("fs");
3+
const { resolve } = require("path");
4+
5+
const getFileContent = path =>
6+
fs.readFileSync(path, { encoding: 'UTF-8' })
7+
8+
const pathIsExist = path =>
9+
path && fs.existsSync(path)
10+
11+
const createFile = (pathTo, fileName, content) =>
12+
fs.writeFileSync(resolve(__dirname, pathTo, `./${fileName}`), content, _.noop)
13+
14+
const getTemplate = templateName =>
15+
getFileContent(resolve(__dirname, `./templates/${templateName}.mustache`))
16+
17+
module.exports = {
18+
getTemplate,
19+
createFile,
20+
pathIsExist,
21+
getFileContent,
22+
}

src/index.js

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
const mustache = require("mustache");
22
const _ = require("lodash");
3-
const fs = require("fs");
4-
const path = require("path");
5-
const { parseSchema } = require('./schema');
3+
const { parseSchemas } = require('./schema');
64
const { parseRoutes, groupRoutes } = require('./routes');
75
const { createApiConfig } = require('./apiConfig');
86
const { getModelType } = require('./modelTypes');
97
const { getSwaggerObject } = require('./swagger');
8+
const { createComponentsMap, filterComponentsMap } = require("./components");
9+
const { getTemplate, createFile, pathIsExist } = require('./files');
10+
const { addToConfig, config: defaults } = require("./config");
1011

1112
mustache.escape = value => value
1213

@@ -16,25 +17,34 @@ module.exports = {
1617
output,
1718
url,
1819
name,
19-
generateRouteTypes = true,
20-
generateClient = true,
20+
generateRouteTypes = defaults.generateRouteTypes,
21+
generateClient = defaults.generateClient,
2122
}) => new Promise((resolve, reject) => {
22-
getSwaggerObject(input, url).then(({ info, paths, servers, components }) => {
23+
addToConfig({
24+
generateRouteTypes,
25+
generateClient,
26+
})
27+
getSwaggerObject(input, url).then(swaggerSchema => {
28+
addToConfig({ swaggerSchema });
29+
const { info, paths, servers, components } = swaggerSchema;
2330
console.log('☄️ start generating your typescript api')
2431

25-
const apiTemplate = fs.readFileSync(path.resolve(__dirname, './templates/api.mustache'), 'utf-8');
26-
const clientTemplate = fs.readFileSync(path.resolve(__dirname, './templates/client.mustache'), 'utf-8');
27-
const routeTypesTemplate = fs.readFileSync(path.resolve(__dirname, './templates/route-types.mustache'), 'utf-8');
32+
const apiTemplate = getTemplate('api');
33+
const clientTemplate = getTemplate('client');
34+
const routeTypesTemplate = getTemplate('route-types');
35+
36+
const componentsMap = createComponentsMap(components);
37+
const schemasMap = filterComponentsMap(componentsMap, "schemas")
2838

29-
const parsedSchemas = _.map(_.get(components, "schemas"), parseSchema)
30-
const routes = parseRoutes(paths, parsedSchemas, components);
39+
const parsedSchemas = parseSchemas(components);
40+
const routes = parseRoutes(swaggerSchema, parsedSchemas, componentsMap, components);
3141
const hasSecurityRoutes = routes.some(route => route.security);
3242
const hasQueryRoutes = routes.some(route => route.hasQuery);
3343
const apiConfig = createApiConfig({ info, servers }, hasSecurityRoutes);
3444

3545
const configuration = {
3646
apiConfig,
37-
modelTypes: _.map(parsedSchemas, getModelType),
47+
modelTypes: _.map(schemasMap, getModelType),
3848
hasSecurityRoutes,
3949
hasQueryRoutes,
4050
routes: groupRoutes(routes),
@@ -46,8 +56,8 @@ module.exports = {
4656
generateClient ? mustache.render(clientTemplate, configuration) : '',
4757
].join('');
4858

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

src/modelTypes.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const _ = require('lodash');
22
const { formatters } = require("./typeFormatters");
33
const { checkAndRenameModelName } = require("./modelNames");
4+
const { getTypeData } = require('./components');
45

56
const CONTENT_KEYWORD = '__CONTENT__';
67

@@ -10,8 +11,9 @@ const contentWrapersByTypeIdentifier = {
1011
'type': `= ${CONTENT_KEYWORD}`,
1112
}
1213

13-
// { typeIdentifier, name, content, type }
14-
const getModelType = ({ typeIdentifier, name: originalName, content, type, description }) => {
14+
const getModelType = typeInfo => {
15+
const { typeIdentifier, name: originalName, content, type, description } = getTypeData(typeInfo);
16+
1517
if (!contentWrapersByTypeIdentifier[typeIdentifier]) {
1618
throw new Error(`${typeIdentifier} - type identifier is unknown for this utility`)
1719
}

src/routes.js

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const _ = require("lodash");
22
const { collect } = require("./utils");
33
const { parseSchema, getRefType } = require("./schema");
44
const { checkAndRenameModelName } = require("./modelNames");
5+
const { getTypeData, typeInfoIsIn } = require("./components");
56
const { inlineExtraFormatters } = require("./typeFormatters");
67

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

16+
const getSchemaFromRequestType = requestType => {
17+
const content = _.get(requestType, "content")
18+
19+
if (!content) return null;
20+
21+
const contentByType = _.find(content, contentByType => contentByType.schema);
22+
23+
return contentByType && contentByType.schema;
24+
}
25+
1526
const getTypeFromRequestInfo = (requestInfo, parsedSchemas, operationId, contentType) => {
1627
// TODO: make more flexible pick schema without content type
17-
const schema = _.get(requestInfo, `content["${contentType}"].schema`);
18-
const refType = getRefType(requestInfo);
28+
const schema = getSchemaFromRequestType(requestInfo);
29+
// const refType = getRefTypeName(requestInfo);
30+
const refTypeInfo = getRefType(requestInfo);
1931

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

31-
if (refType) {
32-
// TODO: its temp solution because sometimes `swagger2openapi` create refs as operationId + name
33-
const refTypeWithoutOpId = refType.replace(operationId, '');
34-
const foundedSchemaByName = _.find(parsedSchemas, ({ name }) => name === refType || name === refTypeWithoutOpId)
35-
return foundedSchemaByName && foundedSchemaByName.name ? checkAndRenameModelName(foundedSchemaByName.name) : 'any'
43+
if (refTypeInfo) {
44+
// const refTypeWithoutOpId = refType.replace(operationId, '');
45+
// const foundedSchemaByName = _.find(parsedSchemas, ({ name }) => name === refType || name === refTypeWithoutOpId)
46+
47+
// TODO:HACK fix problem of swagger2opeanpi
48+
const typeNameWithoutOpId = _.replace(refTypeInfo.typeName, operationId, '')
49+
if (_.find(parsedSchemas, schema => schema.name === typeNameWithoutOpId))
50+
return checkAndRenameModelName(typeNameWithoutOpId);
51+
52+
switch (refTypeInfo.componentName) {
53+
case "schemas":
54+
return checkAndRenameModelName(refTypeInfo.typeName);
55+
case "responses":
56+
case "requestBodies":
57+
return parseSchema(getSchemaFromRequestType(refTypeInfo.rawTypeData), 'none', inlineExtraFormatters).content
58+
default:
59+
return parseSchema(refTypeInfo.rawTypeData, 'none', inlineExtraFormatters).content
60+
}
3661
}
3762

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

58-
const parseRoutes = (routes, parsedSchemas, components) =>
59-
_.entries(routes)
83+
const parseRoutes = ({ paths }, parsedSchemas) =>
84+
_.entries(paths)
6085
.reduce((routes, [route, requestInfoByMethodsMap]) => {
61-
const globalParametersMap = _.get(components, "parameters", {});
6286
parameters = _.get(requestInfoByMethodsMap, 'parameters');
6387

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

93-
const refTypeName = getRefType(parameter);
94-
const globalParam = refTypeName && globalParametersMap[refTypeName]
95-
return globalParam && globalParametersMap[refTypeName].in === "path" && globalParam
117+
const refTypeInfo = getRefType(parameter);
118+
return refTypeInfo && refTypeInfo.rawTypeData.in === "path" && refTypeInfo.rawTypeData
96119
})
97120
const queryParams = collect(parameters, parameter => {
98121
if (parameter.in === 'query') return parameter;
99122

100-
const refTypeName = getRefType(parameter);
101-
const globalParam = refTypeName && globalParametersMap[refTypeName]
102-
return globalParam && globalParametersMap[refTypeName].in === "query" && globalParam;
123+
const refTypeInfo = getRefType(parameter);
124+
return refTypeInfo && refTypeInfo.rawTypeData.in === "query" && refTypeInfo.rawTypeData
103125
})
104126
const moduleName = _.camelCase(route.split('/').filter(Boolean)[0]);
105127

0 commit comments

Comments
 (0)