Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(iotevents): support comparison operators #19329

Merged
merged 9 commits into from
Apr 8, 2022
12 changes: 6 additions & 6 deletions packages/@aws-cdk/aws-iotevents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,20 @@ const coldState = new iotevents.State({
stateName: 'cold',
});

// transit to coldState when temperature is 10
// transit to coldState when temperature is less than 15
warmState.transitionTo(coldState, {
eventName: 'to_coldState', // optional property, default by combining the names of the States
when: iotevents.Expression.eq(
when: iotevents.Expression.lt(
iotevents.Expression.inputAttribute(input, 'payload.temperature'),
iotevents.Expression.fromString('10'),
iotevents.Expression.fromString('15'),
),
executing: [new actions.LambdaInvokeAction(func)], // optional
});
// transit to warmState when temperature is 20
// transit to warmState when temperature is greater than or equal to 15
coldState.transitionTo(warmState, {
when: iotevents.Expression.eq(
when: iotevents.Expression.gte(
iotevents.Expression.inputAttribute(input, 'payload.temperature'),
iotevents.Expression.fromString('20'),
iotevents.Expression.fromString('15'),
),
});

Expand Down
69 changes: 64 additions & 5 deletions packages/@aws-cdk/aws-iotevents/lib/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,56 @@ export abstract class Expression {
* Create a expression for the Equal operator.
*/
public static eq(left: Expression, right: Expression): Expression {
return new BinaryOperationExpression(left, '==', right);
return new BinaryOperationExpression(left, '==', right, 9);
}

/**
* Create a expression for the Not Equal operator.
*/
public static neq(left: Expression, right: Expression): Expression {
return new BinaryOperationExpression(left, '!=', right, 9);
}

/**
* Create a expression for the Less Than operator.
*/
public static lt(left: Expression, right: Expression): Expression {
return new BinaryOperationExpression(left, '<', right, 10);
}

/**
* Create a expression for the Less Than Or Equal operator.
*/
public static lte(left: Expression, right: Expression): Expression {
return new BinaryOperationExpression(left, '<=', right, 10);
}

/**
* Create a expression for the Greater Than operator.
*/
public static gt(left: Expression, right: Expression): Expression {
return new BinaryOperationExpression(left, '>', right, 10);
}

/**
* Create a expression for the Greater Than Or Equal operator.
*/
public static gte(left: Expression, right: Expression): Expression {
return new BinaryOperationExpression(left, '>=', right, 10);
}

/**
* Create a expression for the AND operator.
*/
public static and(left: Expression, right: Expression): Expression {
return new BinaryOperationExpression(left, '&&', right);
return new BinaryOperationExpression(left, '&&', right, 5);
}

/**
* Create a expression for the OR operator.
*/
public static or(left: Expression, right: Expression): Expression {
return new BinaryOperationExpression(left, '||', right, 4);
}

constructor() {
Expand All @@ -47,7 +89,14 @@ export abstract class Expression {
/**
* This is called to evaluate the expression.
*/
public abstract evaluate(): string;
public abstract evaluate(
/**
* The parent operator priority for determine to add parenthesis.
* This is intended to be set according to MDN rules.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#table
*/
parentPriority?: number
): string;
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
}

class StringExpression extends Expression {
Expand All @@ -65,11 +114,21 @@ class BinaryOperationExpression extends Expression {
private readonly left: Expression,
private readonly operator: string,
private readonly right: Expression,
/**
* Indicates the priority of the operator.
* This is intended to be set according to MDN rules.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#table
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have adopted MDN as the operator priority rule. I think it can be used to whatever follow general rules and has concrete numbers.

*/
private readonly priority: number,
) {
super();
}

public evaluate() {
return `${this.left.evaluate()} ${this.operator} ${this.right.evaluate()}`;
public evaluate(parentPriority?: number) {
const expression = `${this.left.evaluate(this.priority)} ${this.operator} ${this.right.evaluate(this.priority)}`;
if (parentPriority === undefined) {
return expression;
}
return parentPriority > this.priority ? `(${expression})` : expression;
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
}
}
112 changes: 19 additions & 93 deletions packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,14 +498,30 @@ test('cannot create transitions that transit to duprecated target state', () =>
});

describe('Expression', () => {
test('currentInput', () => {
const E = iotevents.Expression;
test.each([
['currentInput', (_input: iotevents.IInput) => E.currentInput(_input), 'currentInput("test-input")'],
['inputAttribute', (_input: iotevents.IInput) => E.inputAttribute(_input, 'json.path'), '$input.test-input.json.path'],
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
['eq', () => E.eq(E.fromString('"aaa"'), E.fromString('"bbb"')), '"aaa" == "bbb"'],
['neq', () => E.neq(E.fromString('"aaa"'), E.fromString('"bbb"')), '"aaa" != "bbb"'],
['lt', () => E.lt(E.fromString('5'), E.fromString('2')), '5 < 2'],
['lte', () => E.lte(E.fromString('5'), E.fromString('2')), '5 <= 2'],
['gt', () => E.gt(E.fromString('5'), E.fromString('2')), '5 > 2'],
['gte', () => E.gte(E.fromString('5'), E.fromString('2')), '5 >= 2'],
['and', () => E.and(E.fromString('true'), E.fromString('false')), 'true && false'],
['or', () => E.or(E.fromString('true'), E.fromString('false')), 'true || false'],
['operator priority', () => E.and(
E.and(E.fromString('false'), E.fromString('false')),
E.or(E.fromString('true'), E.fromString('true')),
), 'false && false && (true || true)'],
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
])('%s', (_, getExpression, expectedCondition) => {
// WHEN
new iotevents.DetectorModel(stack, 'MyDetectorModel', {
initialState: new iotevents.State({
stateName: 'test-state',
onEnter: [{
eventName: 'test-eventName',
condition: iotevents.Expression.currentInput(input),
condition: getExpression(input),
}],
}),
});
Expand All @@ -517,97 +533,7 @@ describe('Expression', () => {
Match.objectLike({
OnEnter: {
Events: [Match.objectLike({
Condition: 'currentInput("test-input")',
})],
},
}),
],
},
});
});

test('inputAttribute', () => {
// WHEN
new iotevents.DetectorModel(stack, 'MyDetectorModel', {
initialState: new iotevents.State({
stateName: 'test-state',
onEnter: [{
eventName: 'test-eventName',
condition: iotevents.Expression.inputAttribute(input, 'json.path'),
}],
}),
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', {
DetectorModelDefinition: {
States: [
Match.objectLike({
OnEnter: {
Events: [Match.objectLike({
Condition: '$input.test-input.json.path',
})],
},
}),
],
},
});
});

test('eq', () => {
// WHEN
new iotevents.DetectorModel(stack, 'MyDetectorModel', {
initialState: new iotevents.State({
stateName: 'test-state',
onEnter: [{
eventName: 'test-eventName',
condition: iotevents.Expression.eq(
iotevents.Expression.fromString('"aaa"'),
iotevents.Expression.fromString('"bbb"'),
),
}],
}),
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', {
DetectorModelDefinition: {
States: [
Match.objectLike({
OnEnter: {
Events: [Match.objectLike({
Condition: '"aaa" == "bbb"',
})],
},
}),
],
},
});
});

test('eq', () => {
// WHEN
new iotevents.DetectorModel(stack, 'MyDetectorModel', {
initialState: new iotevents.State({
stateName: 'test-state',
onEnter: [{
eventName: 'test-eventName',
condition: iotevents.Expression.and(
iotevents.Expression.fromString('true'),
iotevents.Expression.fromString('false'),
),
}],
}),
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', {
DetectorModelDefinition: {
States: [
Match.objectLike({
OnEnter: {
Events: [Match.objectLike({
Condition: 'true && false',
Condition: expectedCondition,
})],
},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,49 @@
},
{
"OnInput": {
"Events": [
{
"Actions": [
{
"SetVariable": {
"Value": "31.7 != 31.7",
"VariableName": "neq"
}
},
{
"SetVariable": {
"Value": "31.7 > 31.7",
"VariableName": "gt"
}
},
{
"SetVariable": {
"Value": "31.7 < 31.7",
"VariableName": "lt"
}
},
{
"SetVariable": {
"Value": "31.7 >= 31.7",
"VariableName": "gte"
}
},
{
"SetVariable": {
"Value": "31.7 <= 31.7",
"VariableName": "lte"
}
},
{
"SetVariable": {
"Value": "true || false",
"VariableName": "or"
}
}
],
"EventName": "test-input-event"
}
],
"TransitionEvents": [
{
"Condition": {
Expand Down
11 changes: 11 additions & 0 deletions packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ class TestStack extends cdk.Stack {
});
const offlineState = new iotevents.State({
stateName: 'offline',
onInput: [{
eventName: 'test-input-event',
actions: [
['neq', iotevents.Expression.neq(iotevents.Expression.fromString('31.7'), iotevents.Expression.fromString('31.7'))] as const,
['gt', iotevents.Expression.gt(iotevents.Expression.fromString('31.7'), iotevents.Expression.fromString('31.7'))] as const,
['lt', iotevents.Expression.lt(iotevents.Expression.fromString('31.7'), iotevents.Expression.fromString('31.7'))] as const,
['gte', iotevents.Expression.gte(iotevents.Expression.fromString('31.7'), iotevents.Expression.fromString('31.7'))] as const,
['lte', iotevents.Expression.lte(iotevents.Expression.fromString('31.7'), iotevents.Expression.fromString('31.7'))] as const,
['or', iotevents.Expression.or(iotevents.Expression.fromString('true'), iotevents.Expression.fromString('false'))] as const,
].map(([variableName, value]) => ({ bind: () => ({ configuration: { setVariable: { variableName, value: value.evaluate() } } }) })),
}],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@skinny85
I have added integ test but it is efficient?
I wanted to confirm that these expressions would work on AWS actually. And its purpose is filled. But I wonder this integ code have value for aws-cdk.
WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think I would skip this. Doesn't seem particularly elegant.

I could see doing this in a unit test, but I wouldn't do it an an integ test.

});

// 1st => 2nd
Expand Down