AST Builder component patterns for Marble's rule/condition builder. Use when working with AstBuilder components, editing AST nodes, node state management with sharpstate, validation flows, or any rule builder UI. Covers Provider/Root patterns, edition vs viewing modes, node types (And, Or, Main, Operand), EditModal system, and path-based navigation.
Comprehensive guide for working with the AstBuilder component system - Marble's visual rule and condition builder. This system allows users to create complex logical expressions using a visual UI.
Automatically activates when you mention:
AstBuilder/
index.tsx # Exports: Root, Operand, Provider, EditModal
Provider.tsx # AstBuilderDataSharpFactory - holds builder options
Root.tsx # Routes to edition/viewing based on mode
Operand.tsx # Operand display component
types.ts # Type definitions
edition/ # Edit mode components
node-store.ts # AstBuilderNodeSharpFactory - node state
EditionNode.tsx # Main editing node component
EditionOperand.tsx
EditionAndRoot.tsx
EditionOrWithAndRoot.tsx
EditModal/ # Modal for special node types
modals/ # Aggregation, FuzzyMatch, TimeAdd, etc.
viewing/ # View mode components
ViewingNode.tsx
ViewingOperand.tsx
ViewingAndRoot.tsx
1. AstBuilderDataSharpFactory (Provider.tsx)
dataModel, triggerObjectType, mode, showValuesconst builderMode = AstBuilderDataSharpFactory.select((s) => s.mode);
const data = AstBuilderDataSharpFactory.useSharp().value.$data!.value;
2. AstBuilderNodeSharpFactory (node-store.ts)
setNodeAtPath, validate, copyNode, triggerUpdateconst nodeSharp = AstBuilderNodeSharpFactory.useSharp();
nodeSharp.actions.setNodeAtPath(path, newNode);
nodeSharp.actions.validate();
| Mode | Purpose | Components |
|------|---------|------------|
| edit | User can modify nodes | EditionAstBuilder* |
| view | Read-only display | ViewingAstBuilder* |
// AND node - groups conditions
interface AndAstNode {
id: string;
name: 'And';
children: AstNode[];
namedChildren: Record<string, never>;
}
// OR with AND children - top-level structure
interface OrWithAndAstNode {
id: string;
name: 'Or';
children: AndAstNode[];
namedChildren: Record<string, never>;
}
// Binary operators: =, ≠, <, <=, >, >=, +, -, *, /, IsInList, etc.
interface MainAstBinaryNode {
id: string;
name: BinaryMainAstOperatorFunction;
children: [AstNode, AstNode]; // Left and right operands
}
// Unary operators: IsEmpty, IsNotEmpty
interface MainAstUnaryNode {
id: string;
name: UnaryMainAstOperatorFunction;
children: [AstNode]; // Single operand
}
| Type | Description | Example |
|------|-------------|---------|
| UndefinedAstNode | Empty placeholder | New condition slot |
| ConstantAstNode | Literal value | "hello", 42, true |
| DataAccessorAstNode | Field reference | transaction.amount |
| CustomListAccessAstNode | List reference | blockedCountries |
| EditableAstNode | Complex nodes | Aggregation, FuzzyMatch |
These nodes require a dedicated EditModal:
AggregationAstNode - Count, Sum, Avg operationsTimeAddAstNode - Date arithmeticFuzzyMatchComparatorAstNode - Fuzzy string matchingIsMultipleOfAstNode - Divisibility checkStringTemplateAstNode - String interpolationNodes are accessed via path strings:
import { getAtPath, parsePath, getParentPath } from '@app-builder/utils/tree';
// Get node at path
const node = getAtPath(rootNode, parsePath('children.0.children.1'));
// Get parent path
const parentPath = getParentPath(parsePath('children.0.children.1'));
// Result: { path: 'children.0', childPathSegment: { type: 'children', index: 1 } }
Path format:
children.0 - First childchildren.1.children.0 - First grandchild of second childnamedChildren.left - Named child 'left'import { AstBuilder } from '@app-builder/components/AstBuilder';
// Full setup with Provider
<AstBuilder.Provider scenarioId={scenarioId} mode="edit">
<AstBuilder.Root
node={astNode}
validation={validation}
onUpdate={(node) => handleUpdate(node)}
onValidationUpdate={(v) => setValidation(v)}
/>
</AstBuilder.Provider>
The EditionAstBuilderNode uses ts-pattern to route rendering:
match(node.value)
.when(isMainAstBinaryNode, (node) => {
// Render binary operator with left/right children
return (
<>
<EditionAstBuilderNode path={`${path}.children.0`} />
<OperatorSelect operator={node.name} onOperatorChange={setOperator} />
<EditionAstBuilderNode path={`${path}.children.1`} />
</>
);
})
.when(isMainAstUnaryNode, (node) => {
// Render unary operator with single child
})
.when(isKnownOperandAstNode, (node) => {
// Render operand (leaf node)
return <EditionAstBuilderOperand node={node} onChange={setNode} />;
})
.otherwise(() => <NodeTypeError />);
const nodeSharp = AstBuilderNodeSharpFactory.useSharp();
// Update node at path
const setNode = (newNode: AstNode) => {
nodeSharp.actions.setNodeAtPath(props.path, newNode);
nodeSharp.actions.validate();
};
// Update operator only
const setOperator = (operator: string) => {
node.value.name = operator;
// Adjust children count for unary/binary
if (isUnaryMainAstOperatorFunction(operator) && node.value.children.length > 1) {
node.value.children = [node.value.children[0]!];
}
nodeSharp.actions.triggerUpdate();
nodeSharp.actions.validate();
};
Always use type guards before accessing node properties:
import {
isAndAstNode,
isOrWithAndAstNode,
isMainAstNode,
isMainAstBinaryNode,
isMainAstUnaryNode,
isKnownOperandAstNode,
isEditableAstNode,
} from '@app-builder/models/astNode/builder-ast-node';
import { isConstant } from '@app-builder/models/astNode/constant';
import { isDataAccessorAstNode } from '@app-builder/models/astNode/data-accessor';
import { isAggregation } from '@app-builder/models/astNode/aggregation';
// 1. Node store has validation function
const nodeStore = AstBuilderNodeSharpFactory.createSharp({
initialNode: node,
initialValidation: validation,
validationFn: async (node) => {
// Call API to validate
return await validateAst(scenarioId, node);
},
updateFn: (node) => onUpdate(node),
});
// 2. After changes, trigger validation
nodeSharp.actions.setNodeAtPath(path, newNode);
nodeSharp.actions.validate();
// 3. Get errors for specific node
import { getErrorsForNode } from './edition/helpers';
const errors = getErrorsForNode(validation, node.id, true);
const hasError = errors.length > 0;
builder-ast-node-node-operator.tsallMainAstOperatorFunctionsOptions in EditionNode.tsxscenarios namespacemodels/astNode/isEditableAstNode in builder-ast-node.tsedition/EditModal/modals/EditModal.tsximport { getDataAccessorAstNodeField } from '@app-builder/services/ast-node/getDataAccessorAstNodeField';
if (isDataAccessorAstNode(node)) {
const field = getDataAccessorAstNodeField(node, {
dataModel: data.dataModel,
triggerObjectTable: triggerTable,
});
// field.isEnum, field.values, field.dataType, etc.
}
| File | Purpose |
|------|---------|
| components/AstBuilder/index.tsx | Main exports |
| components/AstBuilder/Provider.tsx | Data provider factory |
| components/AstBuilder/Root.tsx | Entry point, mode routing |
| components/AstBuilder/edition/node-store.ts | Node state management |
| components/AstBuilder/edition/EditionNode.tsx | Main editing component |
| components/AstBuilder/edition/EditionOperand.tsx | Operand editing |
| models/astNode/builder-ast-node.ts | Node types & guards |
| models/astNode/builder-ast-node-node-operator.ts | Operator definitions |
| utils/tree.ts | Path navigation utilities |
nodeSharp.actions.validate() after any node modificationis* functionsmatch().when().otherwise()clone() from remeda when neededsetNodeAtPath instead of direct mutationvalidate() is called after setNodeAtPath()triggerUpdate() is needed for operator changesgetErrorsForNode() is called with correct node IDSkill Status: Initial version for Marble AstBuilder
npx skills add checkmarble/抽象语法树构建器 开发者下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer