Skip to content

Commit

Permalink
feat(iotevents): add DetectorModel L2 Construct (#18049)
Browse files Browse the repository at this point in the history
This PR is proposed by #17711.

The first step of the roadmap in #17711 is implemented in this PR.

> 1. implement DetectorModel and State with only required properties
>    - It will not be able to have multiple states yet.

If this PR is merged, the simplest detector model can be created as following:

![image](https://user-images.githubusercontent.com/11013683/146365658-248bba67-743c-4ba3-a195-56223146525f.png)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
yamatatsu committed Jan 22, 2022
1 parent 3d192a9 commit d0960f1
Show file tree
Hide file tree
Showing 10 changed files with 659 additions and 8 deletions.
23 changes: 19 additions & 4 deletions packages/@aws-cdk/aws-iotevents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,30 @@ Import it into your code:
import * as iotevents from '@aws-cdk/aws-iotevents';
```

## `Input`
## `DetectorModel`

Add an AWS IoT Events input to your stack:
The following example creates an AWS IoT Events detector model to your stack.
The detector model need a reference to at least one AWS IoT Events input.
AWS IoT Events inputs enable the detector to get MQTT payload values from IoT Core rules.

```ts
import * as iotevents from '@aws-cdk/aws-iotevents';

new iotevents.Input(this, 'MyInput', {
inputName: 'my_input',
const input = new iotevents.Input(this, 'MyInput', {
inputName: 'my_input', // optional
attributeJsonPaths: ['payload.temperature'],
});

const onlineState = new iotevents.State({
stateName: 'online',
onEnter: [{
eventName: 'test-event',
condition: iotevents.Expression.currentInput(input),
}],
});

new iotevents.DetectorModel(this, 'MyDetectorModel', {
detectorModelName: 'test-detector-model', // optional
initialState: onlineState,
});
```
82 changes: 82 additions & 0 deletions packages/@aws-cdk/aws-iotevents/lib/detector-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as iam from '@aws-cdk/aws-iam';
import { Resource, IResource } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnDetectorModel } from './iotevents.generated';
import { State } from './state';

/**
* Represents an AWS IoT Events detector model
*/
export interface IDetectorModel extends IResource {
/**
* The name of the detector model.
*
* @attribute
*/
readonly detectorModelName: string;
}

/**
* Properties for defining an AWS IoT Events detector model
*/
export interface DetectorModelProps {
/**
* The name of the detector model.
*
* @default - CloudFormation will generate a unique name of the detector model
*/
readonly detectorModelName?: string;

/**
* The state that is entered at the creation of each detector.
*/
readonly initialState: State;

/**
* The role that grants permission to AWS IoT Events to perform its operations.
*
* @default - a role will be created with default permissions
*/
readonly role?: iam.IRole;
}

/**
* Defines an AWS IoT Events detector model in this stack.
*/
export class DetectorModel extends Resource implements IDetectorModel {
/**
* Import an existing detector model.
*/
public static fromDetectorModelName(scope: Construct, id: string, detectorModelName: string): IDetectorModel {
return new class extends Resource implements IDetectorModel {
public readonly detectorModelName = detectorModelName;
}(scope, id);
}

public readonly detectorModelName: string;

constructor(scope: Construct, id: string, props: DetectorModelProps) {
super(scope, id, {
physicalName: props.detectorModelName,
});

if (!props.initialState._onEnterEventsHaveAtLeastOneCondition()) {
throw new Error('Detector Model must have at least one Input with a condition');
}

const role = props.role ?? new iam.Role(this, 'DetectorModelRole', {
assumedBy: new iam.ServicePrincipal('iotevents.amazonaws.com'),
});

const resource = new CfnDetectorModel(this, 'Resource', {
detectorModelName: this.physicalName,
detectorModelDefinition: {
initialStateName: props.initialState.stateName,
states: [props.initialState._toStateJson()],
},
roleArn: role.roleArn,
});

this.detectorModelName = this.getResourceNameAttribute(resource.ref);
}
}
18 changes: 18 additions & 0 deletions packages/@aws-cdk/aws-iotevents/lib/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Expression } from './expression';

/**
* Specifies the actions to be performed when the condition evaluates to TRUE.
*/
export interface Event {
/**
* The name of the event.
*/
readonly eventName: string;

/**
* The Boolean expression that, when TRUE, causes the actions to be performed.
*
* @default - none (the actions are always executed)
*/
readonly condition?: Expression;
}
75 changes: 75 additions & 0 deletions packages/@aws-cdk/aws-iotevents/lib/expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { IInput } from './input';

/**
* Expression for events in Detector Model state
* @see https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-expressions.html
*/
export abstract class Expression {
/**
* Create a expression from the given string
*/
public static fromString(value: string): Expression {
return new StringExpression(value);
}

/**
* Create a expression for function `currentInput()`.
* It is evaluated to true if the specified input message was received.
*/
public static currentInput(input: IInput): Expression {
return this.fromString(`currentInput("${input.inputName}")`);
}

/**
* Create a expression for get an input attribute as `$input.TemperatureInput.temperatures[2]`.
*/
public static inputAttribute(input: IInput, path: string): Expression {
return this.fromString(`$input.${input.inputName}.${path}`);
}

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

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

constructor() {
}

/**
* this is called to evaluate the expression
*/
public abstract evaluate(): string;
}

class StringExpression extends Expression {
constructor(private readonly value: string) {
super();
}

public evaluate() {
return this.value;
}
}

class BinaryOperationExpression extends Expression {
constructor(
private readonly left: Expression,
private readonly operator: string,
private readonly right: Expression,
) {
super();
}

public evaluate() {
return `${this.left.evaluate()} ${this.operator} ${this.right.evaluate()}`;
}
}
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-iotevents/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export * from './detector-model';
export * from './event';
export * from './expression';
export * from './input';
export * from './state';

// AWS::IoTEvents CloudFormation Resources:
export * from './iotevents.generated';
65 changes: 65 additions & 0 deletions packages/@aws-cdk/aws-iotevents/lib/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Event } from './event';
import { CfnDetectorModel } from './iotevents.generated';

/**
* Properties for defining a state of a detector
*/
export interface StateProps {
/**
* The name of the state.
*/
readonly stateName: string;

/**
* Specifies the events on enter. the conditions of the events are evaluated when the state is entered.
* If the condition is `TRUE`, the actions of the event are performed.
*
* @default - events on enter will not be set
*/
readonly onEnter?: Event[];
}

/**
* Defines a state of a detector
*/
export class State {
/**
* The name of the state
*/
public readonly stateName: string;

constructor(private readonly props: StateProps) {
this.stateName = props.stateName;
}

/**
* Return the state property JSON
*
* @internal
*/
public _toStateJson(): CfnDetectorModel.StateProperty {
const { stateName, onEnter } = this.props;
return {
stateName,
onEnter: onEnter && { events: getEventJson(onEnter) },
};
}

/**
* returns true if this state has at least one condition via events
*
* @internal
*/
public _onEnterEventsHaveAtLeastOneCondition(): boolean {
return this.props.onEnter?.some(event => event.condition) ?? false;
}
}

function getEventJson(events: Event[]): CfnDetectorModel.EventProperty[] {
return events.map(e => {
return {
eventName: e.eventName,
condition: e.condition?.evaluate(),
};
});
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-iotevents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,12 @@
"jest": "^27.4.7"
},
"dependencies": {
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/core": "0.0.0",
"constructs": "^3.3.69"
},
"peerDependencies": {
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/core": "0.0.0",
"constructs": "^3.3.69"
},
Expand Down
Loading

0 comments on commit d0960f1

Please sign in to comment.