Skip to content

Commit

Permalink
feat: 新增 vae 数据验证工具 (#152)
Browse files Browse the repository at this point in the history
  • Loading branch information
fjc0k authored Aug 6, 2023
1 parent 2e412ea commit b28b52e
Show file tree
Hide file tree
Showing 16 changed files with 2,601 additions and 0 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@
"require": "./_cjs/regexp/index.js",
"import": "./regexp/index.js",
"types": "./regexp/index.d.ts"
},
"./vae": {
"require": "./_cjs/vae/index.js",
"import": "./vae/index.js",
"types": "./vae/index.d.ts"
}
},
"main": "_cjs/utils/index.js",
Expand Down
72 changes: 72 additions & 0 deletions src/vae/VaeArraySchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { isArray } from '../utils'
import { VaeLocale, VaeLocaleMessage } from './VaeLocale'
import { VaeSchema, VaeSchemaOf } from './VaeSchema'

export type VaeArraySchemaElementOf<T> = T extends Array<infer X>
? VaeSchemaOf<X>
: never

export class VaeArraySchema<T extends any[] = any[]> extends VaeSchema<T> {
constructor(
element?: VaeArraySchemaElementOf<T>,
message: VaeLocaleMessage = VaeLocale.array.type,
) {
super({
type: 'array',
})

this.check({
fn: isArray,
message: message,
})

if (element) {
this.element(element)
}
}

element(element: VaeArraySchemaElementOf<T>) {
return this.check({
fn: element,
message: '',
tag: 'element',
})
}

nonempty(message: VaeLocaleMessage = VaeLocale.array.nonempty) {
return this.check({
fn: v => v.length > 0,
message: message,
})
}

min(value: number, message: VaeLocaleMessage = VaeLocale.array.min) {
return this.check({
fn: v => v.length >= value,
message: message,
messageParams: {
min: value,
},
})
}

max(value: number, message: VaeLocaleMessage = VaeLocale.array.max) {
return this.check({
fn: v => v.length <= value,
message: message,
messageParams: {
max: value,
},
})
}

length(value: number, message: VaeLocaleMessage = VaeLocale.array.length) {
return this.check({
fn: v => v.length === value,
message: message,
messageParams: {
length: value,
},
})
}
}
17 changes: 17 additions & 0 deletions src/vae/VaeBooleanSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { isBoolean } from '../utils'
import { VaeLocale, VaeLocaleMessage } from './VaeLocale'
import { VaeSchema } from './VaeSchema'

export class VaeBooleanSchema<
T extends boolean = boolean,
> extends VaeSchema<T> {
constructor(message: VaeLocaleMessage = VaeLocale.boolean.type) {
super({
type: 'boolean',
})
this.transform(Boolean as any).check({
fn: isBoolean,
message: message,
})
}
}
23 changes: 23 additions & 0 deletions src/vae/VaeContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { VaeIssue } from './VaeIssue'
import { VaeSchemaPath } from './VaeSchema'

export class VaeContext {
oldPath: VaeSchemaPath = []

path: VaeSchemaPath = []

issues: VaeIssue[] = []

setPath = (path: VaeSchemaPath) => {
this.oldPath = this.path.slice()
this.path = path
}

restorePath = () => {
this.path = this.oldPath
}

addIssue(issue: VaeIssue) {
this.issues.push(issue)
}
}
45 changes: 45 additions & 0 deletions src/vae/VaeDateSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { anyToDate, isDate, isValid } from '../date'
import { VaeLocale, VaeLocaleMessage } from './VaeLocale'
import { VaeSchema } from './VaeSchema'

export class VaeDateSchema<T extends Date = Date> extends VaeSchema<T> {
constructor(message: VaeLocaleMessage = VaeLocale.date.type) {
super({
type: 'date',
})
this.transform(v => anyToDate(v) as any).check({
fn: v => isDate(v) && isValid(v),
message: message,
})
}

min(
value: Date | string | number,
message: VaeLocaleMessage = VaeLocale.date.min,
) {
const minDate = anyToDate(value)
const minTime = minDate.getTime()
return this.check({
fn: v => minTime <= v.getTime(),
message: message,
messageParams: {
min: minDate,
},
})
}

max(
value: Date | string | number,
message: VaeLocaleMessage = VaeLocale.date.max,
) {
const maxDate = anyToDate(value)
const maxTime = maxDate.getTime()
return this.check({
fn: v => maxTime >= v.getTime(),
message: message,
messageParams: {
max: maxDate,
},
})
}
}
14 changes: 14 additions & 0 deletions src/vae/VaeError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { VaeIssue } from './VaeIssue'

export interface VaeErrorIssue {
path: Array<string | number>
message: string
}

export class VaeError extends Error {
name = 'VaeError'

constructor(public issues: VaeIssue[]) {
super()
}
}
6 changes: 6 additions & 0 deletions src/vae/VaeIssue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { VaeSchemaPath } from './VaeSchema'

export type VaeIssue = {
path: VaeSchemaPath
message: string
}
160 changes: 160 additions & 0 deletions src/vae/VaeLocale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { formatDate } from '../date'
import { VaeSchemaPath } from './VaeSchema'

export type VaeLocaleMessagePayload = {
label?: string
path: VaeSchemaPath
value: any
params: Record<string, any>
}

export type VaeLocaleMessage =
| string
| ((payload: VaeLocaleMessagePayload) => string)

export type VaeLocaleShape = {
base: Record<'required' | 'enum', VaeLocaleMessage>
string: Record<
| 'type'
| 'nonempty'
| 'min'
| 'max'
| 'length'
| 'email'
| 'url'
| 'regex'
| 'includes'
| 'startsWith'
| 'endsWith'
| 'phoneNumber'
| 'idCardNumber',
VaeLocaleMessage
>
object: Record<'type', VaeLocaleMessage>
number: Record<
| 'type'
| 'min'
| 'max'
| 'lessThan'
| 'moreThan'
| 'integer'
| 'positive'
| 'nonpositive'
| 'negative'
| 'nonnegative'
| 'positiveInteger',
VaeLocaleMessage
>
boolean: Record<'type', VaeLocaleMessage>
array: Record<
'type' | 'nonempty' | 'min' | 'max' | 'length',
VaeLocaleMessage
>
date: Record<'type' | 'min' | 'max', VaeLocaleMessage>
}

export class VaeLocaleBuilder {
static zhCN(options: {
getLabel: (payload: VaeLocaleMessagePayload) => string
}): VaeLocaleShape {
return {
base: {
required: payload => `${options.getLabel(payload)}应必填`,
enum: payload =>
`${options.getLabel(
payload,
)}应是下列值之一:${payload.params.enum.join(',')}`,
},

string: {
type: payload => `${options.getLabel(payload)}应是字符串类型`,
nonempty: payload => `${options.getLabel(payload)}应非空`,
min: payload =>
`${options.getLabel(payload)}应至少包含${payload.params.min}位字符`,
max: payload =>
`${options.getLabel(payload)}应最多包含${payload.params.max}位字符`,
length: payload =>
`${options.getLabel(payload)}应仅包含${payload.params.length}位字符`,
email: payload => `${options.getLabel(payload)}应是一个合法的邮箱`,
url: payload => `${options.getLabel(payload)}应是一个合法的网址`,
regex: payload =>
`${options.getLabel(payload)}应满足正则表达式${payload.params.regex}`,
includes: payload =>
`${options.getLabel(payload)}应包含字符串${payload.params.includes}`,
startsWith: payload =>
`${options.getLabel(payload)}应以字符串${
payload.params.startsWith
}开头`,
endsWith: payload =>
`${options.getLabel(payload)}应以字符串${
payload.params.endsWith
}结尾`,
phoneNumber: payload =>
`${options.getLabel(payload)}应是一个合法的手机号码`,
idCardNumber: payload =>
`${options.getLabel(payload)}应是一个合法的身份证号码`,
},

object: {
type: payload => `${options.getLabel(payload)}应是对象类型`,
},

number: {
type: payload => `${options.getLabel(payload)}应是数值类型`,
min: payload =>
`${options.getLabel(payload)}应大于或等于${payload.params.min}`,
max: payload =>
`${options.getLabel(payload)}应小于或等于${payload.params.max}`,
lessThan: payload =>
`${options.getLabel(payload)}应小于${payload.params.lessThan}`,
moreThan: payload =>
`${options.getLabel(payload)}应大于${payload.params.moreThan}`,
integer: payload => `${options.getLabel(payload)}应是一个整数`,
positive: payload => `${options.getLabel(payload)}应是一个正数`,
nonpositive: payload => `${options.getLabel(payload)}应是一个非正数`,
negative: payload => `${options.getLabel(payload)}应是一个负数`,
nonnegative: payload => `${options.getLabel(payload)}应是一个非负数`,
positiveInteger: payload =>
`${options.getLabel(payload)}应是一个正整数`,
},

boolean: {
type: payload => `${options.getLabel(payload)}应是布尔类型`,
},

array: {
type: payload => `${options.getLabel(payload)}应是数组类型`,
nonempty: payload => `${options.getLabel(payload)}应非空`,
min: payload =>
`${options.getLabel(payload)}应至少包含${payload.params.min}个元素`,
max: payload =>
`${options.getLabel(payload)}应最多包含${payload.params.max}个元素`,
length: payload =>
`${options.getLabel(payload)}应仅包含${payload.params.max}个元素`,
},

date: {
type: payload => `${options.getLabel(payload)}应是日期类型`,
min: payload =>
`${options.getLabel(payload)}应大于或等于${formatDate(
payload.params.min,
'yyyy-mm-dd hh:ii:ss',
)}`,
max: payload =>
`${options.getLabel(payload)}应小于或等于${formatDate(
payload.params.max,
'yyyy-mm-dd hh:ii:ss',
)}`,
},
}
}
}

export const VaeLocale = {
...VaeLocaleBuilder.zhCN({
getLabel: payload => payload.label || payload.path.join('.') || '.',
}),
$set: (locale: VaeLocaleShape) => {
Object.assign(VaeLocale, locale)
},
}
Loading

0 comments on commit b28b52e

Please sign in to comment.