UNPKG

erra

Version:
246 lines (220 loc) 7.8 kB
import yaml from 'js-yaml'; import { keys, reduce, constant, filter, tap, identity, isArray, isFunction, isPlainObject, isRegExp, map, mapValues, mergeWith, pick, pipe, toPairs, values, __, isObject } from 'lodash/fp'; import { mock } from 'mockjs'; import genUUID from 'uuid'; import { ApiRecord, API_DATA_TYPE, Snippet, SnippetContent, SOCKET_MSG_TAG_API } from '../../lib/interface'; import { Expression, ExpSchema } from '../../lib/exp-yaml'; import ss from '../socket-server'; import configManager from './config-manager'; const snippetsFn: { [key: string]: (data: any) => any } = {} const snippetsMeta: { [key: string]: Snippet } = {} let enableSnippet = true configManager.on('afterConfigInit', () => { Object.entries(configManager.get(configManager.key.SNIPPET) || {}) .forEach(([key, val]: [string, Snippet]) => { snippetsMeta[key] = val snippetsFn[key] = parseSnippetContent(val.content) }) }) function getSnippetMetaList() { return yaml.dump(values(snippetsMeta), { schema: ExpSchema }) } ss.on(SOCKET_MSG_TAG_API.SP_GET, (cb) => { cb(getSnippetMetaList()) }) ss.on(SOCKET_MSG_TAG_API.SP_SAVE, ({ id, code }, cb) => { const { name, content, when } = yaml.load(code, { schema: ExpSchema }) const spId = id || genUUID() snippetsMeta[spId] = Object.assign({}, snippetsMeta[spId], { id: spId, name, when, content, }) configManager.emit('update', configManager.key.SNIPPET, snippetsMeta) snippetsFn[spId] = parseSnippetContent(content) ss.broadcast(SOCKET_MSG_TAG_API.SP_UPDATE, getSnippetMetaList()) }) ss.on(SOCKET_MSG_TAG_API.SP_DELETE, (id) => { // todo: 检查是否被引用 delete snippetsFn[id] delete snippetsMeta[id] configManager.emit('update', configManager.key.SNIPPET, snippetsMeta) ss.broadcast(SOCKET_MSG_TAG_API.SP_UPDATE, getSnippetMetaList()) }) ss.on(SOCKET_MSG_TAG_API.SP_MAIN_ENABLED, (cb) => { cb(enableSnippet) }) ss.on(SOCKET_MSG_TAG_API.SP_SET_MAIN_ENABLED, (val) => { enableSnippet = !!val ss.broadcast(SOCKET_MSG_TAG_API.SP_SET_MAIN_ENABLED, enableSnippet) }) ss.on(SOCKET_MSG_TAG_API.SP_UPDAT_SINGLE_ENABLED, (id, enableStatus) => { snippetsMeta[id].enabled = enableStatus configManager.emit('update', configManager.key.SNIPPET, snippetsMeta) ss.broadcast(SOCKET_MSG_TAG_API.SP_UPDATE, getSnippetMetaList()) }) export enum PARSE_STRATEGY { FIXED = 'fixed', MOCKJS = 'mockjs', SNIPPET = 'snippet', } // 将策略解析成函数 function parseStrategy({ strategy = 'fixed', value, key = null }): (data: any) => any { switch (strategy) { case PARSE_STRATEGY.FIXED: return constant(value) case PARSE_STRATEGY.MOCKJS: if (key) { // parseStrategy`返回value的函数`,所以此处只取mock的值 // key 与 该函数 在上层关联 return () => values(mock({ [key]: value }))[0] } return constant(mock(value)) case PARSE_STRATEGY.SNIPPET: // 支持 ${name}|${snippetId} 这样的接口,方便阅读 const sId = value.split('|').slice(-1)[0] // snippet 是否被解析过,如果没有则解析后更新snippets const parsed = snippetsFn[sId] if (parsed) return parsed const source = configManager.get(configManager.key.SNIPPET)[sId] if (!source) throw new Error(`[snippet解析错误]找不到依赖的snippet:${value}`) const ps = parseSnippetContent(source.content) snippetsFn[sId] = ps return ps } return identity } function parse(snippet: SnippetContent) { const fixedRegx = new RegExp(`^\\$${PARSE_STRATEGY.FIXED}\\s+`) const mockjsRegx = new RegExp(`^\\$${PARSE_STRATEGY.MOCKJS}\\s+`) const snippetRegx = new RegExp(`^\\$${PARSE_STRATEGY.SNIPPET}\\s+`) // 展开对象 { a: 1, ...$snippet: snippetId, b: 3} // 展开数组 [1, ...$snippet: abc, 3] const expSnippetRegx = new RegExp(`^\.{3}\\$${PARSE_STRATEGY.SNIPPET}$`) if (snippet instanceof Expression) { return snippet.fn } else if (isArray(snippet)) { let rs = [] for (let s of snippet) { // 如果当前元素为 需要展开的数组 if ( isPlainObject(s) && keys(s).length === 1 && expSnippetRegx.test(keys(s)[0]) ) { // ...$snippet: snippetId 会被解析为 { 0: v1, 1: v2} rs.push(...values(parse(s))) } else { rs.push(parse(s)) } } return rs } else if (isFunction(snippet)) { return snippet } else if (isObject(snippet)) { return pipe( toPairs, map(([key, value]) => { if (fixedRegx.test(key)) { return [key.replace(fixedRegx, ''), parseStrategy({ strategy: PARSE_STRATEGY.FIXED, value, })] } else if (mockjsRegx.test(key)) { return [ // 去除掉mockjs key中包含的修饰符 key.replace(mockjsRegx, '').replace(/\|.+$/, ''), parseStrategy({ strategy: PARSE_STRATEGY.MOCKJS, key: key.replace(mockjsRegx, ''), value, })] } else if (snippetRegx.test(key)) { return [key.replace(snippetRegx, ''), parseStrategy({ strategy: PARSE_STRATEGY.SNIPPET, value, })] } else if (expSnippetRegx.test(key)) { return [expSnippetRegx.source, parseStrategy({ strategy: PARSE_STRATEGY.SNIPPET, value, })(null)] } return [key, parse(value)] }), // 序对合并为对象 reduce( (sum, [k, v]) => k === expSnippetRegx.source ? ({ ...sum, ...v }) : ({ ...sum, [k]: v }) , {} ), )(snippet) } return parseStrategy({ value: snippet }) } /** * 当源数据 与 snippet字段不匹配时,snippet中有些元素是一个Function * 递归将snippet中的所有函数执行,生成数据 * @param remainder */ function expandSnippet(remainder) { if (isFunction(remainder)) return remainder() if (isArray(remainder)) return map(expandSnippet)(remainder) if (isPlainObject(remainder)) return mapValues(expandSnippet)(remainder) return remainder } /** * 解析SnippetContent * @param snippet 生成数据的策略 * @return Function */ export function parseSnippetContent(snippet: SnippetContent): (data: any) => any { const snippeter = parse(snippet) return (data): any => { if (isFunction(snippeter)) return snippeter(data) if (isPlainObject(data) && isPlainObject(snippeter)) { const rs = mergeWith((subOriginVal, subSnippetVal) => { if (isFunction(subSnippetVal)) { return subSnippetVal(subOriginVal) } return undefined }, data, snippeter) return rs } return expandSnippet(snippeter) } } /** * 获取解析后的snippet * @param id snippetId * @return Function 按配置策略处理传入的参数后返回 */ export function getSnippetFn(id: string): (data: any) => any { if (enableSnippet) return snippetsFn[id] // 关闭Snippet转换功能时,返回一个不对数据做任何处理的函数identity return identity } /** * 获取满足触发的条件Snippet * @param record ApiRecord */ export function matchedSnippetFns(record: ApiRecord): Function[] { if (!enableSnippet) return [] const { req, resp, parsedUrl } = record // url前追加req|resp、method,让正则可以更精确地匹配 // 结构为:DataType#Method#Url const recordMeta = `${resp ? API_DATA_TYPE.RESPONSE : API_DATA_TYPE.REQUEST}#${req.method}#${parsedUrl.href}` return pipe( values, filter(({ enabled, when }) => ( enabled && when && (isRegExp(when) ? when.test(recordMeta) : when(record))) ), map('id'), pick(__, snippetsFn), values )(snippetsMeta) }