norn-cli 1.6.1 → 1.6.2
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 +9 -1
- package/CHANGELOG.md +12 -0
- package/dist/cli.js +161 -59
- package/package.json +1 -1
- package/out/assertionRunner.js +0 -537
- package/out/chatParticipant.js +0 -722
- package/out/cli/colors.js +0 -129
- package/out/cli/formatters/assertion.js +0 -75
- package/out/cli/formatters/index.js +0 -23
- package/out/cli/formatters/response.js +0 -106
- package/out/cli/formatters/summary.js +0 -187
- package/out/cli/redaction.js +0 -237
- package/out/cli/reporters/html.js +0 -634
- package/out/cli/reporters/index.js +0 -22
- package/out/cli/reporters/junit.js +0 -211
- package/out/cli.js +0 -989
- package/out/codeLensProvider.js +0 -248
- package/out/compareContentProvider.js +0 -85
- package/out/completionProvider.js +0 -2404
- package/out/contractDecorationProvider.js +0 -243
- package/out/coverageCalculator.js +0 -837
- package/out/coveragePanel.js +0 -545
- package/out/diagnosticProvider.js +0 -1113
- package/out/environmentProvider.js +0 -442
- package/out/extension.js +0 -1114
- package/out/httpClient.js +0 -269
- package/out/jsonFileReader.js +0 -320
- package/out/nornPrompt.js +0 -580
- package/out/nornapiParser.js +0 -326
- package/out/parser.js +0 -725
- package/out/responsePanel.js +0 -4674
- package/out/schemaGenerator.js +0 -393
- package/out/scriptRunner.js +0 -419
- package/out/sequenceRunner.js +0 -3046
- package/out/swaggerBodyIntellisenseCache.js +0 -147
- package/out/swaggerParser.js +0 -419
- package/out/test/coverageCalculator.test.js +0 -100
- package/out/test/extension.test.js +0 -48
- package/out/testProvider.js +0 -658
- package/out/validationCache.js +0 -245
package/out/httpClient.js
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.createCookieJar = createCookieJar;
|
|
7
|
-
exports.getSharedCookieJar = getSharedCookieJar;
|
|
8
|
-
exports.clearCookies = clearCookies;
|
|
9
|
-
exports.getCookiesForUrl = getCookiesForUrl;
|
|
10
|
-
exports.getAllCookies = getAllCookies;
|
|
11
|
-
exports.sendRequest = sendRequest;
|
|
12
|
-
exports.sendRequestWithJar = sendRequestWithJar;
|
|
13
|
-
const axios_1 = __importDefault(require("axios"));
|
|
14
|
-
const tough_cookie_1 = require("tough-cookie");
|
|
15
|
-
// Shared cookie jar for VS Code extension (persists across requests)
|
|
16
|
-
let sharedCookieJar = new tough_cookie_1.CookieJar();
|
|
17
|
-
/**
|
|
18
|
-
* Creates a fresh cookie jar for isolated execution (CLI, sequences)
|
|
19
|
-
*/
|
|
20
|
-
function createCookieJar() {
|
|
21
|
-
return new tough_cookie_1.CookieJar();
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Gets the shared cookie jar
|
|
25
|
-
*/
|
|
26
|
-
function getSharedCookieJar() {
|
|
27
|
-
return sharedCookieJar;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Clears all cookies from the shared jar
|
|
31
|
-
*/
|
|
32
|
-
function clearCookies() {
|
|
33
|
-
sharedCookieJar = new tough_cookie_1.CookieJar();
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Gets all cookies from a jar for a specific URL
|
|
37
|
-
*/
|
|
38
|
-
async function getCookiesForUrl(url, jar) {
|
|
39
|
-
const targetJar = jar || sharedCookieJar;
|
|
40
|
-
try {
|
|
41
|
-
const cookies = await targetJar.getCookies(url);
|
|
42
|
-
return cookies.map(cookieToInfo);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Gets all cookies from a jar
|
|
50
|
-
*/
|
|
51
|
-
async function getAllCookies(jar) {
|
|
52
|
-
const targetJar = jar || sharedCookieJar;
|
|
53
|
-
const serialized = await targetJar.serialize();
|
|
54
|
-
return (serialized.cookies || []).map((c) => ({
|
|
55
|
-
name: String(c.key || ''),
|
|
56
|
-
value: String(c.value || ''),
|
|
57
|
-
domain: String(c.domain || ''),
|
|
58
|
-
path: String(c.path || '/'),
|
|
59
|
-
expires: c.expires ? String(c.expires) : undefined,
|
|
60
|
-
httpOnly: Boolean(c.httpOnly),
|
|
61
|
-
secure: Boolean(c.secure),
|
|
62
|
-
}));
|
|
63
|
-
}
|
|
64
|
-
function cookieToInfo(cookie) {
|
|
65
|
-
return {
|
|
66
|
-
name: cookie.key,
|
|
67
|
-
value: cookie.value,
|
|
68
|
-
domain: cookie.domain || '',
|
|
69
|
-
path: cookie.path || '/',
|
|
70
|
-
expires: cookie.expires instanceof Date ? cookie.expires.toISOString() : undefined,
|
|
71
|
-
httpOnly: cookie.httpOnly || false,
|
|
72
|
-
secure: cookie.secure || false,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Sends a request using the shared cookie jar (for VS Code extension)
|
|
77
|
-
*/
|
|
78
|
-
async function sendRequest(request, retryOptions) {
|
|
79
|
-
return sendRequestWithJar(request, sharedCookieJar, retryOptions);
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Helper function to wait
|
|
83
|
-
*/
|
|
84
|
-
function sleep(ms) {
|
|
85
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Check if a response indicates failure (should retry)
|
|
89
|
-
* Retries on: network errors, 5xx server errors, 429 rate limit
|
|
90
|
-
*/
|
|
91
|
-
function shouldRetry(response, error) {
|
|
92
|
-
// Always retry on network errors
|
|
93
|
-
if (error)
|
|
94
|
-
return true;
|
|
95
|
-
// Retry on server errors (5xx) or rate limiting (429)
|
|
96
|
-
if (response && (response.status >= 500 || response.status === 429))
|
|
97
|
-
return true;
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Sends a request using a specific cookie jar
|
|
102
|
-
* Manually follows redirects to properly capture Set-Cookie headers at each step
|
|
103
|
-
* Supports automatic retry with linear backoff
|
|
104
|
-
*/
|
|
105
|
-
async function sendRequestWithJar(request, jar, retryOptions) {
|
|
106
|
-
const totalAttempts = (retryOptions?.retryCount ?? 0) + 1; // +1 for initial attempt
|
|
107
|
-
const backoffMs = retryOptions?.backoffMs ?? 1000;
|
|
108
|
-
const retriedErrors = [];
|
|
109
|
-
let lastError = null;
|
|
110
|
-
let attemptsMade = 0;
|
|
111
|
-
for (let attempt = 1; attempt <= totalAttempts; attempt++) {
|
|
112
|
-
attemptsMade = attempt;
|
|
113
|
-
const startTime = Date.now();
|
|
114
|
-
const maxRedirects = 10;
|
|
115
|
-
let redirectCount = 0;
|
|
116
|
-
let currentUrl = request.url;
|
|
117
|
-
let currentMethod = request.method;
|
|
118
|
-
let currentBody = request.body;
|
|
119
|
-
let finalResponse = null;
|
|
120
|
-
let sentRequestBody = undefined;
|
|
121
|
-
let sentRequestHeaders = {};
|
|
122
|
-
try {
|
|
123
|
-
while (redirectCount <= maxRedirects) {
|
|
124
|
-
// Get cookies for current URL
|
|
125
|
-
const existingCookies = await jar.getCookies(currentUrl);
|
|
126
|
-
const cookieHeader = existingCookies.map(c => `${c.key}=${c.value}`).join('; ');
|
|
127
|
-
const headers = { ...request.headers };
|
|
128
|
-
if (cookieHeader) {
|
|
129
|
-
headers['Cookie'] = cookieHeader;
|
|
130
|
-
}
|
|
131
|
-
// Determine how to send the body based on Content-Type
|
|
132
|
-
let data = undefined;
|
|
133
|
-
if (currentBody) {
|
|
134
|
-
const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type');
|
|
135
|
-
const contentTypeValue = contentType ? headers[contentType] : '';
|
|
136
|
-
if (contentTypeValue.includes('application/x-www-form-urlencoded')) {
|
|
137
|
-
const lines = currentBody.split('\n').map(line => line.trim()).filter(line => line);
|
|
138
|
-
const params = lines.map(line => {
|
|
139
|
-
if (line.includes('=')) {
|
|
140
|
-
const eqIndex = line.indexOf('=');
|
|
141
|
-
const key = line.substring(0, eqIndex).trim();
|
|
142
|
-
const value = line.substring(eqIndex + 1).trim();
|
|
143
|
-
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
144
|
-
}
|
|
145
|
-
const colonIndex = line.indexOf(':');
|
|
146
|
-
if (colonIndex > 0) {
|
|
147
|
-
const key = line.substring(0, colonIndex).trim();
|
|
148
|
-
const value = line.substring(colonIndex + 1).trim();
|
|
149
|
-
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
150
|
-
}
|
|
151
|
-
return encodeURIComponent(line);
|
|
152
|
-
});
|
|
153
|
-
data = params.join('&');
|
|
154
|
-
}
|
|
155
|
-
else if (contentTypeValue.includes('application/json')) {
|
|
156
|
-
try {
|
|
157
|
-
data = JSON.parse(currentBody);
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
data = currentBody;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
try {
|
|
165
|
-
data = JSON.parse(currentBody);
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
data = currentBody;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
// Capture what we're actually sending (for debugging)
|
|
173
|
-
sentRequestBody = data;
|
|
174
|
-
sentRequestHeaders = { ...headers };
|
|
175
|
-
const response = await (0, axios_1.default)({
|
|
176
|
-
method: currentMethod,
|
|
177
|
-
url: currentUrl,
|
|
178
|
-
headers,
|
|
179
|
-
data,
|
|
180
|
-
timeout: 30000,
|
|
181
|
-
maxRedirects: 0,
|
|
182
|
-
validateStatus: () => true,
|
|
183
|
-
});
|
|
184
|
-
// Store any Set-Cookie headers in the jar
|
|
185
|
-
const setCookieHeader = response.headers['set-cookie'];
|
|
186
|
-
if (setCookieHeader) {
|
|
187
|
-
const cookies = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
|
188
|
-
for (const cookieStr of cookies) {
|
|
189
|
-
try {
|
|
190
|
-
await jar.setCookie(cookieStr, currentUrl);
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
// Ignore invalid cookies
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
// Check if this is a redirect
|
|
198
|
-
if (response.status >= 300 && response.status < 400 && response.headers['location']) {
|
|
199
|
-
redirectCount++;
|
|
200
|
-
const location = response.headers['location'];
|
|
201
|
-
currentUrl = new URL(location, currentUrl).toString();
|
|
202
|
-
if (currentMethod !== 'GET' && currentMethod !== 'HEAD') {
|
|
203
|
-
currentMethod = 'GET';
|
|
204
|
-
currentBody = undefined;
|
|
205
|
-
}
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
finalResponse = response;
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
if (!finalResponse) {
|
|
212
|
-
throw new Error('Too many redirects');
|
|
213
|
-
}
|
|
214
|
-
// Check if we should retry on this response status
|
|
215
|
-
if (shouldRetry(finalResponse, null) && attempt < totalAttempts) {
|
|
216
|
-
const waitMs = backoffMs * attempt; // Linear backoff
|
|
217
|
-
const errorMessage = `HTTP ${finalResponse.status} ${finalResponse.statusText}`;
|
|
218
|
-
retriedErrors.push(`Attempt ${attempt}: ${errorMessage}`);
|
|
219
|
-
// Call retry callback if provided
|
|
220
|
-
if (retryOptions?.onRetry) {
|
|
221
|
-
retryOptions.onRetry(attempt, totalAttempts, errorMessage, waitMs);
|
|
222
|
-
}
|
|
223
|
-
await sleep(waitMs);
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
// Get ALL cookies in the jar
|
|
227
|
-
const cookies = await getAllCookies(jar);
|
|
228
|
-
const result = {
|
|
229
|
-
status: finalResponse.status,
|
|
230
|
-
statusText: finalResponse.statusText,
|
|
231
|
-
headers: finalResponse.headers,
|
|
232
|
-
body: finalResponse.data,
|
|
233
|
-
duration: Date.now() - startTime,
|
|
234
|
-
cookies,
|
|
235
|
-
requestBody: sentRequestBody,
|
|
236
|
-
requestHeaders: sentRequestHeaders,
|
|
237
|
-
};
|
|
238
|
-
// Add retry info if we made retries
|
|
239
|
-
if (attemptsMade > 1 || totalAttempts > 1) {
|
|
240
|
-
result.retryInfo = {
|
|
241
|
-
attemptsMade,
|
|
242
|
-
totalAttempts,
|
|
243
|
-
retriedErrors,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
return result;
|
|
247
|
-
}
|
|
248
|
-
catch (error) {
|
|
249
|
-
const errorMessage = error.message || String(error);
|
|
250
|
-
lastError = new Error(`${errorMessage}\nURL used: ${currentUrl || request.url}`);
|
|
251
|
-
// If we have more attempts, wait and retry
|
|
252
|
-
if (attempt < totalAttempts) {
|
|
253
|
-
const waitMs = backoffMs * attempt; // Linear backoff
|
|
254
|
-
retriedErrors.push(`Attempt ${attempt}: ${errorMessage}`);
|
|
255
|
-
// Call retry callback if provided
|
|
256
|
-
if (retryOptions?.onRetry) {
|
|
257
|
-
retryOptions.onRetry(attempt, totalAttempts, errorMessage, waitMs);
|
|
258
|
-
}
|
|
259
|
-
await sleep(waitMs);
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
// No more retries, throw the error
|
|
263
|
-
throw lastError;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
// Should never reach here, but TypeScript needs this
|
|
267
|
-
throw lastError || new Error('Request failed');
|
|
268
|
-
}
|
|
269
|
-
//# sourceMappingURL=httpClient.js.map
|
package/out/jsonFileReader.js
DELETED
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.JsonVariableStore = void 0;
|
|
37
|
-
exports.isJsonCommand = isJsonCommand;
|
|
38
|
-
exports.parseJsonCommand = parseJsonCommand;
|
|
39
|
-
exports.readJsonFile = readJsonFile;
|
|
40
|
-
exports.getNestedValue = getNestedValue;
|
|
41
|
-
exports.setNestedValue = setNestedValue;
|
|
42
|
-
exports.isPropertyAssignment = isPropertyAssignment;
|
|
43
|
-
exports.parsePropertyAssignment = parsePropertyAssignment;
|
|
44
|
-
exports.valueToString = valueToString;
|
|
45
|
-
exports.substituteVariablesWithJson = substituteVariablesWithJson;
|
|
46
|
-
const fs = __importStar(require("fs"));
|
|
47
|
-
const path = __importStar(require("path"));
|
|
48
|
-
/**
|
|
49
|
-
* Checks if a line is a JSON file command.
|
|
50
|
-
* Formats:
|
|
51
|
-
* var name = run readJson ./path/to/file.json
|
|
52
|
-
* var name = run readJson "/path with spaces/file.json"
|
|
53
|
-
*/
|
|
54
|
-
function isJsonCommand(line) {
|
|
55
|
-
const trimmed = line.trim();
|
|
56
|
-
return /^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+readJson\s+/i.test(trimmed);
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Parses a JSON file command line.
|
|
60
|
-
* Format: var <name> = run readJson <path>
|
|
61
|
-
*/
|
|
62
|
-
function parseJsonCommand(line) {
|
|
63
|
-
const trimmed = line.trim();
|
|
64
|
-
// Match: var varName = run readJson path
|
|
65
|
-
const match = trimmed.match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*run\s+readJson\s+(.+)$/i);
|
|
66
|
-
if (!match) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
let filePath = match[2].trim();
|
|
70
|
-
// Remove quotes if present
|
|
71
|
-
if ((filePath.startsWith('"') && filePath.endsWith('"')) ||
|
|
72
|
-
(filePath.startsWith("'") && filePath.endsWith("'"))) {
|
|
73
|
-
filePath = filePath.slice(1, -1);
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
varName: match[1],
|
|
77
|
-
filePath
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Reads and parses a JSON file.
|
|
82
|
-
*/
|
|
83
|
-
function readJsonFile(filePath, workingDir) {
|
|
84
|
-
// Resolve relative paths
|
|
85
|
-
const resolvedPath = path.isAbsolute(filePath)
|
|
86
|
-
? filePath
|
|
87
|
-
: path.resolve(workingDir || process.cwd(), filePath);
|
|
88
|
-
try {
|
|
89
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
90
|
-
return {
|
|
91
|
-
success: false,
|
|
92
|
-
error: `File not found: ${resolvedPath}`,
|
|
93
|
-
filePath: resolvedPath
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
97
|
-
const data = JSON.parse(content);
|
|
98
|
-
return {
|
|
99
|
-
success: true,
|
|
100
|
-
data,
|
|
101
|
-
filePath: resolvedPath
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
-
return {
|
|
107
|
-
success: false,
|
|
108
|
-
error: `Failed to read/parse JSON file: ${message}`,
|
|
109
|
-
filePath: resolvedPath
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Gets a value from an object using a dot-notation path.
|
|
115
|
-
* Supports array indexing with brackets: obj.array[0].property
|
|
116
|
-
*
|
|
117
|
-
* @param obj The source object
|
|
118
|
-
* @param path The dot-notation path (e.g., "user.addresses[0].city")
|
|
119
|
-
* @returns The value at the path, or undefined if not found
|
|
120
|
-
*/
|
|
121
|
-
function getNestedValue(obj, path) {
|
|
122
|
-
if (!path || obj === null || obj === undefined) {
|
|
123
|
-
return obj;
|
|
124
|
-
}
|
|
125
|
-
// Convert [0] to .0 and split by dots
|
|
126
|
-
const parts = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(p => p !== '');
|
|
127
|
-
let current = obj;
|
|
128
|
-
for (const part of parts) {
|
|
129
|
-
if (current === null || current === undefined) {
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
current = current[part];
|
|
133
|
-
}
|
|
134
|
-
return current;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Sets a value in an object using a dot-notation path.
|
|
138
|
-
* Creates intermediate objects/arrays as needed.
|
|
139
|
-
* Supports array indexing with brackets: obj.array[0].property
|
|
140
|
-
*
|
|
141
|
-
* @param obj The source object to modify
|
|
142
|
-
* @param path The dot-notation path (e.g., "user.addresses[0].city")
|
|
143
|
-
* @param value The value to set
|
|
144
|
-
* @returns true if successful, false if the path is invalid
|
|
145
|
-
*/
|
|
146
|
-
function setNestedValue(obj, path, value) {
|
|
147
|
-
if (!path || obj === null || obj === undefined || typeof obj !== 'object') {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
// Convert [0] to .0 and split by dots
|
|
151
|
-
const parts = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(p => p !== '');
|
|
152
|
-
if (parts.length === 0) {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
let current = obj;
|
|
156
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
157
|
-
const part = parts[i];
|
|
158
|
-
const nextPart = parts[i + 1];
|
|
159
|
-
if (current[part] === undefined || current[part] === null) {
|
|
160
|
-
// Create intermediate object or array based on next part
|
|
161
|
-
current[part] = /^\d+$/.test(nextPart) ? [] : {};
|
|
162
|
-
}
|
|
163
|
-
current = current[part];
|
|
164
|
-
if (typeof current !== 'object') {
|
|
165
|
-
return false; // Can't navigate further
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const lastPart = parts[parts.length - 1];
|
|
169
|
-
current[lastPart] = value;
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Checks if a line is a property assignment command.
|
|
174
|
-
* Format: varName.property.path = value
|
|
175
|
-
* varName[0].property = value
|
|
176
|
-
*/
|
|
177
|
-
function isPropertyAssignment(line) {
|
|
178
|
-
const trimmed = line.trim();
|
|
179
|
-
// Match: identifier followed by . or [ then = something
|
|
180
|
-
// But NOT starting with 'var ' or other keywords
|
|
181
|
-
return /^[a-zA-Z_][a-zA-Z0-9_]*[\.\[]/.test(trimmed) &&
|
|
182
|
-
/=/.test(trimmed) &&
|
|
183
|
-
!trimmed.startsWith('var ') &&
|
|
184
|
-
!trimmed.startsWith('run ') &&
|
|
185
|
-
!trimmed.startsWith('print ') &&
|
|
186
|
-
!trimmed.startsWith('assert ') &&
|
|
187
|
-
!trimmed.startsWith('sequence ');
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Parses a property assignment command.
|
|
191
|
-
* Format: varName.path.to.property = value
|
|
192
|
-
* Returns the variable name, the property path, and the value.
|
|
193
|
-
*/
|
|
194
|
-
function parsePropertyAssignment(line) {
|
|
195
|
-
const trimmed = line.trim();
|
|
196
|
-
// Match: varName.path = value OR varName[0].path = value
|
|
197
|
-
const match = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])+)\s*=\s*(.+)$/);
|
|
198
|
-
if (!match) {
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
const varName = match[1];
|
|
202
|
-
let propertyPath = match[2];
|
|
203
|
-
const value = match[3].trim();
|
|
204
|
-
// Remove leading dot if present
|
|
205
|
-
if (propertyPath.startsWith('.')) {
|
|
206
|
-
propertyPath = propertyPath.substring(1);
|
|
207
|
-
}
|
|
208
|
-
return {
|
|
209
|
-
varName,
|
|
210
|
-
propertyPath,
|
|
211
|
-
value
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Converts a value to a string for use in variable substitution.
|
|
216
|
-
*/
|
|
217
|
-
function valueToString(value) {
|
|
218
|
-
if (value === null) {
|
|
219
|
-
return 'null';
|
|
220
|
-
}
|
|
221
|
-
if (value === undefined) {
|
|
222
|
-
return '';
|
|
223
|
-
}
|
|
224
|
-
if (typeof value === 'object') {
|
|
225
|
-
return JSON.stringify(value);
|
|
226
|
-
}
|
|
227
|
-
return String(value);
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Storage for JSON objects loaded during sequence execution.
|
|
231
|
-
* Maps variable names to their parsed JSON data.
|
|
232
|
-
*/
|
|
233
|
-
class JsonVariableStore {
|
|
234
|
-
jsonObjects = new Map();
|
|
235
|
-
/**
|
|
236
|
-
* Stores a JSON object under the given variable name.
|
|
237
|
-
*/
|
|
238
|
-
set(varName, data) {
|
|
239
|
-
this.jsonObjects.set(varName, data);
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Gets the raw JSON object for a variable.
|
|
243
|
-
*/
|
|
244
|
-
get(varName) {
|
|
245
|
-
return this.jsonObjects.get(varName);
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Checks if a variable is a JSON object.
|
|
249
|
-
*/
|
|
250
|
-
has(varName) {
|
|
251
|
-
return this.jsonObjects.has(varName);
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Gets a nested value from a JSON variable.
|
|
255
|
-
* @param varName The variable name
|
|
256
|
-
* @param path Optional dot-notation path within the object
|
|
257
|
-
*/
|
|
258
|
-
getValue(varName, path) {
|
|
259
|
-
const obj = this.jsonObjects.get(varName);
|
|
260
|
-
if (obj === undefined) {
|
|
261
|
-
return undefined;
|
|
262
|
-
}
|
|
263
|
-
if (!path) {
|
|
264
|
-
return obj;
|
|
265
|
-
}
|
|
266
|
-
return getNestedValue(obj, path);
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Clears all stored JSON objects.
|
|
270
|
-
*/
|
|
271
|
-
clear() {
|
|
272
|
-
this.jsonObjects.clear();
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Gets all variable names that have JSON objects stored.
|
|
276
|
-
*/
|
|
277
|
-
getVariableNames() {
|
|
278
|
-
return Array.from(this.jsonObjects.keys());
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
exports.JsonVariableStore = JsonVariableStore;
|
|
282
|
-
/**
|
|
283
|
-
* Substitutes variables in text, with support for JSON object property access.
|
|
284
|
-
* Handles both simple variables {{varName}} and nested access {{varName.property.path}}
|
|
285
|
-
*
|
|
286
|
-
* @param text The text containing variable references
|
|
287
|
-
* @param variables Simple string variables (name -> value)
|
|
288
|
-
* @param jsonStore Optional JSON variable store for complex objects
|
|
289
|
-
*/
|
|
290
|
-
function substituteVariablesWithJson(text, variables, jsonStore) {
|
|
291
|
-
// Match {{varName}} or {{varName.path.to.property}} or {{varName[0].property}}
|
|
292
|
-
return text.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)\}\}/g, (match, varName, pathPart) => {
|
|
293
|
-
// First check if it's a JSON object with a path
|
|
294
|
-
if (jsonStore && jsonStore.has(varName)) {
|
|
295
|
-
const path = pathPart ? pathPart.replace(/^\./, '') : '';
|
|
296
|
-
const value = jsonStore.getValue(varName, path);
|
|
297
|
-
return valueToString(value);
|
|
298
|
-
}
|
|
299
|
-
// Check if there's a path and we have a simple variable that might be JSON
|
|
300
|
-
if (pathPart && varName in variables) {
|
|
301
|
-
// Try to parse the variable value as JSON
|
|
302
|
-
try {
|
|
303
|
-
const parsed = JSON.parse(variables[varName]);
|
|
304
|
-
const path = pathPart.replace(/^\./, '');
|
|
305
|
-
const value = getNestedValue(parsed, path);
|
|
306
|
-
return valueToString(value);
|
|
307
|
-
}
|
|
308
|
-
catch {
|
|
309
|
-
// Not JSON, fall through to simple substitution
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
// Simple variable substitution
|
|
313
|
-
if (varName in variables) {
|
|
314
|
-
return variables[varName];
|
|
315
|
-
}
|
|
316
|
-
// Variable not found, return original
|
|
317
|
-
return match;
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
//# sourceMappingURL=jsonFileReader.js.map
|