Skip to content

Commit

Permalink
Merge epic-sequential-condition (#590)
Browse files Browse the repository at this point in the history
  • Loading branch information
derekpierre authored Oct 4, 2024
2 parents ba4285b + 18d29a8 commit 9fa5ffa
Show file tree
Hide file tree
Showing 9 changed files with 565 additions and 7 deletions.
4 changes: 2 additions & 2 deletions packages/taco/src/conditions/base/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from 'zod';

import { Condition } from '../condition';
import { baseConditionSchema, Condition } from '../condition';
import { SUPPORTED_CHAIN_IDS } from '../const';
import {
EthAddressOrUserAddressSchema,
Expand All @@ -12,7 +12,7 @@ import createUnionSchema from '../zod';

export const RpcConditionType = 'rpc';

export const rpcConditionSchema = z.object({
export const rpcConditionSchema = baseConditionSchema.extend({
conditionType: z.literal(RpcConditionType).default(RpcConditionType),
chain: createUnionSchema(SUPPORTED_CHAIN_IDS),
method: z.enum(['eth_getBalance']),
Expand Down
19 changes: 15 additions & 4 deletions packages/taco/src/conditions/compound-condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { z } from 'zod';
import { contractConditionSchema } from './base/contract';
import { rpcConditionSchema } from './base/rpc';
import { timeConditionSchema } from './base/time';
import { Condition, ConditionProps } from './condition';
import { baseConditionSchema, Condition, ConditionProps } from './condition';
import { maxNestedDepth } from './multi-condition';
import { sequentialConditionSchema } from './sequential';
import { OmitConditionType } from './shared';

export const CompoundConditionType = 'compound';

export const compoundConditionSchema: z.ZodSchema = z
.object({
export const compoundConditionSchema: z.ZodSchema = baseConditionSchema
.extend({
conditionType: z
.literal(CompoundConditionType)
.default(CompoundConditionType),
Expand All @@ -22,10 +24,12 @@ export const compoundConditionSchema: z.ZodSchema = z
timeConditionSchema,
contractConditionSchema,
compoundConditionSchema,
sequentialConditionSchema,
]),
),
)
.min(1),
.min(1)
.max(5),
})
.refine(
(condition) => {
Expand All @@ -46,6 +50,13 @@ export const compoundConditionSchema: z.ZodSchema = z
message: `Invalid number of operands ${operands.length} for operator "${operator}"`,
path: ['operands'],
}),
)
.refine(
(condition) => maxNestedDepth(2)(condition),
{
message: 'Exceeded max nested depth of 2 for multi-condition type',
path: ['operands'],
}, // Max nested depth of 2
);

export type CompoundConditionProps = z.infer<typeof compoundConditionSchema>;
Expand Down
4 changes: 4 additions & 0 deletions packages/taco/src/conditions/condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { z } from 'zod';

import { USER_ADDRESS_PARAMS } from './const';

export const baseConditionSchema = z.object({
conditionType: z.string(),
});

type ConditionSchema = z.ZodSchema;
export type ConditionProps = z.infer<ConditionSchema>;

Expand Down
1 change: 1 addition & 0 deletions packages/taco/src/conditions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export * as condition from './condition';
export * as conditionExpr from './condition-expr';
export { ConditionFactory } from './condition-factory';
export * as context from './context';
export * as sequential from './sequential';
export { base, predefined };
30 changes: 30 additions & 0 deletions packages/taco/src/conditions/multi-condition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { CompoundConditionType } from './compound-condition';
import { ConditionProps } from './condition';
import { ConditionVariableProps, SequentialConditionType } from './sequential';

export const maxNestedDepth =
(maxDepth: number) =>
(condition: ConditionProps, currentDepth = 1) => {
if (
condition.conditionType === CompoundConditionType ||
condition.conditionType === SequentialConditionType
) {
if (currentDepth > maxDepth) {
// no more multi-condition types allowed at this level
return false;
}

if (condition.conditionType === CompoundConditionType) {
return condition.operands.every((child: ConditionProps) =>
maxNestedDepth(maxDepth)(child, currentDepth + 1),
);
} else {
return condition.conditionVariables.every(
(child: ConditionVariableProps) =>
maxNestedDepth(maxDepth)(child.condition, currentDepth + 1),
);
}
}

return true;
};
73 changes: 73 additions & 0 deletions packages/taco/src/conditions/sequential.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { z } from 'zod';

import { contractConditionSchema } from './base/contract';
import { rpcConditionSchema } from './base/rpc';
import { timeConditionSchema } from './base/time';
import { compoundConditionSchema } from './compound-condition';
import { baseConditionSchema, Condition } from './condition';
import { maxNestedDepth } from './multi-condition';
import { OmitConditionType, plainStringSchema } from './shared';

export const SequentialConditionType = 'sequential';

export const conditionVariableSchema: z.ZodSchema = z.object({
varName: plainStringSchema,
condition: z.lazy(() =>
z.union([
rpcConditionSchema,
timeConditionSchema,
contractConditionSchema,
compoundConditionSchema,
sequentialConditionSchema,
]),
),
});

export const sequentialConditionSchema: z.ZodSchema = baseConditionSchema
.extend({
conditionType: z
.literal(SequentialConditionType)
.default(SequentialConditionType),
conditionVariables: z.array(conditionVariableSchema).min(2).max(5),
})
.refine(
(condition) => maxNestedDepth(2)(condition),
{
message: 'Exceeded max nested depth of 2 for multi-condition type',
path: ['conditionVariables'],
}, // Max nested depth of 2
)
.refine(
// check for duplicate var names
(condition) => {
const seen = new Set();
return condition.conditionVariables.every(
(child: ConditionVariableProps) => {
if (seen.has(child.varName)) {
return false;
}
seen.add(child.varName);
return true;
},
);
},
{
message: 'Duplicate variable names are not allowed',
path: ['conditionVariables'],
},
);

export type ConditionVariableProps = z.infer<typeof conditionVariableSchema>;

export type SequentialConditionProps = z.infer<
typeof sequentialConditionSchema
>;

export class SequentialCondition extends Condition {
constructor(value: OmitConditionType<SequentialConditionProps>) {
super(sequentialConditionSchema, {
conditionType: SequentialConditionType,
...value,
});
}
}
114 changes: 113 additions & 1 deletion packages/taco/test/conditions/compound-condition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SUPPORTED_CHAIN_IDS } from '../../src/conditions/const';
import {
testContractConditionObj,
testRpcConditionObj,
testSequentialConditionObj,
testTimeConditionObj,
} from '../test-utils';

Expand Down Expand Up @@ -97,7 +98,31 @@ describe('validation', () => {
},
);

it('accepts recursive compound conditions', () => {
it.each([
{
operator: 'and',
numOperands: 6,
},
{
operator: 'or',
numOperands: 6,
},
])('rejects > max number of operands', ({ operator, numOperands }) => {
const result = CompoundCondition.validate(compoundConditionSchema, {
operator,
operands: Array(numOperands).fill(testContractConditionObj),
});

expect(result.error).toBeDefined();
expect(result.data).toBeUndefined();
expect(result.error?.format()).toMatchObject({
operands: {
_errors: [`Array must contain at most 5 element(s)`],
},
});
});

it('accepts nested compound conditions', () => {
const conditionObj = {
conditionType: CompoundConditionType,
operator: 'and',
Expand Down Expand Up @@ -132,6 +157,93 @@ describe('validation', () => {
});
});

it('accepts nested sequential and compound conditions', () => {
const conditionObj = {
conditionType: CompoundConditionType,
operator: 'or',
operands: [
testContractConditionObj,
testTimeConditionObj,
testRpcConditionObj,
{
operator: 'or',
operands: [testTimeConditionObj, testContractConditionObj],
},
testSequentialConditionObj,
],
};
const result = CompoundCondition.validate(
compoundConditionSchema,
conditionObj,
);
expect(result.error).toBeUndefined();
expect(result.data).toEqual({
conditionType: CompoundConditionType,
operator: 'or',
operands: [
testContractConditionObj,
testTimeConditionObj,
testRpcConditionObj,
{
conditionType: CompoundConditionType,
operator: 'or',
operands: [testTimeConditionObj, testContractConditionObj],
},
testSequentialConditionObj,
],
});
});

it('limits max depth of nested compound condition', () => {
const result = CompoundCondition.validate(compoundConditionSchema, {
operator: 'or',
operands: [
testRpcConditionObj,
testContractConditionObj,
{
conditionType: CompoundConditionType,
operator: 'and',
operands: [
testTimeConditionObj,
{
conditionType: CompoundConditionType,
operator: 'or',
operands: [testTimeConditionObj, testRpcConditionObj],
},
],
},
],
});
expect(result.error).toBeDefined();
expect(result.data).toBeUndefined();
expect(result.error?.format()).toMatchObject({
operands: {
_errors: [`Exceeded max nested depth of 2 for multi-condition type`],
},
});
});
it('limits max depth of nested sequential condition', () => {
const result = CompoundCondition.validate(compoundConditionSchema, {
operator: 'or',
operands: [
testRpcConditionObj,
testContractConditionObj,
{
conditionType: CompoundConditionType,
operator: 'not',
operands: [testSequentialConditionObj],
},
],
});
expect(result.error).toBeDefined();
expect(result.data).toBeUndefined();
expect(result.error?.format()).toMatchObject({
operands: {
_errors: ['Exceeded max nested depth of 2 for multi-condition type'],
},
});
});

const multichainCondition: CompoundConditionProps = {
conditionType: CompoundConditionType,
operator: 'and',
Expand Down
Loading

0 comments on commit 9fa5ffa

Please sign in to comment.