better-mock
Version:
Forked from Mockjs. Generate random data & Intercept ajax request. Support miniprogram.
507 lines (463 loc) • 17.9 kB
text/typescript
// 处理数据模板。
// handler.gen( template, name?, context? )
import constant from '../utils/constant'
import * as utils from '../utils'
import { parse } from './parser'
import random from '../random'
import transfer from '../transfer'
import RE from './regexp'
const handler = {
// template 属性值(即数据模板)
// name 属性名
// context 数据上下文,生成后的数据
// templateContext 模板上下文,
//
// Handle.gen(template, name, options)
// context
// currentContext, templateCurrentContext,
// path, templatePath
// root, templateRoot
gen: function (template, name?: string | number, context?: Partial<GenerateContext>) {
name = name === undefined ? '' : name.toString()
context = context || {}
context = {
// 当前访问路径,只有属性名,不包括生成规则
path: context.path || [constant.GUID],
templatePath: context.templatePath || [constant.GUID++], // 最终属性值的上下文
currentContext: context.currentContext, // 属性值模板的上下文
templateCurrentContext: context.templateCurrentContext || template, // 最终值的根
root: context.root || context.currentContext, // 模板的根
templateRoot: context.templateRoot || context.templateCurrentContext || template
}
const rule = parse(name)
const type = utils.type(template)
let data
if (handler[type]) {
data = handler[type]({
type, // 属性值类型
template, // 属性值模板
name, // 属性名
rule,
context,
parsedName: name ? name.replace(constant.RE_KEY, '$1') : name,
})
if (!context.root) {
context.root = data
}
return data
}
return template
},
array: function (options: GenerateOptions) {
let result: any[] = []
// 'name|1': []
// 'name|count': []
// 'name|min-max': []
if (options.template.length === 0) return result
// 'arr': [{ 'email': '@EMAIL' }, { 'email': '@EMAIL' }]
if (!options.rule.parameters) {
for (let i = 0; i < options.template.length; i++) {
options.context.path.push(i)
options.context.templatePath.push(i)
result.push(handler.gen(options.template[i], i, {
path: options.context.path,
templatePath: options.context.templatePath,
currentContext: result,
templateCurrentContext: options.template,
root: options.context.root || result,
templateRoot: options.context.templateRoot || options.template
}))
options.context.path.pop()
options.context.templatePath.pop()
}
} else {
// 'method|1': ['GET', 'POST', 'HEAD', 'DELETE']
if (options.rule.min === 1 && options.rule.max === undefined) {
// fix Mock.js#17
options.context.path.push(options.name)
options.context.templatePath.push(options.name)
result = random.pick(handler.gen(options.template, undefined, {
path: options.context.path,
templatePath: options.context.templatePath,
currentContext: result,
templateCurrentContext: options.template,
root: options.context.root || result,
templateRoot: options.context.templateRoot || options.template
}))
options.context.path.pop()
options.context.templatePath.pop()
} else {
// 'data|+1': [{}, {}]
if (options.rule.parameters[2]) {
options.template.__order_index = options.template.__order_index || 0
options.context.path.push(options.name)
options.context.templatePath.push(options.name)
result = handler.gen(options.template, undefined, {
path: options.context.path,
templatePath: options.context.templatePath,
currentContext: result,
templateCurrentContext: options.template,
root: options.context.root || result,
templateRoot: options.context.templateRoot || options.template
})[options.template.__order_index % options.template.length]
options.template.__order_index += +options.rule.parameters[2]
options.context.path.pop()
options.context.templatePath.pop()
} else if (options.rule.count) {
// 'data|1-10': [{}]
for (let i = 0; i < options.rule.count; i++) {
// 'data|1-10': [{}, {}]
for (let ii = 0; ii < options.template.length; ii++) {
options.context.path.push(result.length)
options.context.templatePath.push(ii)
result.push(handler.gen(options.template[ii], result.length, {
path: options.context.path,
templatePath: options.context.templatePath,
currentContext: result,
templateCurrentContext: options.template,
root: options.context.root || result,
templateRoot: options.context.templateRoot || options.template
}))
options.context.path.pop()
options.context.templatePath.pop()
}
}
}
}
}
return result
},
object: function (options: GenerateOptions) {
const result = {}
// 'obj|min-max': {}
if (options.rule.min != undefined) {
let keys = utils.keys(options.template)
keys = random.shuffle(keys)
keys = keys.slice(0, options.rule.count)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
let parsedKey = key.replace(constant.RE_KEY, '$1')
const transferTypeCtor = handler.getTransferTypeCtor(key)
if (transferTypeCtor) {
parsedKey = parsedKey.replace(constant.RE_TRANSFER_TYPE, '')
}
options.context.path.push(parsedKey)
options.context.templatePath.push(key)
const generatedValue = handler.gen(options.template[key], key, {
path: options.context.path,
templatePath: options.context.templatePath,
currentContext: result,
templateCurrentContext: options.template,
root: options.context.root || result,
templateRoot: options.context.templateRoot || options.template
})
result[parsedKey] = transferTypeCtor(generatedValue)
options.context.path.pop()
options.context.templatePath.pop()
}
} else {
// 'obj': {}
let keys: string[] = []
const fnKeys: string[] = [] // Mock.js#25 改变了非函数属性的顺序,查找起来不方便
for (const key in options.template) {
const target = typeof options.template[key] === 'function' ? fnKeys : keys
target.push(key)
}
keys = keys.concat(fnKeys)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
let parsedKey = key.replace(constant.RE_KEY, '$1')
const transferTypeCtor = handler.getTransferTypeCtor(key)
if (transferTypeCtor) {
parsedKey = parsedKey.replace(constant.RE_TRANSFER_TYPE, '')
}
options.context.path.push(parsedKey)
options.context.templatePath.push(key)
const generatedValue = handler.gen(options.template[key], key, {
path: options.context.path,
templatePath: options.context.templatePath,
currentContext: result,
templateCurrentContext: options.template,
root: options.context.root || result,
templateRoot: options.context.templateRoot || options.template
})
result[parsedKey] = transferTypeCtor(generatedValue)
options.context.path.pop()
options.context.templatePath.pop()
// 'id|+1': 1
const inc = key.match(constant.RE_KEY)
if (inc && inc[2] && utils.type(options.template[key]) === 'number') {
options.template[key] += parseInt(inc[2], 10)
}
}
}
return result
},
number: function (options: GenerateOptions) {
let result
let parts
if (options.rule.decimal) {
// float
options.template += ''
parts = options.template.split('.')
// 'float1|.1-10': 10,
// 'float2|1-100.1-10': 1,
// 'float3|999.1-10': 1,
// 'float4|.3-10': 123.123,
parts[0] = options.rule.range ? options.rule.count : parts[0]
parts[1] = (parts[1] || '').slice(0, options.rule.dcount)
while (parts[1].length < options.rule.dcount!) {
// 最后一位不能为 0:如果最后一位为 0,会被 JS 引擎忽略掉。
parts[1] += parts[1].length < options.rule.dcount! - 1
? random.character('number')
: random.character('123456789')
}
result = parseFloat(parts.join('.'))
} else {
// integer
// 'grade1|1-100': 1,
result = options.rule.range && !options.rule.parameters![2] ? options.rule.count : options.template
}
return result
},
boolean: function (options: GenerateOptions) {
// 'prop|multiple': false, 当前值是相反值的概率倍数
// 'prop|probability-probability': false, 当前值与相反值的概率
const result = options.rule.parameters
? random.bool(Number(options.rule.min), Number(options.rule.max), options.template)
: options.template
return result
},
string: function (options) {
let source = ''
let result = ''
let match
let lastIndex = 0
if (options.template.length) {
// 'foo': '★',
if (options.rule.count === undefined) {
source += options.template
} else {
// 'star|1-5': '★',
for (let i = 0; i < options.rule.count; i++) {
source += options.template
}
}
// 'email|1-10': '@EMAIL, ',
constant.RE_PLACEHOLDER.exec('')
while (match = constant.RE_PLACEHOLDER.exec(source)) {
const index = match.index
const input = match[0]
if (index >= lastIndex) {
// 遇到转义斜杠,不需要解析占位符
if (/^\\/.test(input)) {
result += source.slice(lastIndex, index) + input.slice(1)
lastIndex = index + input.length
continue
}
// console.log(input, options.context.currentContext, options.context.templateCurrentContext, options)
const replaced = handler.placeholder(input, options.context.currentContext, options.context.templateCurrentContext, options)
// 只有一个占位符,并且没有其他字符,例如:'name': '@EMAIL'
if (index === 0 && input.length === source.length) {
result = replaced
} else {
result += source.slice(lastIndex, index) + replaced
}
lastIndex = index + input.length
}
}
if (lastIndex < source.length) {
result += source.slice(lastIndex)
}
} else {
// 'ASCII|1-10': '',
// 'ASCII': '',
result = options.rule.range ? random.string(options.rule.count) : options.template
}
return result
},
function: function (options: GenerateOptions) {
// ( context, options )
return options.template.call(options.context.currentContext, options)
},
regexp: function (options: GenerateOptions) {
let source = ''
// 'name': /regexp/,
if (options.rule.count === undefined) {
source += options.template.source // regexp.source
} else {
// 'name|1-5': /regexp/,
for (let i = 0; i < options.rule.count; i++) {
source += options.template.source
}
}
return RE.Handler.gen(RE.Parser.parse(source))
},
_all: function (): any {
const re = {}
for (const key in random) {
re[key.toLowerCase()] = key
}
return re
},
// 处理占位符,转换为最终值
placeholder: function (placeholder: string, obj, templateContext, options) {
// 1 key, 2 params
// regexp init
constant.RE_PLACEHOLDER.exec('')
const parts = constant.RE_PLACEHOLDER.exec(placeholder)!
const key = parts && parts[1]
const lkey = key && key.toLowerCase()
const okey = handler._all()[lkey!]
const paramsInput: string = (parts && parts[2]) || ''
const pathParts = handler.splitPathToArray(key)
let params: string[] = []
// 解析占位符的参数
try {
// 1. 尝试保持参数的类型
// #24 [Window Firefox 30.0 引用 占位符 抛错](https://github.com/nuysoft/Mock/issues/24)
// [BX9056: 各浏览器下 window.eval 方法的执行上下文存在差异](http://www.w3help.org/zh-cn/causes/BX9056)
// 应该属于 Window Firefox 30.0 的 BUG
params = eval('(function(){ return [].splice.call(arguments, 0 ) })(' + paramsInput + ')')
} catch (error) {
// 2. 如果失败,先使用 `[]` 包裹,用 JSON.parse 尝试解析
try {
const paramsString = paramsInput.replace(/'/g, '"')
params = JSON.parse(`[${paramsString}]`)
} catch (e) {
// 3. 逗号 split 方案兜底
params = paramsInput.split(/,\s*/)
}
}
// 占位符优先引用数据模板中的属性
// { first: '@EMAIL', full: '@first' } => { first: 'dsa@163.com', full: 'dsa@163.com' }
if (obj && key! in obj) {
return obj[key!]
}
// 绝对路径 or 相对路径
if (key!.charAt(0) === '/' || pathParts.length > 1) {
return handler.getValueByKeyPath(key, options)
}
// 递归引用数据模板中的属性
// fix Mock.js#15 避免自己依赖自己)
if (templateContext && typeof templateContext === 'object' && key! in templateContext && placeholder !== templateContext[key!]) {
// 先计算被引用的属性值
templateContext[key!] = handler.gen(templateContext[key!], key, {
currentContext: obj, templateCurrentContext: templateContext
})
return templateContext[key!]
}
// 如果未找到,则原样返回
if (!(key! in random) && !(lkey! in random) && !(okey in random)) {
return placeholder
}
// 递归解析参数中的占位符
for (let i = 0; i < params.length; i++) {
constant.RE_PLACEHOLDER.exec('')
if (constant.RE_PLACEHOLDER.test(params[i])) {
params[i] = handler.placeholder(params[i], obj, templateContext, options)
}
}
const handle = random[key!] || random[lkey!] || random[okey]
if (utils.isFunction(handle)) {
// 执行占位符方法(大多数情况)
handle.options = options
let ret = handle.apply(random, params)
// 因为是在字符串中,所以默认为空字符串。
if (ret === undefined) {
ret = ''
}
delete handle.options
return ret
}
return ''
},
getValueByKeyPath: function (key: string, options) {
const originalKey = key
const keyPathParts: string[] = handler.splitPathToArray(key)
let absolutePathParts: any[] = []
// 绝对路径
if (key.charAt(0) === '/') {
absolutePathParts = [options.context.path[0]].concat(handler.normalizePath(keyPathParts))
} else {
// 相对路径
if (keyPathParts.length > 1) {
absolutePathParts = options.context.path.slice(0)
absolutePathParts.pop()
absolutePathParts = handler.normalizePath(absolutePathParts.concat(keyPathParts))
}
}
try {
key = keyPathParts[keyPathParts.length - 1]
let currentContext = options.context.root
let templateCurrentContext = options.context.templateRoot
for (let i = 1; i < absolutePathParts.length - 1; i++) {
currentContext = currentContext[absolutePathParts[i]]
templateCurrentContext = templateCurrentContext[absolutePathParts[i]]
}
// 引用的值已经计算好
if (currentContext && key in currentContext) {
return currentContext[key]
}
// 尚未计算,递归引用数据模板中的属性
// fix #15 避免自己依赖自己
if (templateCurrentContext &&
typeof templateCurrentContext === 'object' &&
key in templateCurrentContext &&
originalKey !== templateCurrentContext[key]
) {
// 先计算被引用的属性值
templateCurrentContext[key] = handler.gen(templateCurrentContext[key], key, {
currentContext: currentContext,
templateCurrentContext: templateCurrentContext
})
return templateCurrentContext[key]
}
} catch (e) {}
return '@' + keyPathParts.join('/')
},
// https://github.com/kissyteam/kissy/blob/master/src/path/src/path.js
normalizePath: function (pathParts: string[]): string[] {
const newPathParts: string[] = []
for (let i = 0; i < pathParts.length; i++) {
switch (pathParts[i]) {
case '..':
newPathParts.pop()
break
case '.':
break
default:
newPathParts.push(pathParts[i])
}
}
return newPathParts
},
splitPathToArray: function (path: string): string[] {
return path.split(/\/+/).filter(_ => _)
},
getTransferTypeCtor (key: string) {
const matched = key.match(constant.RE_TRANSFER_TYPE)
const type = matched && matched[1]
if (type && transfer.hasOwnProperty(type) && type !== 'extend') {
return transfer[type]
}
return (value) => value
}
}
export default handler
interface GenerateContext {
path: Array<string | number>;
templatePath: Array<string | number>;
currentContext: any;
templateCurrentContext: any;
root: any;
templateRoot: any;
}
interface GenerateOptions {
type: string;
template: any;
name: string;
rule: ReturnType<typeof parse>;
context: GenerateContext;
parsedName: string;
}