Skip to content

Commit

Permalink
feat(aws-events): update eventfield to support embedded string variables
Browse files Browse the repository at this point in the history
  • Loading branch information
thantos committed Mar 23, 2021
1 parent e09250b commit 16e3870
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 31 deletions.
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

0 comments on commit 16e3870

Please sign in to comment.