Skip to content

Commit

Permalink
refactor: simplify generate function
Browse files Browse the repository at this point in the history
  • Loading branch information
zoubingwu committed Feb 11, 2023
1 parent 480bdd2 commit abb56c9
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 177 deletions.
1 change: 1 addition & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ module.exports = {
singleQuote: true,
arrowParens: 'avoid',
semi: true,
printWidth: 120,
};
20 changes: 4 additions & 16 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,11 @@ const cli = cac();

cli
.command('<spec>', 'Generating msw mock definitions with random fake data.')
.option(
'-o, --output <file>',
`Output file path such as \`./mock.js\`, without it'll output to stdout.`
)
.option('-o, --output <file>', `Output file path such as \`./mock.js\`, without it'll output to stdout.`)
.option('-m, --max-array-length <number>', `Max array length, default to 20.`)
.option(
'-t, --includes <keywords>',
`Include the request path with given string, can be seperated with comma.`
)
.option(
'-e, --excludes <keywords>',
`Exclude the request path with given string, can be seperated with comma.`
)
.option(
'--base-url [baseUrl]',
`Use the one you specified or server url in OpenAPI description as base url.`
)
.option('-t, --includes <keywords>', `Include the request path with given string, can be seperated with comma.`)
.option('-e, --excludes <keywords>', `Exclude the request path with given string, can be seperated with comma.`)
.option('--base-url [baseUrl]', `Use the one you specified or server url in OpenAPI description as base url.`)
.option(
'--node',
`By default it will generate code for browser environment, use this flag if you want to use it in Node.js environment.`
Expand Down
194 changes: 89 additions & 105 deletions src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import camelCase from 'lodash/camelCase';

import { getV3Doc } from './swagger';
import { prettify, toExpressLikePath } from './utils';
import { OperationCollection } from './transform';
import { Operation } from './transform';
import { mockTemplate } from './template';
import { CliOptions } from './types';

Expand All @@ -18,117 +18,27 @@ export async function generate(spec: string, options: CliOptions) {
const apiGen = new ApiGenerator(apiDoc, {});

const operationDefinitions = getOperationDefinitions(apiDoc);
const includes = options?.includes?.split(',') ?? null;
const excludes = options?.excludes?.split(',') ?? null;

const operationCollection = operationDefinitions
.filter(op => {
if (includes && !includes.includes(op.path)) {
return false;
}
if (excludes && excludes.includes(op.path)) {
return false;
}
return true;
})
.map(definition => {
const { verb, path, responses, id } = definition;

const responseMap = Object.entries(responses).map(([code, response]) => {
const content = apiGen.resolve(response).content;
if (!content) {
return { code, id: '', responses: {} };
}

const resolvedResponse = Object.keys(content).reduce(
(resolved, type) => {
const schema = content[type].schema;
if (typeof schema !== 'undefined') {
resolved[type] = recursiveResolveSchema(schema);
}

return resolved;
},
{} as Record<string, OpenAPIV3.SchemaObject>
);

return {
code,
id,
responses: resolvedResponse,
};
});

return {
verb,
path: toExpressLikePath(path),
response: responseMap,
};
}) as OperationCollection;
.filter(op => operationFilter(op, options))
.map(definition => toOperation(definition, apiGen));

let baseURL = '';
if (options.baseUrl === true) {
baseURL = getServerUrl(apiDoc);
} else if (typeof options.baseUrl === 'string') {
baseURL = options.baseUrl;
}
code = mockTemplate(
operationCollection,
baseURL,
options
);
code = mockTemplate(operationCollection, baseURL, options);

if (outputFile) {
fs.writeFileSync(
path.resolve(process.cwd(), outputFile),
await prettify(outputFile, code)
);
fs.writeFileSync(path.resolve(process.cwd(), outputFile), await prettify(outputFile, code));
} else {
console.log(await prettify(null, code));
}

function recursiveResolveSchema(
schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject
) {
const resolvedSchema = apiGen.resolve(schema) as OpenAPIV3.SchemaObject;

if (resolvedSchema.type === 'array') {
resolvedSchema.items = apiGen.resolve(resolvedSchema.items);
resolvedSchema.items = recursiveResolveSchema(resolvedSchema.items);
} else if (resolvedSchema.type === 'object') {
if (
!resolvedSchema.properties &&
typeof resolvedSchema.additionalProperties === 'object'
) {
if ('$ref' in resolvedSchema.additionalProperties) {
resolvedSchema.additionalProperties = recursiveResolveSchema(
apiGen.resolve(resolvedSchema.additionalProperties)
);
}
}

if (resolvedSchema.properties) {
resolvedSchema.properties = Object.entries(
resolvedSchema.properties
).reduce((resolved, [key, value]) => {
resolved[key] = recursiveResolveSchema(value);
return resolved;
}, {} as Record<string, OpenAPIV3.SchemaObject>);
}
} else if ('allOf' in schema) {
resolvedSchema.allOf = apiGen.resolveArray(schema.allOf);
} else if ('oneOf' in schema) {
resolvedSchema.oneOf = apiGen.resolveArray(schema.oneOf);
} else if ('anyOf' in schema) {
resolvedSchema.anyOf = apiGen.resolveArray(schema.anyOf);
}

return resolvedSchema;
}
}

function getServerUrl(apidoc: OpenAPIV3.Document) {
let server = apidoc.servers?.at(0);
function getServerUrl(apiDoc: OpenAPIV3.Document) {
let server = apiDoc.servers?.at(0);
let url = '';
if (server) {
url = server.url;
Expand All @@ -142,9 +52,7 @@ function getServerUrl(apidoc: OpenAPIV3.Document) {
return url;
}

const operationKeys = Object.values(
OpenAPIV3.HttpMethods
) as OpenAPIV3.HttpMethods[];
const operationKeys = Object.values(OpenAPIV3.HttpMethods) as OpenAPIV3.HttpMethods[];

type OperationDefinition = {
path: string;
Expand All @@ -158,17 +66,93 @@ function getOperationDefinitions(v3Doc: OpenAPIV3.Document): OperationDefinition
!pathItem
? []
: Object.entries(pathItem)
.filter((arg): arg is [string, OpenAPIV3.OperationObject] =>
operationKeys.includes(arg[0] as any)
)
.filter((arg): arg is [string, OpenAPIV3.OperationObject] => operationKeys.includes(arg[0] as any))
.map(([verb, operation]) => {
const id = camelCase(operation.operationId ?? (verb + '/' + path));
const id = camelCase(operation.operationId ?? verb + '/' + path);
return {
path,
verb,
id,
responses: operation.responses,
}
};
})
);
}

function operationFilter(operation: OperationDefinition, options: CliOptions): boolean {
const includes = options?.includes?.split(',') ?? null;
const excludes = options?.excludes?.split(',') ?? null;

if (includes && !includes.includes(operation.path)) {
return false;
}
if (excludes && excludes.includes(operation.path)) {
return false;
}
return true;
}

function toOperation(definition: OperationDefinition, apiGen: ApiGenerator): Operation {
const { verb, path, responses, id } = definition;

const responseMap = Object.entries(responses).map(([code, response]) => {
const content = apiGen.resolve(response).content;
if (!content) {
return { code, id: '', responses: {} };
}

const resolvedResponse = Object.keys(content).reduce((resolved, type) => {
const schema = content[type].schema;
if (typeof schema !== 'undefined') {
resolved[type] = recursiveResolveSchema(schema, apiGen);
}

return resolved;
}, {} as Record<string, OpenAPIV3.SchemaObject>);

return {
code,
id,
responses: resolvedResponse,
};
});

return {
verb,
path: toExpressLikePath(path),
response: responseMap,
};
}

function recursiveResolveSchema(schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject, apiGen: ApiGenerator) {
const resolvedSchema = apiGen.resolve(schema) as OpenAPIV3.SchemaObject;

if (resolvedSchema.type === 'array') {
resolvedSchema.items = apiGen.resolve(resolvedSchema.items);
resolvedSchema.items = recursiveResolveSchema(resolvedSchema.items, apiGen);
} else if (resolvedSchema.type === 'object') {
if (!resolvedSchema.properties && typeof resolvedSchema.additionalProperties === 'object') {
if ('$ref' in resolvedSchema.additionalProperties) {
resolvedSchema.additionalProperties = recursiveResolveSchema(
apiGen.resolve(resolvedSchema.additionalProperties),
apiGen
);
}
}

if (resolvedSchema.properties) {
resolvedSchema.properties = Object.entries(resolvedSchema.properties).reduce((resolved, [key, value]) => {
resolved[key] = recursiveResolveSchema(value, apiGen);
return resolved;
}, {} as Record<string, OpenAPIV3.SchemaObject>);
}
} else if ('allOf' in schema) {
resolvedSchema.allOf = apiGen.resolveArray(schema.allOf);
} else if ('oneOf' in schema) {
resolvedSchema.oneOf = apiGen.resolveArray(schema.oneOf);
} else if ('anyOf' in schema) {
resolvedSchema.anyOf = apiGen.resolveArray(schema.anyOf);
}

return resolvedSchema;
}
20 changes: 4 additions & 16 deletions src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,14 @@ import { OperationCollection, transformToHandlerCode, transformToResObject } fro

const getSetupCode = (isNode?: boolean) => {
if (isNode) {
return [
`const server = setupServer(...handlers);`,
`server.listen();`,
].join('\n');
return [`const server = setupServer(...handlers);`, `server.listen();`].join('\n');
}

return [`const worker = setupWorker(...handlers);`, `worker.start();`].join(
'\n'
);
return [`const worker = setupWorker(...handlers);`, `worker.start();`].join('\n');
};

const getImportsCode = (options?: CliOptions) => {
const imports = [
`import { setupWorker, rest } from 'msw';`,
`import { faker } from '@faker-js/faker';`,
];
const imports = [`import { setupWorker, rest } from 'msw';`, `import { faker } from '@faker-js/faker';`];

if (options?.node) {
imports.push(`import { setupServer } from 'msw/node'`);
Expand All @@ -27,11 +19,7 @@ const getImportsCode = (options?: CliOptions) => {
return imports.join('\n');
};

export const mockTemplate = (
operationCollection: OperationCollection,
baseURL: string,
options?: CliOptions
) => `/**
export const mockTemplate = (operationCollection: OperationCollection, baseURL: string, options?: CliOptions) => `/**
* This file is AUTO GENERATED by [msw-auto-mock](https://github.com/zoubingwu/msw-auto-mock)
* Feel free to commit/edit it as you need.
*/
Expand Down
Loading

0 comments on commit abb56c9

Please sign in to comment.