norn-cli 1.3.17 → 1.3.19
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/AGENTS.md +72 -0
- package/CHANGELOG.md +39 -1
- package/README.md +7 -3
- package/dist/cli.js +113 -54
- package/out/assertionRunner.js +537 -0
- package/out/cli/colors.js +129 -0
- package/out/cli/formatters/assertion.js +75 -0
- package/out/cli/formatters/index.js +23 -0
- package/out/cli/formatters/response.js +106 -0
- package/out/cli/formatters/summary.js +187 -0
- package/out/cli/redaction.js +237 -0
- package/out/cli/reporters/html.js +634 -0
- package/out/cli/reporters/index.js +22 -0
- package/out/cli/reporters/junit.js +211 -0
- package/out/cli.js +926 -0
- package/out/codeLensProvider.js +254 -0
- package/out/compareContentProvider.js +85 -0
- package/out/completionProvider.js +1886 -0
- package/out/contractDecorationProvider.js +243 -0
- package/out/coverageCalculator.js +756 -0
- package/out/coveragePanel.js +542 -0
- package/out/diagnosticProvider.js +980 -0
- package/out/environmentProvider.js +373 -0
- package/out/extension.js +1025 -0
- package/out/httpClient.js +269 -0
- package/out/jsonFileReader.js +320 -0
- package/out/nornapiParser.js +326 -0
- package/out/parser.js +725 -0
- package/out/responsePanel.js +4674 -0
- package/out/schemaGenerator.js +393 -0
- package/out/scriptRunner.js +419 -0
- package/out/sequenceRunner.js +3046 -0
- package/out/swaggerParser.js +339 -0
- package/out/test/extension.test.js +48 -0
- package/out/testProvider.js +658 -0
- package/out/validationCache.js +245 -0
- package/package.json +1 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Parser for .nornapi files
|
|
4
|
+
*
|
|
5
|
+
* Parses header groups and endpoint definitions from .nornapi files.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.parseNornApiFile = parseNornApiFile;
|
|
9
|
+
exports.getHeaderGroup = getHeaderGroup;
|
|
10
|
+
exports.getEndpoint = getEndpoint;
|
|
11
|
+
exports.resolveEndpointPath = resolveEndpointPath;
|
|
12
|
+
exports.resolveHeaderValues = resolveHeaderValues;
|
|
13
|
+
exports.isApiRequestLine = isApiRequestLine;
|
|
14
|
+
exports.parseApiRequest = parseApiRequest;
|
|
15
|
+
/**
|
|
16
|
+
* Extracts parameter names from an endpoint path
|
|
17
|
+
* e.g., /users/{id}/orders/{orderId} -> ['id', 'orderId']
|
|
18
|
+
*
|
|
19
|
+
* Note: Does NOT extract {{variable}} references (double braces) - only {param} (single braces)
|
|
20
|
+
*/
|
|
21
|
+
function extractPathParameters(path) {
|
|
22
|
+
const params = [];
|
|
23
|
+
// Match {param} but NOT {{variable}}
|
|
24
|
+
// Use negative lookbehind (?<!{) and negative lookahead (?!})
|
|
25
|
+
const regex = /(?<!\{)\{([a-zA-Z_][a-zA-Z0-9_]*)\}(?!\})/g;
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = regex.exec(path)) !== null) {
|
|
28
|
+
params.push(match[1]);
|
|
29
|
+
}
|
|
30
|
+
return params;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parses a .nornapi file and returns header groups and endpoint definitions
|
|
34
|
+
*/
|
|
35
|
+
function parseNornApiFile(content) {
|
|
36
|
+
const lines = content.split('\n');
|
|
37
|
+
const headerGroups = [];
|
|
38
|
+
const endpoints = [];
|
|
39
|
+
let currentHeaderGroup = null;
|
|
40
|
+
let inEndpointsBlock = false;
|
|
41
|
+
for (let i = 0; i < lines.length; i++) {
|
|
42
|
+
const line = lines[i];
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
// Skip empty lines and comments
|
|
45
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// Check for headers block start: headers Name
|
|
49
|
+
const headersStartMatch = trimmed.match(/^headers\s+([a-zA-Z_][a-zA-Z0-9_]*)$/i);
|
|
50
|
+
if (headersStartMatch) {
|
|
51
|
+
currentHeaderGroup = {
|
|
52
|
+
name: headersStartMatch[1],
|
|
53
|
+
headers: {}
|
|
54
|
+
};
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// Check for headers block end
|
|
58
|
+
if (/^end\s+headers$/i.test(trimmed)) {
|
|
59
|
+
if (currentHeaderGroup) {
|
|
60
|
+
headerGroups.push(currentHeaderGroup);
|
|
61
|
+
currentHeaderGroup = null;
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// Inside headers block - parse header lines
|
|
66
|
+
if (currentHeaderGroup) {
|
|
67
|
+
const headerMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9\-]*)\s*:\s*(.+)$/);
|
|
68
|
+
if (headerMatch) {
|
|
69
|
+
currentHeaderGroup.headers[headerMatch[1]] = headerMatch[2].trim();
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// Check for endpoints block start
|
|
74
|
+
if (/^endpoints$/i.test(trimmed)) {
|
|
75
|
+
inEndpointsBlock = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Check for endpoints block end
|
|
79
|
+
if (/^end\s+endpoints$/i.test(trimmed)) {
|
|
80
|
+
inEndpointsBlock = false;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Inside endpoints block - parse endpoint lines
|
|
84
|
+
// Format: EndpointName: METHOD /path/{param}
|
|
85
|
+
if (inEndpointsBlock) {
|
|
86
|
+
const endpointMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
|
|
87
|
+
if (endpointMatch) {
|
|
88
|
+
const path = endpointMatch[3].trim();
|
|
89
|
+
endpoints.push({
|
|
90
|
+
name: endpointMatch[1],
|
|
91
|
+
method: endpointMatch[2].toUpperCase(),
|
|
92
|
+
path,
|
|
93
|
+
parameters: extractPathParameters(path)
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { headerGroups, endpoints };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Gets a header group by name (case-sensitive)
|
|
103
|
+
*/
|
|
104
|
+
function getHeaderGroup(definition, name) {
|
|
105
|
+
return definition.headerGroups.find(hg => hg.name === name);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Gets an endpoint by name (case-sensitive)
|
|
109
|
+
*/
|
|
110
|
+
function getEndpoint(definition, name) {
|
|
111
|
+
return definition.endpoints.find(ep => ep.name === name);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolves an endpoint path with provided parameters
|
|
115
|
+
* e.g., path: /users/{id}/orders, params: { id: '123' } -> /users/123/orders
|
|
116
|
+
*/
|
|
117
|
+
function resolveEndpointPath(endpoint, params) {
|
|
118
|
+
let resolvedPath = endpoint.path;
|
|
119
|
+
for (const paramName of endpoint.parameters) {
|
|
120
|
+
const value = params[paramName];
|
|
121
|
+
if (value !== undefined) {
|
|
122
|
+
resolvedPath = resolvedPath.replace(`{${paramName}}`, value);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return resolvedPath;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Applies variable substitution to header values
|
|
129
|
+
*/
|
|
130
|
+
function resolveHeaderValues(headerGroup, variables) {
|
|
131
|
+
const resolved = {};
|
|
132
|
+
for (const [name, value] of Object.entries(headerGroup.headers)) {
|
|
133
|
+
// Substitute {{variable}} references
|
|
134
|
+
resolved[name] = value.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
|
|
135
|
+
return variables[varName] ?? `{{${varName}}}`;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return resolved;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Checks if a request line uses endpoint syntax instead of a URL
|
|
142
|
+
* Endpoint syntax: METHOD EndpointName(param, ...) [HeaderGroups...]
|
|
143
|
+
* URL syntax: METHOD http://... or METHOD /path
|
|
144
|
+
*
|
|
145
|
+
* Returns true if the URL part starts with a word that matches an endpoint name
|
|
146
|
+
*/
|
|
147
|
+
function isApiRequestLine(requestContent, endpoints) {
|
|
148
|
+
const lines = requestContent.split('\n');
|
|
149
|
+
// Find the first non-empty, non-comment, non-import line
|
|
150
|
+
let firstLine = '';
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
const trimmed = line.trim();
|
|
153
|
+
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('import ')) {
|
|
154
|
+
firstLine = trimmed;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!firstLine) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
// Match METHOD followed by non-URL pattern
|
|
162
|
+
const match = firstLine.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([a-zA-Z_][a-zA-Z0-9_]*)/i);
|
|
163
|
+
if (!match) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const potentialEndpointName = match[2];
|
|
167
|
+
// Check if this matches a known endpoint
|
|
168
|
+
return endpoints.some(ep => ep.name === potentialEndpointName);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Parses an API request block (endpoint-based syntax)
|
|
172
|
+
*
|
|
173
|
+
* Handles two syntax styles:
|
|
174
|
+
* 1. Single line: GET GetUser("123") Auth Json
|
|
175
|
+
* 2. Multi-line:
|
|
176
|
+
* GET GetUser("123")
|
|
177
|
+
* Auth
|
|
178
|
+
* Json
|
|
179
|
+
*
|
|
180
|
+
* Returns parsed information or undefined if not an API request
|
|
181
|
+
*/
|
|
182
|
+
function parseApiRequest(requestContent, endpoints, headerGroups) {
|
|
183
|
+
const lines = requestContent.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#') && !l.startsWith('import '));
|
|
184
|
+
if (lines.length === 0) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const firstLine = lines[0];
|
|
188
|
+
// Parse: METHOD EndpointName(params...) [HeaderGroups...]
|
|
189
|
+
// Params can be: positional values or key=value pairs
|
|
190
|
+
const lineMatch = firstLine.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:\(([^)]*)\))?\s*(.*)$/i);
|
|
191
|
+
if (!lineMatch) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const methodFromLine = lineMatch[1].toUpperCase();
|
|
195
|
+
const endpointName = lineMatch[2];
|
|
196
|
+
const paramsStr = lineMatch[3] || '';
|
|
197
|
+
const headerGroupsOnLine = lineMatch[4]?.trim() || '';
|
|
198
|
+
// Find the endpoint
|
|
199
|
+
const endpoint = endpoints.find(ep => ep.name === endpointName);
|
|
200
|
+
if (!endpoint) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
// Verify the method matches (or allow method from line to override)
|
|
204
|
+
// Actually, the user might use a different method - we use the line's method
|
|
205
|
+
const method = methodFromLine;
|
|
206
|
+
// Parse parameters: positional or key=value
|
|
207
|
+
const params = {};
|
|
208
|
+
if (paramsStr) {
|
|
209
|
+
// Split by comma, respecting quotes
|
|
210
|
+
const paramTokens = parseParamTokens(paramsStr);
|
|
211
|
+
// Check if key=value or positional
|
|
212
|
+
if (paramTokens.length > 0 && paramTokens[0].includes(':')) {
|
|
213
|
+
// Key=value format: GetUser(id: "123")
|
|
214
|
+
for (const token of paramTokens) {
|
|
215
|
+
const kvMatch = token.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.+)$/);
|
|
216
|
+
if (kvMatch) {
|
|
217
|
+
params[kvMatch[1]] = unquote(kvMatch[2].trim());
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// Positional format: GetUser("123") -> map to endpoint parameters in order
|
|
223
|
+
for (let i = 0; i < paramTokens.length && i < endpoint.parameters.length; i++) {
|
|
224
|
+
params[endpoint.parameters[i]] = unquote(paramTokens[i].trim());
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Collect header group names and inline headers
|
|
229
|
+
const headerGroupNames = [];
|
|
230
|
+
const inlineHeaders = {};
|
|
231
|
+
// From same line
|
|
232
|
+
if (headerGroupsOnLine) {
|
|
233
|
+
const names = headerGroupsOnLine.split(/\s+/).filter(n => n);
|
|
234
|
+
for (const name of names) {
|
|
235
|
+
if (headerGroups.some(hg => hg.name === name)) {
|
|
236
|
+
headerGroupNames.push(name);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// From subsequent lines:
|
|
241
|
+
// - Header group names (single word matching a defined group)
|
|
242
|
+
// - Inline headers (HeaderName: value format)
|
|
243
|
+
// - Body content (everything else after headers section)
|
|
244
|
+
const bodyLines = [];
|
|
245
|
+
let inBodySection = false;
|
|
246
|
+
for (let i = 1; i < lines.length; i++) {
|
|
247
|
+
const line = lines[i];
|
|
248
|
+
// Once we start the body section, everything goes to body
|
|
249
|
+
if (inBodySection) {
|
|
250
|
+
bodyLines.push(line);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
// Check if this line is a header group name
|
|
254
|
+
if (headerGroups.some(hg => hg.name === line)) {
|
|
255
|
+
headerGroupNames.push(line);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
// Check if this is an inline header (HeaderName: value)
|
|
259
|
+
const headerMatch = line.match(/^([a-zA-Z][a-zA-Z0-9\-]*)\s*:\s*(.+)$/);
|
|
260
|
+
if (headerMatch) {
|
|
261
|
+
let headerValue = headerMatch[2].trim();
|
|
262
|
+
// Remove surrounding quotes if present
|
|
263
|
+
if ((headerValue.startsWith('"') && headerValue.endsWith('"')) ||
|
|
264
|
+
(headerValue.startsWith("'") && headerValue.endsWith("'"))) {
|
|
265
|
+
headerValue = headerValue.slice(1, -1);
|
|
266
|
+
}
|
|
267
|
+
inlineHeaders[headerMatch[1]] = headerValue;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
// Not a header group or inline header - start of body section
|
|
271
|
+
inBodySection = true;
|
|
272
|
+
bodyLines.push(line);
|
|
273
|
+
}
|
|
274
|
+
const body = bodyLines.length > 0 ? bodyLines.join('\n') : undefined;
|
|
275
|
+
return {
|
|
276
|
+
method,
|
|
277
|
+
endpointName,
|
|
278
|
+
params,
|
|
279
|
+
headerGroupNames,
|
|
280
|
+
inlineHeaders,
|
|
281
|
+
body
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Splits parameter string by commas, respecting quotes
|
|
286
|
+
*/
|
|
287
|
+
function parseParamTokens(paramsStr) {
|
|
288
|
+
const tokens = [];
|
|
289
|
+
let current = '';
|
|
290
|
+
let inQuote = false;
|
|
291
|
+
let quoteChar = '';
|
|
292
|
+
for (const char of paramsStr) {
|
|
293
|
+
if ((char === '"' || char === "'") && !inQuote) {
|
|
294
|
+
inQuote = true;
|
|
295
|
+
quoteChar = char;
|
|
296
|
+
current += char;
|
|
297
|
+
}
|
|
298
|
+
else if (char === quoteChar && inQuote) {
|
|
299
|
+
inQuote = false;
|
|
300
|
+
quoteChar = '';
|
|
301
|
+
current += char;
|
|
302
|
+
}
|
|
303
|
+
else if (char === ',' && !inQuote) {
|
|
304
|
+
tokens.push(current.trim());
|
|
305
|
+
current = '';
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
current += char;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (current.trim()) {
|
|
312
|
+
tokens.push(current.trim());
|
|
313
|
+
}
|
|
314
|
+
return tokens;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Removes surrounding quotes from a string value
|
|
318
|
+
*/
|
|
319
|
+
function unquote(value) {
|
|
320
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
321
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
322
|
+
return value.slice(1, -1);
|
|
323
|
+
}
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=nornapiParser.js.map
|