@suss/contract-terraform 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -3
- package/dist/index.cjs +555 -35
- package/dist/index.d.cts +147 -5
- package/dist/index.d.ts +147 -5
- package/dist/index.js +552 -35
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,42 +5,398 @@ import hcl2 from "hcl2-parser";
|
|
|
5
5
|
import semver from "semver";
|
|
6
6
|
import {
|
|
7
7
|
messageBusBinding,
|
|
8
|
+
metricBinding,
|
|
8
9
|
namePatternFromSub,
|
|
9
10
|
storageBinding
|
|
10
11
|
} from "@suss/behavioral-ir";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
|
|
13
|
+
// src/filterQuery.ts
|
|
14
|
+
var OPERATORS = ["!=", ">=", "<=", "=", ">", "<", ":"];
|
|
15
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["AND", "OR", "NOT"]);
|
|
16
|
+
function filterValuesFor(query, key) {
|
|
17
|
+
return filterTerms(query).filter((term) => term.key === key && term.operator === "=").map((term) => term.value);
|
|
18
|
+
}
|
|
19
|
+
function filterTerms(query) {
|
|
20
|
+
if (query.type === "term") {
|
|
21
|
+
return [query];
|
|
22
|
+
}
|
|
23
|
+
if (query.type === "call") {
|
|
17
24
|
return [];
|
|
18
25
|
}
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
if (query.type === "negation") {
|
|
27
|
+
return filterTerms(query.operand);
|
|
28
|
+
}
|
|
29
|
+
return query.operands.flatMap(filterTerms);
|
|
30
|
+
}
|
|
31
|
+
function filterCalls(query) {
|
|
32
|
+
if (query.type === "call") {
|
|
33
|
+
return [query];
|
|
34
|
+
}
|
|
35
|
+
if (query.type === "term") {
|
|
21
36
|
return [];
|
|
22
37
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
if (query.type === "negation") {
|
|
39
|
+
return filterCalls(query.operand);
|
|
40
|
+
}
|
|
41
|
+
return query.operands.flatMap(filterCalls);
|
|
42
|
+
}
|
|
43
|
+
function parseFilterQuery(source) {
|
|
44
|
+
const tokens = tokenize(source);
|
|
45
|
+
if (!tokens.ok) {
|
|
46
|
+
return { ok: false, reason: tokens.reason };
|
|
47
|
+
}
|
|
48
|
+
const reader = new TokenReader(tokens.tokens);
|
|
49
|
+
const query = reader.readExpression();
|
|
50
|
+
if (query === null) {
|
|
51
|
+
return { ok: false, reason: reader.reason };
|
|
52
|
+
}
|
|
53
|
+
if (!reader.atEnd()) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
reason: `nothing joins the term at "${reader.rest()}"`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { ok: true, query };
|
|
60
|
+
}
|
|
61
|
+
function tokenize(source) {
|
|
62
|
+
const tokens = [];
|
|
63
|
+
let at = 0;
|
|
64
|
+
while (at < source.length) {
|
|
65
|
+
const char = source[at];
|
|
66
|
+
if (/\s/.test(char) || char === ",") {
|
|
67
|
+
at += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (char === "(" || char === ")") {
|
|
71
|
+
tokens.push({ type: char === "(" ? "open" : "close", text: char });
|
|
72
|
+
at += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (char === '"' || char === "'") {
|
|
76
|
+
const quoted = readQuoted(source, at, char);
|
|
77
|
+
if (quoted === null) {
|
|
78
|
+
return { ok: false, reason: `a quote is never closed at ${at}` };
|
|
30
79
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
80
|
+
const previous = tokens[tokens.length - 1];
|
|
81
|
+
if (previous !== void 0 && isPartialKey(previous)) {
|
|
82
|
+
previous.text += quoted.value;
|
|
83
|
+
} else {
|
|
84
|
+
tokens.push({ type: "string", text: quoted.value });
|
|
85
|
+
}
|
|
86
|
+
at = quoted.next;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const operator = OPERATORS.find(
|
|
90
|
+
(candidate) => source.startsWith(candidate, at)
|
|
91
|
+
);
|
|
92
|
+
if (operator !== void 0) {
|
|
93
|
+
tokens.push({ type: "operator", text: operator });
|
|
94
|
+
at += operator.length;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const word = readWord(source, at);
|
|
98
|
+
if (word === null) {
|
|
99
|
+
return { ok: false, reason: `nothing readable at "${char}"` };
|
|
34
100
|
}
|
|
101
|
+
tokens.push({ type: "word", text: word.value });
|
|
102
|
+
at = word.next;
|
|
35
103
|
}
|
|
36
|
-
return
|
|
104
|
+
return { ok: true, tokens };
|
|
105
|
+
}
|
|
106
|
+
function readQuoted(source, start, quote) {
|
|
107
|
+
let value = "";
|
|
108
|
+
let at = start + 1;
|
|
109
|
+
while (at < source.length) {
|
|
110
|
+
const char = source[at];
|
|
111
|
+
if (char === "\\" && at + 1 < source.length) {
|
|
112
|
+
value += source[at + 1];
|
|
113
|
+
at += 2;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (char === quote) {
|
|
117
|
+
return { value, next: at + 1 };
|
|
118
|
+
}
|
|
119
|
+
value += char;
|
|
120
|
+
at += 1;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function isPartialKey(token) {
|
|
125
|
+
return token.type === "word" && token.text.endsWith(".");
|
|
126
|
+
}
|
|
127
|
+
function readWord(source, start) {
|
|
128
|
+
const match = /^[^\s(),="'!<>:]+/.exec(source.slice(start));
|
|
129
|
+
if (match === null) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return { value: match[0], next: start + match[0].length };
|
|
133
|
+
}
|
|
134
|
+
var TokenReader = class {
|
|
135
|
+
constructor(tokens) {
|
|
136
|
+
this.tokens = tokens;
|
|
137
|
+
}
|
|
138
|
+
tokens;
|
|
139
|
+
at = 0;
|
|
140
|
+
/** What stopped the last read, for the caller to report. */
|
|
141
|
+
reason = "the filter states no comparison";
|
|
142
|
+
atEnd() {
|
|
143
|
+
return this.at >= this.tokens.length;
|
|
144
|
+
}
|
|
145
|
+
rest() {
|
|
146
|
+
return this.tokens.slice(this.at).map((token) => token.text).join(" ");
|
|
147
|
+
}
|
|
148
|
+
readExpression() {
|
|
149
|
+
return this.readJunction(
|
|
150
|
+
"or",
|
|
151
|
+
"OR",
|
|
152
|
+
() => this.readJunction("and", "AND", () => this.readUnary())
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
readJunction(operator, keyword, readOperand) {
|
|
156
|
+
const first = readOperand();
|
|
157
|
+
if (first === null) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const operands = [first];
|
|
161
|
+
while (!this.atEnd()) {
|
|
162
|
+
const joined = this.keywordAhead(keyword);
|
|
163
|
+
if (!joined && !(operator === "and" && this.termAhead())) {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
if (joined) {
|
|
167
|
+
this.at += 1;
|
|
168
|
+
}
|
|
169
|
+
const next = readOperand();
|
|
170
|
+
if (next === null) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
operands.push(next);
|
|
174
|
+
}
|
|
175
|
+
return operands.length === 1 ? operands[0] : { type: "junction", operator, operands };
|
|
176
|
+
}
|
|
177
|
+
readUnary() {
|
|
178
|
+
if (this.keywordAhead("NOT")) {
|
|
179
|
+
this.at += 1;
|
|
180
|
+
const operand = this.readUnary();
|
|
181
|
+
return operand === null ? null : { type: "negation", operand };
|
|
182
|
+
}
|
|
183
|
+
return this.readPrimary();
|
|
184
|
+
}
|
|
185
|
+
readPrimary() {
|
|
186
|
+
const token = this.tokens[this.at];
|
|
187
|
+
if (token === void 0) {
|
|
188
|
+
this.reason = "the filter ends where a comparison should be";
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (token.type === "open") {
|
|
192
|
+
this.at += 1;
|
|
193
|
+
const inner = this.readExpression();
|
|
194
|
+
if (inner === null) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
if (this.tokens[this.at]?.type !== "close") {
|
|
198
|
+
this.reason = "a parenthesis is never closed";
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
this.at += 1;
|
|
202
|
+
return inner;
|
|
203
|
+
}
|
|
204
|
+
if (token.type === "word" && this.tokens[this.at + 1]?.type === "open") {
|
|
205
|
+
return this.readCall();
|
|
206
|
+
}
|
|
207
|
+
return this.readTerm();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* A call where a comparison would be. Cloud Monitoring writes an SLO
|
|
211
|
+
* burn-rate condition that way, and the call says what the condition
|
|
212
|
+
* is about instead of any comparison.
|
|
213
|
+
*/
|
|
214
|
+
readCall() {
|
|
215
|
+
const name = this.tokens[this.at];
|
|
216
|
+
this.at += 2;
|
|
217
|
+
const args = [];
|
|
218
|
+
while (!this.atEnd() && this.tokens[this.at]?.type !== "close") {
|
|
219
|
+
const argument = this.tokens[this.at];
|
|
220
|
+
if (argument.type === "word" || argument.type === "string") {
|
|
221
|
+
args.push(argument.text);
|
|
222
|
+
}
|
|
223
|
+
this.at += 1;
|
|
224
|
+
}
|
|
225
|
+
if (this.atEnd()) {
|
|
226
|
+
this.reason = `the call to "${name.text}" is never closed`;
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
this.at += 1;
|
|
230
|
+
return { type: "call", name: name.text, arguments: args };
|
|
231
|
+
}
|
|
232
|
+
readTerm() {
|
|
233
|
+
const key = this.tokens[this.at];
|
|
234
|
+
const operator = this.tokens[this.at + 1];
|
|
235
|
+
const value = this.tokens[this.at + 2];
|
|
236
|
+
if (key === void 0 || key.type !== "word") {
|
|
237
|
+
this.reason = `"${key?.text ?? ""}" is not a key`;
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
if (operator === void 0 || operator.type !== "operator") {
|
|
241
|
+
this.reason = `"${key.text}" is compared to nothing`;
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
if (value === void 0 || value.type !== "word" && value.type !== "string") {
|
|
245
|
+
this.reason = `"${key.text}" is compared to no value`;
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
this.at += 3;
|
|
249
|
+
return {
|
|
250
|
+
type: "term",
|
|
251
|
+
key: key.text,
|
|
252
|
+
operator: operator.text,
|
|
253
|
+
value: value.text
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** Whether the next token is this keyword rather than a key. */
|
|
257
|
+
keywordAhead(keyword) {
|
|
258
|
+
const token = this.tokens[this.at];
|
|
259
|
+
return token !== void 0 && token.type === "word" && token.text === keyword;
|
|
260
|
+
}
|
|
261
|
+
/** Whether a new comparison starts here, with no keyword joining it. */
|
|
262
|
+
termAhead() {
|
|
263
|
+
const token = this.tokens[this.at];
|
|
264
|
+
if (token === void 0) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
if (token.type === "open") {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
return token.type === "word" && !KEYWORDS.has(token.text);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/references.ts
|
|
275
|
+
var SUB_TOKEN = /\$\{([^}]*)\}/g;
|
|
276
|
+
var RESOURCE_ATTRIBUTE = /^([A-Za-z][\w-]*)\.([A-Za-z_][\w-]*)\.([A-Za-z_][\w-]*)$/;
|
|
277
|
+
var LOCAL_VALUE = /^local\.([A-Za-z_][\w-]*)$/;
|
|
278
|
+
var LOCALS = "local";
|
|
279
|
+
var CHAIN_LIMIT = 4;
|
|
280
|
+
function referenceScope(resources, locals = []) {
|
|
281
|
+
const scope = /* @__PURE__ */ new Map();
|
|
282
|
+
for (const [resourceType, label, body] of resources) {
|
|
283
|
+
const address = `${resourceType}.${label}`;
|
|
284
|
+
if (!scope.has(address)) {
|
|
285
|
+
scope.set(address, body);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const stated = {};
|
|
289
|
+
for (const block of locals) {
|
|
290
|
+
for (const [name, value] of Object.entries(block)) {
|
|
291
|
+
if (!(name in stated)) {
|
|
292
|
+
stated[name] = value;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
scope.set(LOCALS, stated);
|
|
297
|
+
return scope;
|
|
298
|
+
}
|
|
299
|
+
function resolveReferences(value, scope) {
|
|
300
|
+
return expand(value, scope, []) ?? value;
|
|
301
|
+
}
|
|
302
|
+
function expand(value, scope, resolving) {
|
|
303
|
+
let cycled = false;
|
|
304
|
+
const expanded = value.replace(SUB_TOKEN, (written, inner) => {
|
|
305
|
+
const reference = inner.trim();
|
|
306
|
+
if (resolving.includes(reference)) {
|
|
307
|
+
cycled = true;
|
|
308
|
+
return written;
|
|
309
|
+
}
|
|
310
|
+
const stated = statedValue(reference, scope);
|
|
311
|
+
if (stated === null || resolving.length >= CHAIN_LIMIT) {
|
|
312
|
+
return written;
|
|
313
|
+
}
|
|
314
|
+
const nested = expand(stated, scope, [...resolving, reference]);
|
|
315
|
+
if (nested === null) {
|
|
316
|
+
cycled = true;
|
|
317
|
+
return written;
|
|
318
|
+
}
|
|
319
|
+
return nested;
|
|
320
|
+
});
|
|
321
|
+
return cycled ? null : expanded;
|
|
322
|
+
}
|
|
323
|
+
function statedValue(reference, scope) {
|
|
324
|
+
const local = LOCAL_VALUE.exec(reference);
|
|
325
|
+
if (local !== null) {
|
|
326
|
+
const stated2 = scope.get(LOCALS)?.[local[1]];
|
|
327
|
+
return typeof stated2 === "string" ? stated2 : null;
|
|
328
|
+
}
|
|
329
|
+
const parsed = RESOURCE_ATTRIBUTE.exec(reference);
|
|
330
|
+
if (parsed === null) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
const [, resourceType, label, attribute] = parsed;
|
|
334
|
+
const stated = scope.get(`${resourceType}.${label}`)?.[attribute];
|
|
335
|
+
return typeof stated === "string" ? stated : null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/index.ts
|
|
339
|
+
function terraformToSummaries(source, sourceFile, options) {
|
|
340
|
+
return summariesForFiles([{ source, sourceFile }], options);
|
|
37
341
|
}
|
|
38
342
|
function terraformFileToSummaries(target, options) {
|
|
39
343
|
const files = fs.statSync(target).isDirectory() ? fs.readdirSync(target).filter((name) => name.endsWith(".tf")).map((name) => path.join(target, name)) : [target];
|
|
40
|
-
return
|
|
41
|
-
(file) =>
|
|
344
|
+
return summariesForFiles(
|
|
345
|
+
files.map((file) => ({
|
|
346
|
+
source: fs.readFileSync(file, "utf8"),
|
|
347
|
+
sourceFile: file
|
|
348
|
+
})),
|
|
349
|
+
options
|
|
42
350
|
);
|
|
43
351
|
}
|
|
352
|
+
function summariesForFiles(files, options) {
|
|
353
|
+
const parsed = files.map((file) => parseSource(file)).filter((file) => file !== null);
|
|
354
|
+
const scope = referenceScope(
|
|
355
|
+
parsed.flatMap((file) => file.resources),
|
|
356
|
+
parsed.flatMap((file) => file.locals)
|
|
357
|
+
);
|
|
358
|
+
const summaries = [];
|
|
359
|
+
for (const file of parsed) {
|
|
360
|
+
for (const [resourceType, label, body] of file.resources) {
|
|
361
|
+
for (const pack of options.packs) {
|
|
362
|
+
const pattern = patternFor(pack, resourceType, file.constraints);
|
|
363
|
+
if (pattern === void 0 || !entryApplies(pattern, body)) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
summaries.push(
|
|
367
|
+
...summariesFor({
|
|
368
|
+
pattern,
|
|
369
|
+
label,
|
|
370
|
+
body,
|
|
371
|
+
sourceFile: file.sourceFile,
|
|
372
|
+
resourceType,
|
|
373
|
+
scope
|
|
374
|
+
})
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return summaries;
|
|
380
|
+
}
|
|
381
|
+
function parseSource(file) {
|
|
382
|
+
let read;
|
|
383
|
+
try {
|
|
384
|
+
const [parsed] = hcl2.parseToObject(file.source);
|
|
385
|
+
read = parsed;
|
|
386
|
+
} catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
const document = asRecord(read);
|
|
390
|
+
if (document === null) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
sourceFile: file.sourceFile,
|
|
395
|
+
constraints: providerConstraints(document),
|
|
396
|
+
resources: resourcesIn(document),
|
|
397
|
+
locals: localsIn(document)
|
|
398
|
+
};
|
|
399
|
+
}
|
|
44
400
|
function providerConstraints(document) {
|
|
45
401
|
const constraints = /* @__PURE__ */ new Map();
|
|
46
402
|
for (const block of arrayOf(document.terraform)) {
|
|
@@ -79,22 +435,39 @@ function patternFor(pack, resourceType, constraints) {
|
|
|
79
435
|
}
|
|
80
436
|
});
|
|
81
437
|
}
|
|
438
|
+
function entryApplies(pattern, body) {
|
|
439
|
+
const gate = pattern.appliesWhen;
|
|
440
|
+
if (gate === void 0) {
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
const value = body[gate.attribute];
|
|
444
|
+
if (value === void 0) {
|
|
445
|
+
return gate.whenUnset === "read";
|
|
446
|
+
}
|
|
447
|
+
const text = stringOf(value);
|
|
448
|
+
return text !== null && gate.equals.includes(text);
|
|
449
|
+
}
|
|
450
|
+
var READERS = {
|
|
451
|
+
storage: (site, boundary) => {
|
|
452
|
+
const types = fieldTypes(site.body, boundary);
|
|
453
|
+
return keyedShapes(site.body, boundary).map(
|
|
454
|
+
(shape) => storageSummary({ ...site, boundary, shape, types })
|
|
455
|
+
);
|
|
456
|
+
},
|
|
457
|
+
"message-bus": (site, boundary) => [busSummary({ ...site, boundary })],
|
|
458
|
+
metric: (site, boundary) => [metricSummary({ ...site, boundary })],
|
|
459
|
+
"metric-reading": (site, boundary) => readingSummaries({ ...site, boundary })
|
|
460
|
+
};
|
|
82
461
|
function summariesFor(opts) {
|
|
83
|
-
const { pattern } = opts;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
const boundary = pattern.boundary;
|
|
88
|
-
const types = fieldTypes(opts.body, boundary);
|
|
89
|
-
return keyedShapes(opts.body, boundary).map(
|
|
90
|
-
(shape) => storageSummary({ ...opts, boundary, shape, types })
|
|
91
|
-
);
|
|
462
|
+
const { pattern, ...site } = opts;
|
|
463
|
+
const read = READERS[pattern.boundary.kind];
|
|
464
|
+
return read(site, pattern.boundary);
|
|
92
465
|
}
|
|
93
466
|
function storageSummary(opts) {
|
|
94
467
|
const { boundary, label, shape, types } = opts;
|
|
95
468
|
const address = `${opts.resourceType}.${label}`;
|
|
96
|
-
const
|
|
97
|
-
const physicalTable =
|
|
469
|
+
const declaresContainer = boundary.declares !== "store";
|
|
470
|
+
const physicalTable = !declaresContainer || boundary.nameAttribute === void 0 ? null : namePattern(opts.body[boundary.nameAttribute], opts.scope);
|
|
98
471
|
return {
|
|
99
472
|
kind: "library",
|
|
100
473
|
location: {
|
|
@@ -110,7 +483,7 @@ function storageSummary(opts) {
|
|
|
110
483
|
storageSystem: boundary.storageSystem,
|
|
111
484
|
...boundary.transport !== void 0 ? { transport: boundary.transport } : {},
|
|
112
485
|
scope: "default",
|
|
113
|
-
container: label,
|
|
486
|
+
container: declaresContainer ? label : null,
|
|
114
487
|
accessPath: shape.accessPath
|
|
115
488
|
})
|
|
116
489
|
},
|
|
@@ -138,8 +511,7 @@ function storageSummary(opts) {
|
|
|
138
511
|
}
|
|
139
512
|
function busSummary(opts) {
|
|
140
513
|
const { boundary, label } = opts;
|
|
141
|
-
const
|
|
142
|
-
const physicalName = declaredName === null ? null : namePatternFromSub(declaredName);
|
|
514
|
+
const physicalName = boundary.nameAttribute === void 0 ? null : namePattern(opts.body[boundary.nameAttribute], opts.scope);
|
|
143
515
|
return {
|
|
144
516
|
kind: "library",
|
|
145
517
|
location: {
|
|
@@ -169,6 +541,132 @@ function busSummary(opts) {
|
|
|
169
541
|
}
|
|
170
542
|
};
|
|
171
543
|
}
|
|
544
|
+
var NAME_HOLE = "{name}";
|
|
545
|
+
function metricSummary(opts) {
|
|
546
|
+
const { boundary } = opts;
|
|
547
|
+
const declaredName = namePattern(
|
|
548
|
+
opts.body[boundary.nameAttribute],
|
|
549
|
+
opts.scope
|
|
550
|
+
);
|
|
551
|
+
const metricType = declaredName === null ? null : boundary.metricTypeTemplate.replace(NAME_HOLE, declaredName);
|
|
552
|
+
return {
|
|
553
|
+
kind: "library",
|
|
554
|
+
location: {
|
|
555
|
+
file: opts.sourceFile,
|
|
556
|
+
range: { start: 1, end: 1 },
|
|
557
|
+
exportName: null
|
|
558
|
+
},
|
|
559
|
+
identity: {
|
|
560
|
+
name: `${opts.resourceType}.${opts.label}`,
|
|
561
|
+
exportPath: null,
|
|
562
|
+
boundaryBinding: metricBinding({
|
|
563
|
+
recognition: "terraform",
|
|
564
|
+
metricSystem: boundary.metricSystem,
|
|
565
|
+
metricType
|
|
566
|
+
})
|
|
567
|
+
},
|
|
568
|
+
inputs: [],
|
|
569
|
+
transitions: [],
|
|
570
|
+
gaps: [],
|
|
571
|
+
confidence: { source: "declared", level: "high" },
|
|
572
|
+
metadata: { metricContract: metricContract(opts.body, boundary) }
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function metricContract(body, boundary) {
|
|
576
|
+
const values = meaningOf(body, boundary.values);
|
|
577
|
+
const accumulates = meaningOf(body, boundary.accumulates);
|
|
578
|
+
return {
|
|
579
|
+
...values !== void 0 ? { values } : {},
|
|
580
|
+
...accumulates !== void 0 ? { accumulates } : {}
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function readingSummaries(opts) {
|
|
584
|
+
const { boundary } = opts;
|
|
585
|
+
const summaries = [];
|
|
586
|
+
for (const reading of blocksAt(opts.body, boundary.readingBlocks)) {
|
|
587
|
+
const query = stringOf(valueAt(reading, boundary.queryAttribute));
|
|
588
|
+
for (const metricType of metricTypesIn(
|
|
589
|
+
query,
|
|
590
|
+
boundary.queryIdentityKey,
|
|
591
|
+
opts.scope
|
|
592
|
+
)) {
|
|
593
|
+
summaries.push({
|
|
594
|
+
kind: "consumer",
|
|
595
|
+
location: {
|
|
596
|
+
file: opts.sourceFile,
|
|
597
|
+
range: { start: 1, end: 1 },
|
|
598
|
+
exportName: null
|
|
599
|
+
},
|
|
600
|
+
identity: {
|
|
601
|
+
name: `${opts.resourceType}.${opts.label}#${summaries.length}`,
|
|
602
|
+
exportPath: null,
|
|
603
|
+
boundaryBinding: metricBinding({
|
|
604
|
+
recognition: "terraform",
|
|
605
|
+
metricSystem: boundary.metricSystem,
|
|
606
|
+
metricType
|
|
607
|
+
})
|
|
608
|
+
},
|
|
609
|
+
inputs: [],
|
|
610
|
+
transitions: [],
|
|
611
|
+
gaps: [],
|
|
612
|
+
confidence: { source: "declared", level: "high" },
|
|
613
|
+
metadata: { metricReading: metricReading(reading, boundary) }
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return summaries;
|
|
618
|
+
}
|
|
619
|
+
function metricTypesIn(query, identityKey, scope) {
|
|
620
|
+
if (query === null) {
|
|
621
|
+
return [null];
|
|
622
|
+
}
|
|
623
|
+
const parsed = parseFilterQuery(query);
|
|
624
|
+
if (!parsed.ok) {
|
|
625
|
+
return [null];
|
|
626
|
+
}
|
|
627
|
+
const found = filterValuesFor(parsed.query, identityKey).map((value) => namePattern(value, scope)).filter((value) => value !== null);
|
|
628
|
+
return found.length === 0 ? [null] : found;
|
|
629
|
+
}
|
|
630
|
+
function metricReading(reading, boundary) {
|
|
631
|
+
const compares = boundary.comparesTo;
|
|
632
|
+
const comparesTo = compares !== void 0 && valueAt(reading, compares.attribute) !== void 0 ? compares.whenSet : void 0;
|
|
633
|
+
const reduces = boundary.reducesTo;
|
|
634
|
+
const reducesTo = meaningOf(reading, reduces);
|
|
635
|
+
return {
|
|
636
|
+
...comparesTo !== void 0 ? { comparesTo } : {},
|
|
637
|
+
...reducesTo !== void 0 ? { reducesTo } : {},
|
|
638
|
+
...reduces === void 0 ? {} : { reduction: { setting: reduces.attribute, leaves: reduces.means } }
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function meaningOf(body, spec) {
|
|
642
|
+
if (spec === void 0) {
|
|
643
|
+
return void 0;
|
|
644
|
+
}
|
|
645
|
+
const stated = stringOf(valueAt(body, spec.attribute));
|
|
646
|
+
return stated === null ? void 0 : spec.means[stated];
|
|
647
|
+
}
|
|
648
|
+
function valueAt(body, path2) {
|
|
649
|
+
const steps = path2.split(".");
|
|
650
|
+
const last = steps.pop();
|
|
651
|
+
let current = body;
|
|
652
|
+
for (const step of steps) {
|
|
653
|
+
const nested = arrayOf(current[step])[0];
|
|
654
|
+
current = asRecord(nested);
|
|
655
|
+
if (current === null) {
|
|
656
|
+
return void 0;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return current[last];
|
|
660
|
+
}
|
|
661
|
+
function blocksAt(body, blocks) {
|
|
662
|
+
let found = [body];
|
|
663
|
+
for (const block of blocks) {
|
|
664
|
+
found = found.flatMap(
|
|
665
|
+
(record) => arrayOf(record[block]).map(asRecord).filter((nested) => nested !== null)
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
return found;
|
|
669
|
+
}
|
|
172
670
|
function keyedShapes(body, boundary) {
|
|
173
671
|
if (boundary.identifies === void 0) {
|
|
174
672
|
return [{ accessPath: null, keyFields: [], serves: null }];
|
|
@@ -233,6 +731,16 @@ function fieldTypes(body, boundary) {
|
|
|
233
731
|
}
|
|
234
732
|
return types;
|
|
235
733
|
}
|
|
734
|
+
function localsIn(document) {
|
|
735
|
+
const found = [];
|
|
736
|
+
for (const block of arrayOf(document.locals)) {
|
|
737
|
+
const stated = asRecord(block);
|
|
738
|
+
if (stated !== null) {
|
|
739
|
+
found.push(stated);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return found;
|
|
743
|
+
}
|
|
236
744
|
function resourcesIn(document) {
|
|
237
745
|
const found = [];
|
|
238
746
|
for (const resource of arrayOf(document.resource)) {
|
|
@@ -259,6 +767,11 @@ function resourcesIn(document) {
|
|
|
259
767
|
}
|
|
260
768
|
return found;
|
|
261
769
|
}
|
|
770
|
+
function namePattern(value, scope) {
|
|
771
|
+
return namePatternFromSub(
|
|
772
|
+
typeof value === "string" ? resolveReferences(value, scope) : value
|
|
773
|
+
);
|
|
774
|
+
}
|
|
262
775
|
function arrayOf(value) {
|
|
263
776
|
if (value === void 0 || value === null) {
|
|
264
777
|
return [];
|
|
@@ -272,6 +785,10 @@ function stringOf(value) {
|
|
|
272
785
|
return typeof value === "string" ? value : null;
|
|
273
786
|
}
|
|
274
787
|
export {
|
|
788
|
+
filterCalls,
|
|
789
|
+
filterTerms,
|
|
790
|
+
filterValuesFor,
|
|
791
|
+
parseFilterQuery,
|
|
275
792
|
terraformFileToSummaries,
|
|
276
793
|
terraformToSummaries
|
|
277
794
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/contract-terraform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Read the storage a Terraform configuration declares, for checking code against it.",
|
|
6
6
|
"keywords": [
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typecheck": "tsc --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@suss/behavioral-ir": "0.
|
|
48
|
+
"@suss/behavioral-ir": "0.10.0",
|
|
49
49
|
"hcl2-parser": "^1.0.3",
|
|
50
50
|
"semver": "^7.6.0"
|
|
51
51
|
},
|