| name | ast-builder-developer |
| description | 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. |
AST Builder Developer Guide
Purpose
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.
When to Use This Skill
Automatically activates when you mention:
- AstBuilder components
- Rule builder / condition builder
- AST nodes or node types
- Edition mode / viewing mode
- Node validation
- Operand editing
- sharpstate in AstBuilder context
Architecture Overview
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
Core Concepts
Two State Factories
1. AstBuilderDataSharpFactory (Provider.tsx)
- Holds scenario data, builder options, mode
- Created once per AstBuilder instance
- Provides:
dataModel, triggerObjectType, mode, showValues
const builderMode = AstBuilderDataSharpFactory.select((s) => s.mode);
const data = AstBuilderDataSharpFactory.useSharp().value.$data!.value;
2. AstBuilderNodeSharpFactory (node-store.ts)
- Holds current node state, validation, actions
- Created per Root component
- Actions:
setNodeAtPath, validate, copyNode, triggerUpdate
const nodeSharp = AstBuilderNodeSharpFactory.useSharp();
nodeSharp.actions.setNodeAtPath(path, newNode);
nodeSharp.actions.validate();
Modes
| Mode | Purpose | Components |
|---|
edit | User can modify nodes | EditionAstBuilder* |
view | Read-only display | ViewingAstBuilder* |
Node Types
Logical Structure Nodes
interface AndAstNode {
id: string;
name: 'And';
children: AstNode[];
namedChildren: Record<string, never>;
}
interface OrWithAndAstNode {
id: string;
name: 'Or';
children: AndAstNode[];
namedChildren: Record<string, never>;
}
Operator Nodes
interface MainAstBinaryNode {
id: string;
name: BinaryMainAstOperatorFunction;
children: [AstNode, AstNode];
}
interface MainAstUnaryNode {
id: string;
name: UnaryMainAstOperatorFunction;
children: [AstNode];
}
Operand Nodes (KnownOperandAstNode)
| 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 |
Editable Nodes (Modal-based)
These nodes require a dedicated EditModal:
AggregationAstNode - Count, Sum, Avg operations
TimeAddAstNode - Date arithmetic
FuzzyMatchComparatorAstNode - Fuzzy string matching
IsMultipleOfAstNode - Divisibility check
StringTemplateAstNode - String interpolation
Path-Based Navigation
Nodes are accessed via path strings:
import { getAtPath, parsePath, getParentPath } from '@app-builder/utils/tree';
const node = getAtPath(rootNode, parsePath('children.0.children.1'));
const parentPath = getParentPath(parsePath('children.0.children.1'));
Path format:
children.0 - First child
children.1.children.0 - First grandchild of second child
namedChildren.left - Named child 'left'
Component Patterns
Using AstBuilder
import { AstBuilder } from '@app-builder/components/AstBuilder';
<AstBuilder.Provider scenarioId={scenarioId} mode="edit">
<AstBuilder.Root
node={astNode}
validation={validation}
onUpdate={(node) => handleUpdate(node)}
onValidationUpdate={(v) => setValidation(v)}
/>
</AstBuilder.Provider>
EditionNode Pattern
The EditionAstBuilderNode uses ts-pattern to route rendering:
match(node.value)
.when(isMainAstBinaryNode, (node) => {
return (
<>
<EditionAstBuilderNode path={`${path}.children.0`} />
<OperatorSelect operator={node.name} onOperatorChange={setOperator} />
<EditionAstBuilderNode path={`${path}.children.1`} />
</>
);
})
.when(isMainAstUnaryNode, (node) => {
})
.when(isKnownOperandAstNode, (node) => {
return <EditionAstBuilderOperand node={node} onChange={setNode} />;
})
.otherwise(() => <NodeTypeError />);
Updating Nodes
const nodeSharp = AstBuilderNodeSharpFactory.useSharp();
const setNode = (newNode: AstNode) => {
nodeSharp.actions.setNodeAtPath(props.path, newNode);
nodeSharp.actions.validate();
};
const setOperator = (operator: string) => {
node.value.name = operator;
if (isUnaryMainAstOperatorFunction(operator) && node.value.children.length > 1) {
node.value.children = [node.value.children[0]!];
}
nodeSharp.actions.triggerUpdate();
nodeSharp.actions.validate();
};
Type Guards
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';
Validation Flow
const nodeStore = AstBuilderNodeSharpFactory.createSharp({
initialNode: node,
initialValidation: validation,
validationFn: async (node) => {
return await validateAst(scenarioId, node);
},
updateFn: (node) => onUpdate(node),
});
nodeSharp.actions.setNodeAtPath(path, newNode);
nodeSharp.actions.validate();
import { getErrorsForNode } from './edition/helpers';
const errors = getErrorsForNode(validation, node.id, true);
const hasError = errors.length > 0;
Common Tasks
Adding a New Operator
- Add to
builder-ast-node-node-operator.ts
- Update
allMainAstOperatorFunctionsOptions in EditionNode.tsx
- Add i18n translation in
scenarios namespace
Creating a New Editable Node Type
- Define interface in
models/astNode/
- Add type guard function
- Update
isEditableAstNode in builder-ast-node.ts
- Create modal in
edition/EditModal/modals/
- Register in
EditModal.tsx
Working with Data Accessors
import { getDataAccessorAstNodeField } from '@app-builder/services/ast-node/getDataAccessorAstNodeField';
if (isDataAccessorAstNode(node)) {
const field = getDataAccessorAstNodeField(node, {
dataModel: data.dataModel,
triggerObjectTable: triggerTable,
});
}
Key Files Reference
| 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 |
Best Practices
- Always validate after changes - Call
nodeSharp.actions.validate() after any node modification
- Use type guards - Never assume node type, always check with
is* functions
- Use ts-pattern - For node type routing, use
match().when().otherwise()
- Clone before mutating - Use
clone() from remeda when needed
- Path-based updates - Use
setNodeAtPath instead of direct mutation
- Handle both modes - Check if component needs to support edit AND view modes
Troubleshooting
Node not updating visually
- Ensure
validate() is called after setNodeAtPath()
- Check if
triggerUpdate() is needed for operator changes
Validation not showing errors
- Verify
getErrorsForNode() is called with correct node ID
- Check if validation response contains the expected structure
Type errors with node properties
- Use appropriate type guard before accessing properties
- Check if node type matches expected interface
Skill Status: Initial version for Marble AstBuilder