Commit 7bae8ce5 authored by nanahira's avatar nanahira

now its just stub

parent 27853d9a
# koishi-utils-schemagen # koishi-utils-schemagen
在 Koishi.js 中,使用类装饰器定义 Schema。 Deprecated. Use [schemastery-gen](https://npmjs.com/package/schemastery-gen) instead.
## 安装 Things in this package is just for compatibility.
\ No newline at end of file
```
npm install koishi-utils-schemagen reflect-metadata
```
## 使用
### 类 Schema 定义
```ts
// 使用装饰器定义 Schema。
// SchemaConf 填写 Schema 本身的顶级属性。
@SchemaConf({
desc: 'my desc',
})
export class Config {
// 基本的数据类型
@DefineSchema({ type: 'number', required: true })
foo: number;
// 非数组类型会尝试自动推断类型
@DefineSchema()
fooAutomated: number;
@DefineSchema({ type: 'string', default: 'shigma' })
bar: string;
@DefineSchema({ type: 'boolean', default: true, hidden: true })
baz: boolean;
// 数组,系统会自动推断该类型为数组,但是此时 type 不可省略
@DefineSchema({ type: 'string', default: ['foo', 'bar'] })
ant: string[];
// 也可以用定义好的 Schema
@DefineSchema({ schema: Schema.string() })
dream: string;
// 上例的另一种快捷写法
@UseSchema(Schema.string())
sapnap: string;
// 对象,这里引用另一个具有 Schema 定义类。
@DefineSchema({ type: B })
bi: B;
// 上面的另一种写法
@ObjectSchema(B)
anotherB: B;
// 字典,type 也不可省略
@DefineSchema({ type: B, dict: true })
biDict: Record<string, B>;
// 数组和字典组合会让字典在内而数组在外。此外 ObjectSchema 后面也可以指定属性。
@ObjectSchema(B, { dict: true, array: true })
biDictArr: Record<string, B>[];
}
```
### 使用
```ts
// 获取 Schema 定义
const schema = schemaFromClass(Config);
// 直接获取 Config 对象并实例化,可以代替 Schema.validate 使用。对于嵌套类会进行循环实例化。
const config = schemaTransform(Config, someObject);
```
### 类型推断
koishi-utils-schemagen 会尝试自动从类定义推断 Schema 类型,但是无法自动推断数组和字典类型。
export * from './src/def'; import { ClassType, RegisterSchema } from 'schemastery-gen';
export * from './src/decorators'; import Schema from 'schemastery';
export * from './src/methods'; export * from 'schemastery-gen';
export * from './src/constants';
export function schemaFromClass<T>(cl: ClassType<T>): Schema<T> {
if ((cl as Schema<T>).type) {
return cl as Schema<T>;
}
return RegisterSchema()(cl);
}
export function schemaTransform<T>(cl: ClassType<T>, obj: any): T {
return new cl(obj);
}
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -27,26 +27,22 @@ ...@@ -27,26 +27,22 @@
], ],
"author": "Nanahira <nanahira@momobako.com>", "author": "Nanahira <nanahira@momobako.com>",
"license": "MIT", "license": "MIT",
"peerDependencies": {
"koishi": "^4.0.0-alpha.10",
"reflect-metadata": "^0.1.13"
},
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.14.175",
"@types/node": "^16.10.3", "@types/node": "^16.10.3",
"@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0", "@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.1", "eslint-plugin-prettier": "^3.4.1",
"koishi": "^4.0.0-alpha.10",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"proxy-agent": "^5.0.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"schemastery": "^1.0.0",
"typescript": "^4.4.3" "typescript": "^4.4.3"
}, },
"peerDependencies": {
"schemastery": "^1.0.0"
},
"dependencies": { "dependencies": {
"class-transformer": "^0.4.0", "schemastery-gen": "^1.0.2"
"lodash": "^4.17.21"
} }
} }
export const SchemaMetaKey = '_koishi_schema_meta';
export const SchemaKeysMetaKey = '_koishi_schemakeys_meta';
export const SchemaClassKey = '_koishi_schema_class_meta';
import { SchemaClassOptions, SchemaOptions } from './def';
import 'reflect-metadata';
import { SchemaClassKey, SchemaKeysMetaKey, SchemaMetaKey } from './constants';
import { Schema } from 'koishi';
import {
ClassConstructor,
Type,
Transform,
plainToClass,
} from 'class-transformer';
import _ from 'lodash';
function transformDict<T>(cl: ClassConstructor<T>, val: any, array: boolean) {
if (array) {
return (val as Record<string, any>[]).map((v) =>
_.mapValues(v, (_v) => plainToClass(cl, _v)),
);
} else {
return _.mapValues(val, (_v) => plainToClass(cl, _v));
}
}
function getStringFromNativeType(nativeType: any) {
if (!nativeType) {
return;
}
const nativeTypeString = nativeType.toString() as string;
if (!nativeTypeString) {
return;
}
if (nativeTypeString.startsWith('class')) {
return 'class';
}
if (!nativeTypeString.startsWith('function ')) {
return;
}
return nativeType.name?.toLowerCase();
}
export function DefineSchema(options: SchemaOptions = {}): PropertyDecorator {
return (obj, key) => {
const objClass = obj.constructor;
const keys: string[] =
Reflect.getMetadata(SchemaKeysMetaKey, objClass) || [];
keys.push(key.toString());
Reflect.defineMetadata(SchemaKeysMetaKey, keys, objClass);
const nativeType = Reflect.getMetadata('design:type', obj, key);
const nativeTypeString = getStringFromNativeType(nativeType);
if (!options.type) {
if (nativeTypeString && nativeTypeString !== 'array') {
options.type =
nativeTypeString === 'class' ? nativeType : nativeTypeString;
} else {
options.type = 'any';
}
}
if (nativeTypeString === 'array') {
options.array = true;
}
Reflect.defineMetadata(SchemaMetaKey, options, objClass, key);
if (options.type && typeof options.type !== 'string') {
const cl = options.type as ClassConstructor<any>;
if (options.dict) {
const transformDecorator = Transform(({ value }) =>
transformDict(cl, value, options.array),
);
transformDecorator(obj, key);
} else {
const typeDecorator = Type(() => cl);
typeDecorator(obj, key);
}
}
};
}
export function SchemaConf(options: SchemaClassOptions): ClassDecorator {
return (obj) => {
const objClass = obj;
Reflect.defineMetadata(SchemaClassKey, options, objClass);
};
}
export function UseSchema(schema: Schema) {
return DefineSchema({ schema });
}
export function ObjectSchema(
cl: ClassConstructor<any>,
options: SchemaOptions = {},
) {
return DefineSchema({ type: cl, ...options });
}
import { Schema } from 'koishi';
import { ClassConstructor } from 'class-transformer';
export type SchemaType =
| 'string'
| 'number'
| 'boolean'
| 'object'
| 'any'
| 'never'
| ClassConstructor<any>;
export interface SchemaClassOptions {
desc?: string;
required?: boolean;
hidden?: boolean;
allowUnknown?: boolean;
comment?: string;
default?: any;
}
export interface SchemaOptions extends SchemaClassOptions {
schema?: Schema<any>;
dict?: boolean;
array?: boolean;
type?: SchemaType;
}
export type SchemaOptionsDict<T> = { [P in keyof T]?: SchemaOptions };
import { Schema } from 'koishi';
import { ClassConstructor, plainToClass } from 'class-transformer';
import { SchemaClassOptions, SchemaOptions, SchemaOptionsDict } from './def';
import 'reflect-metadata';
import { SchemaClassKey, SchemaKeysMetaKey, SchemaMetaKey } from './constants';
import _ from 'lodash';
function getBasePropertySchemaFromOptions(options: SchemaOptions) {
if (options.schema) {
return options.schema;
}
if (typeof options.type !== 'string') {
return schemaFromClass(options.type);
}
switch (options.type as string) {
case 'any':
return Schema.any();
case 'never':
return Schema.never();
case 'string':
return Schema.string();
case 'number':
return Schema.number();
case 'boolean':
return Schema.boolean();
case 'object':
return Schema.object({}, true);
default:
return Schema.any();
}
}
function applyOptionsToSchema(schema: Schema, options: SchemaClassOptions) {
if (options.required != undefined) {
schema._required = options.required;
}
if (options.hidden != undefined) {
schema._hidden = options.hidden;
}
if (options.default != undefined) {
schema._default = options.default;
}
if (options.comment != undefined) {
schema._comment = options.comment;
}
if (options.allowUnknown != undefined) {
schema.flag = options.allowUnknown;
}
if (options.desc != undefined) {
schema.desc = options.desc;
}
}
function getPropertySchemaFromOptions<PT>(options: SchemaOptions): Schema<PT> {
let schema = getBasePropertySchemaFromOptions(options);
if (options.dict) {
schema = Schema.dict(schema);
}
if (options.array) {
schema = Schema.array(schema);
}
applyOptionsToSchema(schema, options);
return schema;
}
function schemasFromDict<T>(dict: SchemaOptionsDict<T>): Schema<T> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return Schema.object<T>(
_.mapValues(dict, (opt) => getPropertySchemaFromOptions(opt)),
);
}
function schemaOptionsFromClass<T>(
cl: ClassConstructor<T>,
): SchemaOptionsDict<T> {
const keys: (keyof T)[] = Reflect.getMetadata(SchemaKeysMetaKey, cl);
if (!keys) {
return null;
}
const result: SchemaOptionsDict<T> = {};
for (const key of keys) {
const option: SchemaOptions = Reflect.getMetadata(
SchemaMetaKey,
cl,
key as string | symbol,
);
result[key] = option;
}
return result;
}
export function schemaFromClass<T>(cl: ClassConstructor<T>): Schema<T> {
let schema: Schema;
const optionsDict = schemaOptionsFromClass(cl);
if (!optionsDict) {
schema = Schema.object({}, true);
} else {
schema = schemasFromDict<T>(optionsDict);
}
const extraOptions: SchemaClassOptions =
Reflect.getMetadata(SchemaClassKey, cl) || {};
applyOptionsToSchema(schema, extraOptions);
return schema;
}
export function schemaTransform<T>(cl: ClassConstructor<T>, data: any): T {
const validatedObj = Schema.validate(data, schemaFromClass(cl));
return plainToClass(cl, validatedObj);
}
import 'reflect-metadata';
function LogType(): PropertyDecorator {
return (target, key) => {
const t = Reflect.getMetadata('design:type', target, key);
console.log(key, t, typeof t, t.toString());
};
}
class B<T> {
foo: T;
}
class A {
@LogType()
foo: string;
@LogType()
bar: number;
@LogType()
a: Date;
@LogType()
m: Map<string, any>;
@LogType()
s: Set<any>;
@LogType()
b: B<string>;
@LogType()
c: boolean;
@LogType()
d: B<any>[];
@LogType()
aaa: any;
}
import { DefineSchema, SchemaConf, schemaFromClass, schemaTransform } from '..';
import { Schema } from 'koishi';
@SchemaConf({
desc: 'my desc',
})
class B {
@DefineSchema({ default: 2, desc: 'aaaaa' })
aa: number;
@DefineSchema({ default: true })
bb: boolean;
}
@SchemaConf({
desc: 'my base desc aaa',
})
class ABase {
@DefineSchema({ required: true, desc: 'a from base' })
a: number;
@DefineSchema({ default: 'shigma' })
b: string;
}
@SchemaConf({
desc: 'my base desc',
})
class A extends ABase {
@DefineSchema({ required: false, desc: 'a from ex' })
a: number;
@DefineSchema({ type: 'string', default: ['foo', 'bar'] })
c: string[];
@DefineSchema()
bi: B;
@DefineSchema({ type: B })
biArr: B[];
@DefineSchema({ type: B, dict: true })
biDict: Record<string, B>[];
}
const schema = schemaFromClass(A);
console.log(JSON.stringify(schema, null, 2));
const testObject = {
a: 4,
bi: { aa: 3 },
c: ['a', 'b'],
biArr: [{ aa: 4 }, { bb: false }],
biDict: [{ cccc: { aa: 5 } }],
};
// const testValidate = Schema.validate(testObject, schema);
// console.log(JSON.stringify(testValidate, null, 2));
const testTransform = schemaTransform(A, testObject);
//console.log(JSON.stringify(testTransform, null, 2));
console.log(testTransform.c);
console.log(testTransform.bi instanceof B);
console.log(testTransform.biArr[0] instanceof B);
console.log(testTransform.biDict[0].cccc instanceof B);
import 'reflect-metadata';
function TheProperty(): PropertyDecorator {
return (obj, key) => {
Reflect.defineMetadata('test1', 'www', obj);
Reflect.defineMetadata('test', 'www', obj, key);
};
}
class A {
@TheProperty()
foo: string;
}
const a = new A();
console.log(Reflect.getMetadata('test', A, 'foo'));
console.log(Reflect.getMetadata('test', a, 'foo'));
console.log(Reflect.getMetadata('test', a.constructor, 'foo'));
console.log(Reflect.getMetadata('test1', A));
console.log(Reflect.getMetadata('test1', a));
console.log(Reflect.getMetadata('test1', a.constructor));
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
"compileOnSave": true, "compileOnSave": true,
"allowJs": true, "allowJs": true,
"include": [ "include": [
"*.ts", "index.ts",
"src/**/*.ts", "./src/**/*.ts",
"test/**/*.ts" "./test/**/*.ts"
] ]
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment