From 5e91a0abf345c1b0e1055b6ff5efbef9d8225035 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 22 Jan 2019 15:13:03 -0800 Subject: [PATCH] fix(aws-events): ergonomics improvements to CloudWatch Events (#1570) Fixes the following things for CloudWatch events: * Support newlines in CloudWatch Events textTemplate as intended, by making a newline-separated list of JSON strings. Fixes #1514. * `jsonTemplate` now accepts arbitrary objects. They will be JSONified automatically. Fixes #1198. * Explicitly implement `IEventRuleTarget` on stepfunctions StateMachine so that Java/.NET users can trigger StateMachines using CloudWatch Events. Fixes part of #1275. --- .../cloudwatch-events/cdk.json | 3 + .../cloudwatch-events/index.ts | 25 ++++ examples/cdk-examples-typescript/package.json | 1 + packages/@aws-cdk/aws-events/lib/rule.ts | 13 +- .../@aws-cdk/aws-events/test/test.rule.ts | 121 +++++++++++++++++- .../aws-stepfunctions/lib/state-machine.ts | 2 +- .../test/test.state-machine-resources.ts | 34 ++++- 7 files changed, 190 insertions(+), 9 deletions(-) create mode 100644 examples/cdk-examples-typescript/cloudwatch-events/cdk.json create mode 100644 examples/cdk-examples-typescript/cloudwatch-events/index.ts diff --git a/examples/cdk-examples-typescript/cloudwatch-events/cdk.json b/examples/cdk-examples-typescript/cloudwatch-events/cdk.json new file mode 100644 index 0000000000000..2f0e44c6fd27b --- /dev/null +++ b/examples/cdk-examples-typescript/cloudwatch-events/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "node index" +} diff --git a/examples/cdk-examples-typescript/cloudwatch-events/index.ts b/examples/cdk-examples-typescript/cloudwatch-events/index.ts new file mode 100644 index 0000000000000..4c2c5219d3412 --- /dev/null +++ b/examples/cdk-examples-typescript/cloudwatch-events/index.ts @@ -0,0 +1,25 @@ +import events = require('@aws-cdk/aws-events'); +import sns = require('@aws-cdk/aws-sns'); +import cdk = require('@aws-cdk/cdk'); + +const app = new cdk.App(); + +class CloudWatchEventsExample extends cdk.Stack { + constructor(scope: cdk.App, id: string) { + super(scope, id); + + const topic = new sns.Topic(this, 'TestTopic'); + + const event = new events.EventRule(this, 'Rule', { + scheduleExpression: 'rate(1 minute)' + }); + + event.addTarget(topic, { + textTemplate: 'one line\nsecond line' + }); + } +} + +new CloudWatchEventsExample(app, 'CWE-Example'); + +app.run(); diff --git a/examples/cdk-examples-typescript/package.json b/examples/cdk-examples-typescript/package.json index 91f5dc2d3f5b0..358e5badf0621 100644 --- a/examples/cdk-examples-typescript/package.json +++ b/examples/cdk-examples-typescript/package.json @@ -37,6 +37,7 @@ "@aws-cdk/aws-rds": "^0.22.0", "@aws-cdk/aws-s3": "^0.22.0", "@aws-cdk/aws-sns": "^0.22.0", + "@aws-cdk/aws-events": "^0.22.0", "@aws-cdk/aws-sqs": "^0.22.0", "@aws-cdk/cdk": "^0.22.0", "@aws-cdk/runtime-values": "^0.22.0" diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index 56b58e73c96c5..9932b4160c9fd 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -116,6 +116,7 @@ export class EventRule extends Construct implements IEventRule { */ public addTarget(target?: IEventRuleTarget, inputOptions?: TargetInputTemplate) { if (!target) { return; } + const self = this; const targetProps = target.asEventRuleTarget(this.ruleArn, this.node.uniqueId); @@ -145,11 +146,15 @@ export class EventRule extends Construct implements IEventRule { let inputTemplate: any; if (inputOptions.jsonTemplate) { - inputTemplate = inputOptions.jsonTemplate; - } else if (typeof(inputOptions.textTemplate) === 'string') { - inputTemplate = JSON.stringify(inputOptions.textTemplate); + inputTemplate = typeof inputOptions.jsonTemplate === 'string' + ? inputOptions.jsonTemplate + : self.node.stringifyJson(inputOptions.jsonTemplate); } else { - inputTemplate = `"${inputOptions.textTemplate}"`; + inputTemplate = typeof(inputOptions.textTemplate) === 'string' + // Newline separated list of JSON-encoded strings + ? inputOptions.textTemplate.split('\n').map(x => self.node.stringifyJson(x)).join('\n') + // Some object, stringify it, then stringify the string for proper escaping + : self.node.stringifyJson(self.node.stringifyJson(inputOptions.textTemplate)); } return { diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index 870194bde5ead..6dbbe2e92cd7a 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; @@ -351,5 +351,122 @@ export = { test.deepEqual(importedRule.ruleArn, 'arn:of:rule'); test.done(); - } + }, + + 'json template': { + 'can just be a JSON object'(test: Test) { + // GIVEN + const stack = new Stack(); + const rule = new EventRule(stack, 'Rule', { + scheduleExpression: 'rate(1 minute)' + }); + + // WHEN + rule.addTarget(new SomeTarget(), { + jsonTemplate: { SomeObject: 'withAValue' }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + InputTransformer: { + InputTemplate: "{\"SomeObject\":\"withAValue\"}" + }, + } + ] + })); + test.done(); + }, + }, + + 'text templates': { + 'strings with newlines are serialized to a newline-delimited list of JSON strings'(test: Test) { + // GIVEN + const stack = new Stack(); + const rule = new EventRule(stack, 'Rule', { + scheduleExpression: 'rate(1 minute)' + }); + + // WHEN + rule.addTarget(new SomeTarget(), { + textTemplate: 'I have\nmultiple lines', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + InputTransformer: { + InputTemplate: "\"I have\"\n\"multiple lines\"" + }, + } + ] + })); + + test.done(); + }, + + 'escaped newlines are not interpreted as newlines'(test: Test) { + // GIVEN + const stack = new Stack(); + const rule = new EventRule(stack, 'Rule', { + scheduleExpression: 'rate(1 minute)' + }); + + // WHEN + rule.addTarget(new SomeTarget(), { + textTemplate: 'this is not\\na real newline', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + InputTransformer: { + InputTemplate: "\"this is not\\\\na real newline\"" + }, + } + ] + })); + + test.done(); + }, + + 'can use Tokens in text templates'(test: Test) { + // GIVEN + const stack = new Stack(); + const rule = new EventRule(stack, 'Rule', { + scheduleExpression: 'rate(1 minute)' + }); + + const world = new cdk.Token(() => 'world'); + + // WHEN + rule.addTarget(new SomeTarget(), { + textTemplate: `hello ${world}`, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + InputTransformer: { + InputTemplate: "\"hello world\"" + }, + } + ] + })); + + test.done(); + } + }, }; + +class SomeTarget implements IEventRuleTarget { + public asEventRuleTarget() { + return { + id: 'T1', arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' } + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index cba9fdbd0fb35..7744cc7e2e914 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -40,7 +40,7 @@ export interface StateMachineProps { /** * Define a StepFunctions State Machine */ -export class StateMachine extends cdk.Construct implements IStateMachine { +export class StateMachine extends cdk.Construct implements IStateMachine, events.IEventRuleTarget { /** * Import a state machine */ diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts index d717524180690..f384df648e286 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts @@ -1,4 +1,5 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; @@ -134,7 +135,36 @@ export = { }); test.done(); - } + }, + + 'State machine can be used as Event Rule target'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const rule = new events.EventRule(stack, 'Rule', { + scheduleExpression: 'rate(1 minute)' + }); + const stateMachine = new stepfunctions.StateMachine(stack, 'SM', { + definition: new stepfunctions.Wait(stack, 'Hello', { }) + }); + + // WHEN + rule.addTarget(stateMachine, { + jsonTemplate: { SomeParam: 'SomeValue' }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + InputTransformer: { + InputTemplate: "{\"SomeParam\":\"SomeValue\"}" + }, + } + ] + })); + + test.done(); + }, }; class FakeResource implements stepfunctions.IStepFunctionsTaskResource {