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): add DetectorModel L2 Construct #18049

Merged
merged 26 commits into from
Jan 22, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e5f18b2
feat(iotevents): add DetectorModel L2 Construct
yamatatsu Dec 16, 2021
7873d17
cut out the event.
yamatatsu Dec 16, 2021
07081ed
fix readme
yamatatsu Dec 16, 2021
3d2db07
address comments
yamatatsu Dec 17, 2021
9c24a19
Merge branch 'master' into iotevents-dm
yamatatsu Jan 5, 2022
0723a1c
implement Expressions
yamatatsu Jan 19, 2022
8f82cbb
Update packages/@aws-cdk/aws-iotevents/README.md
yamatatsu Jan 21, 2022
fceea45
Update packages/@aws-cdk/aws-iotevents/README.md
yamatatsu Jan 21, 2022
8ae95cd
Update packages/@aws-cdk/aws-iotevents/lib/detector-model.ts
yamatatsu Jan 21, 2022
ff69d77
Update packages/@aws-cdk/aws-iotevents/lib/detector-model.ts
yamatatsu Jan 21, 2022
d197256
Update packages/@aws-cdk/aws-iotevents/lib/event.ts
yamatatsu Jan 21, 2022
f3511b2
Update packages/@aws-cdk/aws-iotevents/lib/event.ts
yamatatsu Jan 21, 2022
e5baa25
Update packages/@aws-cdk/aws-iotevents/lib/detector-model.ts
yamatatsu Jan 21, 2022
581ddef
Update packages/@aws-cdk/aws-iotevents/lib/state.ts
yamatatsu Jan 21, 2022
50c183c
Update packages/@aws-cdk/aws-iotevents/lib/detector-model.ts
yamatatsu Jan 21, 2022
971d5c4
Update packages/@aws-cdk/aws-iotevents/lib/detector-model.ts
yamatatsu Jan 21, 2022
fbfe028
Update packages/@aws-cdk/aws-iotevents/lib/detector-model.ts
yamatatsu Jan 21, 2022
0b6600d
Update packages/@aws-cdk/aws-iotevents/lib/expression.ts
yamatatsu Jan 21, 2022
18e86d0
Update packages/@aws-cdk/aws-iotevents/lib/state.ts
yamatatsu Jan 21, 2022
9744d78
Update packages/@aws-cdk/aws-iotevents/lib/state.ts
yamatatsu Jan 21, 2022
d89d0a0
fix tests
yamatatsu Jan 21, 2022
6ff671e
address comments
yamatatsu Jan 21, 2022
78fc88c
fix JSDoc of onEnter and rename
yamatatsu Jan 21, 2022
5506cb5
Update packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts
yamatatsu Jan 21, 2022
087bb42
address comments
yamatatsu Jan 21, 2022
2b4301e
Merge branch 'master' into iotevents-dm
mergify[bot] Jan 22, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 dodel to your stack.
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
The detector model need a reference to at least one AWS IoT Events input.
AWS IoT Events input enable that the detector can get MQTT payload values from IoT Core rules.
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved

```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',
onEnterEvents: [{
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
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly detectorModelName: string;
}

/**
* Properties for defining an AWS IoT Events detector model
*/
export interface DetectorModelProps {
/**
* The name of the detector model
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
*
* @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.
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly role?: iam.IRole;
}

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

public readonly detectorModelName: string;

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

if (!props.initialState.hasCondition()) {
throw new Error('Detector Model must use at least one Input in a condition.');
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
}
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved

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);
}
}
28 changes: 28 additions & 0 deletions packages/@aws-cdk/aws-iotevents/lib/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Expression } from './expression';
import { CfnDetectorModel } from './iotevents.generated';

/**
* Specifies the actions to be performed when the condition evaluates to TRUE.
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
*/
export interface Event {
/**
* The name of the event
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly eventName: string;

/**
* The Boolean expression that, when TRUE, causes the actions to be performed.
*
* @default None - Defaults to perform the actions always.
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly condition?: Expression;
}

export function getEventJson(events: Event[]): CfnDetectorModel.EventProperty[] {
return events.map(e => {
return {
eventName: e.eventName,
condition: e.condition?.evaluate(),
};
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is only used in the state.ts file, right?

Can we move it there then, and make it non-exported?

In general, we should try to minimize the surface area of the exported library API as much as possible, and I don't think there's a reason to surface this function to users of this module.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I'll move it!

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 {
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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}")`);
}
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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 operater: string,
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
private readonly right: Expression,
) {
super();
}

public evaluate() {
return `${this.left.evaluate()} ${this.operater} ${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';
51 changes: 51 additions & 0 deletions packages/@aws-cdk/aws-iotevents/lib/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Event, getEventJson } from './event';
import { CfnDetectorModel } from './iotevents.generated';

/**
* Properties for defining a state of a detector
*/
export interface StateProps {
/**
* The name of the state
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly stateName: string;

/**
* Specifies the actions that are performed when the state is entered and the `condition` is `TRUE`
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

I have to say, I find the constant name changes here super confusing 😕. This is a good example. We are writing documentation for a property called onEnterEvents, its type is Event[], and the description says "Specifies the actions that are performed". Doesn't even mention the word "event" anywhere.

Are we missing something here? Should what in this PR is called an Event actually be some sort of Action?

Copy link
Contributor Author

@yamatatsu yamatatsu Jan 21, 2022

Choose a reason for hiding this comment

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

Naming

In CloudFormation, State has OnEnter and OnEnter has only events (typed as Event[]). I easily concated onEnter and events...

If I named it just onEnter, would it be less uncomfortable?

JSDoc

I should fix it!

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 fix it in 78fc88c (#18049).

*
* @default None
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly onEnterEvents?: 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
*/
public toStateJson(): CfnDetectorModel.StateProperty {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make this method @internal? I don't think there's a need to expose it in the public API of this module.

It will require renaming it to _toStateJson().

const { stateName, onEnterEvents } = this.props;
return {
stateName,
onEnter: onEnterEvents && { events: getEventJson(onEnterEvents) },
};
}

/**
* returns true if this state has at least one condition via events
*/
public hasCondition(): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

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

I actually don't like this method. This is actually a DetectorModel concern, not a State concern, that this validation has to be performed.

Can we remove this method, and perform this validation in DetectorModel by calling toStateJson() on its initialState instead?

Copy link
Contributor Author

@yamatatsu yamatatsu Jan 21, 2022

Choose a reason for hiding this comment

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

If we implement this validation with toStateJson(), It will be as following I think:

const stateJson = props.initialState._toStateJson();

if (!stateJson.onEnter?.events.some(event => event.condition)) {
  throw new Error('Detector Model must have at least one Input with a condition');
}

But we can't get onEnter?.events because onEnter can be cdk.IResolvable, same for events.some() and event.condition.

Should toStateJson() return more exact type that is compatible with CfnDetectorModel.StateProperty instead of CfnDetectorModel.StateProperty itself?

Following is the type CfnDetectorModel.StateProperty that toStateJson() return:

namespace CfnDetectorModel {
  export interface StateProperty {
    readonly onEnter?: CfnDetectorModel.OnEnterProperty | cdk.IResolvable;
    readonly onExit?: CfnDetectorModel.OnExitProperty | cdk.IResolvable;
    readonly onInput?: CfnDetectorModel.OnInputProperty | cdk.IResolvable;
    readonly stateName: string;
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, I see the problem. I think we could just check whether onEnter is a CfnDetectorModel.OnEnterProperty, and skip the validation if it's an IResolvable. But I don't want you to write all of that code if a simpler alternative is possible here.

So, let's keep _toStateJson() as it is now, but let's make this method @internal, and rename it to _onEnterEventsHaveAtLeastOneCondition().

return this.props.onEnterEvents?.some(event => event.condition) ?? false;
}
}
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.5"
},
"dependencies": {
"@aws-cdk/aws-iam": "0.0.0",
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
"@aws-cdk/core": "0.0.0",
"constructs": "^3.3.69"
},
"peerDependencies": {
"@aws-cdk/aws-iam": "0.0.0",
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
"@aws-cdk/core": "0.0.0",
"constructs": "^3.3.69"
},
Expand Down
Loading