From 20b1dc1ae92ac722eeb4c13dd1f745528b5eb60e Mon Sep 17 00:00:00 2001 From: Yiwei Mao Date: Mon, 7 Apr 2025 15:30:22 +0800 Subject: [PATCH] feat: add ASTMatch API in variable-core (#127) --- .../variable-selector/use-variable-tree.ts | 11 +-- .../variable-selector/use-variable-tree.ts | 11 +-- apps/docs/src/en/guide/advanced/variable.mdx | 13 ++-- apps/docs/src/zh/guide/advanced/variable.mdx | 13 ++-- .../ast/variable-declaration.test.ts | 6 +- .../__tests__/ast/variable-match.test.ts | 74 +++++++++++++++++++ .../variable-core/src/ast/index.ts | 1 + .../variable-core/src/ast/match.ts | 73 ++++++++++++++++++ .../variable-core/src/ast/utils/helpers.ts | 9 ++- 9 files changed, 177 insertions(+), 34 deletions(-) create mode 100644 packages/variable-engine/variable-core/__tests__/ast/variable-match.test.ts create mode 100644 packages/variable-engine/variable-core/src/ast/match.ts diff --git a/apps/demo-fixed-layout/src/plugins/sync-variable-plugin/variable-selector/use-variable-tree.ts b/apps/demo-fixed-layout/src/plugins/sync-variable-plugin/variable-selector/use-variable-tree.ts index 9711d4be..504a92ae 100644 --- a/apps/demo-fixed-layout/src/plugins/sync-variable-plugin/variable-selector/use-variable-tree.ts +++ b/apps/demo-fixed-layout/src/plugins/sync-variable-plugin/variable-selector/use-variable-tree.ts @@ -1,15 +1,12 @@ import { useCallback, useMemo } from 'react'; import { - ArrayType, ASTFactory, ASTKind, type BaseType, - CustomType, - isMatchAST, - ObjectType, type UnionJSON, useScopeAvailable, + ASTMatch, } from '@flowgram.ai/fixed-layout-editor'; import { createASTFromJSONSchema } from '../utils'; @@ -47,14 +44,14 @@ export function useVariableTree({ const getVariableTypeIcon = useCallback((variable: VariableField) => { const _type = variable.type; - if (isMatchAST(_type, ArrayType)) { + if (ASTMatch.isArray(_type)) { return ( (ArrayIcons as any)[_type.items?.kind.toLowerCase()] || VariableTypeIcons[ASTKind.Array.toLowerCase()] ); } - if (isMatchAST(_type, CustomType)) { + if (ASTMatch.isCustomType(_type)) { return VariableTypeIcons[_type.typeName.toLowerCase()]; } @@ -96,7 +93,7 @@ export function useVariableTree({ const isTypeFiltered = checkTypeFiltered(type); let children: TreeData[] | undefined; - if (isMatchAST(type, ObjectType)) { + if (ASTMatch.isObject(type)) { children = (type.properties || []) .map((_property) => renderVariable(_property as VariableField, [...parentFields, variable])) .filter(Boolean) as TreeData[]; diff --git a/apps/demo-free-layout/src/plugins/sync-variable-plugin/variable-selector/use-variable-tree.ts b/apps/demo-free-layout/src/plugins/sync-variable-plugin/variable-selector/use-variable-tree.ts index 194e2478..ba23fdf6 100644 --- a/apps/demo-free-layout/src/plugins/sync-variable-plugin/variable-selector/use-variable-tree.ts +++ b/apps/demo-free-layout/src/plugins/sync-variable-plugin/variable-selector/use-variable-tree.ts @@ -1,15 +1,12 @@ import { useCallback, useMemo } from 'react'; import { - ArrayType, ASTFactory, ASTKind, type BaseType, - CustomType, - isMatchAST, - ObjectType, type UnionJSON, useScopeAvailable, + ASTMatch, } from '@flowgram.ai/free-layout-editor'; import { createASTFromJSONSchema } from '../utils'; @@ -47,14 +44,14 @@ export function useVariableTree({ const getVariableTypeIcon = useCallback((variable: VariableField) => { const _type = variable.type; - if (isMatchAST(_type, ArrayType)) { + if (ASTMatch.isArray(_type)) { return ( (ArrayIcons as any)[_type.items?.kind.toLowerCase()] || VariableTypeIcons[ASTKind.Array.toLowerCase()] ); } - if (isMatchAST(_type, CustomType)) { + if (ASTMatch.isCustomType(_type)) { return VariableTypeIcons[_type.typeName.toLowerCase()]; } @@ -96,7 +93,7 @@ export function useVariableTree({ const isTypeFiltered = checkTypeFiltered(type); let children: TreeData[] | undefined; - if (isMatchAST(type, ObjectType)) { + if (ASTMatch.isObject(type)) { children = (type.properties || []) .map((_property) => renderVariable(_property as VariableField, [...parentFields, variable])) .filter(Boolean) as TreeData[]; diff --git a/apps/docs/src/en/guide/advanced/variable.mdx b/apps/docs/src/en/guide/advanced/variable.mdx index 1dcf285b..b1633296 100644 --- a/apps/docs/src/en/guide/advanced/variable.mdx +++ b/apps/docs/src/en/guide/advanced/variable.mdx @@ -233,8 +233,7 @@ return available.variables.map(renderVariable) ```tsx pure title="use-variable-tree.tsx" import { type BaseVariableField, - isMatchAST, - ObjectType, + ASTMatch, } from '@flowgram.ai/fixed-layout-editor'; // .... @@ -243,7 +242,7 @@ const renderVariable = (variable: BaseVariableField) => ({ title: variable.meta?.title, key: variable.key, // Only Object Type can drilldown - children: isMatchAST(type, ObjectType) ? type.properties.map(renderVariable) : [], + children: ASTMatch.isObject(type) ? type.properties.map(renderVariable) : [], }); // .... @@ -256,9 +255,7 @@ const renderVariable = (variable: BaseVariableField) => ({ import { type BaseVariableField, type BaseType, - isMatchAST, - ObjectType, - ArrayType, + ASTMatch, } from '@flowgram.ai/fixed-layout-editor'; // .... @@ -267,10 +264,10 @@ const getTypeChildren = (type?: BaseType): BaseVariableField[] => { if (!type) return []; // get properties of Object - if (isMatchAST(type, ObjectType)) return type.properties; + if (ASTMatch.isObject(type)) return type.properties; // get items type of Array - if (isMatchAST(type, ArrayType)) return getTypeChildren(type.items); + if (ASTMatch.isArray(type)) return getTypeChildren(type.items); }; const renderVariable = (variable: BaseVariableField) => ({ diff --git a/apps/docs/src/zh/guide/advanced/variable.mdx b/apps/docs/src/zh/guide/advanced/variable.mdx index 684776a8..497da3e4 100644 --- a/apps/docs/src/zh/guide/advanced/variable.mdx +++ b/apps/docs/src/zh/guide/advanced/variable.mdx @@ -235,8 +235,7 @@ return available.variables.map(renderVariable) ```tsx pure title="use-variable-tree.tsx" import { type BaseVariableField, - isMatchAST, - ObjectType, + ASTMatch, } from '@flowgram.ai/fixed-layout-editor'; // .... @@ -245,7 +244,7 @@ const renderVariable = (variable: BaseVariableField) => ({ title: variable.meta?.title, key: variable.key, // Only Object Type can drilldown - children: isMatchAST(type, ObjectType) ? type.properties.map(renderVariable) : [], + children: ASTMatch.isObject(type) ? type.properties.map(renderVariable) : [], }); // .... @@ -258,9 +257,7 @@ const renderVariable = (variable: BaseVariableField) => ({ import { type BaseVariableField, type BaseType, - isMatchAST, - ObjectType, - ArrayType, + ASTMatch, } from '@flowgram.ai/fixed-layout-editor'; // .... @@ -269,10 +266,10 @@ const getTypeChildren = (type?: BaseType): BaseVariableField[] => { if (!type) return []; // get properties of Object - if (isMatchAST(type, ObjectType)) return type.properties; + if (ASTMatch.isObject(type)) return type.properties; // get items type of Array - if (isMatchAST(type, ArrayType)) return getTypeChildren(type.items); + if (ASTMatch.isArray(type)) return getTypeChildren(type.items); }; const renderVariable = (variable: BaseVariableField) => ({ diff --git a/packages/variable-engine/variable-core/__tests__/ast/variable-declaration.test.ts b/packages/variable-engine/variable-core/__tests__/ast/variable-declaration.test.ts index 5be2837f..c5f2c0a4 100644 --- a/packages/variable-engine/variable-core/__tests__/ast/variable-declaration.test.ts +++ b/packages/variable-engine/variable-core/__tests__/ast/variable-declaration.test.ts @@ -2,11 +2,11 @@ import { vi, describe, test, expect } from 'vitest'; import { ASTKind, + ASTMatch, ObjectType, NumberType, VariableEngine, VariableDeclaration, - isMatchAST, } from '../../src'; import { simpleVariableList } from '../../__mocks__/variables'; import { getContainer } from '../../__mocks__/container'; @@ -167,7 +167,7 @@ describe('test Basic Variable Declaration', () => { type: ASTKind.Number, meta: { label: 'test Label' }, }); - expect(isMatchAST(declaration.type, NumberType)).toBeTruthy(); + expect(ASTMatch.is(declaration.type, NumberType)).toBeTruthy(); expect(declarationChangeTimes).toBe(2); expect(anyVariableChangeTimes).toBe(2); expect(typeChangeTimes).toBe(1); @@ -186,7 +186,7 @@ describe('test Basic Variable Declaration', () => { }, meta: { label: 'test Label' }, }); - expect(isMatchAST(declaration.type, ObjectType)).toBeTruthy(); + expect(ASTMatch.is(declaration.type, ObjectType)).toBeTruthy(); expect(declarationChangeTimes).toBe(3); expect(anyVariableChangeTimes).toBe(3); expect(typeChangeTimes).toBe(2); diff --git a/packages/variable-engine/variable-core/__tests__/ast/variable-match.test.ts b/packages/variable-engine/variable-core/__tests__/ast/variable-match.test.ts new file mode 100644 index 00000000..564de938 --- /dev/null +++ b/packages/variable-engine/variable-core/__tests__/ast/variable-match.test.ts @@ -0,0 +1,74 @@ +import { vi, describe, test, expect } from 'vitest'; + +import { + ASTMatch, + ObjectType, + NumberType, + VariableEngine, + StringType, + VariableDeclarationList, + BooleanType, + IntegerType, + MapType, + ArrayType, +} from '../../src'; +import { simpleVariableList } from '../../__mocks__/variables'; +import { getContainer } from '../../__mocks__/container'; + +vi.mock('nanoid', () => { + let mockId = 0; + return { + nanoid: () => 'mocked-id-' + mockId++, + }; +}); + +/** + * 测试基本的变量声明场景 + */ +describe('test Basic Variable Declaration', () => { + const container = getContainer(); + const variableEngine = container.get(VariableEngine); + const testScope = variableEngine.createScope('test'); + + test('test simple variable match', () => { + const simpleCase = testScope.ast.set('simple case', simpleVariableList); + + if (!ASTMatch.isVariableDeclarationList(simpleCase)) { + throw new Error('simpleCase is not a VariableDeclarationList'); + } + expect(ASTMatch.isVariableDeclarationList(simpleCase)).toBeTruthy(); + expect(ASTMatch.is(simpleCase, VariableDeclarationList)).toBeTruthy(); + + const stringDeclaration = simpleCase.declarations[0]; + const booleanDeclaration = simpleCase.declarations[1]; + const numberDeclaration = simpleCase.declarations[2]; + const integerDeclaration = simpleCase.declarations[3]; + const objectDeclaration = simpleCase.declarations[4]; + const mapDeclaration = simpleCase.declarations[5]; + const arrayProperty = testScope.output.globalVariableTable.getByKeyPath(['object', 'key4']); + + expect(ASTMatch.isString(stringDeclaration.type)).toBeTruthy(); + expect(ASTMatch.is(stringDeclaration.type, StringType)).toBeTruthy(); + + expect(ASTMatch.isBoolean(booleanDeclaration.type)).toBeTruthy(); + expect(ASTMatch.is(booleanDeclaration.type, BooleanType)).toBeTruthy(); + + expect(ASTMatch.isNumber(numberDeclaration.type)).toBeTruthy(); + expect(ASTMatch.is(numberDeclaration.type, NumberType)).toBeTruthy(); + + expect(ASTMatch.isInteger(integerDeclaration.type)).toBeTruthy(); + expect(ASTMatch.is(integerDeclaration.type, IntegerType)).toBeTruthy(); + + expect(ASTMatch.isObject(objectDeclaration.type)).toBeTruthy(); + expect(ASTMatch.is(objectDeclaration.type, ObjectType)).toBeTruthy(); + + expect(ASTMatch.isMap(mapDeclaration.type)).toBeTruthy(); + expect(ASTMatch.is(mapDeclaration.type, MapType)).toBeTruthy(); + + if (!ASTMatch.isProperty(arrayProperty)) { + throw new Error('arrayProperty is not a Property'); + } + expect(ASTMatch.isArray(arrayProperty.type)).toBeTruthy(); + expect(ASTMatch.is(arrayProperty.type, ArrayType)).toBeTruthy(); + }); +}); diff --git a/packages/variable-engine/variable-core/src/ast/index.ts b/packages/variable-engine/variable-core/src/ast/index.ts index b8885980..d7cf9ecf 100644 --- a/packages/variable-engine/variable-core/src/ast/index.ts +++ b/packages/variable-engine/variable-core/src/ast/index.ts @@ -16,5 +16,6 @@ export * from './type'; export * from './expression'; export { ASTFactory } from './factory'; +export { ASTMatch } from './match'; export { injectToAST, postConstructAST } from './utils/inversify'; export { isMatchAST } from './utils/helpers'; diff --git a/packages/variable-engine/variable-core/src/ast/match.ts b/packages/variable-engine/variable-core/src/ast/match.ts new file mode 100644 index 00000000..0a0f9dc2 --- /dev/null +++ b/packages/variable-engine/variable-core/src/ast/match.ts @@ -0,0 +1,73 @@ +import { ASTKind } from './types'; +import { + type StringType, + type NumberType, + type BooleanType, + type IntegerType, + type ObjectType, + type ArrayType, + type MapType, + type CustomType, +} from './type'; +import { type EnumerateExpression, type KeyPathExpression } from './expression'; +import { + type Property, + type VariableDeclaration, + type VariableDeclarationList, +} from './declaration'; +import { type ASTNode } from './ast-node'; + +export namespace ASTMatch { + /** + * 类型相关 + * @returns + */ + export const isString = (node?: ASTNode): node is StringType => node?.kind === ASTKind.String; + + export const isNumber = (node?: ASTNode): node is NumberType => node?.kind === ASTKind.Number; + + export const isBoolean = (node?: ASTNode): node is BooleanType => node?.kind === ASTKind.Boolean; + + export const isInteger = (node?: ASTNode): node is IntegerType => node?.kind === ASTKind.Integer; + + export const isObject = (node?: ASTNode): node is ObjectType => node?.kind === ASTKind.Object; + + export const isArray = (node?: ASTNode): node is ArrayType => node?.kind === ASTKind.Array; + + export const isMap = (node?: ASTNode): node is MapType => node?.kind === ASTKind.Map; + + export const isCustomType = (node?: ASTNode): node is CustomType => + node?.kind === ASTKind.CustomType; + + /** + * 声明相关 + */ + export const isVariableDeclaration = ( + node?: ASTNode + ): node is VariableDeclaration => node?.kind === ASTKind.VariableDeclaration; + + export const isProperty = (node?: ASTNode): node is Property => + node?.kind === ASTKind.Property; + + export const isVariableDeclarationList = (node?: ASTNode): node is VariableDeclarationList => + node?.kind === ASTKind.VariableDeclarationList; + + /** + * 表达式相关 + */ + export const isEnumerateExpression = (node?: ASTNode): node is EnumerateExpression => + node?.kind === ASTKind.EnumerateExpression; + + export const isKeyPathExpression = (node?: ASTNode): node is KeyPathExpression => + node?.kind === ASTKind.KeyPathExpression; + + /** + * Check AST Match by ASTClass + */ + export function is( + node?: ASTNode, + targetType?: { kind: string; new (...args: any[]): TargetASTNode } + ): node is TargetASTNode { + return node?.kind === targetType?.kind; + } +} diff --git a/packages/variable-engine/variable-core/src/ast/utils/helpers.ts b/packages/variable-engine/variable-core/src/ast/utils/helpers.ts index 4e75cfc9..bef94a74 100644 --- a/packages/variable-engine/variable-core/src/ast/utils/helpers.ts +++ b/packages/variable-engine/variable-core/src/ast/utils/helpers.ts @@ -1,4 +1,5 @@ import { ASTNodeJSON, ASTNodeJSONOrKind } from '../types'; +import { ASTMatch } from '../match'; import { ASTNode } from '../ast-node'; export function updateChildNodeHelper( @@ -53,9 +54,15 @@ export function getAllChildren(ast: ASTNode): ASTNode[] { return [...ast.children, ...ast.children.map((_child) => getAllChildren(_child)).flat()]; } +/** + * isMatchAST is same as ASTMatch.is + * @param node + * @param targetType + * @returns + */ export function isMatchAST( node?: ASTNode, targetType?: { kind: string; new (...args: any[]): TargetASTNode } ): node is TargetASTNode { - return node?.kind === targetType?.kind; + return ASTMatch.is(node, targetType); }