diff --git a/packages/core/src/getNodeExtraInfo.ts b/packages/core/src/getNodeExtraInfo.ts index fb92c64..fc85eef 100644 --- a/packages/core/src/getNodeExtraInfo.ts +++ b/packages/core/src/getNodeExtraInfo.ts @@ -22,6 +22,16 @@ export const removeEmptyComments = (input: string): string => { return lines; }; +export const getPropNodeTypeRefName = (node: Node, smallCaseOnly?: boolean) => { + let refName = ""; + if (Node.isPropertyDeclaration(node) || Node.isPropertySignature(node)) { + const typeNode = node.getTypeNode(); + refName = Node.isTypeReference(typeNode) + ? typeNode.getTypeName().getText() + : ""; + } + return smallCaseOnly ? (/^[a-z]/.test(refName) ? refName : "") : ""; +}; export const parseAndRemoveAnnotations = (input: string) => { const regex = /@(\w+)(?:\(([^)]+)\)|\s+(\S+))?/g; const result: Record = {}; @@ -70,41 +80,47 @@ export const getNodeExtraInfo = (node: Node) => { } // 获取装饰器信息 const decorators = Node.isDecoratable(node) - ? node.getDecorators().reduce((map, dec) => { - const propName = dec.getName(); - const args = dec.getArguments(); - const propValue = args - // 移除开头和末尾的 " 和 ' - .map((arg) => - arg - .getText() - .replace(/^("|')/g, "") - .replace(/("|')$/, "") - ) - .join("/"); - map[propName] = args.length > 0 ? propValue : true; - return map; - }, {} as Record) + ? node.getDecorators().reduce( + (map, dec) => { + const propName = dec.getName(); + const args = dec.getArguments(); + const propValue = args + // 移除开头和末尾的 " 和 ' + .map((arg) => + arg + .getText() + .replace(/^("|')/g, "") + .replace(/("|')$/, "") + ) + .join("/"); + map[propName] = args.length > 0 ? propValue : true; + return map; + }, + {} as Record + ) : {}; // 获取JSDoc标签信息 const jsDocs = Node.isJSDocable(node) - ? node.getJsDocs().reduce((map, doc) => { - const tags = doc - .getTags() - .filter( - (tag) => - Node.isJSDocTypeTag(tag) || - Node.isJSDocTag(tag) || - Node.isJSDocDeprecatedTag(tag) - ) - .reduce((tmap, tag) => { - tmap[tag.getTagName()] = tag.getCommentText() ?? true; - return tmap; - }, {}); + ? node.getJsDocs().reduce( + (map, doc) => { + const tags = doc + .getTags() + .filter( + (tag) => + Node.isJSDocTypeTag(tag) || + Node.isJSDocTag(tag) || + Node.isJSDocDeprecatedTag(tag) + ) + .reduce((tmap, tag) => { + tmap[tag.getTagName()] = tag.getCommentText() ?? true; + return tmap; + }, {}); - return { ...map, ...tags }; - }, {} as Record) + return { ...map, ...tags }; + }, + {} as Record + ) : {}; // 获取前置和后置注释 diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2df770c..77f8873 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,3 @@ export { debugInfo, debugNode, debugSymbol, debugType } from "./debug"; -export { getNodeExtraInfo } from "./getNodeExtraInfo"; +export { getNodeExtraInfo, getPropNodeTypeRefName } from "./getNodeExtraInfo"; export { TypeResolver } from "./TypeResolver"; diff --git a/packages/formily-schema/__tests__/__snapshots__/index.test.ts.snap b/packages/formily-schema/__tests__/__snapshots__/index.test.ts.snap index 32ea674..d700bb8 100644 --- a/packages/formily-schema/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/formily-schema/__tests__/__snapshots__/index.test.ts.snap @@ -1,37 +1,308 @@ // Bun Snapshot v1, https://goo.gl/fbAQLP -exports[`formily schema tests 应该正确转换用户信息模式 / should transform user information schema correctly 1`] = ` +exports[`formily schema tests 复杂类型的转换 / should transfomer complex 1`] = ` "import { ISchema } from "@formily/json-schema"; -const Form: ISchema = { +const Order: ISchema = { "type": "object", "properties": { - "user": User + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "petId": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "quantity": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "shipDate": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "status": { + "x-decorator": "FormItem", + "x-component": "Input", + "enum": [ + { + "label": "placed", + "value": "placed" + }, + { + "label": "approved", + "value": "approved" + }, + { + "label": "delivered", + "value": "delivered" + } + ] + }, + "complete": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "boolean" + } + } +}; + +const Customer: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "username": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "address": Address + } +}; + +const Address: ISchema = { + "type": "object", + "properties": { + "street": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "city": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "state": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "zip": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + } + } +}; + +const Category: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "name": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + } } }; const User: ISchema = { "type": "object", "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "username": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "firstName": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "lastName": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "email": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "password": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "phone": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "userStatus": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + } + } +}; + +const Tag: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, "name": { "x-decorator": "FormItem", "x-component": "Input", - "title": "用户名", + "type": "string" + } + } +}; + +const Pet: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "description": "Pet's id", + "type": "number" + }, + "category": Category, + "name": { + "x-decorator": "FormItem", + "x-component": "Input", + "description": "name with default", + "default": "hi", "type": "string" }, - "address": Address, - "addressList": { - "description": "地址列表", + "photoUrls": { + "description": "photos", "type": "array", "x-component": "ArrayTable", "items": { "type": "void", "x-component": "ArrayTable.Item", - "properties": "#Address" + "properties": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + } + } + }, + "tags": { + "type": "array", + "x-component": "ArrayTable", + "items": { + "type": "void", + "x-component": "ArrayTable.Item", + "properties": Tag } + }, + "status": { + "x-decorator": "FormItem", + "x-component": "Input", + "enum": [ + { + "label": "placed", + "value": "placed" + }, + { + "label": "approved", + "value": "approved" + }, + { + "label": "delivered", + "value": "delivered" + } + ] + } + } +}; +" +`; + +exports[`formily schema tests 应该正确转换用户信息模式 / should transform user information schema correctly 1`] = ` +"import { ISchema } from "@formily/json-schema"; +const Order: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "petId": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "quantity": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "shipDate": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "status": { + "x-decorator": "FormItem", + "x-component": "Input", + "enum": [ + { + "label": "placed", + "value": "placed" + }, + { + "label": "approved", + "value": "approved" + }, + { + "label": "delivered", + "value": "delivered" + } + ] + }, + "complete": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "boolean" } } }; +const Customer: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "username": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "address": Address + } +}; + const Address: ISchema = { "type": "object", "properties": { @@ -58,36 +329,621 @@ const Address: ISchema = { } } }; -" -`; -exports[`formily schema tests 应该处理循环引用 / should handle circular references 1`] = ` -"import { ISchema } from "@formily/json-schema"; -const Form: ISchema = { +const Category: ISchema = { "type": "object", "properties": { - "a": A + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "name": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + } } }; -const A: ISchema = { +const User: ISchema = { "type": "object", "properties": { - "b": B + "name": { + "x-decorator": "FormItem", + "x-component": "Input", + "title": "用户名", + "type": "string" + }, + "address": Address, + "addressList": { + "description": "地址列表", + "type": "array", + "x-component": "ArrayTable", + "items": { + "type": "void", + "x-component": "ArrayTable.Item", + "properties": Address + } + } } }; -const B: ISchema = { +const Tag: ISchema = { "type": "object", "properties": { - "a": A - } -}; -" -`; + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "name": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + } + } +}; + +const Pet: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "description": "Pet's id", + "type": "number" + }, + "category": Category, + "name": { + "x-decorator": "FormItem", + "x-component": "Input", + "description": "name with default", + "default": "hi", + "type": "string" + }, + "photoUrls": { + "description": "photos", + "type": "array", + "x-component": "ArrayTable", + "items": { + "type": "void", + "x-component": "ArrayTable.Item", + "properties": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + } + } + }, + "tags": { + "type": "array", + "x-component": "ArrayTable", + "items": { + "type": "void", + "x-component": "ArrayTable.Item", + "properties": Tag + } + }, + "status": { + "x-decorator": "FormItem", + "x-component": "Input", + "enum": [ + { + "label": "placed", + "value": "placed" + }, + { + "label": "approved", + "value": "approved" + }, + { + "label": "delivered", + "value": "delivered" + } + ] + } + } +}; + +const Form: ISchema = { + "type": "object", + "properties": { + "user": User + } +}; +" +`; + +exports[`formily schema tests 应该处理循环引用 / should handle circular references 1`] = ` +"import { ISchema } from "@formily/json-schema"; +const Order: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "petId": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "quantity": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "shipDate": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "status": { + "x-decorator": "FormItem", + "x-component": "Input", + "enum": [ + { + "label": "placed", + "value": "placed" + }, + { + "label": "approved", + "value": "approved" + }, + { + "label": "delivered", + "value": "delivered" + } + ] + }, + "complete": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "boolean" + } + } +}; + +const Customer: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "username": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "address": Address + } +}; + +const Address: ISchema = { + "type": "object", + "properties": { + "street": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "city": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "state": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "zip": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + } + } +}; + +const Category: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "name": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + } + } +}; + +const User: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "username": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "firstName": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "lastName": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "email": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "password": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "phone": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "userStatus": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + } + } +}; + +const Tag: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "name": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + } + } +}; + +const Pet: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "description": "Pet's id", + "type": "number" + }, + "category": Category, + "name": { + "x-decorator": "FormItem", + "x-component": "Input", + "description": "name with default", + "default": "hi", + "type": "string" + }, + "photoUrls": { + "description": "photos", + "type": "array", + "x-component": "ArrayTable", + "items": { + "type": "void", + "x-component": "ArrayTable.Item", + "properties": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + } + } + }, + "tags": { + "type": "array", + "x-component": "ArrayTable", + "items": { + "type": "void", + "x-component": "ArrayTable.Item", + "properties": Tag + } + }, + "status": { + "x-decorator": "FormItem", + "x-component": "Input", + "enum": [ + { + "label": "placed", + "value": "placed" + }, + { + "label": "approved", + "value": "approved" + }, + { + "label": "delivered", + "value": "delivered" + } + ] + } + } +}; + +const Form: ISchema = { + "type": "object", + "properties": { + "a": A + } +}; + +const A: ISchema = { + "type": "object", + "properties": { + "b": B + } +}; + +const B: ISchema = { + "type": "object", + "properties": { + "a": A + } +}; +" +`; exports[`formily schema tests 应该正确转换基本类型 / should transform basic types correctly 1`] = ` "import { ISchema } from "@formily/json-schema"; +const Order: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "petId": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "quantity": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "shipDate": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "status": { + "x-decorator": "FormItem", + "x-component": "Input", + "enum": [ + { + "label": "placed", + "value": "placed" + }, + { + "label": "approved", + "value": "approved" + }, + { + "label": "delivered", + "value": "delivered" + } + ] + }, + "complete": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "boolean" + } + } +}; + +const Customer: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "username": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "address": Address + } +}; + +const Address: ISchema = { + "type": "object", + "properties": { + "street": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "city": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "state": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "zip": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + } + } +}; + +const Category: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "name": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + } + } +}; + +const User: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "username": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "firstName": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "lastName": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "email": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "password": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "phone": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + }, + "userStatus": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + } + } +}; + +const Tag: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "number" + }, + "name": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + } + } +}; + +const Pet: ISchema = { + "type": "object", + "properties": { + "id": { + "x-decorator": "FormItem", + "x-component": "Input", + "description": "Pet's id", + "type": "number" + }, + "category": Category, + "name": { + "x-decorator": "FormItem", + "x-component": "Input", + "description": "name with default", + "default": "hi", + "type": "string" + }, + "photoUrls": { + "description": "photos", + "type": "array", + "x-component": "ArrayTable", + "items": { + "type": "void", + "x-component": "ArrayTable.Item", + "properties": { + "x-decorator": "FormItem", + "x-component": "Input", + "type": "string" + } + } + }, + "tags": { + "type": "array", + "x-component": "ArrayTable", + "items": { + "type": "void", + "x-component": "ArrayTable.Item", + "properties": Tag + } + }, + "status": { + "x-decorator": "FormItem", + "x-component": "Input", + "enum": [ + { + "label": "placed", + "value": "placed" + }, + { + "label": "approved", + "value": "approved" + }, + { + "label": "delivered", + "value": "delivered" + } + ] + } + } +}; + const Form: ISchema = { "type": "object", "properties": { diff --git a/packages/formily-schema/__tests__/index.test.ts b/packages/formily-schema/__tests__/index.test.ts index 4c8a7ee..a919bc2 100644 --- a/packages/formily-schema/__tests__/index.test.ts +++ b/packages/formily-schema/__tests__/index.test.ts @@ -6,6 +6,83 @@ import { getEntryNodes } from "../src/entry"; describe("formily schema tests", () => { const project = new Project({}); + it("复杂类型的转换 / should transfomer complex", () => { + const sourceFile = project.createSourceFile( + "case0.ts", + + ` +type int32 = number; +type int64 = number; +type float = number; +type double = number; +type date = string; +type datetime = string; +type password = string; +type binary = string; +type byte = string; +type Status = "placed" | "approved" | "delivered"; + +class Order { + id: int64; + petId: int64; + quantity: int32; + shipDate: datetime; + status: Status; + complete: boolean; +} + +class Customer { + id: int64; + username: string; + address: Address; +} + +class Address { + street: string; + city: string; + state: string; + zip: number; +} + +class Category { + id: int64; + name: string; +} + +class User { + id: int64; + username: string; + firstName: string; + lastName: string; + email: string; + password: string; + phone: string; + userStatus: int32; +} + +class Tag { + id: int64; + name: string; +} + +class Pet { + /** Pet's id */ + id: int64 = 0; + // getJsDocs() cannot see me + category: Category; // but getLeadingCommentRanges/getTrailingCommentRanges can do it! + // name with default + name: string = "hi"; + /** photos */ + photoUrls: string[]; + tags: Tag[]; + status: Status; +} + ` + ); + const result = transform(project); + expect(result).toMatchSnapshot(); + }); + it("应该正确转换用户信息模式 / should transform user information schema correctly", () => { const sourceFile = project.createSourceFile( "case1.ts", diff --git a/packages/formily-schema/src/index.ts b/packages/formily-schema/src/index.ts index 155a0eb..7fd4ae6 100644 --- a/packages/formily-schema/src/index.ts +++ b/packages/formily-schema/src/index.ts @@ -22,7 +22,7 @@ export const transform = (project: Project) => { schemaDefs[def], null, 2 - )};\n`.replace(/"#\w+"/, (m) => { + )};\n`.replace(/"#\w+"/g, (m) => { return m.replace(/"/g, "").replace("#", ""); }); }); diff --git a/packages/formily-schema/src/transformer.ts b/packages/formily-schema/src/transformer.ts index ed1b34e..35d675b 100644 --- a/packages/formily-schema/src/transformer.ts +++ b/packages/formily-schema/src/transformer.ts @@ -159,18 +159,25 @@ const schemaResolver = new TypeResolver() return { ...extra, type: "object", - properties: type.getProperties().reduce((map, propSymbol) => { - const propNode = - propSymbol.getValueDeclaration() ?? propSymbol.getDeclarations()[0]; - const porpType = propNode - ? propSymbol.getTypeAtLocation(propNode) - : propSymbol.getDeclaredType(); - if (propNode) { - ctx.extra = getNodeExtraInfo(propNode).info; - } - map[propSymbol.getName()] = resolver.resolve(porpType, ctx); - return map; - }, {} as ISchema["properties"]), + properties: type.getProperties().reduce( + (map, propSymbol) => { + const propNode = + propSymbol.getValueDeclaration() ?? propSymbol.getDeclarations()[0]; + const porpType = propNode + ? propSymbol.getTypeAtLocation(propNode) + : propSymbol.getDeclaredType(); + if (propNode) { + const extra = getNodeExtraInfo(propNode); + ctx.extra = extra.info; + if (extra.initialValue) { + ctx.extra["default"] = extra.initialValue; + } + } + map[propSymbol.getName()] = resolver.resolve(porpType, ctx); + return map; + }, + {} as ISchema["properties"] + ), }; }); @@ -192,8 +199,12 @@ export const transformDefinitions = ( type: "object", properties: def.getProperties().reduce((map, prop) => { const extra = getNodeExtraInfo(prop); + const ctxExtra = extra.info; + if (extra.initialValue) { + ctxExtra["default"] = extra.initialValue; + } map[prop.getName()] = schemaResolver.resolve(prop.getType(), { - extra: extra.info, + extra: ctxExtra, refs: circularRefs, defs: globalDefs, self: def.getName(), diff --git a/packages/oas/__tests__/__snapshots__/index.test.ts.snap b/packages/oas/__tests__/__snapshots__/index.test.ts.snap index 5e6eaad..0b23d12 100644 --- a/packages/oas/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/oas/__tests__/__snapshots__/index.test.ts.snap @@ -29,6 +29,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` "Category": { "properties": { "id": { + "format": "int64", "type": "number", }, "name": { @@ -47,6 +48,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` "$ref": "#components/schemas/Address", }, "id": { + "format": "int64", "type": "number", }, "username": { @@ -66,15 +68,19 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` "type": "boolean", }, "id": { + "format": "int64", "type": "number", }, "petId": { + "format": "int64", "type": "number", }, "quantity": { + "format": "int32", "type": "number", }, "shipDate": { + "format": "datetime", "type": "string", }, "status": { @@ -110,6 +116,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` "id": { "default": 0, "description": "Pet's id", + "format": "int64", "type": "number", }, "name": { @@ -157,6 +164,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` "Tag": { "properties": { "id": { + "format": "int64", "type": "number", }, "name": { @@ -178,6 +186,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` "type": "string", }, "id": { + "format": "int64", "type": "number", }, "lastName": { @@ -190,6 +199,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` "type": "string", }, "userStatus": { + "format": "int32", "type": "number", }, "username": { @@ -265,6 +275,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` "type": "string", }, "userStatus": { + "format": "int32", "type": "number", }, "username": { @@ -340,6 +351,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` "type": "string", }, "userStatus": { + "format": "int32", "type": "number", }, "username": { @@ -371,6 +383,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "data": { @@ -388,6 +401,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "data": { @@ -425,6 +439,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -436,6 +451,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -478,6 +494,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` "resp": { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -510,6 +527,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -521,6 +539,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -575,6 +594,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -586,6 +606,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -643,6 +664,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -693,6 +715,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -752,6 +775,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -781,6 +805,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -833,6 +858,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -844,6 +870,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -882,6 +909,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -893,6 +921,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -944,6 +973,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "name": { + "default": "hi", "description": "name with default", "type": "string", }, @@ -982,6 +1012,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -993,6 +1024,7 @@ exports[`to-oas transformer 全量测试 / Full Test 1`] = ` { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -1020,6 +1052,7 @@ schema: type: string format: binary" , + "format": "binary", "type": "string", }, "method": { @@ -1051,6 +1084,7 @@ format: binary" { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -1083,6 +1117,7 @@ format: binary" "query": { "properties": { "name": { + "default": "hi", "description": "name with default", "type": "string", }, @@ -1105,6 +1140,7 @@ format: binary" "resp": { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -1137,6 +1173,7 @@ format: binary" { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -1148,6 +1185,7 @@ format: binary" { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -1159,6 +1197,7 @@ format: binary" { "properties": { "code": { + "format": "int32", "type": "number", }, "data": { @@ -1199,6 +1238,7 @@ format: binary" { "properties": { "code": { + "format": "int32", "type": "number", }, "data": { @@ -1213,6 +1253,7 @@ format: binary" { "properties": { "code": { + "format": "int32", "type": "number", }, "data": { @@ -1227,6 +1268,7 @@ format: binary" { "properties": { "code": { + "format": "int32", "type": "number", }, "reason": { @@ -1251,6 +1293,7 @@ format: binary" "resp": { "properties": { "code": { + "format": "int32", "type": "number", }, "data": { diff --git a/packages/oas/src/transformer.ts b/packages/oas/src/transformer.ts index d8b1cda..8eed9df 100644 --- a/packages/oas/src/transformer.ts +++ b/packages/oas/src/transformer.ts @@ -1,7 +1,11 @@ import { OasSchema30 } from "@hyperjump/json-schema/openapi-3-0"; import { JsonSchemaType } from "@hyperjump/json-schema/lib/common"; -import { getNodeExtraInfo, TypeResolver } from "@typeto/core"; +import { + getNodeExtraInfo, + getPropNodeTypeRefName, + TypeResolver, +} from "@typeto/core"; import { ClassDeclaration, Node, Type, TypeAliasDeclaration } from "ts-morph"; export type OasSchema = OasSchema30 & { @@ -116,13 +120,20 @@ const schemaResolver = new TypeResolver() if (definedType) return definedType; if (propNode) { const extra = getNodeExtraInfo(propNode); - ctx.extra = ctx.wantRequired - ? { - ...extra.info, - required: propSymbol.isOptional() ? false : true, - default: extra.initialValue, - } - : getNodeExtraInfo(propNode).info; + const typeRefName = getPropNodeTypeRefName(propNode, true); + const ctxExtra = extra.info; + + if (typeRefName) { + ctxExtra.format = typeRefName; + } + if (extra.initialValue) { + ctxExtra.default = extra.initialValue; + } + if (ctx.wantRequired && !propSymbol.isOptional()) { + ctxExtra.required = true; + } + + ctx.extra = ctxExtra; } map[propSymbol.getName()] = resolver.resolve(porpType, ctx); @@ -163,6 +174,12 @@ export const transformDefinitions = (definitions: ClassDeclaration[]) => { type: "object", properties: def.getProperties().reduce((map, prop) => { const extra = getNodeExtraInfo(prop); + const typeRefName = getPropNodeTypeRefName(prop, true); + + if (typeRefName) { + extra.info.format = typeRefName; + } + const symbol = prop.getSymbol(); const schema = schemaResolver.resolve(prop.getType(), { defs, @@ -201,6 +218,11 @@ export const transformOperations = ( type: "object", properties: typeNode.getProperties().reduce((map, prop) => { const extra = getNodeExtraInfo(prop); + const typeRefName = getPropNodeTypeRefName(prop, true); + + if (typeRefName) { + extra.info.format = typeRefName; + } const schema = schemaResolver.resolve(prop.getType(), { defs: defNameMap, extra: { diff --git a/packages/zod/__tests__/__snapshots__/index.test.ts.snap b/packages/zod/__tests__/__snapshots__/index.test.ts.snap index 595445f..a8906ea 100644 --- a/packages/zod/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/zod/__tests__/__snapshots__/index.test.ts.snap @@ -1,5 +1,70 @@ // Bun Snapshot v1, https://goo.gl/fbAQLP +exports[`zod schema tests 复杂组合测试 / should handle complex 1`] = ` +"import { z } from "zod"; + +export const Order = z.object({ + id: z.number().int64(), + petId: z.number().int64(), + quantity: z.number().int32(), + shipDate: z.string().datetime(), + status: z.union([z.literal("placed"), z.literal("approved"), z.literal("delivered")]), + complete: z.boolean() +}); + +export const Customer = z.object({ + id: z.number().int64(), + username: z.string(), + address: z.object({ + street: z.string(), + city: z.string(), + state: z.string(), + zip: z.number() +}) +}); + +export const Address = z.object({ + street: z.string(), + city: z.string(), + state: z.string(), + zip: z.number() +}); + +export const Category = z.object({ + id: z.number().int64(), + name: z.string() +}); + +export const User = z.object({ + id: z.number().int64(), + username: z.string(), + firstName: z.string(), + lastName: z.string(), + email: z.string(), + password: z.string(), + phone: z.string(), + userStatus: z.number().int32() +}); + +export const Tag = z.object({ + id: z.number().int64(), + name: z.string() +}); + +export const Pet = z.object({ + id: z.number({ message: "Pet's id"}).int64(), + category: Category, + name: z.string({ message: "name with default"}), + photoUrls: z.array(z.string({ message: "photos"})), + tags: z.array(Tag), + status: z.union([z.literal("placed"), z.literal("approved"), z.literal("delivered")]) +}); + +export const TreeLike: z.ZodSchema = z.lazy(() => z.object({ + children: z.array(TreeLike) +}))" +`; + exports[`zod schema tests 应该正确处理基本类型 / should handle basic types correctly 1`] = ` "import { z } from "zod"; diff --git a/packages/zod/__tests__/index.test.ts b/packages/zod/__tests__/index.test.ts index 89e05c7..41f706c 100644 --- a/packages/zod/__tests__/index.test.ts +++ b/packages/zod/__tests__/index.test.ts @@ -5,6 +5,90 @@ import { transform } from "../src"; describe("zod schema tests", () => { const project = new Project({}); + it("复杂组合测试 / should handle complex ", () => { + const sourceFile = project.createSourceFile( + "test.ts", + ` +type int32 = number; +type int64 = number; +type float = number; +type double = number; +type date = string; +type datetime = string; +type password = string; +type binary = string; +type byte = string; + +type Status = "placed" | "approved" | "delivered"; + +class Order { + id: int64; + petId: int64; + quantity: int32; + shipDate: datetime; + status: Status; + complete: boolean; +} + +class Customer { + id: int64; + username: string; + address: Address; +} + +class Address { + street: string; + city: string; + state: string; + zip: number; +} + +class Category { + id: int64; + name: string; +} + +class User { + id: int64; + username: string; + firstName: string; + lastName: string; + email: string; + password: string; + phone: string; + userStatus: int32; +} + +class Tag { + id: int64; + name: string; +} + +class Pet { + /** Pet's id */ + id: int64 = 0; + // getJsDocs() cannot see me + category: Category; // but getLeadingCommentRanges/getTrailingCommentRanges can do it! + // name with default + name: string = "hi"; + /** photos */ + photoUrls: string[]; + tags: Tag[]; + status: Status; +} + +class TreeLike { + children: TreeLike[] +} + + ` + ); + + const result = transform(project); + expect(result).toMatchSnapshot(); + project.removeSourceFile(sourceFile); + }); + it("应该正确处理基本类型 / should handle basic types correctly", () => { const sourceFile = project.createSourceFile( "test.ts", diff --git a/packages/zod/src/transformer.ts b/packages/zod/src/transformer.ts index bfdbc4f..d280881 100644 --- a/packages/zod/src/transformer.ts +++ b/packages/zod/src/transformer.ts @@ -1,10 +1,15 @@ -import { ClassDeclaration, Type } from "ts-morph"; -import { TypeResolver } from "@typeto/core"; +import { ClassDeclaration, Node, Type, YieldExpression } from "ts-morph"; +import { + TypeResolver, + getNodeExtraInfo, + getPropNodeTypeRefName, +} from "@typeto/core"; interface ResolveContext { globalRefs: WeakMap; selfCircularRefs: WeakMap; touched: { current: boolean }; + self: string; } const zodResolver = new TypeResolver() @@ -20,7 +25,7 @@ const zodResolver = new TypeResolver() // 检测循环引用 if (ctx.globalRefs.has(type)) { const has = ctx.globalRefs.get(type); - return has; + if (has !== ctx.self) return has; } }) .null(() => `z.null()`) @@ -30,7 +35,9 @@ const zodResolver = new TypeResolver() ) .numberLiteral((type) => `z.literal(${type.getLiteralValue()})`) .booleanLiteral((type) => `z.literal(${type.getText()})`) - .string(() => `z.string()`) + .string((type) => { + return `z.string()`; + }) .boolean(() => `z.boolean()`) .number(() => `z.number()`) .literal((type) => `z.literal(${JSON.stringify(type.getLiteralValue())})`) @@ -66,6 +73,10 @@ const zodResolver = new TypeResolver() const propNode = propSymbol.getValueDeclaration() ?? propSymbol.getDeclarations()[0]; + const extra = getNodeExtraInfo(propNode); + + const message = extra.info["message"] || extra.comment; + // 获取属性的类型 const propType = propNode ? propSymbol.getTypeAtLocation(propNode) @@ -73,11 +84,18 @@ const zodResolver = new TypeResolver() if (!propType) return ""; let zodType = resolver.resolve(propType, ctx); + const stringSpecific = getPropNodeTypeRefName(propNode, true); + // 处理可选属性 const isOptional = propSymbol.isOptional(); // 检查是否为 nullish (null 或 undefined) const isNullable = propType.isNull() || propType.isNullable(); - + if (message) { + zodType = zodType.replace("()", `({ message: "${message}"})`); + } + if (stringSpecific) { + zodType = `${zodType}.${stringSpecific}()`; + } if (isNullable) { zodType = `${zodType}.nullable()`; } @@ -105,10 +123,12 @@ export const transformDefinitions = ( const className = def.getName(); const selfCircularRefs = new WeakMap(); const touched = { current: false }; + const schema = zodResolver.resolve(def.getType(), { globalRefs, selfCircularRefs, touched, + self: def.getName(), }); if (touched.current) {