norn-cli 1.3.17 → 1.3.18

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.
@@ -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