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(events): support embedded string variables #13487

Merged
merged 8 commits into from
Jun 4, 2021
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-events/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ onCommitRule.addTarget(new targets.SnsTopic(topic, {
}));
```

Or using an Object:

```ts
onCommitRule.addTarget(new targets.SnsTopic(topic, {
message: events.RuleTargetInput.fromObject(
{
DataType: `custom_${events.EventField.fromPath('$.detail-type')}`
}
)
}));
```

## Scheduling

You can configure a Rule to run on a schedule (cron or rate).
Expand Down
44 changes: 13 additions & 31 deletions packages/@aws-cdk/aws-events/lib/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,6 @@ class FieldAwareEventInput extends RuleTargetInput {
return key;
}

const self = this;

class EventFieldReplacer extends DefaultTokenResolver {
constructor() {
super(new StringConcat());
Expand All @@ -167,7 +165,7 @@ class FieldAwareEventInput extends RuleTargetInput {
}
inputPathsMap[key] = t.path;

return self.keyPlaceholder(key);
return `<${key}>`;
}
}

Expand All @@ -188,35 +186,32 @@ class FieldAwareEventInput extends RuleTargetInput {
}));
}

if (Object.keys(inputPathsMap).length === 0) {
const keys = Object.keys(inputPathsMap);

if (keys.length === 0) {
// Nothing special, just return 'input'
return { input: resolved };
}

return {
inputTemplate: this.unquoteKeyPlaceholders(resolved),
inputTemplate: this.unquoteKeyPlaceholders(resolved, keys),
inputPathsMap,
};
}

/**
* Return a template placeholder for the given key
*
* In object scope we'll need to get rid of surrounding quotes later on, so
* return a bracing that's unlikely to occur naturally (like tokens).
*/
private keyPlaceholder(key: string) {
if (this.inputType !== InputType.Object) { return `<${key}>`; }
return UNLIKELY_OPENING_STRING + key + UNLIKELY_CLOSING_STRING;
}

/**
* Removing surrounding quotes from any object placeholders
* when key is the lone value.
*
* Those have been put there by JSON.stringify(), but we need to
* remove them.
*
* Do not remove quotes when the key is part of a larger string.
*
* Valid: { "data": "Some string with \"quotes\"<key>" } // key will be string
* Valid: { "data": <key> } // Key could be number, bool, obj, or string
*/
private unquoteKeyPlaceholders(sub: string) {
private unquoteKeyPlaceholders(sub: string, keys: string[]) {
if (this.inputType !== InputType.Object) { return sub; }

return Lazy.uncachedString({ produce: (ctx: IResolveContext) => Token.asString(deepUnquote(ctx.resolve(sub))) });
Expand All @@ -230,19 +225,13 @@ class FieldAwareEventInput extends RuleTargetInput {
}
return resolved;
} else if (typeof(resolved) === 'string') {
return resolved.replace(OPENING_STRING_REGEX, '<').replace(CLOSING_STRING_REGEX, '>');
return keys.reduce((r, key) => r.replace(new RegExp(`(?<!\\\\)\"\<${key}\>\"`, 'g'), `<${key}>`), resolved);
}
return resolved;
}
}
}

const UNLIKELY_OPENING_STRING = '<<${';
const UNLIKELY_CLOSING_STRING = '}>>';

const OPENING_STRING_REGEX = new RegExp(regexQuote('"' + UNLIKELY_OPENING_STRING), 'g');
const CLOSING_STRING_REGEX = new RegExp(regexQuote(UNLIKELY_CLOSING_STRING + '"'), 'g');

/**
* Represents a field in the event pattern
*/
Expand Down Expand Up @@ -339,10 +328,3 @@ function isEventField(x: any): x is EventField {
}

const EVENT_FIELD_SYMBOL = Symbol.for('@aws-cdk/aws-events.EventField');

/**
* Quote a string for use in a regex
*/
function regexQuote(s: string) {
return s.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&');
}
156 changes: 156 additions & 0 deletions packages/@aws-cdk/aws-events/test/test.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,162 @@ export = {
test.done();
},

'can use joined JSON containing refs in JSON object with tricky inputs'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const rule = new Rule(stack, 'Rule', {
schedule: Schedule.rate(cdk.Duration.minutes(1)),
});

// WHEN
rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({
data: `they said \"hello\"${EventField.fromPath('$')}`,
stackName: cdk.Aws.STACK_NAME,
})));

// THEN
expect(stack).to(haveResourceLike('AWS::Events::Rule', {
Targets: [
{
InputTransformer: {
InputPathsMap: {
f1: '$',
},
InputTemplate: {
'Fn::Join': [
'',
[
'{"data":"they said \\\"hello\\\"<f1>","stackName":"',
{ Ref: 'AWS::StackName' },
'"}',
],
],
},
},
},
],
}));

test.done();
},

'can use joined JSON containing refs in JSON object and concat'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const rule = new Rule(stack, 'Rule', {
schedule: Schedule.rate(cdk.Duration.minutes(1)),
});

// WHEN
rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({
data: `more text ${EventField.fromPath('$')}`,
stackName: cdk.Aws.STACK_NAME,
})));

// THEN
expect(stack).to(haveResourceLike('AWS::Events::Rule', {
Targets: [
{
InputTransformer: {
InputPathsMap: {
f1: '$',
},
InputTemplate: {
'Fn::Join': [
'',
[
'{"data":"more text <f1>","stackName":"',
{ Ref: 'AWS::StackName' },
'"}',
],
],
},
},
},
],
}));

test.done();
},

'can use joined JSON containing refs in JSON object and quotes'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const rule = new Rule(stack, 'Rule', {
schedule: Schedule.rate(cdk.Duration.minutes(1)),
});

// WHEN
rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({
data: `more text "${EventField.fromPath('$')}"`,
stackName: cdk.Aws.STACK_NAME,
})));

// THEN
expect(stack).to(haveResourceLike('AWS::Events::Rule', {
Targets: [
{
InputTransformer: {
InputPathsMap: {
f1: '$',
},
InputTemplate: {
'Fn::Join': [
'',
[
'{"data":"more text \\\"<f1>\\\"","stackName":"',
{ Ref: 'AWS::StackName' },
'"}',
],
],
},
},
},
],
}));

test.done();
},

'can use joined JSON containing refs in JSON object and multiple keys'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const rule = new Rule(stack, 'Rule', {
schedule: Schedule.rate(cdk.Duration.minutes(1)),
});

// WHEN
rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({
data: `${EventField.fromPath('$')}${EventField.fromPath('$.other')}`,
stackName: cdk.Aws.STACK_NAME,
})));

// THEN
expect(stack).to(haveResourceLike('AWS::Events::Rule', {
Targets: [
{
InputTransformer: {
InputPathsMap: {
f1: '$',
},
InputTemplate: {
'Fn::Join': [
'',
[
'{"data":"<f1><other>","stackName":"',
{ Ref: 'AWS::StackName' },
'"}',
],
],
},
},
},
],
}));

test.done();
},

'can use token'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
Expand Down