├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── package-lock.json ├── package.json ├── src ├── Error.ts ├── JSONTypes.ts ├── Walker.ts ├── __test__ │ ├── DeserializerConfig.spec.ts │ ├── deserialize.spec.ts │ └── validate.spec.ts ├── config │ ├── Config.ts │ ├── DeserializeConfigTypes.ts │ ├── DeserializerConfig.ts │ ├── ValidatorConfig.ts │ └── ValidatorConfigTypes.ts ├── deserialize.ts ├── index.ts ├── parsers │ ├── BooleanParser.ts │ ├── DateParser.ts │ ├── NumberParser.ts │ ├── Parser.ts │ ├── SeparateArrayParser.ts │ └── StringParser.ts ├── utils.ts ├── validate.ts └── validators │ ├── VEUIRule.d.ts │ ├── VEUIRulesValidator.ts │ └── Validator.ts ├── tool └── tag.js ├── tsconfig.json └── tslint.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": ["env", "stage-2"], 5 | "plugins": [ 6 | "istanbul", 7 | [ 8 | "transform-runtime", 9 | { 10 | "polyfill": false, 11 | "regenerator": false 12 | } 13 | ] 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | types 4 | .vscode 5 | coverage 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tslint.json 3 | .editorconfig 4 | .gitignore 5 | src 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | script: 5 | - npm test 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Deserializer 2 | 3 | [![Build Status](https://travis-ci.org/yibuyisheng/json-deserializer.svg?branch=master)](https://travis-ci.org/yibuyisheng/json-deserializer) 4 | 5 | # 安装 6 | 7 | JSON Deserializer 使用 [moment](https://github.com/moment/moment) 处理 JSON 数据中的日期字符串。 8 | 9 | ``` 10 | npm install json-deserializer 11 | ``` 12 | 13 | # 为什么开发这个库? 14 | 15 | 在大型 SPA 应用开发中,前后端数据交互基本都是基于 JSON 的。如果后端使用的是强类型语言(比如 Java ),在获取到前端传递的 JSON 数据的时候,一般会反序列化为某个(或某几个)类的对象,以便后续处理。 16 | 17 | 那么前端在拿到后端传递的 JSON 数据的时候,由于 JavaScript 对 JSON 天然友好的支持,所以很多时候可以直接 `JSON.parse(jsonString)` 一下就可以使用了。但是后端开发者很多时候对 JSON 数据理解的不一样,或者强类型语言操作 JSON 很麻烦,造成传递给前端的 JSON 数据总是不尽人意,此时就少不了对后端数据做 normalize 了。 18 | 19 | # 如何使用? 20 | 21 | JSON Deserializer 的输入是一个 JSON 对象,输出是一个 JavaScript 对象。 22 | 23 | ## API 24 | 25 | ### deserialize(jsonObject, config, options) 26 | 27 | 反序列化 JSON 对象。 28 | 29 | * 参数 30 | 31 | - `jsonObject` JSON 对象 32 | - `config` schema 配置 33 | - `options` 选项配置 34 | - `option.noCircular` `boolean` 是否支持反序列化循环引用对象。如果是 `true` ,则遇到循环引用就会抛出异常;如果是 `false` ,则会自动跳过循环引用部分。默认 `true` 。 35 | - `option.inputFirst` `boolean` 是否优先按照输入对象的数据结构进行遍历处理。默认 `true` 。 36 | 37 | * 返回值 38 | 39 | JavaScript 对象。 40 | 41 | #### 使用示例 42 | 43 | * 转换 JSON 数组 44 | 45 | ```js 46 | import deserialize, {StringParser} from 'json-deserializer'; 47 | 48 | const jsonObject = [20]; 49 | const schema = [StringParser]; 50 | deserialize(jsonObject, schema); // result: ['20'] 51 | ``` 52 | 53 | * 转换 JSON 对象 54 | 55 | ```js 56 | import deserialize, {StringParser} from 'json-deserializer'; 57 | 58 | const jsonObject = {"name": "yibuyisheng", "age": 20}; 59 | const schema = {name: StringParser}; 60 | deserialize(jsonObject, schema); // result: {name: 'yibuyisheng'} 61 | ``` 62 | 63 | * 转换 JSON 基本类型 64 | 65 | ```js 66 | import deserialize, {StringParser} from 'json-deserializer'; 67 | 68 | const jsonObject = 'yibuyisheng'; 69 | const schema = StringParser; 70 | deserialize(jsonObject, schema); // result: 'yibuyisheng' 71 | ``` 72 | 73 | ### validate(obj, config, option) 74 | 75 | 校验 JS 对象。 76 | 77 | * 参数 78 | 79 | - `obj` JS 对象 80 | - `config` schema 配置 81 | - `options` 选项配置 82 | - `option.noCircular` `boolean` 是否支持反序列化循环引用对象。如果是 `true` ,则遇到循环引用就会抛出异常;如果是 `false` ,则会自动跳过循环引用部分。默认 `false` 。 83 | - `option.inputFirst` `boolean` 是否优先按照输入对象的数据结构进行遍历处理。默认 `false` 。 84 | - `option.all` `boolean` 是否一次性搜集对象上所有不合法的属性信息。默认 `false` 。 85 | - `option.flattenResult` `boolean` 是否将搜集到的错误信息打平。默认 `false` 。 86 | 87 | * 返回值 88 | 89 | 共有三种情况: 90 | 91 | * `true` 校验成功,没有非法数据。 92 | * `FlattenResult` 当 `option.flattenResult` 设为 `true` 是,如果校验有错,则错误信息会被打平成一个数组,每个数据项都会带有固定属性。 93 | * `any` 其余类型数据,跟 `obj` 参数的结构一致,叶子节点上都会包含相应数据的校验结果。 94 | 95 | #### 使用示例 96 | 97 | * 校验原始数据 98 | 99 | ```js 100 | import validate, {VEUIRulesValidator} from 'json-deserializer'; 101 | 102 | validate('123', { 103 | validator: VEUIRulesValidator, 104 | rules: 'required' 105 | }); // result: true 106 | 107 | validate(null, { 108 | validator: VEUIRulesValidator, 109 | rules: 'required' 110 | }); 111 | // result: 112 | // { 113 | // detail: [ 114 | // { 115 | // message: '请填写', 116 | // name: 'required' 117 | // } 118 | // ], 119 | // keyPath: [], 120 | // message: 'Validate fail with VEUI rules.', 121 | // } 122 | ``` 123 | 124 | * 校验对象 125 | 126 | ```js 127 | import validate, {VEUIRulesValidator} from 'json-deserializer'; 128 | 129 | validate( 130 | { 131 | name: 'yibuyisheng' 132 | }, 133 | { 134 | name: { 135 | validator: VEUIRulesValidator, 136 | rules: [ 137 | { 138 | name: 'pattern', 139 | value: /^[a-z]+$/ 140 | } 141 | ] 142 | } 143 | } 144 | ); // result: {name: true} 145 | ``` 146 | 147 | * 拿到打平的结果 148 | 149 | ```js 150 | import validate, {VEUIRulesValidator} from 'json-deserializer'; 151 | 152 | validate( 153 | { 154 | name: { 155 | first: 'Li', 156 | last: 'Zhang', 157 | }, 158 | }, 159 | { 160 | name: { 161 | validator: VEUIRulesValidator, 162 | rules: [ 163 | { 164 | name: 'pattern', 165 | value: /^[a-z]+$/, 166 | message: '格式不符合要求', 167 | } 168 | ] 169 | }, 170 | }, 171 | { 172 | flattenResult: true, 173 | } 174 | ); 175 | // result: 176 | // [ 177 | // { 178 | // keyPath: ['name'], 179 | // result: { 180 | // detail: [{message: '格式不符合要求', name: 'pattern'}], 181 | // keyPath: ['name'], 182 | // message: 'Validate fail with VEUI rules.' 183 | // } 184 | // } 185 | // ] 186 | ``` 187 | 188 | # License 189 | 190 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fyibuyisheng%2Fjson-deserializer.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fyibuyisheng%2Fjson-deserializer?ref=badge_large) 191 | 192 | # Changelog 193 | 194 | ## v0.0.6 195 | 196 | ### 💡 主要变更 197 | 198 | * [+] 支持 shouldIgnoreUndefined 配置。 199 | 200 | ## v0.0.5 201 | 202 | ### 💡 主要变更 203 | 204 | * [+] 支持 `inputFirst` 配置,即以被处理对象的数据结构为准,还是以配置对象的数据结构为准。 205 | 206 | ## v0.0.4 207 | 208 | ### 💡 主要变更 209 | 210 | * [+] 在 index.js 中导出模块。 211 | 212 | ## v0.0.3 213 | 214 | ### 💡 主要变更 215 | 216 | * [+] 新增校验器,可以对 JS 对象进行全方位校验,支持检测包含循环引用的对象。 217 | * [+] 支持检测带有循环引用的 JSON 对象。 218 | 219 | ## v0.0.2 220 | 221 | ### 💡 主要变更 222 | 223 | * [+] 支持对 JSON 对象的 normalize 。 224 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-deserializer", 3 | "version": "0.0.6", 4 | "description": "通过配置 schema 将 JSON 数据转换成指定的类型和格式。", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "jest --coverage", 8 | "test-debug": "node --inspect-brk node_modules/.bin/jest --runInBand", 9 | "publish-npm": "tsc && npm publish && npm run tag", 10 | "tag": "node tool/tag.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/yibuyisheng/json-deserializer.git" 15 | }, 16 | "keywords": [ 17 | "JSON", 18 | "deserializer", 19 | "validator" 20 | ], 21 | "author": "yibuyisheng", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/yibuyisheng/json-deserializer/issues" 25 | }, 26 | "homepage": "https://github.com/yibuyisheng/json-deserializer#readme", 27 | "typings": "types/index.d.ts", 28 | "peerDependencies": { 29 | "moment": "^2.21.0", 30 | "veui": "^1.0.0-alpha.9" 31 | }, 32 | "sideEffects": false, 33 | "jest": { 34 | "transform": { 35 | "^.+\\.tsx?$": "ts-jest", 36 | "^.+\\.jsx?$": "babel-jest" 37 | }, 38 | "transformIgnorePatterns": [ 39 | "/node_modules/(moment|core-js|babel-runtime|regenerator-runtime|lodash)/" 40 | ], 41 | "collectCoverage": false, 42 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(tsx?)$", 43 | "moduleFileExtensions": [ 44 | "ts", 45 | "js" 46 | ], 47 | "globals": { 48 | "ts-jest": { 49 | "skipBabel": true 50 | } 51 | }, 52 | "testPathIgnorePatterns": [ 53 | "/(node_modules|lib|coverage|types)/" 54 | ] 55 | }, 56 | "devDependencies": { 57 | "@types/jest": "^22.2.2", 58 | "babel-plugin-transform-runtime": "^6.23.0", 59 | "babel-preset-env": "^1.6.1", 60 | "babel-preset-stage-2": "^6.24.1", 61 | "jest": "^22.4.3", 62 | "moment": "^2.21.0", 63 | "ts-jest": "^22.4.2", 64 | "typescript": "^2.8.1", 65 | "veui": "^1.0.0-alpha.9" 66 | }, 67 | "dependencies": {} 68 | } 69 | -------------------------------------------------------------------------------- /src/Error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Error 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | export enum ErrorCode { 6 | ERR_REQUIRED, 7 | // 经过 parseInt 或者 parseFloat 转换之后,结果是 NaN 8 | ERR_NUMBER_FORMAT, 9 | // parseInt 中的 radis 范围是 [2,36] 10 | ERR_RADIX, 11 | 12 | ERR_INVALID_DATE, 13 | 14 | ERR_SCHEMA_NOT_MATCH, 15 | 16 | ERR_CONFIG_END, 17 | 18 | ERR_CONFIG_NOT_INITED, 19 | 20 | ERR_CONFIG_ALREADY_END, 21 | 22 | ERR_CONFIG_CURRENT_IS_NOT_OBJECT, 23 | 24 | ERR_CONFIG_EMPTY_KEY, 25 | 26 | ERR_CONFIG_KEY_EXISTS, 27 | 28 | ERR_CONFIG_CURRENT_NOT_ARRAY, 29 | 30 | ERR_CIRCULAR_OBJECT, 31 | 32 | ERR_WRONG_UP_CONFIG, 33 | 34 | ERR_WRONG_CONFIG, 35 | } 36 | 37 | declare global { 38 | interface Error { 39 | code?: ErrorCode; 40 | extra?: any; 41 | } 42 | } 43 | 44 | /** 45 | * 包一层,给 error 加上 code 。 46 | * 47 | * @param {ErrorCode} code 48 | * @param {string} message 49 | * @param {T} extra 50 | * @return {Error} 51 | */ 52 | export function createError(code: ErrorCode, message: string = 'Error.', extra?: T): Error { 53 | const error = new Error(message); 54 | error.code = code; 55 | error.extra = extra; 56 | return error; 57 | } 58 | -------------------------------------------------------------------------------- /src/JSONTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file JSONTypes 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | 6 | export type JSONBaseType = string | number | true | false | null; 7 | 8 | export type JSONValue = IJSONObject | IJSONArray | JSONBaseType; 9 | 10 | export interface IJSONObject { 11 | [key: string]: JSONValue; 12 | } 13 | 14 | export interface IJSONArray extends Array { 15 | [index: number]: JSONValue; 16 | } 17 | -------------------------------------------------------------------------------- /src/Walker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Walker 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import Config, {IObject} from './config/Config'; 6 | import {isArray, isObject} from './utils'; 7 | import {createError, ErrorCode} from './Error'; 8 | import {KeyPath} from './validate'; 9 | 10 | export interface IResult { 11 | shouldBreak: boolean; 12 | result?: R; 13 | } 14 | 15 | export interface IWalkerOption { 16 | noCircular: boolean; 17 | 18 | /** 19 | * 是否优先根据输入的数据结构来遍历 20 | * 21 | * @type {boolean} 22 | */ 23 | inputFirst: boolean; 24 | 25 | shouldIgnoreUndefined: boolean; 26 | } 27 | 28 | export interface IHandled { 29 | input: any; 30 | result: any; 31 | } 32 | 33 | export default abstract class Walker { 34 | 35 | private config: C; 36 | 37 | protected keyPath: KeyPath = []; 38 | 39 | protected option: IWalkerOption; 40 | 41 | protected handledInputs: IHandled[] = []; 42 | 43 | protected originInput: any; 44 | 45 | public constructor(ConfigType: new (cfg: any) => C, config: any, option: Partial = {}) { 46 | this.config = new ConfigType(config); 47 | this.option = { 48 | noCircular: true, 49 | inputFirst: true, 50 | shouldIgnoreUndefined: true, 51 | ...option, 52 | }; 53 | } 54 | 55 | public run(input: any): any { 56 | this.originInput = input; 57 | const result = this.walk(input, this.config.config).result; 58 | return result; 59 | } 60 | 61 | // config 只能是一个配置节点,类似于`{parser: NumberParser}` 62 | protected abstract handleLeaf(input: any, config: IObject): IResult; 63 | 64 | protected abstract isMatchConfig(input: any, config: any): boolean; 65 | 66 | private findHandled(input: any): IHandled | undefined { 67 | let ret: IHandled | undefined; 68 | this.handledInputs.some((item) => { 69 | if (input === item.input) { 70 | ret = item; 71 | return true; 72 | } 73 | return false; 74 | }); 75 | return ret; 76 | } 77 | 78 | protected walk(input: any, config: any): IResult { 79 | if (isObject(input) || isArray(input)) { 80 | const prevHandled = this.findHandled(input); 81 | if (prevHandled) { 82 | if (this.option.noCircular) { 83 | throw createError(ErrorCode.ERR_CIRCULAR_OBJECT, 'Circular object'); 84 | } 85 | return { 86 | shouldBreak: false, 87 | result: prevHandled.result, 88 | }; 89 | } 90 | } 91 | 92 | if (this.isLeafConfig(config)) { 93 | if (isArray(input)) { 94 | const handled = {input, result: []}; 95 | this.handledInputs.push(handled); 96 | 97 | const ret = this.walkArray(input, config, handled.result); 98 | return ret; 99 | } 100 | 101 | return this.handleLeaf(input, config); 102 | } 103 | 104 | if (isArray(config)) { 105 | if (isArray(input)) { 106 | const handled = {input, result: []}; 107 | this.handledInputs.push(handled); 108 | 109 | const ret = this.walkArray(input, config, handled.result); 110 | return ret; 111 | } 112 | 113 | throw createError( 114 | ErrorCode.ERR_SCHEMA_NOT_MATCH, 115 | `Not match: [val] ${JSON.stringify(input)} [config] ${this.config.stringifyConfig(config)}`, 116 | {config, val: input}, 117 | ); 118 | } 119 | 120 | if (this.config.isUpper(config)) { 121 | const upperConfig = this.config.upConfig(config); 122 | if (!upperConfig) { 123 | throw createError(ErrorCode.ERR_WRONG_UP_CONFIG, `Wrong up config: ${upperConfig}.`); 124 | } 125 | 126 | if (this.isMatchConfig(input, upperConfig)) { 127 | return this.walk(input, upperConfig); 128 | } 129 | } 130 | 131 | if (isObject(config) && isObject(input)) { 132 | const handled = {input, result: {}}; 133 | this.handledInputs.push(handled); 134 | 135 | const ret = this.walkObject(input, config, handled.result); 136 | return ret; 137 | } 138 | 139 | throw createError( 140 | ErrorCode.ERR_SCHEMA_NOT_MATCH, 141 | `Not match: [val] ${JSON.stringify(input)} [config] ${this.config.stringifyConfig(config)}`, 142 | {config, val: input}, 143 | ); 144 | } 145 | 146 | private walkObject(input: IObject, config: IObject, result: IObject): IResult { 147 | this.keyPath.push(''); 148 | 149 | /* tslint:disable forin */ 150 | for (const field in config) { 151 | /* tslint:enable forin */ 152 | const fieldConfig = config[field]; 153 | if (this.option.inputFirst && !(field in input)) { 154 | continue; 155 | } 156 | 157 | this.keyPath[this.keyPath.length - 1] = field; 158 | const ret = this.walk(input[field], fieldConfig); 159 | if (ret.shouldBreak) { 160 | return {shouldBreak: true, result: {...result, [field]: ret.result}}; 161 | } 162 | result[field] = ret.result; 163 | } 164 | 165 | this.keyPath.pop(); 166 | 167 | return { 168 | result, 169 | shouldBreak: false, 170 | }; 171 | } 172 | 173 | // config 要么是一个配置节点,类似于`{parser: NumberParser}`,要么是一个数组。 174 | private walkArray(input: any[], config: any, result: any[]): IResult { 175 | if (isArray(config)) { 176 | this.keyPath.push(-1); 177 | 178 | let lastConfig: any; 179 | let lastResult: any; 180 | const shouldBreak = input.some((val, index) => { 181 | const itemConfig = config[index]; 182 | if (itemConfig) { 183 | this.keyPath[this.keyPath.length - 1] = index; 184 | const ret = this.walk(val, itemConfig); 185 | if (ret.shouldBreak) { 186 | lastResult = ret.result; 187 | return true; 188 | } 189 | result[index] = ret.result; 190 | } 191 | // 配置节点数量少于输入数据量,就直接用之前的配置节点来处理剩下的元素 192 | else if (lastConfig) { 193 | // 预先检查前面的配置节点是否能够应用到当前数据的处理,如果不能,就直接放弃了,而不是抛出“不匹配”的错误。 194 | const isMatch: boolean = this.isMatchConfig(val, lastConfig); 195 | 196 | if (isMatch) { 197 | this.keyPath[this.keyPath.length - 1] = index; 198 | const ret = this.walk(val, lastConfig); 199 | if (ret.shouldBreak) { 200 | lastResult = ret.result; 201 | return true; 202 | } 203 | if (!this.option.shouldIgnoreUndefined || ret.result !== undefined) { 204 | result[index] = ret.result; 205 | } 206 | } 207 | } 208 | 209 | if (itemConfig) { 210 | lastConfig = itemConfig; 211 | } 212 | 213 | return false; 214 | }); 215 | 216 | this.keyPath.pop(); 217 | 218 | if (shouldBreak) { 219 | return {shouldBreak, result: [...result, lastResult]}; 220 | } 221 | } 222 | // 对应一个配置节点 223 | else { 224 | let lastResult: any; 225 | this.keyPath.push(-1); 226 | const shouldBreak = input.some((val, index) => { 227 | this.keyPath[this.keyPath.length - 1] = index; 228 | const ret = this.walk(val, config); 229 | if (ret.shouldBreak) { 230 | lastResult = ret.result; 231 | return true; 232 | } 233 | result[index] = ret.result; 234 | return false; 235 | }, result); 236 | this.keyPath.pop(); 237 | 238 | if (shouldBreak) { 239 | return {shouldBreak, result: [...result, lastResult]}; 240 | } 241 | } 242 | 243 | return { 244 | shouldBreak: false, 245 | result, 246 | }; 247 | } 248 | 249 | protected isLeafConfig(itemConfig: {[key: string]: any}): boolean { 250 | return this.config.isLeaf(itemConfig); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/__test__/DeserializerConfig.spec.ts: -------------------------------------------------------------------------------- 1 | import DeserializerConfig from '../config/DeserializerConfig'; 2 | import NumberParser from '../parsers/NumberParser'; 3 | 4 | describe('DeserializerConfig', () => { 5 | it('should normalize basic config.', () => { 6 | const config = new DeserializerConfig(NumberParser); 7 | expect(config.config.parser).toBe(NumberParser); 8 | }); 9 | 10 | it('should normalize basic object parser config.', () => { 11 | const config = new DeserializerConfig({parser: NumberParser}); 12 | expect(config.config.parser).toBe(NumberParser); 13 | }); 14 | 15 | it('should normalize basic array config.', () => { 16 | const config = new DeserializerConfig([NumberParser]); 17 | expect(config.config[0].parser).toEqual(NumberParser); 18 | }); 19 | 20 | it('should normalize object config nested in array config.', () => { 21 | const config = new DeserializerConfig([ 22 | {parser: NumberParser}, 23 | {age: NumberParser}, 24 | {age: {parser: NumberParser}}, 25 | ]); 26 | expect(config.config[0].parser).toBe(NumberParser); 27 | expect(config.config[1].age.parser).toBe(NumberParser); 28 | expect(config.config[2].age.parser).toBe(NumberParser); 29 | }); 30 | 31 | it('should normalize array config nested in object config.', () => { 32 | const config = new DeserializerConfig({ 33 | age: [NumberParser, {parser: NumberParser}], 34 | }); 35 | expect(config.config.age[0].parser).toBe(NumberParser); 36 | expect(config.config.age[1].parser).toBe(NumberParser); 37 | }); 38 | 39 | it('should save the parent reference.', () => { 40 | const config = new DeserializerConfig([{age: NumberParser}]); 41 | expect(config.up(config.config[0].age, 1)).toMatchObject({age: {parser: NumberParser}}); 42 | expect(config.up(config.config[0], 1)).toMatchObject([{age: {parser: NumberParser}}]); 43 | expect(config.up(config.config[0].age, 2)).toBe(config.up(config.config[0], 1)); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/__test__/deserialize.spec.ts: -------------------------------------------------------------------------------- 1 | import deserialize from '../deserialize'; 2 | import NumberParser from '../parsers/NumberParser'; 3 | import StringParser from '../parsers/StringParser'; 4 | 5 | function testEqual(jsonObject: any, schema: any, result: any) { 6 | expect(deserialize(jsonObject, schema)).toEqual(result); 7 | } 8 | 9 | describe('deserialize', () => { 10 | it('should handle basic convert.', () => { 11 | const jsonObject = { 12 | name: 'yibuyisheng', 13 | age: 20, 14 | }; 15 | const schema = { 16 | name: StringParser, 17 | age: NumberParser, 18 | }; 19 | testEqual(jsonObject, schema, {name: 'yibuyisheng', age: 20}); 20 | }); 21 | 22 | it('should convert number to string.', () => { 23 | const jsonObject = { 24 | name: 123, 25 | }; 26 | const schema = { 27 | name: StringParser, 28 | }; 29 | testEqual(jsonObject, schema, {name: '123'}); 30 | }); 31 | 32 | it('should handle Array.', () => { 33 | testEqual(['yibuyisheng', 20], [StringParser, NumberParser], ['yibuyisheng', 20]); 34 | testEqual({arr: ['yibuyisheng', 20]}, {arr: [StringParser, NumberParser]}, {arr: ['yibuyisheng', 20]}); 35 | }); 36 | 37 | it('should use the last parser to parse the rest elements in a array.', () => { 38 | const jsonObject = ['yibuyisheng', 20]; 39 | const schema = [StringParser]; 40 | testEqual(jsonObject, schema, ['yibuyisheng', '20']); 41 | }); 42 | 43 | it('should support short config for array.', () => { 44 | testEqual(['yibuyisheng', 20], StringParser, ['yibuyisheng', '20']); 45 | testEqual({arr: ['yibuyisheng', 20]}, {arr: StringParser}, {arr: ['yibuyisheng', '20']}); 46 | }); 47 | 48 | it('should handle nested array.', () => { 49 | testEqual( 50 | ['yibuyisheng', 20, ['shanghai', '123']], 51 | [StringParser, NumberParser, StringParser], 52 | ['yibuyisheng', 20, ['shanghai', '123']], 53 | ); 54 | testEqual( 55 | [['shanghai', '123'], 'yibuyisheng', 20], 56 | [StringParser], 57 | [['shanghai', '123'], 'yibuyisheng', '20'], 58 | ); 59 | testEqual( 60 | [['shanghai', '123'], 'yibuyisheng', 20], 61 | [[StringParser]], 62 | [['shanghai', '123']], 63 | ); 64 | testEqual( 65 | [['shanghai', '123'], 'yibuyisheng', 20], 66 | [[StringParser], StringParser], 67 | [['shanghai', '123'], 'yibuyisheng', '20'], 68 | ); 69 | testEqual( 70 | ['yibuyisheng'], 71 | {parser: StringParser}, 72 | ['yibuyisheng'], 73 | ); 74 | testEqual( 75 | [['yibuyisheng']], 76 | [{parser: StringParser}], 77 | [['yibuyisheng']], 78 | ); 79 | }); 80 | 81 | it('should handle normalized parser config in array', () => { 82 | testEqual( 83 | ['yibuyisheng', 20], 84 | [{parser: StringParser}], 85 | ['yibuyisheng', '20'], 86 | ); 87 | }); 88 | 89 | it('should handle the object in array.', () => { 90 | testEqual( 91 | [{name: 'yibuyisheng', age: 20}], 92 | [{name: StringParser, age: NumberParser}], 93 | [{name: 'yibuyisheng', age: 20}], 94 | ); 95 | }); 96 | 97 | it('should handle array with short config.', () => { 98 | testEqual( 99 | ['yibuyisheng', 20], 100 | StringParser, 101 | ['yibuyisheng', '20'], 102 | ); 103 | testEqual( 104 | ['yibuyisheng', 20], 105 | {parser: StringParser}, 106 | ['yibuyisheng', '20'], 107 | ); 108 | testEqual( 109 | {arr: ['yibuyisheng', 20]}, 110 | {arr: {parser: StringParser}}, 111 | {arr: ['yibuyisheng', '20']}, 112 | ); 113 | }); 114 | 115 | it('should handle object with normalized config.', () => { 116 | testEqual( 117 | {name: 'yibuyisheng', age: 20}, 118 | {age: {parser: StringParser}}, 119 | {age: '20'}, 120 | ); 121 | testEqual( 122 | 'yibuyisheng', 123 | {parser: StringParser}, 124 | 'yibuyisheng', 125 | ); 126 | }); 127 | 128 | it('should handle nested object.', () => { 129 | testEqual( 130 | {name: 'yibuyisheng', nested: {name: 'zhangsan', age: 10}}, 131 | {nested: {name: StringParser}}, 132 | {nested: {name: 'zhangsan'}}, 133 | ); 134 | }); 135 | 136 | it('should handle basic value.', () => { 137 | testEqual( 138 | 'yibuyisheng', 139 | StringParser, 140 | 'yibuyisheng', 141 | ); 142 | }); 143 | 144 | it('should throw Error while the array schema is not match.', () => { 145 | expect( 146 | () => deserialize({}, [StringParser]), 147 | ).toThrowError( 148 | `Not match: [val] {} [config] [{parser: StringParser{}}]`, 149 | ); 150 | }); 151 | 152 | it('should throw Error while the object schema is not match.', () => { 153 | expect( 154 | () => deserialize('yibuyisheng', {name: NumberParser}), 155 | ).toThrowError( 156 | 'Not match: [val] "yibuyisheng" [config] {name: {parser: NumberParser{}}}', 157 | ); 158 | }); 159 | 160 | it('should throw Error while the nested schema is not match.', () => { 161 | expect( 162 | () => deserialize([{}], [[StringParser]]), 163 | ).toThrowError( 164 | 'Not match: [val] {} [config] [{parser: StringParser{}}]', 165 | ); 166 | }); 167 | 168 | it('should throw Error while the array nested object is not match.', () => { 169 | expect( 170 | () => deserialize(['yibuyisheng'], [{name: {age: NumberParser}}]), 171 | ).toThrowError( 172 | 'Not match: [val] "yibuyisheng" [config] {name: {age: {parser: NumberParser{}}}}', 173 | ); 174 | }); 175 | 176 | it('should throw Error while the data is not array and the schema is array.', () => { 177 | expect( 178 | () => deserialize({name: 'yibuyisheng'}, {name: [StringParser]}), 179 | ).toThrowError( 180 | 'Not match: [val] "yibuyisheng" [config] [{parser: StringParser{}}]', 181 | ); 182 | }); 183 | 184 | it('should throw Error while the nested object is not match.', () => { 185 | expect( 186 | () => deserialize({name: 'yibuyisheng'}, {name: {age: StringParser}}), 187 | ).toThrowError( 188 | 'Not match: [val] "yibuyisheng" [config] {age: {parser: StringParser{}}}', 189 | ); 190 | }); 191 | 192 | it('should output the `undefined` in scheme.', () => { 193 | expect( 194 | () => deserialize({name: 'yibuyisheng'}, {name: {age: undefined}}), 195 | ).toThrowError( 196 | 'Unknown config: undefined', 197 | ); 198 | }); 199 | 200 | it('should handle the same shape in array.', () => { 201 | testEqual( 202 | [{name: 'yibuyisheng', age: 21}, {name: 'zhangsan', age: 22}], 203 | [{name: StringParser, age: StringParser}], 204 | [{name: 'yibuyisheng', age: '21'}, {name: 'zhangsan', age: '22'}], 205 | ); 206 | }); 207 | 208 | it('should handle recursive schema.', () => { 209 | testEqual( 210 | [{label: '四川', value: 'SC', children: [{label: '达州', value: 'DZ'}, {label: '简阳', value: 'JY'}]}], 211 | [{label: StringParser, value: StringParser, children: '^2'}], 212 | [{label: '四川', value: 'SC', children: [{label: '达州', value: 'DZ'}, {label: '简阳', value: 'JY'}]}], 213 | ); 214 | }); 215 | 216 | }); 217 | -------------------------------------------------------------------------------- /src/__test__/validate.spec.ts: -------------------------------------------------------------------------------- 1 | import validate from '../validate'; 2 | import VEUIRulesValidator from '../validators/VEUIRulesValidator'; 3 | 4 | describe('validate', () => { 5 | it('should validate primary value.', () => { 6 | expect(validate('123', { 7 | validator: VEUIRulesValidator, 8 | rules: 'required' 9 | })).toBe(true); 10 | 11 | expect(validate(null, {validator: VEUIRulesValidator, rules: 'required'})).toEqual( 12 | { 13 | detail: [ 14 | { 15 | message: '请填写', 16 | name: 'required' 17 | } 18 | ], 19 | keyPath: [], 20 | message: 'Validate fail with VEUI rules.', 21 | } 22 | ); 23 | 24 | expect(validate(undefined, {validator: VEUIRulesValidator, rules: 'required'})).toEqual( 25 | { 26 | detail: [ 27 | { 28 | message: '请填写', 29 | name: 'required' 30 | } 31 | ], 32 | keyPath: [], 33 | message: 'Validate fail with VEUI rules.', 34 | } 35 | ); 36 | }); 37 | 38 | it('should handle circular object.', () => { 39 | const obj1: Record = {}; 40 | const obj2: Record = {}; 41 | 42 | obj1.name = 'zhangsan'; 43 | obj2.name = 'lisi'; 44 | 45 | obj1.child = obj2; 46 | obj2.child = obj1; 47 | 48 | const result = validate( 49 | obj1, 50 | { 51 | name: { 52 | validator: VEUIRulesValidator, rules: [{name: 'required'}], 53 | }, 54 | child: '^1', 55 | } 56 | ); 57 | expect(result.name).toBe(true); 58 | expect(result.child.child).toBe(result); 59 | }); 60 | 61 | it('should validate object.', () => { 62 | const result = validate( 63 | { 64 | name: 'yibuyisheng' 65 | }, 66 | { 67 | name: { 68 | validator: VEUIRulesValidator, 69 | rules: [ 70 | { 71 | name: 'pattern', 72 | value: /^[a-z]+$/ 73 | } 74 | ] 75 | } 76 | } 77 | ); 78 | expect(result).toEqual({name: true}); 79 | }); 80 | 81 | it('should get invalid messages while the validation fails.', () => { 82 | const result = validate( 83 | { 84 | name: { 85 | first: 'Li', 86 | last: 'Zhang', 87 | } 88 | }, 89 | { 90 | name: { 91 | validator: VEUIRulesValidator, 92 | rules: [ 93 | { 94 | name: 'pattern', 95 | value: /^[a-z]+$/, 96 | message: '格式不符合要求', 97 | } 98 | ] 99 | } 100 | } 101 | ); 102 | expect(result).toEqual( 103 | { 104 | name: { 105 | message: 'Validate fail with VEUI rules.', 106 | detail: [ 107 | { 108 | name: 'pattern', 109 | message: '格式不符合要求' 110 | } 111 | ], 112 | keyPath: ['name'] 113 | } 114 | } 115 | ); 116 | }); 117 | 118 | it('should get `true` while the validation succeed.', () => { 119 | const result = validate( 120 | { 121 | name: { 122 | first: 'Li', 123 | last: 'Zhang', 124 | } 125 | }, 126 | { 127 | name: { 128 | first: { 129 | validator: VEUIRulesValidator, 130 | rules: [ 131 | { 132 | name: 'pattern', 133 | value: /^[a-z]+$/i, 134 | message: '格式不符合要求', 135 | } 136 | ], 137 | } 138 | } 139 | } 140 | ); 141 | expect(result).toEqual({name: {first: true}}); 142 | }); 143 | 144 | it('should get flatten error message while set the flatten option.', () => { 145 | const result = validate( 146 | { 147 | name: { 148 | first: 'Li', 149 | last: 'Zhang', 150 | }, 151 | }, 152 | { 153 | name: { 154 | validator: VEUIRulesValidator, 155 | rules: [ 156 | { 157 | name: 'pattern', 158 | value: /^[a-z]+$/, 159 | message: '格式不符合要求', 160 | } 161 | ] 162 | }, 163 | }, 164 | { 165 | flattenResult: true, 166 | } 167 | ); 168 | expect(result).toEqual([ 169 | { 170 | keyPath: ['name'], 171 | result: { 172 | detail: [{message: '格式不符合要求', name: 'pattern'}], 173 | keyPath: ['name'], 174 | message: 'Validate fail with VEUI rules.', 175 | }, 176 | } 177 | ]); 178 | }); 179 | 180 | it('should get all flatten error messages while set the `all` option.', () => { 181 | const result = validate( 182 | { 183 | name: ['Li', 'Zhang'], 184 | }, 185 | { 186 | name: { 187 | validator: VEUIRulesValidator, 188 | rules: [ 189 | { 190 | name: 'pattern', 191 | value: /^[a-z]+$/, 192 | message: '格式不符合要求', 193 | } 194 | ] 195 | }, 196 | }, 197 | { 198 | flattenResult: true, 199 | all: true, 200 | } 201 | ); 202 | expect(result).toEqual([ 203 | { 204 | keyPath: ['name', 0], 205 | result: { 206 | detail: [{message: '格式不符合要求', name: 'pattern'}], 207 | keyPath: ['name', 0], 208 | message: 'Validate fail with VEUI rules.', 209 | }, 210 | }, 211 | { 212 | keyPath: ['name', 1], 213 | result: { 214 | detail: [{message: '格式不符合要求', name: 'pattern'}], 215 | keyPath: ['name', 1], 216 | message: 'Validate fail with VEUI rules.', 217 | }, 218 | } 219 | ]); 220 | }); 221 | 222 | it('should handle nested arrays.', () => { 223 | let result = validate( 224 | [[20]], 225 | [[{validator: VEUIRulesValidator, rules: [{name: 'max', value: 21}]}]], 226 | { 227 | flattenResult: true, 228 | all: true, 229 | } 230 | ); 231 | expect(result).toEqual(true); 232 | 233 | result = validate( 234 | [[18, 20, 17, 23]], 235 | [[{validator: VEUIRulesValidator, rules: [{name: 'max', value: 19}]}]], 236 | { 237 | flattenResult: true, 238 | all: true, 239 | } 240 | ); 241 | expect(result).toEqual([ 242 | { 243 | keyPath: [0, 1], 244 | result: { 245 | detail: [ 246 | { 247 | message: '不能大于19', 248 | name: 'max', 249 | } 250 | ], 251 | keyPath: [0, 1], 252 | message: 'Validate fail with VEUI rules.', 253 | } 254 | }, 255 | { 256 | keyPath: [0, 3], 257 | result: { 258 | detail: [ 259 | { 260 | message: '不能大于19', 261 | name: 'max' 262 | } 263 | ], 264 | keyPath: [0, 3], 265 | message: 'Validate fail with VEUI rules.' 266 | } 267 | } 268 | ]); 269 | }); 270 | 271 | it('should throw error by default while encounter the circular object.', () => { 272 | const obj1: Record = {}; 273 | const obj2: Record = {}; 274 | 275 | obj1.name = 'zhangsan'; 276 | obj2.name = 'lisi'; 277 | 278 | obj1.child = obj2; 279 | obj2.child = obj1; 280 | 281 | const fn = () => validate( 282 | obj1, 283 | { 284 | name: { 285 | validator: VEUIRulesValidator, rules: [{name: 'required'}], 286 | }, 287 | child: '^1', 288 | }, 289 | { 290 | noCircular: true, 291 | } 292 | ); 293 | expect(fn).toThrowError('Circular object'); 294 | }); 295 | 296 | it('should return error while the target is not exist on the object.', () => { 297 | const result = validate( 298 | {}, 299 | {name: {validator: VEUIRulesValidator, rules: [{name: 'required'}]}} 300 | ); 301 | expect(result).toEqual({"name": {"detail": [{"message": "请填写", "name": "required"}], "keyPath": ["name"], "message": "Validate fail with VEUI rules."}}); 302 | }); 303 | 304 | it('should not return error while validating the non-exist property if set the `inputFirst` option to `true`.', () => { 305 | const result = validate( 306 | {}, 307 | {name: {validator: VEUIRulesValidator, rules: [{name: 'required'}]}}, 308 | {inputFirst: true, flattenResult: true} 309 | ); 310 | expect(result).toEqual(true); 311 | }); 312 | 313 | }); 314 | -------------------------------------------------------------------------------- /src/config/Config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Config 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import {isArray, isObject} from '../utils'; 6 | import {createError, ErrorCode} from '../Error'; 7 | 8 | const PARENT_KEY = typeof (window as any).Symbol === 'undefined' ? '__parent__' : Symbol('parent'); 9 | const STEP_KEY = typeof (window as any).Symbol === 'undefined' ? '__step__' : Symbol('step'); 10 | 11 | export interface IObject { 12 | [key: string]: any; 13 | } 14 | 15 | export default abstract class Config { 16 | public readonly config: any; 17 | 18 | public constructor(config: any) { 19 | this.config = this.normalize(config); 20 | } 21 | 22 | public upConfig(item: any): any { 23 | if (!isObject(item) || !item[STEP_KEY]) { 24 | throw createError(ErrorCode.ERR_WRONG_UP_CONFIG, `Wrong up config: ${item}.`); 25 | } 26 | 27 | const current = item; 28 | const step = item[STEP_KEY]; 29 | return this.up(current, step); 30 | } 31 | 32 | public up(current: any, step: number): any { 33 | if (step === 0) { 34 | return current; 35 | } 36 | 37 | if (!isObject(current)) { 38 | return; 39 | } 40 | 41 | return this.up((current as any)[PARENT_KEY], step - 1); 42 | } 43 | 44 | public isUpper(item: any): boolean { 45 | return typeof item === 'string' && /^\^[0-9]+$/.test(item) || (isObject(item) && item[STEP_KEY]); 46 | } 47 | 48 | public stringifyConfig(config: any): string { 49 | if (config instanceof Array) { 50 | const output: string[] = []; 51 | config.reduce((prev, cur) => { 52 | prev.push(this.stringifyConfig(cur)); 53 | return prev; 54 | }, output); 55 | return `[${output.join(', ')}]`; 56 | } 57 | 58 | if (isObject(config)) { 59 | const output: string[] = []; 60 | /* tslint:disable forin */ 61 | for (const key in config) { 62 | /* tslint:enable forin */ 63 | output.push(`${key}: ${this.stringifyConfig(config[key])}`); 64 | } 65 | return `{${output.join(', ')}}`; 66 | } 67 | 68 | if (this.isLeaf(config)) { 69 | return config.toString(); 70 | } 71 | 72 | return JSON.stringify(config); 73 | } 74 | 75 | public abstract isLeaf(item: any): boolean; 76 | 77 | protected abstract normalizeLeaf(item: any): any; 78 | 79 | private normalizeArray(config: any[]): any[] | undefined { 80 | const result: any[] = []; 81 | config.forEach((item) => { 82 | const itemConfig = this.normalize(item); 83 | if (isObject(itemConfig) || isArray(itemConfig)) { 84 | itemConfig[PARENT_KEY] = config; 85 | result.push(itemConfig); 86 | } 87 | }); 88 | return result.length ? result : undefined; 89 | } 90 | 91 | private normalizeObject(config: IObject): IObject | undefined { 92 | const result: IObject = config; 93 | /* tslint:disable forin */ 94 | for (const key in config) { 95 | /* tslint:enable forin */ 96 | const cfg = this.normalize(config[key]); 97 | if (isObject(cfg) || isArray(cfg)) { 98 | cfg[PARENT_KEY] = config; 99 | result[key] = cfg; 100 | } 101 | } 102 | return Object.keys(result).length ? result : undefined; 103 | } 104 | 105 | private normalize(config: any) { 106 | if (this.isLeaf(config)) { 107 | const result = this.normalizeLeaf(config); 108 | return result; 109 | } 110 | 111 | if (this.isUpper(config)) { 112 | return { 113 | [STEP_KEY]: parseInt(config.replace('^', ''), 10) 114 | }; 115 | } 116 | 117 | if (isArray(config)) { 118 | const result = this.normalizeArray(config); 119 | return result; 120 | } 121 | 122 | if (isObject(config)) { 123 | const result = this.normalizeObject(config); 124 | return result; 125 | } 126 | 127 | throw createError(ErrorCode.ERR_WRONG_CONFIG, `Unknown config: ${JSON.stringify(config)}`); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/config/DeserializeConfigTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ConfigTypes 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import Parser, {IParserOption} from '../parsers/Parser'; 6 | 7 | export interface IParserConstructor { 8 | new (options?: IParserOption | Record): Parser; 9 | } 10 | 11 | export interface IArrayConfig extends Array {} 12 | 13 | export interface IFieldParserConfig { 14 | parser: IParserConstructor; 15 | from?: string; 16 | [key: string]: any; 17 | } 18 | 19 | export interface IObjectConfig { 20 | [field: string]: IParserConstructor | IFieldParserConfig | IArrayConfig | IObjectConfig; 21 | } 22 | 23 | export type ConfigValue = IParserConstructor | IFieldParserConfig | IArrayConfig | IObjectConfig; 24 | -------------------------------------------------------------------------------- /src/config/DeserializerConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 经过 normalize 的配置 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import Config from './Config'; 6 | import {ConfigValue, IFieldParserConfig, IParserConstructor} from './DeserializeConfigTypes'; 7 | import {isParserConfig, isParserConstructor} from '../utils'; 8 | 9 | export type ParserNode = IParserConstructor | IFieldParserConfig; 10 | 11 | export default class DeserializerConfig extends Config { 12 | public isLeaf(item: ConfigValue): boolean { 13 | return isParserConstructor(item) || isParserConfig(item); 14 | } 15 | 16 | protected normalizeLeaf(value: ParserNode): IFieldParserConfig { 17 | return isParserConfig(value) 18 | ? {...value} 19 | : {parser: value}; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/config/ValidatorConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ValidatorConfig 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import Config from './Config'; 6 | import {ConfigValue, IFieldValidatorConfig, IValidatorConstructor} from './ValidatorConfigTypes'; 7 | import {isValidatorConfig, isValidatorConstructor} from '../utils'; 8 | 9 | export type ValidatorNode = IValidatorConstructor | IFieldValidatorConfig; 10 | 11 | export default class ValidatorConfig extends Config { 12 | public isLeaf(item: ConfigValue): boolean { 13 | return isValidatorConstructor(item) || isValidatorConfig(item); 14 | } 15 | 16 | protected normalizeLeaf(value: ValidatorNode): IFieldValidatorConfig { 17 | return isValidatorConfig(value) 18 | ? {...(value as IFieldValidatorConfig)} 19 | : {validator: value as IValidatorConstructor}; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/config/ValidatorConfigTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ValidatorConfigTypes 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import Validator, {IValidatorOption} from '../validators/Validator'; 6 | 7 | export interface IValidatorConstructor { 8 | new (options?: IValidatorOption | Record): Validator; 9 | } 10 | 11 | export interface IArrayConfig extends Array { 12 | [field: number]: ConfigValue; 13 | } 14 | 15 | export interface IFieldValidatorConfig { 16 | validator: IValidatorConstructor; 17 | from?: string; 18 | [key: string]: any; 19 | } 20 | 21 | export interface IObjectConfig { 22 | [field: string]: IValidatorConstructor | IFieldValidatorConfig | IArrayConfig | IObjectConfig; 23 | } 24 | 25 | export type ConfigValue = IValidatorConstructor | IFieldValidatorConfig | IArrayConfig | IObjectConfig; 26 | -------------------------------------------------------------------------------- /src/deserialize.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file deserialize 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import { 6 | ConfigValue, 7 | IFieldParserConfig, 8 | } from './config/DeserializeConfigTypes'; 9 | import {JSONValue} from './JSONTypes'; 10 | import { 11 | isArrayConfig, 12 | isJSONArray, 13 | isJSONObject, 14 | isObjectConfig, 15 | isParserConfig, 16 | } from './utils'; 17 | import DeserializerConfig from './config/DeserializerConfig'; 18 | import Walker, {IResult, IWalkerOption} from './Walker'; 19 | 20 | export interface IDeserializeOption extends IWalkerOption {} 21 | 22 | class Deserializer extends Walker { 23 | 24 | public constructor(config: any, option: Partial = {}) { 25 | super(DeserializerConfig, config, option); 26 | } 27 | 28 | protected handleLeaf(input: any, config: IFieldParserConfig): IResult { 29 | const ParserClass = config.parser; 30 | const parser = new ParserClass(config); 31 | return { 32 | result: parser.parse(input), 33 | shouldBreak: false, 34 | }; 35 | } 36 | 37 | protected isMatchConfig(input: any, config: any): boolean { 38 | // 预先检查前面的 parser 是否能够应用到当前 JSON 数据的转换,如果不能,就直接放弃了,而不是抛出“不匹配”的错误。 39 | const isMatch: boolean = 40 | // **注意:**在 JSON 里面没有 undefined ,所以遇到 undefined ,其实就是在原 JSON 数据里面不存在。 41 | // 如果 lastParserConfig 是一个展开的 parser 配置( {parser: ParserClass } ),那么只要当前元素存在就可以转换。 42 | (isParserConfig(config) && (!this.option.shouldIgnoreUndefined || input !== undefined)) 43 | // 如果 lastParserConfig 是一个数组,那么只要 input 也是数组就可以转换。 44 | || (isArrayConfig(config) && isJSONArray(input)) 45 | // 如果 lastParserConfig 是一个对象配置,那么只要 input 也对应是对象,就可以转换。 46 | || (isObjectConfig(config) && isJSONObject(input)); 47 | return isMatch; 48 | } 49 | } 50 | 51 | export default function deserializer( 52 | jsonObject: JSONValue, 53 | config: ConfigValue, 54 | option: Partial = {}, 55 | ): any { 56 | return new Deserializer(config, option).run(jsonObject); 57 | } 58 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 导出外部能用的。 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | 6 | export {default as BooleanParser} from './parsers/BooleanParser'; 7 | export {default as DateParser} from './parsers/DateParser'; 8 | export {default as NumberParser} from './parsers/NumberParser'; 9 | export {default as StringParser} from './parsers/StringParser'; 10 | export {default as SeparateArrayParser} from './parsers/SeparateArrayParser'; 11 | 12 | export {default as VEUIRulesValidator} from './validators/VEUIRulesValidator'; 13 | 14 | export {ErrorCode} from './Error'; 15 | 16 | export {default as deserialize} from './deserialize'; 17 | export {default as validate} from './validate'; 18 | -------------------------------------------------------------------------------- /src/parsers/BooleanParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file BooleanParser 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import Parser from './Parser'; 6 | 7 | /** 8 | * 字符串转换器 9 | */ 10 | export default class BooleanParser extends Parser { 11 | /** 12 | * @override 13 | */ 14 | public parse(input: I): boolean | undefined { 15 | return input === undefined ? undefined : !!input; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/parsers/DateParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file DateParser 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import * as moment from 'moment'; 6 | import {createError, ErrorCode} from '../Error'; 7 | import Parser, {IParserOption} from './Parser'; 8 | 9 | export interface IDateOption extends IParserOption { 10 | format?: string; 11 | } 12 | 13 | /** 14 | * 字符串转换器 15 | */ 16 | export default class DateParser extends Parser { 17 | 18 | /** 19 | * 输入日期的格式。 20 | * 21 | * @public 22 | * @type {string} 23 | */ 24 | private format: string; 25 | 26 | /** 27 | * @override 28 | */ 29 | public constructor(options: IDateOption = {}) { 30 | super(options); 31 | 32 | this.format = options.format || 'YYYYMMDDHHmmss'; 33 | } 34 | 35 | /** 36 | * @override 37 | */ 38 | public parse(input: I): Date | undefined { 39 | this.checkEmpty(input); 40 | if (input === undefined) { 41 | return undefined; 42 | } 43 | 44 | const result = moment('' + input, this.format); 45 | if (!result.isValid()) { 46 | throw createError(ErrorCode.ERR_INVALID_DATE, undefined, result); 47 | } 48 | return result.toDate(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/parsers/NumberParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NumberParser 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import {createError, ErrorCode} from '../Error'; 6 | import Parser, {IParserOption} from './Parser'; 7 | 8 | export interface INumberOption extends IParserOption { 9 | isInt?: boolean; 10 | radix?: 10; 11 | } 12 | 13 | /** 14 | * 数字转换器 15 | */ 16 | export default class NumberParser extends Parser { 17 | /** 18 | * 是否使用 parseInt 来转换 19 | * 20 | * @private 21 | * @type {boolean} 22 | */ 23 | private isInt: boolean; 24 | 25 | /** 26 | * 使用 parseInt 转换时的 radix ,范围是 [2,36] 27 | * 28 | * @private 29 | * @type {number} 30 | */ 31 | private radix: number; 32 | 33 | /** 34 | * @override 35 | */ 36 | public constructor(options: INumberOption = {}) { 37 | super(options); 38 | 39 | this.isInt = options.isInt || false; 40 | 41 | this.radix = options.radix || 10; 42 | if (this.radix < 2 && this.radix > 36) { 43 | throw createError(ErrorCode.ERR_RADIX); 44 | } 45 | } 46 | 47 | /** 48 | * @override 49 | */ 50 | public parse(input: I): number | undefined { 51 | this.checkEmpty(input); 52 | if (input === undefined) { 53 | return undefined; 54 | } 55 | 56 | const result = this.isInt ? parseInt('' + input, this.radix) : parseFloat('' + input); 57 | if (isNaN(result)) { 58 | throw createError(ErrorCode.ERR_NUMBER_FORMAT); 59 | } 60 | return result; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/parsers/Parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Parser 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import {createError, ErrorCode} from '../Error'; 6 | 7 | export interface IParserOption { 8 | isRequired?: boolean; 9 | } 10 | 11 | /** 12 | * 对某一个属性值进行转换的转换器 13 | */ 14 | export default abstract class Parser { 15 | /** 16 | * @override 17 | */ 18 | public static toString() { 19 | return `${this.name}{}`; 20 | } 21 | 22 | /** 23 | * 该属性值是否是必须的。 24 | * 25 | * @protected 26 | * @type {boolean} 27 | */ 28 | protected isRequired: boolean; 29 | 30 | /** 31 | * 构造函数 32 | * @param {boolean} isRequired 是否必须。 33 | */ 34 | public constructor(options: IParserOption = {}) { 35 | this.isRequired = options.isRequired || false; 36 | } 37 | 38 | /** 39 | * 转换方法。 40 | * 41 | * @public 42 | * @param {I} input 待转换的值 43 | * @return {O} 44 | */ 45 | public abstract parse(input: I): O; 46 | 47 | protected checkEmpty(input: any): void { 48 | if (this.isRequired && this.isEmpty(input)) { 49 | throw createError(ErrorCode.ERR_REQUIRED); 50 | } 51 | } 52 | 53 | /** 54 | * 判断是否为空( null 或者 undefined ) 55 | * 56 | * @param {any} val 待判断的值 57 | * @return {boolean} 58 | */ 59 | protected isEmpty(val: any): boolean { 60 | return val == null; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/parsers/SeparateArrayParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 将特定字符分割的字符串转换成字符串数组,比如:'1,2,3' -> [1,2,3] 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import Parser, {IParserOption} from './Parser'; 6 | 7 | export interface ISeparateArrayOption extends IParserOption { 8 | separator?: RegExp; 9 | noEmpty?: boolean; 10 | } 11 | 12 | export default class SeparateArrayParser extends Parser { 13 | /** 14 | * 分割器 15 | * 16 | * @private 17 | * @type {RegExp} 18 | */ 19 | private separator: RegExp; 20 | 21 | /** 22 | * 最后得到的数组是否能包含空字符串 23 | * 24 | * @private 25 | * @type {boolean} 26 | */ 27 | private noEmpty: boolean; 28 | 29 | /** 30 | * @override 31 | */ 32 | public constructor(options: ISeparateArrayOption = {}) { 33 | super(options); 34 | 35 | this.separator = options.separator || new RegExp('/,/'); 36 | this.noEmpty = options.noEmpty || true; 37 | } 38 | 39 | /** 40 | * @override 41 | */ 42 | public parse(input: T): string[] | undefined { 43 | this.checkEmpty(input); 44 | if (input === undefined) { 45 | return undefined; 46 | } 47 | 48 | const result = ('' + input).split(this.separator); 49 | return this.noEmpty ? result.filter((i) => !!i) : result; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/parsers/StringParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file StringParser 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import Parser from './Parser'; 6 | 7 | /** 8 | * 字符串转换器 9 | */ 10 | export default class StringParser extends Parser { 11 | /** 12 | * @override 13 | */ 14 | public parse(input: I): string | undefined { 15 | this.checkEmpty(input); 16 | if (input === undefined) { 17 | return undefined; 18 | } 19 | 20 | return '' + input; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import Parser from './parsers/Parser'; 6 | import Validator from './validators/Validator'; 7 | import {IJSONArray, IJSONObject} from './JSONTypes'; 8 | import {IArrayConfig, IParserConstructor, IFieldParserConfig} from './config/DeserializeConfigTypes'; 9 | import {IObjectConfig, IValidatorConstructor} from './config/ValidatorConfigTypes'; 10 | import ValidatorConfig from './config/ValidatorConfig'; 11 | 12 | export function isArray(val: any): val is any[] { 13 | return Array.isArray(val); 14 | } 15 | 16 | export function isObject(val: any): val is Record { 17 | return val !== null && typeof val === 'object' && !isArray(val); 18 | } 19 | 20 | export function isJSONArray(val: any): val is IJSONArray { 21 | return isArray(val); 22 | } 23 | 24 | export function isArrayConfig(val: any): val is IArrayConfig { 25 | return isArray(val); 26 | } 27 | 28 | export function isJSONObject(val: any): val is IJSONObject { 29 | return isObject(val); 30 | } 31 | 32 | export function isObjectConfig(val: any): val is IObjectConfig { 33 | return isObject(val); 34 | } 35 | 36 | export function isParserConstructor(parser: any): parser is IParserConstructor { 37 | if (!parser) { 38 | return false; 39 | } 40 | 41 | const proto = Object.getPrototypeOf(parser); 42 | return proto === Parser || proto instanceof Parser; 43 | } 44 | 45 | export function isParserConfig(config: any): config is IFieldParserConfig { 46 | return isObject(config) && isParserConstructor(config.parser); 47 | } 48 | 49 | export function isValidatorConstructor(validator: any): validator is IValidatorConstructor { 50 | if (!validator) { 51 | return false; 52 | } 53 | 54 | const proto = Object.getPrototypeOf(validator); 55 | return proto === Validator || proto instanceof Validator; 56 | } 57 | 58 | export function isValidatorConfig(config: any): config is ValidatorConfig { 59 | return isObject(config) && isValidatorConstructor(config.validator); 60 | } 61 | -------------------------------------------------------------------------------- /src/validate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file validate 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import Walker, {IResult, IWalkerOption} from './Walker'; 6 | import ValidatorConfig from './config/ValidatorConfig'; 7 | import {IFieldValidatorConfig} from './config/ValidatorConfigTypes'; 8 | import {isValidatorConfig, isArray, isObject} from './utils'; 9 | import {ValidateResult, IValidateError} from './validators/Validator'; 10 | 11 | export interface IValidatorControllerOption extends IWalkerOption { 12 | all: boolean; 13 | flattenResult: boolean; 14 | } 15 | 16 | export type KeyPath = Array; 17 | export type FlattenResult = Array<{keyPath: KeyPath, result: ValidateResult}>; 18 | 19 | class ValidatorController extends Walker { 20 | private flattenResult: FlattenResult = []; 21 | 22 | protected option: IValidatorControllerOption; 23 | 24 | public constructor(config: any, option: Partial = {}) { 25 | super(ValidatorConfig, config, option); 26 | 27 | this.option = { 28 | all: false, 29 | flattenResult: false, 30 | noCircular: false, 31 | inputFirst: false, 32 | shouldIgnoreUndefined: true, 33 | ...option, 34 | }; 35 | } 36 | 37 | public run(input: any): any { 38 | const result = super.run(input); 39 | 40 | if (result === true) { 41 | return true; 42 | } 43 | 44 | if (this.option.flattenResult) { 45 | return this.flattenResult.length ? this.flattenResult : true; 46 | } 47 | 48 | return result; 49 | } 50 | 51 | protected handleLeaf(input: any, config: IFieldValidatorConfig): IResult> { 52 | const ValidatorClass = config.validator; 53 | const validator = new ValidatorClass(config); 54 | const result = validator.validate(input, [...this.keyPath], this.originInput); 55 | 56 | if (this.option.flattenResult) { 57 | if (result !== true) { 58 | this.flattenResult.push({ 59 | keyPath: [...this.keyPath], 60 | result, 61 | }); 62 | } 63 | 64 | return !this.option.all && result !== true 65 | ? {result, shouldBreak: true} 66 | : {result, shouldBreak: false}; 67 | } 68 | 69 | if (result !== true) { 70 | return { 71 | result, 72 | shouldBreak: !this.option.all, 73 | }; 74 | } 75 | 76 | return {result: true, shouldBreak: false}; 77 | } 78 | 79 | protected isMatchConfig(input: any, config: any): boolean { 80 | const isMatch: boolean = (isValidatorConfig(config) && input !== undefined) 81 | || (isArray(config) && isArray(input)) 82 | || (isObject(config) && isObject(input)); 83 | return isMatch; 84 | } 85 | } 86 | 87 | export default function validate(input: any, config: any, option?: Partial) { 88 | return new ValidatorController(config, option).run(input); 89 | } 90 | -------------------------------------------------------------------------------- /src/validators/VEUIRule.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file VEUIRule 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | declare module 'veui/managers/rule' { 6 | export type VEUIValidateResult = true | Array<{name: string; message: string;}>; 7 | 8 | const rule: { 9 | validate(val: any, rules: string | any[]): VEUIValidateResult; 10 | }; 11 | export default rule; 12 | } 13 | -------------------------------------------------------------------------------- /src/validators/VEUIRulesValidator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file VEUIRulesValidators 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import Validator, {ValidateResult, IValidatorOption, IValidateError} from './Validator'; 6 | import rule, {VEUIValidateResult} from 'veui/managers/rule'; 7 | import {KeyPath} from '../validate'; 8 | 9 | export interface IVEUIRulesValidatorOption extends IValidatorOption { 10 | rules: string | Array<{name: string;}>; 11 | } 12 | 13 | export interface IVEUIRuleValidateError extends IValidateError { 14 | detail: VEUIValidateResult; 15 | } 16 | 17 | export default class VEUIRulesValidator extends Validator { 18 | private rules: Array<{name: string;}>; 19 | 20 | public constructor(option: Partial = {}) { 21 | super(option); 22 | 23 | if (option.rules === undefined) { 24 | this.rules = []; 25 | } else if (typeof option.rules === 'string') { 26 | this.rules = [{name: option.rules}]; 27 | } else { 28 | this.rules = option.rules; 29 | } 30 | } 31 | 32 | public validate( 33 | input: I, 34 | keyPath: KeyPath, 35 | _: FI, 36 | ): ValidateResult { 37 | const ret = rule.validate(input, this.rules); 38 | if (ret === true) { 39 | return true; 40 | } 41 | 42 | return { 43 | message: 'Validate fail with VEUI rules.', 44 | detail: ret, 45 | keyPath: keyPath, 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/validators/Validator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Validator 3 | * @author yibuyisheng(yibuyisheng@163.com) 4 | */ 5 | import {createError, ErrorCode} from '../Error'; 6 | import {KeyPath} from '../validate'; 7 | 8 | export interface IValidatorOption { 9 | isRequired: boolean; 10 | } 11 | 12 | export interface IValidateError { 13 | keyPath: KeyPath; 14 | message: string; 15 | detail: any; 16 | } 17 | 18 | export type ValidateResult = true | E; 19 | 20 | export default abstract class Validator { 21 | /** 22 | * 该属性值是否是必须的。 23 | * 24 | * @protected 25 | * @type {boolean} 26 | */ 27 | protected isRequired: boolean; 28 | 29 | /** 30 | * @override 31 | */ 32 | public static toString() { 33 | return `${this.name}{}`; 34 | } 35 | 36 | /** 37 | * 构造函数 38 | * @param {boolean} isRequired 是否必须。 39 | */ 40 | public constructor(options: Partial = {}) { 41 | this.isRequired = options.isRequired || false; 42 | } 43 | 44 | /** 45 | * 转换方法。 46 | * 47 | * @public 48 | * @param {I} input 待验证的值 49 | * @param {KeyPath} keyPath key path 50 | * @param {FI} fullInput 完整输入数据 51 | * @return {ValidateResult} 52 | */ 53 | public abstract validate( 54 | input: I, 55 | keyPath: KeyPath, 56 | fullInput: FI, 57 | ): ValidateResult; 58 | 59 | protected checkEmpty(input: any): void { 60 | if (this.isRequired && this.isEmpty(input)) { 61 | throw createError(ErrorCode.ERR_REQUIRED); 62 | } 63 | } 64 | 65 | /** 66 | * 判断是否为空( null 或者 undefined ) 67 | * 68 | * @param {any} val 待判断的值 69 | * @return {boolean} 70 | */ 71 | protected isEmpty(val: T): boolean { 72 | return val == null || (typeof val === 'string' && val === '') || (Array.isArray(val) && !val.length); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tool/tag.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const cp = require('child_process'); 3 | const version = require('../package.json').version; 4 | 5 | cp.execSync(`git tag v${version}`, {stdio: 'inherit'}); 6 | cp.execSync(`git push origin v${version}`, {stdio: 'inherit'}); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "preserveSymlinks": true, 5 | "strict": true, 6 | "traceResolution": true, 7 | "version": true, 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "module": "esnext", 11 | "target": "es2015", 12 | "lib": [ 13 | "es2015", 14 | "dom" 15 | ], 16 | "outDir": "./lib", 17 | "watch": false, 18 | "declaration": true, 19 | "declarationDir": "./types", 20 | "baseUrl": ".", 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true 23 | }, 24 | "include": [ 25 | "./src/**/*" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "lib", 30 | "bin", 31 | "types", 32 | "**/*.spec.ts" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended"], 3 | "rules": { 4 | "quotemark": [true, "single"], 5 | "no-submodule-imports": false, 6 | "object-literal-sort-keys": false, 7 | "space-before-function-paren": false, 8 | "no-empty": false, 9 | "one-line": false, 10 | "no-unused-variable": true 11 | } 12 | } 13 | --------------------------------------------------------------------------------