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 transition events #18768

Merged
merged 15 commits into from
Feb 7, 2022
Merged
27 changes: 23 additions & 4 deletions packages/@aws-cdk/aws-iotevents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,39 @@ const input = new iotevents.Input(this, 'MyInput', {
attributeJsonPaths: ['payload.deviceId', 'payload.temperature'],
});

const onlineState = new iotevents.State({
stateName: 'online',
const warmState = new iotevents.State({
stateName: 'warm',
onEnter: [{
eventName: 'test-event',
condition: iotevents.Expression.currentInput(input),
when: iotevents.Expression.currentInput(input),
Copy link
Contributor

Choose a reason for hiding this comment

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

BTW, is this not a weird condition? What does just currentInput() mean here? Is this true whenever the current input is not empty?

Copy link
Contributor Author

@yamatatsu yamatatsu Feb 5, 2022

Choose a reason for hiding this comment

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

Is this true whenever the current input is not empty?

Yes🙂. In this case, Every messages to this input cause this event and create new detector by the property key.

}],
});
const coldState = new iotevents.State({
stateName: 'cold',
});

// transit to coldState when temperature is 10
warmState.transitionTo(coldState, {
eventName: 'to_coldState', // optional property, default by combining the names of the States
when: iotevents.Expression.eq(
iotevents.Expression.inputAttribute(input, 'payload.temperature'),
iotevents.Expression.fromString('10'),
),
});
// transit to warmState when temperature is 20
coldState.transitionTo(warmState, {
when: iotevents.Expression.eq(
iotevents.Expression.inputAttribute(input, 'payload.temperature'),
iotevents.Expression.fromString('20'),
),
});

new iotevents.DetectorModel(this, 'MyDetectorModel', {
detectorModelName: 'test-detector-model', // optional
description: 'test-detector-model-description', // optional property, default is none
evaluationMethod: iotevents.EventEvaluation.SERIAL, // optional property, default is iotevents.EventEvaluation.BATCH
detectorKey: 'payload.deviceId', // optional property, default is none and single detector instance will be created and all inputs will be routed to it
initialState: onlineState,
initialState: warmState,
});
```

Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-iotevents/lib/detector-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export class DetectorModel extends Resource implements IDetectorModel {
key: props.detectorKey,
detectorModelDefinition: {
initialStateName: props.initialState.stateName,
states: [props.initialState._toStateJson()],
states: props.initialState._collectStateJsons(new Set<State>()),
},
roleArn: role.roleArn,
});
Expand Down
5 changes: 3 additions & 2 deletions packages/@aws-cdk/aws-iotevents/lib/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ export interface Event {
readonly eventName: string;

/**
* The Boolean expression that, when TRUE, causes the actions to be performed.
* The condition that is used to determine to cause the actions.
* When this was evaluated to TRUE, the actions are triggered.
*
* @default - none (the actions are always executed)
*/
readonly condition?: Expression;
readonly when?: Expression;
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, not sure about this change. My comment was only changing this in TransitionOptions, not here (it doesn't read that well here as it does in transitionTo().

I would leave this as-is. If you feel passionate that this is the way to go, fine, but then we need a "Breaking change" note in the PR description.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah. I assumed by mistake😅.

}
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
121 changes: 108 additions & 13 deletions packages/@aws-cdk/aws-iotevents/lib/state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
import { Event } from './event';
import { Expression } from './expression';
import { CfnDetectorModel } from './iotevents.generated';

/**
* Properties for options of state transition.
*/
export interface TransitionOptions {
/**
* The name of the event.
*
* @default string combining the names of the States as `${originStateName}_to_${targetStateName}`
*/
readonly eventName?: string;

/**
* The condition that is used to determine to cause the state transition and the actions.
* When this was evaluated to TRUE, the state transition and the actions are triggered.
*/
readonly when: Expression;
}

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

/**
* The condition that is used to determine to cause the state transition and the actions.
* When this was evaluated to TRUE, the state transition and the actions are triggered.
*/
readonly when: Expression;
Copy link
Contributor

Choose a reason for hiding this comment

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

It's fine to leave this name as condition (this is a package-private interface, it doesn't really matter what this name is - we can change it at any time. For that same reason, I'm pretty sure you can remove all of the documentation from this interface if you don't need it).


/**
* The next state to transit to. When the resuld of condition expression is TRUE, the state is transited.
*/
readonly nextState: State;
}

/**
* Properties for defining a state of a detector.
*/
Expand Down Expand Up @@ -28,38 +68,93 @@ export class State {
*/
public readonly stateName: string;

private readonly transitionEvents: TransitionEvent[] = [];

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

/**
* Return the state property JSON.
* Add a transition event to the state.
* The transition event will be triggered if condition is evaluated to TRUE.
*
* @param targetState the state that will be transit to when the event triggered
* @param options transition options including the condition that causes the state transition
*/
public transitionTo(targetState: State, options: TransitionOptions) {
const alreadyAdded = this.transitionEvents.some((event) => event.nextState === targetState);
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
if (alreadyAdded) {
throw new Error(`State '${this.stateName}' already has a transition defined to '${targetState.stateName}'`);
}

this.transitionEvents.push({
eventName: options.eventName ?? `${this.stateName}_to_${targetState.stateName}`,
nextState: targetState,
when: options.when,
});
}

/**
* Collect states in dependency gragh that constructed by state transitions,
* and return the JSONs of the states.
* This function is called recursively and collect the states.
*
* @internal
*/
public _toStateJson(): CfnDetectorModel.StateProperty {
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
const { stateName, onEnter } = this.props;
return {
stateName,
onEnter: onEnter && { events: getEventJson(onEnter) },
};
public _collectStateJsons(collectedStates: Set<State>): CfnDetectorModel.StateProperty[] {
if (collectedStates.has(this)) {
return [];
}
collectedStates.add(this);

return [
this.toStateJson(),
...this.transitionEvents.flatMap(transitionEvent => {
return transitionEvent.nextState._collectStateJsons(collectedStates);
}),
];
}

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

private toStateJson(): CfnDetectorModel.StateProperty {
const { onEnter } = this.props;
return {
stateName: this.stateName,
onEnter: onEnter && { events: toEventsJson(onEnter) },
onInput: {
transitionEvents: toTransitionEventsJson(this.transitionEvents),
},
};
}
}

function toEventsJson(events: Event[]): CfnDetectorModel.EventProperty[] {
return events.map(event => {
return {
eventName: event.eventName,
condition: event.when?.evaluate(),
};
});
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
}

function getEventJson(events: Event[]): CfnDetectorModel.EventProperty[] {
return events.map(e => {
function toTransitionEventsJson(transitionEvents: TransitionEvent[]): CfnDetectorModel.TransitionEventProperty[] | undefined {
if (transitionEvents.length === 0) {
return undefined;
}

return transitionEvents.map(transitionEvent => {
return {
eventName: e.eventName,
condition: e.condition?.evaluate(),
eventName: transitionEvent.eventName,
condition: transitionEvent.when.evaluate(),
nextState: transitionEvent.nextState.stateName,
};
});
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
}
Loading