-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
input.ts
334 lines (291 loc) · 8.88 KB
/
input.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
import { IRule } from './rule-ref';
import {
captureStackTrace, DefaultTokenResolver, IResolvable,
IResolveContext, Lazy, Stack, StringConcat, Token, Tokenization,
} from '../../core';
/**
* The input to send to the event target
*/
export abstract class RuleTargetInput {
/**
* Pass text to the event target
*
* May contain strings returned by `EventField.from()` to substitute in parts of the
* matched event.
*
* The Rule Target input value will be a single string: the string you pass
* here. Do not use this method to pass a complex value like a JSON object to
* a Rule Target. Use `RuleTargetInput.fromObject()` instead.
*/
public static fromText(text: string): RuleTargetInput {
return new FieldAwareEventInput(text, InputType.Text);
}
/**
* Pass text to the event target, splitting on newlines.
*
* This is only useful when passing to a target that does not
* take a single argument.
*
* May contain strings returned by `EventField.from()` to substitute in parts
* of the matched event.
*/
public static fromMultilineText(text: string): RuleTargetInput {
return new FieldAwareEventInput(text, InputType.Multiline);
}
/**
* Pass a JSON object to the event target
*
* May contain strings returned by `EventField.from()` to substitute in parts of the
* matched event.
*/
public static fromObject(obj: any): RuleTargetInput {
return new FieldAwareEventInput(obj, InputType.Object);
}
/**
* Take the event target input from a path in the event JSON
*/
public static fromEventPath(path: string): RuleTargetInput {
return new LiteralEventInput({ inputPath: path });
}
protected constructor() {
}
/**
* Return the input properties for this input object
*/
public abstract bind(rule: IRule): RuleTargetInputProperties;
}
/**
* The input properties for an event target
*/
export interface RuleTargetInputProperties {
/**
* Literal input to the target service (must be valid JSON)
*
* @default - input for the event target. If the input contains a paths map
* values wil be extracted from event and inserted into the `inputTemplate`.
*/
readonly input?: string;
/**
* JsonPath to take input from the input event
*
* @default - None. The entire matched event is passed as input
*/
readonly inputPath?: string;
/**
* Input template to insert paths map into
*
* @default - None.
*/
readonly inputTemplate?: string;
/**
* Paths map to extract values from event and insert into `inputTemplate`
*
* @default - No values extracted from event.
*/
readonly inputPathsMap?: { [key: string]: string };
}
/**
* Event Input that is directly derived from the construct
*/
class LiteralEventInput extends RuleTargetInput {
constructor(private readonly props: RuleTargetInputProperties) {
super();
}
/**
* Return the input properties for this input object
*/
public bind(_rule: IRule): RuleTargetInputProperties {
return this.props;
}
}
/**
* Input object that can contain field replacements
*
* Evaluation is done in the bind() method because token resolution
* requires access to the construct tree.
*
* Multiple tokens that use the same path will use the same substitution
* key.
*
* One weird exception: if we're in object context, we MUST skip the quotes
* around the placeholder. I assume this is so once a trivial string replace is
* done later on by EventBridge, numbers are still numbers.
*
* So in string context:
*
* "this is a string with a <field>"
*
* But in object context:
*
* "{ \"this is the\": <field> }"
*
* To achieve the latter, we postprocess the JSON string to remove the surrounding
* quotes by using a string replace.
*/
class FieldAwareEventInput extends RuleTargetInput {
constructor(private readonly input: any, private readonly inputType: InputType) {
super();
}
public bind(rule: IRule): RuleTargetInputProperties {
let fieldCounter = 0;
const pathToKey = new Map<string, string>();
const inputPathsMap: {[key: string]: string} = {};
function keyForField(f: EventField) {
const existing = pathToKey.get(f.path);
if (existing !== undefined) { return existing; }
fieldCounter += 1;
const key = f.displayHint || `f${fieldCounter}`;
pathToKey.set(f.path, key);
return key;
}
class EventFieldReplacer extends DefaultTokenResolver {
constructor() {
super(new StringConcat());
}
public resolveToken(t: Token, _context: IResolveContext) {
if (!isEventField(t)) { return Token.asString(t); }
const key = keyForField(t);
if (inputPathsMap[key] && inputPathsMap[key] !== t.path) {
throw new Error(`Single key '${key}' is used for two different JSON paths: '${t.path}' and '${inputPathsMap[key]}'`);
}
inputPathsMap[key] = t.path;
return `<${key}>`;
}
}
const stack = Stack.of(rule);
let resolved: string;
if (this.inputType === InputType.Multiline) {
// JSONify individual lines
resolved = Tokenization.resolve(this.input, {
scope: rule,
resolver: new EventFieldReplacer(),
});
resolved = resolved.split('\n').map(stack.toJsonString).join('\n');
} else {
resolved = stack.toJsonString(Tokenization.resolve(this.input, {
scope: rule,
resolver: new EventFieldReplacer(),
}));
}
const keys = Object.keys(inputPathsMap);
if (keys.length === 0) {
// Nothing special, just return 'input'
return { input: resolved };
}
return {
inputTemplate: this.unquoteKeyPlaceholders(resolved, keys),
inputPathsMap,
};
}
/**
* 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, keys: string[]) {
if (this.inputType !== InputType.Object) { return sub; }
return Lazy.uncachedString({ produce: (ctx: IResolveContext) => Token.asString(deepUnquote(ctx.resolve(sub))) });
function deepUnquote(resolved: any): any {
if (Array.isArray(resolved)) {
return resolved.map(deepUnquote);
} else if (typeof(resolved) === 'object' && resolved !== null) {
for (const [key, value] of Object.entries(resolved)) {
resolved[key] = deepUnquote(value);
}
return resolved;
} else if (typeof(resolved) === 'string') {
return keys.reduce((r, key) => r.replace(new RegExp(`(?<!\\\\)\"\<${key}\>\"`, 'g'), `<${key}>`), resolved);
}
return resolved;
}
}
}
/**
* Represents a field in the event pattern
*/
export class EventField implements IResolvable {
/**
* Extract the event ID from the event
*/
public static get eventId(): string {
return this.fromPath('$.id');
}
/**
* Extract the detail type from the event
*/
public static get detailType(): string {
return this.fromPath('$.detail-type');
}
/**
* Extract the source from the event
*/
public static get source(): string {
return this.fromPath('$.source');
}
/**
* Extract the account from the event
*/
public static get account(): string {
return this.fromPath('$.account');
}
/**
* Extract the time from the event
*/
public static get time(): string {
return this.fromPath('$.time');
}
/**
* Extract the region from the event
*/
public static get region(): string {
return this.fromPath('$.region');
}
/**
* Extract a custom JSON path from the event
*/
public static fromPath(path: string): string {
return new EventField(path).toString();
}
/**
* Human readable display hint about the event pattern
*/
public readonly displayHint: string;
public readonly creationStack: string[];
/**
*
* @param path the path to a field in the event pattern
*/
private constructor(public readonly path: string) {
this.displayHint = this.path.replace(/^[^a-zA-Z0-9_-]+/, '').replace(/[^a-zA-Z0-9_-]/g, '-');
Object.defineProperty(this, EVENT_FIELD_SYMBOL, { value: true });
this.creationStack = captureStackTrace();
}
public resolve(_ctx: IResolveContext): any {
return this.path;
}
public toString() {
return Token.asString(this, { displayHint: this.displayHint });
}
/**
* Convert the path to the field in the event pattern to JSON
*/
public toJSON() {
return `<path:${this.path}>`;
}
}
enum InputType {
Object,
Text,
Multiline,
}
function isEventField(x: any): x is EventField {
return EVENT_FIELD_SYMBOL in x;
}
const EVENT_FIELD_SYMBOL = Symbol.for('@aws-cdk/aws-events.EventField');