Skip to content

Commit

Permalink
feat(vae): 新增 clone, pickFields, omitFields, optionalFields, required…
Browse files Browse the repository at this point in the history
…Fields
  • Loading branch information
fjc0k committed Aug 7, 2023
1 parent e991fb0 commit 5fe2131
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 43 deletions.
5 changes: 5 additions & 0 deletions src/vae/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# TODO

clone
pick
omit
61 changes: 59 additions & 2 deletions src/vae/VaeObjectSchema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isPlainObject } from '../utils'
import { PartialBy, RequiredBy } from '../types'
import { difference, intersection, isPlainObject } from '../utils'
import { VaeLocale, VaeLocaleMessage } from './VaeLocale'
import { VaeSchema, VaeSchemaOf } from './VaeSchema'

Expand Down Expand Up @@ -29,14 +30,70 @@ export class VaeObjectSchema<

shape(shape: VaeObjectSchemaShapeOf<T>) {
const keys = Object.keys(shape)
this._objectKeys = keys
this._options.objectKeys = keys
keys.forEach(key => {
this.check({
fn: shape[key],
path: [key],
message: '',
tag: 'field',
})
})
return this
}

pickFields<K extends keyof T>(keys: K[]): VaeObjectSchema<Pick<T, K>> {
this._options.processors = this._options.processors.filter(item =>
typeof item === 'object' && item.tag === 'field'
? (keys as any).includes(item.path![0])
: true,
)
this._options.objectKeys = intersection(
this._options.objectKeys,
keys as any,
)
return this as any
}

omitFields<K extends keyof T>(keys: K[]): VaeObjectSchema<Omit<T, K>> {
this._options.processors = this._options.processors.filter(item =>
typeof item === 'object' && item.tag === 'field'
? !(keys as any).includes(item.path![0])
: true,
)
this._options.objectKeys = difference(this._options.objectKeys, keys as any)
return this as any
}

optionalFields<K extends keyof T>(keys: K[]): VaeObjectSchema<PartialBy<T, K>>
optionalFields(): VaeObjectSchema<Partial<T>>
optionalFields(keys?: string[]): any {
this._options.processors.forEach(item => {
if (
typeof item === 'object' &&
item.tag === 'field' &&
(keys ? (keys as any).includes(item.path![0]) : true)
) {
;(item.fn as VaeSchema).optional()
}
})
return this
}

requiredFields<K extends keyof T>(
keys: K[],
): VaeObjectSchema<RequiredBy<T, K>>
requiredFields(): VaeObjectSchema<Required<T>>
requiredFields(keys?: string[]): any {
this._options.processors.forEach(item => {
if (
typeof item === 'object' &&
item.tag === 'field' &&
(keys ? (keys as any).includes(item.path![0]) : true)
) {
;(item.fn as VaeSchema).required()
}
})
return this
}
}
103 changes: 65 additions & 38 deletions src/vae/VaeSchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { DotPath, RequiredDeep } from '../types'
import { DotPath, RequiredBy, RequiredDeep } from '../types'
import {
assign,
cloneDeepFast,
get,
includes,
isArray,
Expand Down Expand Up @@ -27,8 +29,16 @@ export type VaeSchemaType =
| 'boolean'
| 'array'

export type VaeSchemaOptions = {
export type VaeSchemaOptions<T> = {
type: VaeSchemaType
label?: string
default?: T | (() => T)
required?: boolean
requiredMessage?: VaeLocaleMessage
objectKeys?: string[]
stringTrim?: boolean
stringEmptyable?: boolean
processors?: Array<VaeSchemaCheckPayload<T> | VaeSchemaTransformPayload<T>>
}

export type VaeSchemaPath = Array<string | number>
Expand Down Expand Up @@ -93,47 +103,44 @@ export type VaeSchemaOf<T0, T = RequiredDeep<T0>> =
VaeObjectSchema<T>

export abstract class VaeSchema<T extends any = any> {
constructor(private _options: VaeSchemaOptions) {}
protected _options!: RequiredBy<VaeSchemaOptions<T>, 'processors'>

private _label: string | undefined

private _default: T | (() => T) | undefined

private _required: boolean | undefined
private _requiredMessage: VaeLocaleMessage | undefined

protected _objectKeys: string[] | undefined

protected _stringTrim: boolean | undefined
protected _stringEmptyable: boolean | undefined

private _processors: Array<
VaeSchemaCheckPayload<T> | VaeSchemaTransformPayload<T>
> = []
constructor(options: VaeSchemaOptions<T>) {
this._options = {
...options,
processors: options.processors || [],
}
}

check(payload: VaeSchemaCheckPayload<T>) {
this._processors.push(payload)
this._options.processors.push(payload)
return this
}

transform(payload: VaeSchemaTransformPayload<T>) {
this._processors.push(payload)
this._options.processors.push(payload)
return this
}

label(label: string) {
this._label = label
this._options.label = label
return this
}

default(value: T | (() => T)) {
this._default = value
this._options.default = value
return this
}

required(message: VaeLocaleMessage = VaeLocale.base.required) {
this._required = true
this._requiredMessage = message
this._options.required = true
this._options.requiredMessage = message
return this
}

optional() {
this._options.required = false
this._options.requiredMessage = undefined
return this
}

Expand All @@ -160,10 +167,28 @@ export abstract class VaeSchema<T extends any = any> {
return this.check({ fn, message, path })
}

clone() {
// https://stackoverflow.com/a/44782052
const newSchema: this = assign(
Object.create(Object.getPrototypeOf(this)),
this,
)
newSchema._options = cloneDeepFast(
newSchema._options,
v => v instanceof VaeSchema,
)
newSchema._options.processors.forEach(item => {
if (typeof item === 'object' && item.fn instanceof VaeSchema) {
item.fn = item.fn.clone()
}
})
return newSchema
}

parse(data: T, options?: VaeSchemaParseOptions): VaeSchemaParseResult<T> {
// 字符串 trim
if (
this._stringTrim &&
this._options.stringTrim &&
this._options.type === 'string' &&
typeof data === 'string'
) {
Expand All @@ -172,36 +197,38 @@ export abstract class VaeSchema<T extends any = any> {

let dataIsNil =
data == null ||
(this._options.type === 'string' && !this._stringEmptyable && data === '')
(this._options.type === 'string' &&
!this._options.stringEmptyable &&
data === '')

// 默认值
if (dataIsNil && this._default != null) {
if (dataIsNil && this._options.default != null) {
data =
typeof this._default === 'function'
? (this._default as any)()
: this._default
typeof this._options.default === 'function'
? (this._options.default as any)()
: this._options.default
dataIsNil =
data == null ||
(this._options.type === 'string' &&
!this._stringEmptyable &&
!this._options.stringEmptyable &&
data === '')
}

// 非必填
if (dataIsNil && !this._required) {
if (dataIsNil && !this._options.required) {
return {
success: true,
data: data,
}
}

const processors = this._processors.slice()
const processors = this._options.processors.slice()

// 必填规则始终前置
if (this._required) {
if (this._options.required) {
processors.unshift({
fn: () => !dataIsNil,
message: this._requiredMessage!,
message: this._options.requiredMessage!,
})
}

Expand Down Expand Up @@ -273,7 +300,7 @@ export abstract class VaeSchema<T extends any = any> {
path: fullPath,
params: messageParams || {},
value: data,
label: this._label,
label: this._options.label,
})
: message,
})
Expand All @@ -300,8 +327,8 @@ export abstract class VaeSchema<T extends any = any> {
data:
!options.preserveUnknownKeys &&
this._options.type === 'object' &&
this._objectKeys?.length
? (pick(data, this._objectKeys) as any)
this._options.objectKeys?.length
? (pick(data, this._options.objectKeys) as any)
: data,
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/vae/VaeStringSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,12 @@ export class VaeStringSchema<T extends string = string> extends VaeSchema<T> {
}

trim() {
this._stringTrim = true
this._options.stringTrim = true
return this
}

emptyable() {
this._stringEmptyable = true
this._options.stringEmptyable = true
return this
}
}
Loading

0 comments on commit 5fe2131

Please sign in to comment.