Skip to content

Commit

Permalink
feat: implementation of IfThenElseCondition (#593)
Browse files Browse the repository at this point in the history
  • Loading branch information
derekpierre authored Oct 16, 2024
2 parents 7f30464 + bf85a9c commit fcdb1da
Show file tree
Hide file tree
Showing 8 changed files with 410 additions and 5 deletions.
20 changes: 18 additions & 2 deletions packages/taco/src/conditions/condition-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,39 @@ import {
CompoundConditionType,
} from './compound-condition';
import { Condition, ConditionProps } from './condition';
import {
IfThenElseCondition,
IfThenElseConditionProps,
IfThenElseConditionType,
} from './if-then-else-condition';
import {
SequentialCondition,
SequentialConditionProps,
SequentialConditionType,
} from './sequential';

const ERR_INVALID_CONDITION_TYPE = (type: string) =>
`Invalid condition type: ${type}`;

export class ConditionFactory {
public static conditionFromProps(props: ConditionProps): Condition {
switch (props.conditionType) {
// Base Conditions
case RpcConditionType:
return new RpcCondition(props as RpcConditionProps);
case TimeConditionType:
return new TimeCondition(props as TimeConditionProps);
case ContractConditionType:
return new ContractCondition(props as ContractConditionProps);
case CompoundConditionType:
return new CompoundCondition(props as CompoundConditionProps);
case JsonApiConditionType:
return new JsonApiCondition(props as JsonApiConditionProps);
// Logical Conditions
case CompoundConditionType:
return new CompoundCondition(props as CompoundConditionProps);
case SequentialConditionType:
return new SequentialCondition(props as SequentialConditionProps);
case IfThenElseConditionType:
return new IfThenElseCondition(props as IfThenElseConditionProps);
default:
throw new Error(ERR_INVALID_CONDITION_TYPE(props.conditionType));
}
Expand Down
22 changes: 22 additions & 0 deletions packages/taco/src/conditions/if-then-else-condition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Condition } from './condition';
import {
IfThenElseConditionProps,
ifThenElseConditionSchema,
IfThenElseConditionType,
} from './schemas/if-then-else';
import { OmitConditionType } from './shared';

export {
IfThenElseConditionProps,
ifThenElseConditionSchema,
IfThenElseConditionType,
} from './schemas/if-then-else';

export class IfThenElseCondition extends Condition {
constructor(value: OmitConditionType<IfThenElseConditionProps>) {
super(ifThenElseConditionSchema, {
conditionType: IfThenElseConditionType,
...value,
});
}
}
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,5 +6,6 @@ export * as condition from './condition';
export * as conditionExpr from './condition-expr';
export { ConditionFactory } from './condition-factory';
export * as context from './context';
export * as ifThenElse from './if-then-else-condition';
export * as sequential from './sequential';
export { base, predefined };
19 changes: 16 additions & 3 deletions packages/taco/src/conditions/multi-condition.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { CompoundConditionType } from './compound-condition';
import { ConditionProps } from './condition';
import { IfThenElseConditionType } from './if-then-else-condition';
import { ConditionVariableProps, SequentialConditionType } from './sequential';

export const maxNestedDepth =
(maxDepth: number) =>
(condition: ConditionProps, currentDepth = 1) => {
(condition: ConditionProps, currentDepth = 1): boolean => {
if (
condition.conditionType === CompoundConditionType ||
condition.conditionType === SequentialConditionType
condition.conditionType === SequentialConditionType ||
condition.conditionType === IfThenElseConditionType
) {
if (currentDepth > maxDepth) {
// no more multi-condition types allowed at this level
Expand All @@ -18,11 +20,22 @@ export const maxNestedDepth =
return condition.operands.every((child: ConditionProps) =>
maxNestedDepth(maxDepth)(child, currentDepth + 1),
);
} else {
} else if (condition.conditionType === SequentialConditionType) {
return condition.conditionVariables.every(
(child: ConditionVariableProps) =>
maxNestedDepth(maxDepth)(child.condition, currentDepth + 1),
);
} else {
// if-then-else condition
const ifThenElseConditions = [];
ifThenElseConditions.push(condition.ifCondition);
ifThenElseConditions.push(condition.thenCondition);
if (typeof condition.elseCondition !== 'boolean') {
ifThenElseConditions.push(condition.elseCondition);
}
return ifThenElseConditions.every((child: ConditionProps) =>
maxNestedDepth(maxDepth)(child, currentDepth + 1),
);
}
}

Expand Down
53 changes: 53 additions & 0 deletions packages/taco/src/conditions/schemas/if-then-else.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { z } from 'zod';

import { maxNestedDepth } from '../multi-condition';

import { baseConditionSchema } from './common';
import { anyConditionSchema } from './utils';

export const IfThenElseConditionType = 'if-then-else';

export const ifThenElseConditionSchema: z.ZodSchema = z.lazy(() =>
baseConditionSchema
.extend({
conditionType: z
.literal(IfThenElseConditionType)
.default(IfThenElseConditionType),
ifCondition: anyConditionSchema,
thenCondition: anyConditionSchema,
elseCondition: z.union([anyConditionSchema, z.boolean()]),
})
.refine(
// already at 2nd level since checking member condition
(condition) => maxNestedDepth(2)(condition.ifCondition, 2),
{
message: 'Exceeded max nested depth of 2 for multi-condition type',
path: ['ifCondition'],
}, // Max nested depth of 2
)
.refine(
// already at 2nd level since checking member condition
(condition) => maxNestedDepth(2)(condition.thenCondition, 2),
{
message: 'Exceeded max nested depth of 2 for multi-condition type',
path: ['thenCondition'],
}, // Max nested depth of 2
)
.refine(
(condition) => {
if (typeof condition.elseCondition !== 'boolean') {
// already at 2nd level since checking member condition
return maxNestedDepth(2)(condition.elseCondition, 2);
}
return true;
},
{
message: 'Exceeded max nested depth of 2 for multi-condition type',
path: ['elseCondition'],
}, // Max nested depth of 2
),
);

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

import { contractConditionSchema } from './contract';
import { ifThenElseConditionSchema } from './if-then-else';
import { jsonApiConditionSchema } from './json-api';
import { rpcConditionSchema } from './rpc';
import { sequentialConditionSchema } from './sequential';
Expand All @@ -16,5 +17,6 @@ export const anyConditionSchema: z.ZodSchema = z.lazy(() =>
compoundConditionSchema,
jsonApiConditionSchema,
sequentialConditionSchema,
ifThenElseConditionSchema,
]),
);
176 changes: 176 additions & 0 deletions packages/taco/test/conditions/if-then-else-condition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { describe, expect, it } from 'vitest';

import {
IfThenElseCondition,
ifThenElseConditionSchema,
IfThenElseConditionType,
} from '../../src/conditions/if-then-else-condition';
import { TimeConditionType } from '../../src/conditions/schemas/time';
import {
testCompoundConditionObj,
testContractConditionObj,
testRpcConditionObj,
testSequentialConditionObj,
testTimeConditionObj,
} from '../test-utils';

describe('validation', () => {
it('infers default condition type from constructor', () => {
const condition = new IfThenElseCondition({
ifCondition: testRpcConditionObj,
thenCondition: testTimeConditionObj,
elseCondition: testContractConditionObj,
});
expect(condition.value.conditionType).toEqual(IfThenElseConditionType);
});

it('validates type', () => {
const result = IfThenElseCondition.validate(ifThenElseConditionSchema, {
conditionType: TimeConditionType,
ifCondition: testRpcConditionObj,
thenCondition: testTimeConditionObj,
elseCondition: testContractConditionObj,
});
expect(result.error).toBeDefined();
expect(result.data).toBeUndefined();
expect(result.error?.format()).toMatchObject({
conditionType: {
_errors: [
`Invalid literal value, expected "${IfThenElseConditionType}"`,
],
},
});
});

it('accepts recursive if-then-else conditions', () => {
const nestedIfThenElseConditionObj = {
conditionType: IfThenElseConditionType,
ifCondition: testRpcConditionObj,
thenCondition: testTimeConditionObj,
elseCondition: testContractConditionObj,
};

const conditionObj = {
conditionType: IfThenElseConditionType,
ifCondition: testTimeConditionObj,
thenCondition: nestedIfThenElseConditionObj,
elseCondition: nestedIfThenElseConditionObj,
};

const result = IfThenElseCondition.validate(
ifThenElseConditionSchema,
conditionObj,
);
expect(result.error).toBeUndefined();
expect(result.data).toEqual({
conditionType: IfThenElseConditionType,
ifCondition: testTimeConditionObj,
thenCondition: nestedIfThenElseConditionObj,
elseCondition: nestedIfThenElseConditionObj,
});
});

it('accepts nested sequential and compound conditions', () => {
const conditionObj = {
conditionType: IfThenElseConditionType,
ifCondition: testRpcConditionObj,
thenCondition: testSequentialConditionObj,
elseCondition: testCompoundConditionObj,
};
const result = IfThenElseCondition.validate(
ifThenElseConditionSchema,
conditionObj,
);
expect(result.error).toBeUndefined();
expect(result.data).toEqual({
conditionType: IfThenElseConditionType,
ifCondition: testRpcConditionObj,
thenCondition: testSequentialConditionObj,
elseCondition: testCompoundConditionObj,
});
});

it.each([true, false])(
'accepts boolean for else condition',
(booleanValue) => {
const conditionObj = {
conditionType: IfThenElseConditionType,
ifCondition: testTimeConditionObj,
thenCondition: testRpcConditionObj,
elseCondition: booleanValue,
};

const result = IfThenElseCondition.validate(
ifThenElseConditionSchema,
conditionObj,
);
expect(result.error).toBeUndefined();
expect(result.data).toEqual({
conditionType: IfThenElseConditionType,
ifCondition: testTimeConditionObj,
thenCondition: testRpcConditionObj,
elseCondition: booleanValue,
});
},
);

it('limits max depth of nested if condition', () => {
const result = IfThenElseCondition.validate(ifThenElseConditionSchema, {
ifCondition: {
conditionType: IfThenElseConditionType,
ifCondition: testRpcConditionObj,
thenCondition: testCompoundConditionObj,
elseCondition: testTimeConditionObj,
},
thenCondition: testRpcConditionObj,
elseCondition: testTimeConditionObj,
});
expect(result.error).toBeDefined();
expect(result.data).toBeUndefined();
expect(result.error?.format()).toMatchObject({
ifCondition: {
_errors: [`Exceeded max nested depth of 2 for multi-condition type`],
},
});
});

it('limits max depth of nested then condition', () => {
const result = IfThenElseCondition.validate(ifThenElseConditionSchema, {
ifCondition: testRpcConditionObj,
thenCondition: {
conditionType: IfThenElseConditionType,
ifCondition: testRpcConditionObj,
thenCondition: testCompoundConditionObj,
elseCondition: true,
},
elseCondition: testTimeConditionObj,
});
expect(result.error).toBeDefined();
expect(result.data).toBeUndefined();
expect(result.error?.format()).toMatchObject({
thenCondition: {
_errors: [`Exceeded max nested depth of 2 for multi-condition type`],
},
});
});

it('limits max depth of nested else condition', () => {
const result = IfThenElseCondition.validate(ifThenElseConditionSchema, {
ifCondition: testRpcConditionObj,
thenCondition: testTimeConditionObj,
elseCondition: {
conditionType: IfThenElseConditionType,
ifCondition: testRpcConditionObj,
thenCondition: testSequentialConditionObj,
elseCondition: testTimeConditionObj,
},
});
expect(result.error).toBeDefined();
expect(result.data).toBeUndefined();
expect(result.error?.format()).toMatchObject({
elseCondition: {
_errors: [`Exceeded max nested depth of 2 for multi-condition type`],
},
});
});
});
Loading

0 comments on commit fcdb1da

Please sign in to comment.