mock-restful-api
Version:
This is dev support mock RESTful API. Refer to django-rest-framework implementation.
192 lines (181 loc) • 5.61 kB
JavaScript
const fs = require("fs");
const { join: pathJoin } = require("node:path/posix");
const logger = require("./logger.js");
const { MethodEnum } = require("./enums.js");
const { validateConfig } = require("./validator.js");
global.jsonConfig = {
// 格式示例
// filePath: {
// config: {},
// routes: [],
// }
};
/**
* 递归获取目录下所有json文件
* @param {string} filePath 可以是目录也可以是文件
*/
const getJsonFileList = filePath => {
let allFilePaths = [];
const stats = fs.lstatSync(filePath);
if (stats.isFile()) {
if (filePath.endsWith(".json")) {
logger.debug(`find file: ${filePath}`);
allFilePaths.push(filePath);
} else {
logger.warn(`ignore file: ${filePath} , because not endswith .json`);
}
} else if (stats.isDirectory()) {
const files = fs.readdirSync(filePath);
files.forEach(fileName => {
// fileName 是文件名称(不包含文件路径)
const childPath = pathJoin(filePath, fileName);
allFilePaths = allFilePaths.concat(getJsonFileList(childPath));
});
}
return allFilePaths;
};
/**
* 读取配置文件转化为json数据
* @param {string} filePath 必须是文件路径
* @returns
*/
const loadFileContent = filePath => {
let config;
try {
const stats = fs.statSync(filePath);
if (!stats.isFile()) {
logger.warn(`The filePath is not a file or has been deleted: ${filePath}`);
return;
}
const content = fs.readFileSync(filePath, "utf8");
config = JSON.parse(content);
// logger.debug(`load file success: ${filePath}`);
config["file_path"] = filePath;
} catch (err) {
logger.error(`load file fail: ${filePath} ${err}`);
}
return config;
};
/**
* 加载配置文件生成路由routes
* @param {string} filePath
* @returns
*/
const genConfigToRoutes = filePath => {
const config = loadFileContent(filePath);
if (!config) {
return;
}
try {
validateConfig(config);
} catch (err) {
logger.error(`The file content is illegal: ${filePath} ${err}`);
return;
}
// 重置该文件配置
let routes = [];
const { restful, actions, apis } = config;
if (restful) {
// 判断是否要添加斜线后缀
const appendSlash = restful.endsWith("/") ? "/" : "";
// pk_field 不仅仅是递增的数字,可能使用其他的例如uuid
const detailUrl = pathJoin(restful, ":pk([\\w-]+)", appendSlash);
// 添加restful接口操作
const baseRoute = { restful };
routes = [
{ ...baseRoute, method: MethodEnum.GET, path: restful }, // 列表
{ ...baseRoute, method: MethodEnum.POST, path: restful }, // 创建
{ ...baseRoute, method: MethodEnum.GET, path: detailUrl, detail: true }, // 详情
{ ...baseRoute, method: MethodEnum.PATCH, path: detailUrl, detail: true }, // 部分字段修改
{ ...baseRoute, method: MethodEnum.PUT, path: detailUrl, detail: true }, // 修改
{ ...baseRoute, method: MethodEnum.DELETE, path: detailUrl, detail: true }, // 删除
];
// 额外操作
(actions || []).forEach(action => {
const item = {
method: action.method.toLowerCase(),
path: pathJoin(action.detail ? detailUrl : restful, action.url_path),
response: action.response,
};
routes.push(item);
});
}
if (apis) {
routes = routes.concat(apis);
}
// 最后添加相关配置
return { routes, config: { rows: [], pk_field: "id", page_size: 20, ...config } };
};
/**
* 校验路由配置数据
* @param {Object} route
*/
const validateRoute = (route, allData) => {
const { restful, method, path } = route;
if (!restful) {
if (!method) {
throw Error(`"method" cannot be empty`);
}
if (!path) {
throw Error(`"path" cannot be empty`);
}
}
for (let filePath in allData) {
const { config, routes } = allData[filePath];
if (restful && restful === config?.restful) {
// restful接口重复
throw Error(`The restful interface already exists in ${filePath}: restful=${restful}`);
}
for (let item of routes) {
if (method.toLowerCase() === item.method.toLowerCase() && path === item.path) {
// 其他接口重复
throw Error(`The current method+path already exists in ${filePath}: method=${method} path=${path}`);
}
}
}
};
const loadFileToConfig = filePath => {
if (global.jsonConfig[filePath]) {
// 删除旧的peizhi
logger.debug(`delete old config: ${filePath}`);
delete global.jsonConfig[filePath];
}
const data = genConfigToRoutes(filePath);
if (!data) {
logger.error(`load config fail: ${filePath}`);
return;
}
logger.info(`loading config: ${filePath}`);
const { routes, config } = data;
const fileConfig = { config, routes: [] };
for (let route of routes) {
const { path: urlPath } = route;
try {
validateRoute(route, global.jsonConfig);
fileConfig.routes.push(route);
logger.debug(`config add route ${route.method.padEnd(8, " ")}${urlPath}`);
} catch (err) {
logger.error(`append route fail: ${filePath} ${err}`);
}
}
global.jsonConfig[filePath] = fileConfig;
};
/**
* 初始化并加载目录中的所有配置文件
* @param {string} filePath 可以是目录也可以是文件
* @returns
*/
const initJsonFiles = filePath => {
const files = getJsonFileList(filePath);
for (let jsonFile of files) {
loadFileToConfig(jsonFile);
}
};
module.exports = {
getJsonFileList,
loadFileContent,
genConfigToRoutes,
validateRoute,
loadFileToConfig,
initJsonFiles,
};