This is page 2 of 2. Use http://codebase.md/djkz/bruno-api-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitignore ├── babel.config.js ├── coverage │ ├── clover.xml │ ├── coverage-final.json │ ├── lcov-report │ │ ├── base.css │ │ ├── block-navigation.js │ │ ├── favicon.png │ │ ├── index.html │ │ ├── prettify.css │ │ ├── prettify.js │ │ ├── sort-arrow-sprite.png │ │ └── sorter.js │ └── lcov.info ├── examples │ └── oauth2-integration.ts ├── jest.config.cjs ├── package.json ├── README.md ├── src │ ├── auth │ │ ├── adapter.ts │ │ ├── factory.ts │ │ ├── handlers │ │ │ ├── apikey.ts │ │ │ ├── basic.ts │ │ │ ├── bearer.ts │ │ │ └── oauth2.ts │ │ ├── index.ts │ │ ├── integration.ts │ │ ├── service.ts │ │ ├── token-manager.ts │ │ └── types.ts │ ├── bruno-lang │ │ ├── brulang.d.ts │ │ ├── brulang.js │ │ ├── bruToJson.d.ts │ │ ├── bruToJson.js │ │ ├── collectionBruToJson.d.ts │ │ ├── collectionBruToJson.js │ │ ├── dotenvToJson.js │ │ ├── envToJson.d.ts │ │ └── envToJson.js │ ├── bruno-parser.ts │ ├── bruno-tools.ts │ ├── bruno-utils.ts │ ├── index.ts │ ├── request-executor.ts │ ├── types │ │ └── bru-js.d.ts │ ├── types.d.ts │ └── types.ts ├── test │ ├── auth-module.test.ts │ ├── bruno-collection.test.ts │ ├── bruno-env.test.ts │ ├── bruno-params-docs.test.ts │ ├── bruno-parser-auth.test.ts │ ├── bruno-request.test.ts │ ├── bruno-tools-integration.test.ts │ ├── bruno-tools.test.ts │ ├── defaults.spec.ts │ ├── fixtures │ │ ├── collection.bru │ │ ├── collection2.bru │ │ ├── deal.bru │ │ ├── deals-list.bru │ │ ├── direct-auth.bru │ │ ├── environments │ │ │ ├── dev.bru │ │ │ ├── local.bru │ │ │ └── remote.bru │ │ ├── json │ │ │ ├── collection.json │ │ │ └── self-company.json │ │ ├── self-company.bru │ │ ├── user.bru │ │ └── V2-deals-show.bru │ ├── oauth2-auth.test.ts │ ├── parser.test.ts │ ├── request-executor.test.ts │ └── token-manager.test.ts ├── tsconfig.json └── tsconfig.test.json ``` # Files -------------------------------------------------------------------------------- /src/bruno-parser.ts: -------------------------------------------------------------------------------- ```typescript 1 | import fs from "fs-extra"; 2 | import * as path from "path"; 3 | import axios from "axios"; 4 | import debug from "debug"; 5 | import { 6 | bruToJson, 7 | envToJson, 8 | collectionBruToJson, 9 | } from "./bruno-lang/brulang.js"; 10 | import { applyAuthToParsedRequest } from "./auth/index.js"; 11 | 12 | const log = debug("bruno-parser"); 13 | const debugReq = debug("bruno-request"); 14 | 15 | // Match {{baseUrl}} or any other template variable {{varName}} 16 | const TEMPLATE_VAR_REGEX = /{{([^}]+)}}/g; 17 | 18 | interface BrunoResponse { 19 | status: number; 20 | headers: any; 21 | data: any; 22 | isJson?: boolean; 23 | error?: boolean; 24 | } 25 | 26 | export interface ParsedRequest { 27 | name: string; 28 | method: string; 29 | url: string; 30 | rawRequest: any; 31 | headers: Record<string, string>; 32 | queryParams: Record<string, string>; 33 | body?: { 34 | type: string; 35 | content: any; 36 | }; 37 | filePath?: string; 38 | } 39 | 40 | export interface EnvironmentData { 41 | name: string; 42 | variables: Record<string, string>; 43 | rawData: any; 44 | } 45 | 46 | export class BrunoParser { 47 | collectionPath: string; 48 | basePath: string; 49 | envVars: Record<string, string> = {}; 50 | environment?: string; 51 | availableEnvironments: Map<string, EnvironmentData> = new Map(); 52 | parsedRequests: Map<string, any> = new Map(); 53 | parsedCollection: any = null; 54 | 55 | constructor(collectionPath: string, environment?: string) { 56 | this.collectionPath = collectionPath; 57 | this.basePath = path.dirname(collectionPath); 58 | this.environment = environment; 59 | } 60 | 61 | async init() { 62 | // Check if the collection path exists 63 | try { 64 | await fs.access(this.collectionPath); 65 | } catch (error: unknown) { 66 | throw new Error(`Collection path does not exist: ${this.collectionPath}`); 67 | } 68 | 69 | try { 70 | // Load all available environments 71 | await this.loadAllEnvironments(); 72 | 73 | // Load the collection 74 | try { 75 | this.parsedCollection = await this.parseCollection(); 76 | } catch (error) { 77 | log(`Error parsing collection: ${error}`); 78 | this.parsedCollection = { 79 | meta: { name: "collection", type: "collection" }, 80 | }; 81 | } 82 | 83 | // Load all request files 84 | await this.loadAllRequests(); 85 | 86 | // Set the active environment if specified 87 | if (this.environment) { 88 | this.setEnvironment(this.environment); 89 | } 90 | } catch (error: unknown) { 91 | log(`Error during parser initialization: ${error}`); 92 | throw error; 93 | } 94 | } 95 | 96 | async loadAllEnvironments() { 97 | const envPath = path.join(this.basePath, "environments"); 98 | 99 | try { 100 | // Check if the environments directory exists 101 | if (await fs.pathExists(envPath)) { 102 | const files = await fs.readdir(envPath); 103 | const envFiles = files.filter( 104 | (file) => file.endsWith(".env") || file.endsWith(".bru") 105 | ); 106 | 107 | // Load all environment files 108 | for (const envFile of envFiles) { 109 | const envName = path.basename( 110 | envFile, 111 | envFile.endsWith(".bru") ? ".bru" : ".env" 112 | ); 113 | const envFilePath = path.join(envPath, envFile); 114 | const envContent = await fs.readFile(envFilePath, "utf-8"); 115 | 116 | try { 117 | const envData = envToJson(envContent); 118 | const variables: Record<string, string> = {}; 119 | 120 | // Extract variables to our simplified format 121 | if (envData) { 122 | if (envData.vars) { 123 | // Legacy .env format 124 | Object.entries(envData.vars).forEach(([name, value]) => { 125 | variables[name] = String(value); 126 | }); 127 | } else if (envData.variables) { 128 | // New .bru format 129 | envData.variables.forEach((variable: any) => { 130 | if (variable.enabled && variable.name) { 131 | variables[variable.name] = variable.value || ""; 132 | } 133 | }); 134 | } 135 | } 136 | 137 | // Store the environment data 138 | this.availableEnvironments.set(envName, { 139 | name: envName, 140 | variables, 141 | rawData: envData, 142 | }); 143 | log(`Environment loaded: ${envName}`); 144 | 145 | // If this is the first environment and no specific one was requested, 146 | // set it as the default 147 | if (!this.environment && this.availableEnvironments.size === 1) { 148 | this.environment = envName; 149 | this.envVars = { ...variables }; 150 | log(`Set default environment: ${envName}`); 151 | } 152 | } catch (error: unknown) { 153 | const errorMessage = 154 | error instanceof Error ? error.message : String(error); 155 | log( 156 | `Error parsing environment file ${envFilePath}: ${errorMessage}` 157 | ); 158 | } 159 | } 160 | 161 | log( 162 | "Available environments:", 163 | Array.from(this.availableEnvironments.keys()) 164 | ); 165 | log("Current environment variables:", this.envVars); 166 | } 167 | } catch (error: unknown) { 168 | const errorMessage = 169 | error instanceof Error ? error.message : String(error); 170 | log(`Error loading environments: ${errorMessage}`); 171 | } 172 | } 173 | 174 | setEnvironment(envName: string): boolean { 175 | const env = this.availableEnvironments.get(envName); 176 | if (env) { 177 | this.environment = envName; 178 | this.envVars = { ...env.variables }; 179 | log(`Environment set to: ${envName}`); 180 | return true; 181 | } 182 | log(`Environment not found: ${envName}`); 183 | return false; 184 | } 185 | 186 | getAvailableEnvironments(): string[] { 187 | return Array.from(this.availableEnvironments.keys()); 188 | } 189 | 190 | getEnvironment(envName: string): EnvironmentData | undefined { 191 | return this.availableEnvironments.get(envName); 192 | } 193 | 194 | getCurrentEnvironment(): EnvironmentData | undefined { 195 | return this.environment 196 | ? this.availableEnvironments.get(this.environment) 197 | : undefined; 198 | } 199 | 200 | async loadAllRequests() { 201 | try { 202 | log(`Loading request files from ${this.basePath}`); 203 | const files = await fs.readdir(this.basePath); 204 | log(`Found ${files.length} files in directory:`, files); 205 | 206 | const requestFiles = files.filter( 207 | (file) => 208 | file.endsWith(".bru") && 209 | file !== path.basename(this.collectionPath) && 210 | !file.includes("env") 211 | ); 212 | 213 | log(`Filtered request files: ${requestFiles.length}`, requestFiles); 214 | 215 | for (const file of requestFiles) { 216 | const requestPath = path.join(this.basePath, file); 217 | try { 218 | log(`Loading request from ${requestPath}`); 219 | const content = await fs.readFile(requestPath, "utf-8"); 220 | const parsed = bruToJson(content); 221 | const requestName = path.basename(file, ".bru"); 222 | this.parsedRequests.set(requestName, parsed); 223 | log(`Request loaded: ${requestName}`); 224 | } catch (error: unknown) { 225 | const errorMessage = 226 | error instanceof Error ? error.message : String(error); 227 | log(`Error parsing request file ${file}: ${errorMessage}`); 228 | } 229 | } 230 | log(`Loaded ${this.parsedRequests.size} requests`); 231 | } catch (error: unknown) { 232 | const errorMessage = 233 | error instanceof Error ? error.message : String(error); 234 | log(`Error loading request files: ${errorMessage}`); 235 | } 236 | } 237 | 238 | getAvailableRequests(): string[] { 239 | return Array.from(this.parsedRequests.keys()); 240 | } 241 | 242 | getRawRequest(requestName: string): any | undefined { 243 | return this.parsedRequests.get(requestName); 244 | } 245 | 246 | async parseCollection(): Promise<any> { 247 | try { 248 | const content = await fs.readFile(this.collectionPath, "utf-8"); 249 | return collectionBruToJson(content); 250 | } catch (error: unknown) { 251 | const errorMessage = 252 | error instanceof Error ? error.message : String(error); 253 | log(`Error parsing collection file: ${errorMessage}`); 254 | throw error; 255 | } 256 | } 257 | 258 | getCollection(): any { 259 | return this.parsedCollection; 260 | } 261 | 262 | async parseRequest(requestInput: string): Promise<ParsedRequest> { 263 | let rawRequest; 264 | let requestName; 265 | let filePath = requestInput; 266 | 267 | // If the input is a name and not a path, get the request from loaded requests 268 | if (!requestInput.includes(path.sep) && !requestInput.endsWith(".bru")) { 269 | requestName = requestInput; 270 | rawRequest = this.getRawRequest(requestName); 271 | if (!rawRequest) { 272 | throw new Error(`Request not found: ${requestName}`); 273 | } 274 | } else { 275 | // Input is a file path 276 | requestName = path.basename(requestInput, ".bru"); 277 | try { 278 | const content = await fs.readFile(filePath, "utf-8"); 279 | rawRequest = bruToJson(content); 280 | } catch (error: unknown) { 281 | const errorMessage = 282 | error instanceof Error ? error.message : String(error); 283 | throw new Error( 284 | `Error parsing request file ${filePath}: ${errorMessage}` 285 | ); 286 | } 287 | } 288 | 289 | // Extract HTTP method and URL 290 | let method = "GET"; 291 | let url = ""; 292 | 293 | if (rawRequest.http && rawRequest.http.method) { 294 | method = rawRequest.http.method.toUpperCase(); 295 | } 296 | 297 | if (rawRequest.http && rawRequest.http.url) { 298 | // Store the original URL without processing variables 299 | url = rawRequest.http.url; 300 | } 301 | 302 | // Parse headers 303 | const headers: Record<string, string> = {}; 304 | 305 | // Handle auth inheritance 306 | if ( 307 | rawRequest.http && 308 | rawRequest.http.auth === "inherit" && 309 | this.parsedCollection 310 | ) { 311 | const collectionAuth = this.parsedCollection.auth; 312 | if (collectionAuth && collectionAuth.mode === "apikey") { 313 | const apiKeyAuth = collectionAuth.apikey; 314 | if ( 315 | apiKeyAuth && 316 | (!apiKeyAuth.addTo || apiKeyAuth.addTo === "header") 317 | ) { 318 | headers[apiKeyAuth.key] = this.processTemplateVariables( 319 | apiKeyAuth.value || "" 320 | ); 321 | } 322 | } 323 | } 324 | 325 | // Parse request-specific headers from headers section 326 | if (rawRequest.headers) { 327 | for (const header of rawRequest.headers) { 328 | if (header.enabled !== false && header.name) { 329 | headers[header.name] = this.processTemplateVariables( 330 | header.value || "" 331 | ); 332 | } 333 | } 334 | } 335 | 336 | // Parse request-specific headers from http.headers (for backward compatibility) 337 | if (rawRequest.http && rawRequest.http.headers) { 338 | for (const header of rawRequest.http.headers) { 339 | if (header.enabled !== false && header.name) { 340 | headers[header.name] = this.processTemplateVariables( 341 | header.value || "" 342 | ); 343 | } 344 | } 345 | } 346 | 347 | // Parse query parameters 348 | const queryParams: Record<string, string> = {}; 349 | 350 | // Parse from params:query section (new format) 351 | if (rawRequest.params) { 352 | // Check if params is an array (from paramsquery handler) 353 | if (Array.isArray(rawRequest.params)) { 354 | // Find query parameters in params array 355 | const queryParamsArray = rawRequest.params.filter( 356 | (param: any) => param.type === "query" 357 | ); 358 | for (const param of queryParamsArray) { 359 | if (param.enabled !== false && param.name) { 360 | queryParams[param.name] = this.processTemplateVariables( 361 | param.value || "" 362 | ); 363 | } 364 | } 365 | } else if (rawRequest.params.query) { 366 | // Handle legacy structure 367 | if (Array.isArray(rawRequest.params.query)) { 368 | for (const param of rawRequest.params.query) { 369 | if (param.enabled !== false && param.name) { 370 | queryParams[param.name] = this.processTemplateVariables( 371 | param.value || "" 372 | ); 373 | } 374 | } 375 | } else if (typeof rawRequest.params.query === "object") { 376 | Object.entries(rawRequest.params.query).forEach(([name, value]) => { 377 | queryParams[name] = this.processTemplateVariables(String(value)); 378 | }); 379 | } 380 | } 381 | } 382 | 383 | // Parse from http.query section (backward compatibility) 384 | if (rawRequest.http && rawRequest.http.query) { 385 | for (const param of rawRequest.http.query) { 386 | if (param.enabled !== false && param.name) { 387 | queryParams[param.name] = this.processTemplateVariables( 388 | param.value || "" 389 | ); 390 | } 391 | } 392 | } 393 | 394 | // Handle query parameter auth 395 | if ( 396 | rawRequest.http && 397 | rawRequest.http.auth === "inherit" && 398 | this.parsedCollection 399 | ) { 400 | const collectionAuth = this.parsedCollection.auth; 401 | if (collectionAuth && collectionAuth.mode === "apikey") { 402 | const apiKeyAuth = collectionAuth.apikey; 403 | if (apiKeyAuth && apiKeyAuth.addTo === "queryParams") { 404 | queryParams[apiKeyAuth.key] = this.processTemplateVariables( 405 | apiKeyAuth.value || "" 406 | ); 407 | log( 408 | `Added auth query param: ${apiKeyAuth.key}=${ 409 | queryParams[apiKeyAuth.key] 410 | }` 411 | ); 412 | } 413 | } 414 | } 415 | 416 | // Parse body content 417 | let body; 418 | if (rawRequest.http && rawRequest.http.body) { 419 | const bodyContent = rawRequest.http.body; 420 | const bodyMode = bodyContent.mode || "json"; 421 | 422 | // Process body content based on mode 423 | if (bodyMode === "json" && bodyContent.json) { 424 | try { 425 | // If it's a string, try to parse it as JSON 426 | let processedContent = this.processTemplateVariables( 427 | bodyContent.json 428 | ); 429 | let jsonContent; 430 | 431 | try { 432 | jsonContent = JSON.parse(processedContent); 433 | } catch (e) { 434 | // If not valid JSON, use as is 435 | jsonContent = processedContent; 436 | } 437 | 438 | body = { 439 | type: "json", 440 | content: jsonContent, 441 | }; 442 | } catch (error: unknown) { 443 | const errorMessage = 444 | error instanceof Error ? error.message : String(error); 445 | log(`Error processing JSON body: ${errorMessage}`); 446 | body = { 447 | type: "json", 448 | content: bodyContent.json, 449 | }; 450 | } 451 | } else if (bodyMode === "text" && bodyContent.text) { 452 | body = { 453 | type: "text", 454 | content: this.processTemplateVariables(bodyContent.text), 455 | }; 456 | } else if (bodyMode === "form-urlencoded" && bodyContent.formUrlEncoded) { 457 | const formData: Record<string, string> = {}; 458 | for (const param of bodyContent.formUrlEncoded) { 459 | if (param.enabled !== false && param.name) { 460 | formData[param.name] = this.processTemplateVariables( 461 | param.value || "" 462 | ); 463 | } 464 | } 465 | body = { 466 | type: "form-urlencoded", 467 | content: formData, 468 | }; 469 | } else { 470 | // For other body types, store as is 471 | body = { 472 | type: bodyMode, 473 | content: bodyContent[bodyMode], 474 | }; 475 | } 476 | } 477 | 478 | return { 479 | name: requestName, 480 | method, 481 | url, 482 | rawRequest, 483 | headers, 484 | queryParams, 485 | body, 486 | filePath, 487 | }; 488 | } 489 | 490 | processTemplateVariables(input: string): string { 491 | if (!input || typeof input !== "string") { 492 | return input; 493 | } 494 | 495 | return input.replace( 496 | TEMPLATE_VAR_REGEX, 497 | (match: string, varName: string) => { 498 | const trimmedVarName = varName.trim(); 499 | return this.envVars[trimmedVarName] !== undefined 500 | ? this.envVars[trimmedVarName] 501 | : match; 502 | } 503 | ); 504 | } 505 | 506 | extractTemplateVariables(input: string): string[] { 507 | if (!input || typeof input !== "string") { 508 | return []; 509 | } 510 | 511 | const variables: string[] = []; 512 | let match; 513 | while ((match = TEMPLATE_VAR_REGEX.exec(input)) !== null) { 514 | variables.push(match[1].trim()); 515 | } 516 | return variables; 517 | } 518 | 519 | async executeRequest( 520 | parsedRequest: ParsedRequest, 521 | params: { 522 | variables?: Record<string, any>; 523 | query?: Record<string, string>; 524 | body?: any; 525 | } = {} 526 | ): Promise<BrunoResponse> { 527 | // Create a temporary copy of environment variables 528 | const originalEnvVars = { ...this.envVars }; 529 | console.log("originalEnvVars", originalEnvVars); 530 | 531 | try { 532 | const { method, body, queryParams, rawRequest } = parsedRequest; 533 | const { variables, query, ...requestParams } = params; 534 | 535 | // Apply any custom variables if provided 536 | if (variables && typeof variables === "object") { 537 | debugReq(`Applying temporary variables: ${JSON.stringify(variables)}`); 538 | // Temporarily override environment variables 539 | Object.entries(variables).forEach(([key, value]) => { 540 | this.envVars[key] = String(value); 541 | 542 | // If a variable matches a query parameter name, update the query parameter as well 543 | if (Object.prototype.hasOwnProperty.call(queryParams, key)) { 544 | queryParams[key] = String(value); 545 | } 546 | }); 547 | } 548 | 549 | // Get the original URL from rawRequest instead of using the pre-processed URL 550 | const originalUrl = rawRequest?.http?.url || parsedRequest.url; 551 | 552 | // Process template variables in the URL with current environment variables 553 | let finalUrl = this.processTemplateVariables(originalUrl); 554 | debugReq(`Final URL: ${finalUrl}`); 555 | 556 | // Add query parameters that are not already in the URL 557 | const urlObj = new URL(finalUrl); 558 | 559 | // Apply authentication using our new auth module 560 | const authResult = applyAuthToParsedRequest( 561 | rawRequest, 562 | this.parsedCollection, 563 | this.envVars 564 | ); 565 | 566 | // Merge any headers from auth with existing headers from parsedRequest 567 | const headers = { 568 | ...parsedRequest.headers, 569 | ...authResult.headers, 570 | }; 571 | 572 | // Apply parameters to query parameters 573 | if (queryParams) { 574 | Object.entries(requestParams).forEach(([key, value]) => { 575 | if (Object.prototype.hasOwnProperty.call(queryParams, key)) { 576 | queryParams[key] = String(value); 577 | } 578 | }); 579 | } 580 | 581 | // Add dedicated query parameters if provided 582 | if (query && typeof query === "object") { 583 | debugReq( 584 | `Applying dedicated query parameters: ${JSON.stringify(query)}` 585 | ); 586 | Object.entries(query).forEach(([key, value]) => { 587 | queryParams[key] = String(value); 588 | }); 589 | } 590 | 591 | // Add all query parameters to URL, including those from auth 592 | // First add existing query params from the request 593 | Object.entries(queryParams).forEach(([key, value]) => { 594 | urlObj.searchParams.set(key, value); 595 | }); 596 | 597 | // Then add auth query params if any 598 | if (authResult.queryParams) { 599 | Object.entries(authResult.queryParams).forEach(([key, value]) => { 600 | urlObj.searchParams.set(key, value); 601 | }); 602 | } 603 | 604 | finalUrl = urlObj.toString(); 605 | 606 | // Process body content with parameters if it's JSON 607 | let requestData = params.body; 608 | 609 | debugReq(`Executing ${method} request to ${finalUrl}`); 610 | debugReq(`Headers: ${JSON.stringify(headers)}`); 611 | if (requestData) { 612 | debugReq( 613 | `Body: ${ 614 | typeof requestData === "object" 615 | ? JSON.stringify(requestData) 616 | : requestData 617 | }` 618 | ); 619 | } 620 | 621 | // Send the request 622 | const response = await axios({ 623 | method, 624 | url: finalUrl, 625 | headers, 626 | data: requestData, 627 | validateStatus: () => true, // Don't throw on any status code 628 | }); 629 | 630 | // Log response status 631 | debugReq(`Response status: ${response.status}`); 632 | 633 | // Check if the response is JSON by examining the content-type header 634 | const contentType = response.headers["content-type"] || ""; 635 | const isJson = contentType.includes("application/json"); 636 | 637 | if (!isJson) { 638 | debugReq( 639 | `Warning: Response is not JSON (content-type: ${contentType})` 640 | ); 641 | } 642 | console.log("response.data", response.data); 643 | 644 | // Return structured response 645 | return { 646 | status: response.status, 647 | headers: response.headers, 648 | data: response.data, 649 | isJson, 650 | }; 651 | } catch (error: unknown) { 652 | const errorMessage = 653 | error instanceof Error ? error.message : String(error); 654 | debugReq(`Error executing request: ${errorMessage}`); 655 | return { 656 | status: 0, 657 | headers: {}, 658 | data: errorMessage, 659 | error: true, 660 | }; 661 | } finally { 662 | // Restore original environment variables 663 | this.envVars = originalEnvVars; 664 | } 665 | } 666 | 667 | hasTemplateVariable(url: string, varName: string): boolean { 668 | const templateVars = this.extractTemplateVariables(url); 669 | return templateVars.includes(varName); 670 | } 671 | } 672 | ``` -------------------------------------------------------------------------------- /src/bruno-lang/bruToJson.js: -------------------------------------------------------------------------------- ```javascript 1 | import ohm from "ohm-js"; 2 | import _ from "lodash"; 3 | import { outdentString } from "../bruno-utils.js"; 4 | 5 | /** 6 | * A Bru file is made up of blocks. 7 | * There are two types of blocks 8 | * 9 | * 1. Dictionary Blocks - These are blocks that have key value pairs 10 | * ex: 11 | * headers { 12 | * content-type: application/json 13 | * } 14 | * 15 | * 2. Text Blocks - These are blocks that have text 16 | * ex: 17 | * body:json { 18 | * { 19 | * "username": "John Nash", 20 | * "password": "governingdynamics 21 | * } 22 | * 23 | */ 24 | const grammar = ohm.grammar(`Bru { 25 | BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)* 26 | auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey 27 | bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body 28 | bodyforms = bodyformurlencoded | bodymultipart | bodyfile 29 | params = paramspath | paramsquery 30 | 31 | nl = "\\r"? "\\n" 32 | st = " " | "\\t" 33 | stnl = st | nl 34 | tagend = nl "}" 35 | optionalnl = ~tagend nl 36 | keychar = ~(tagend | st | nl | ":") any 37 | valuechar = ~(nl | tagend) any 38 | 39 | // Multiline text block surrounded by ''' 40 | multilinetextblockdelimiter = "'''" 41 | multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter 42 | 43 | // Dictionary Blocks 44 | dictionary = st* "{" pairlist? tagend 45 | pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)* 46 | pair = st* key st* ":" st* value st* 47 | key = keychar* 48 | value = multilinetextblock | valuechar* 49 | 50 | // Dictionary for Assert Block 51 | assertdictionary = st* "{" assertpairlist? tagend 52 | assertpairlist = optionalnl* assertpair (~tagend stnl* assertpair)* (~tagend space)* 53 | assertpair = st* assertkey st* ":" st* value st* 54 | assertkey = ~tagend assertkeychar* 55 | assertkeychar = ~(tagend | nl | ":") any 56 | 57 | // Text Blocks 58 | textblock = textline (~tagend nl textline)* 59 | textline = textchar* 60 | textchar = ~nl any 61 | 62 | meta = "meta" dictionary 63 | 64 | http = get | post | put | delete | patch | options | head | connect | trace 65 | get = "get" dictionary 66 | post = "post" dictionary 67 | put = "put" dictionary 68 | delete = "delete" dictionary 69 | patch = "patch" dictionary 70 | options = "options" dictionary 71 | head = "head" dictionary 72 | connect = "connect" dictionary 73 | trace = "trace" dictionary 74 | 75 | headers = "headers" dictionary 76 | 77 | query = "query" dictionary 78 | paramspath = "params:path" dictionary 79 | paramsquery = "params:query" dictionary 80 | 81 | varsandassert = varsreq | varsres | assert 82 | varsreq = "vars:pre-request" dictionary 83 | varsres = "vars:post-response" dictionary 84 | assert = "assert" assertdictionary 85 | 86 | authawsv4 = "auth:awsv4" dictionary 87 | authbasic = "auth:basic" dictionary 88 | authbearer = "auth:bearer" dictionary 89 | authdigest = "auth:digest" dictionary 90 | authNTLM = "auth:ntlm" dictionary 91 | authOAuth2 = "auth:oauth2" dictionary 92 | authwsse = "auth:wsse" dictionary 93 | authapikey = "auth:apikey" dictionary 94 | 95 | body = "body" st* "{" nl* textblock tagend 96 | bodyjson = "body:json" st* "{" nl* textblock tagend 97 | bodytext = "body:text" st* "{" nl* textblock tagend 98 | bodyxml = "body:xml" st* "{" nl* textblock tagend 99 | bodysparql = "body:sparql" st* "{" nl* textblock tagend 100 | bodygraphql = "body:graphql" st* "{" nl* textblock tagend 101 | bodygraphqlvars = "body:graphql:vars" st* "{" nl* textblock tagend 102 | 103 | bodyformurlencoded = "body:form-urlencoded" dictionary 104 | bodymultipart = "body:multipart-form" dictionary 105 | bodyfile = "body:file" dictionary 106 | 107 | script = scriptreq | scriptres 108 | scriptreq = "script:pre-request" st* "{" nl* textblock tagend 109 | scriptres = "script:post-response" st* "{" nl* textblock tagend 110 | tests = "tests" st* "{" nl* textblock tagend 111 | docs = "docs" st* "{" nl* textblock tagend 112 | }`); 113 | 114 | const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => { 115 | if (!pairList.length) { 116 | return []; 117 | } 118 | return _.map(pairList[0], (pair) => { 119 | let name = _.keys(pair)[0]; 120 | let value = pair[name]; 121 | 122 | if (!parseEnabled) { 123 | return { 124 | name, 125 | value, 126 | }; 127 | } 128 | 129 | let enabled = true; 130 | if (name && name.length && name.charAt(0) === "~") { 131 | name = name.slice(1); 132 | enabled = false; 133 | } 134 | 135 | return { 136 | name, 137 | value, 138 | enabled, 139 | }; 140 | }); 141 | }; 142 | 143 | const mapRequestParams = (pairList = [], type) => { 144 | if (!pairList.length) { 145 | return []; 146 | } 147 | return _.map(pairList[0], (pair) => { 148 | let name = _.keys(pair)[0]; 149 | let value = pair[name]; 150 | let enabled = true; 151 | if (name && name.length && name.charAt(0) === "~") { 152 | name = name.slice(1); 153 | enabled = false; 154 | } 155 | 156 | return { 157 | name, 158 | value, 159 | enabled, 160 | type, 161 | }; 162 | }); 163 | }; 164 | 165 | const multipartExtractContentType = (pair) => { 166 | if (_.isString(pair.value)) { 167 | const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/); 168 | if (match != null && match.length > 2) { 169 | pair.value = match[1]; 170 | pair.contentType = match[2]; 171 | } else { 172 | pair.contentType = ""; 173 | } 174 | } 175 | }; 176 | 177 | const fileExtractContentType = (pair) => { 178 | if (_.isString(pair.value)) { 179 | const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/); 180 | if (match && match.length > 2) { 181 | pair.value = match[1].trim(); 182 | pair.contentType = match[2].trim(); 183 | } else { 184 | pair.contentType = ""; 185 | } 186 | } 187 | }; 188 | 189 | const mapPairListToKeyValPairsMultipart = ( 190 | pairList = [], 191 | parseEnabled = true 192 | ) => { 193 | const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); 194 | 195 | return pairs.map((pair) => { 196 | pair.type = "text"; 197 | multipartExtractContentType(pair); 198 | 199 | if (pair.value.startsWith("@file(") && pair.value.endsWith(")")) { 200 | let filestr = pair.value.replace(/^@file\(/, "").replace(/\)$/, ""); 201 | pair.type = "file"; 202 | pair.value = filestr.split("|"); 203 | } 204 | 205 | return pair; 206 | }); 207 | }; 208 | 209 | const mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => { 210 | const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); 211 | return pairs.map((pair) => { 212 | fileExtractContentType(pair); 213 | 214 | if (pair.value.startsWith("@file(") && pair.value.endsWith(")")) { 215 | let filePath = pair.value.replace(/^@file\(/, "").replace(/\)$/, ""); 216 | pair.filePath = filePath; 217 | pair.selected = pair.enabled; 218 | 219 | // Remove pair.value as it only contains the file path reference 220 | delete pair.value; 221 | // Remove pair.name as it is auto-generated (e.g., file1, file2, file3, etc.) 222 | delete pair.name; 223 | delete pair.enabled; 224 | } 225 | 226 | return pair; 227 | }); 228 | }; 229 | 230 | const concatArrays = (objValue, srcValue) => { 231 | if (_.isArray(objValue) && _.isArray(srcValue)) { 232 | return objValue.concat(srcValue); 233 | } 234 | }; 235 | 236 | const mapPairListToKeyValPair = (pairList = []) => { 237 | if (!pairList || !pairList.length) { 238 | return {}; 239 | } 240 | 241 | return _.merge({}, ...pairList[0]); 242 | }; 243 | 244 | const sem = grammar.createSemantics().addAttribute("ast", { 245 | BruFile(tags) { 246 | if (!tags || !tags.ast || !tags.ast.length) { 247 | return {}; 248 | } 249 | 250 | return _.reduce( 251 | tags.ast, 252 | (result, item) => { 253 | return _.mergeWith(result, item, concatArrays); 254 | }, 255 | {} 256 | ); 257 | }, 258 | dictionary(_1, _2, pairlist, _3) { 259 | return pairlist.ast; 260 | }, 261 | pairlist(_1, pair, _2, rest, _3) { 262 | return [pair.ast, ...rest.ast]; 263 | }, 264 | pair(_1, key, _2, _3, _4, value, _5) { 265 | let res = {}; 266 | res[key.ast] = value.ast ? value.ast.trim() : ""; 267 | return res; 268 | }, 269 | key(chars) { 270 | return chars.sourceString ? chars.sourceString.trim() : ""; 271 | }, 272 | value(chars) { 273 | try { 274 | let isMultiline = 275 | chars.sourceString?.startsWith(`'''`) && 276 | chars.sourceString?.endsWith(`'''`); 277 | if (isMultiline) { 278 | const multilineString = chars.sourceString?.replace(/^'''|'''$/g, ""); 279 | return multilineString 280 | .split("\n") 281 | .map((line) => line.slice(4)) 282 | .join("\n"); 283 | } 284 | return chars.sourceString ? chars.sourceString.trim() : ""; 285 | } catch (err) { 286 | console.error(err); 287 | } 288 | return chars.sourceString ? chars.sourceString.trim() : ""; 289 | }, 290 | assertdictionary(_1, _2, pairlist, _3) { 291 | return pairlist.ast; 292 | }, 293 | assertpairlist(_1, pair, _2, rest, _3) { 294 | return [pair.ast, ...rest.ast]; 295 | }, 296 | assertpair(_1, key, _2, _3, _4, value, _5) { 297 | let res = {}; 298 | res[key.ast] = value.ast ? value.ast.trim() : ""; 299 | return res; 300 | }, 301 | assertkey(chars) { 302 | return chars.sourceString ? chars.sourceString.trim() : ""; 303 | }, 304 | textblock(line, _1, rest) { 305 | return [line.ast, ...rest.ast].join("\n"); 306 | }, 307 | textline(chars) { 308 | return chars.sourceString; 309 | }, 310 | textchar(char) { 311 | return char.sourceString; 312 | }, 313 | nl(_1, _2) { 314 | return ""; 315 | }, 316 | st(_) { 317 | return ""; 318 | }, 319 | tagend(_1, _2) { 320 | return ""; 321 | }, 322 | _iter(...elements) { 323 | return elements.map((e) => e.ast); 324 | }, 325 | meta(_1, dictionary) { 326 | let meta = mapPairListToKeyValPair(dictionary.ast); 327 | 328 | if (!meta.seq) { 329 | meta.seq = 1; 330 | } 331 | 332 | if (!meta.type) { 333 | meta.type = "http"; 334 | } 335 | 336 | return { 337 | meta, 338 | }; 339 | }, 340 | get(_1, dictionary) { 341 | return { 342 | http: { 343 | method: "get", 344 | ...mapPairListToKeyValPair(dictionary.ast), 345 | }, 346 | }; 347 | }, 348 | post(_1, dictionary) { 349 | return { 350 | http: { 351 | method: "post", 352 | ...mapPairListToKeyValPair(dictionary.ast), 353 | }, 354 | }; 355 | }, 356 | put(_1, dictionary) { 357 | return { 358 | http: { 359 | method: "put", 360 | ...mapPairListToKeyValPair(dictionary.ast), 361 | }, 362 | }; 363 | }, 364 | delete(_1, dictionary) { 365 | return { 366 | http: { 367 | method: "delete", 368 | ...mapPairListToKeyValPair(dictionary.ast), 369 | }, 370 | }; 371 | }, 372 | patch(_1, dictionary) { 373 | return { 374 | http: { 375 | method: "patch", 376 | ...mapPairListToKeyValPair(dictionary.ast), 377 | }, 378 | }; 379 | }, 380 | options(_1, dictionary) { 381 | return { 382 | http: { 383 | method: "options", 384 | ...mapPairListToKeyValPair(dictionary.ast), 385 | }, 386 | }; 387 | }, 388 | head(_1, dictionary) { 389 | return { 390 | http: { 391 | method: "head", 392 | ...mapPairListToKeyValPair(dictionary.ast), 393 | }, 394 | }; 395 | }, 396 | connect(_1, dictionary) { 397 | return { 398 | http: { 399 | method: "connect", 400 | ...mapPairListToKeyValPair(dictionary.ast), 401 | }, 402 | }; 403 | }, 404 | query(_1, dictionary) { 405 | return { 406 | params: mapRequestParams(dictionary.ast, "query"), 407 | }; 408 | }, 409 | paramspath(_1, dictionary) { 410 | return { 411 | params: mapRequestParams(dictionary.ast, "path"), 412 | }; 413 | }, 414 | paramsquery(_1, dictionary) { 415 | return { 416 | params: mapRequestParams(dictionary.ast, "query"), 417 | }; 418 | }, 419 | headers(_1, dictionary) { 420 | return { 421 | headers: mapPairListToKeyValPairs(dictionary.ast), 422 | }; 423 | }, 424 | authawsv4(_1, dictionary) { 425 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 426 | const accessKeyIdKey = _.find(auth, { name: "accessKeyId" }); 427 | const secretAccessKeyKey = _.find(auth, { name: "secretAccessKey" }); 428 | const sessionTokenKey = _.find(auth, { name: "sessionToken" }); 429 | const serviceKey = _.find(auth, { name: "service" }); 430 | const regionKey = _.find(auth, { name: "region" }); 431 | const profileNameKey = _.find(auth, { name: "profileName" }); 432 | const accessKeyId = accessKeyIdKey ? accessKeyIdKey.value : ""; 433 | const secretAccessKey = secretAccessKeyKey ? secretAccessKeyKey.value : ""; 434 | const sessionToken = sessionTokenKey ? sessionTokenKey.value : ""; 435 | const service = serviceKey ? serviceKey.value : ""; 436 | const region = regionKey ? regionKey.value : ""; 437 | const profileName = profileNameKey ? profileNameKey.value : ""; 438 | return { 439 | auth: { 440 | awsv4: { 441 | accessKeyId, 442 | secretAccessKey, 443 | sessionToken, 444 | service, 445 | region, 446 | profileName, 447 | }, 448 | }, 449 | }; 450 | }, 451 | authbasic(_1, dictionary) { 452 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 453 | const usernameKey = _.find(auth, { name: "username" }); 454 | const passwordKey = _.find(auth, { name: "password" }); 455 | const username = usernameKey ? usernameKey.value : ""; 456 | const password = passwordKey ? passwordKey.value : ""; 457 | return { 458 | auth: { 459 | basic: { 460 | username, 461 | password, 462 | }, 463 | }, 464 | }; 465 | }, 466 | authbearer(_1, dictionary) { 467 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 468 | const tokenKey = _.find(auth, { name: "token" }); 469 | const token = tokenKey ? tokenKey.value : ""; 470 | return { 471 | auth: { 472 | bearer: { 473 | token, 474 | }, 475 | }, 476 | }; 477 | }, 478 | authdigest(_1, dictionary) { 479 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 480 | const usernameKey = _.find(auth, { name: "username" }); 481 | const passwordKey = _.find(auth, { name: "password" }); 482 | const username = usernameKey ? usernameKey.value : ""; 483 | const password = passwordKey ? passwordKey.value : ""; 484 | return { 485 | auth: { 486 | digest: { 487 | username, 488 | password, 489 | }, 490 | }, 491 | }; 492 | }, 493 | authNTLM(_1, dictionary) { 494 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 495 | const usernameKey = _.find(auth, { name: "username" }); 496 | const passwordKey = _.find(auth, { name: "password" }); 497 | const domainKey = _.find(auth, { name: "domain" }); 498 | 499 | const username = usernameKey ? usernameKey.value : ""; 500 | const password = passwordKey ? passwordKey.value : ""; 501 | const domain = passwordKey ? domainKey.value : ""; 502 | 503 | return { 504 | auth: { 505 | ntlm: { 506 | username, 507 | password, 508 | domain, 509 | }, 510 | }, 511 | }; 512 | }, 513 | authOAuth2(_1, dictionary) { 514 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 515 | const grantTypeKey = _.find(auth, { name: "grant_type" }); 516 | const usernameKey = _.find(auth, { name: "username" }); 517 | const passwordKey = _.find(auth, { name: "password" }); 518 | const callbackUrlKey = _.find(auth, { name: "callback_url" }); 519 | const authorizationUrlKey = _.find(auth, { name: "authorization_url" }); 520 | const accessTokenUrlKey = _.find(auth, { name: "access_token_url" }); 521 | const refreshTokenUrlKey = _.find(auth, { name: "refresh_token_url" }); 522 | const clientIdKey = _.find(auth, { name: "client_id" }); 523 | const clientSecretKey = _.find(auth, { name: "client_secret" }); 524 | const scopeKey = _.find(auth, { name: "scope" }); 525 | const stateKey = _.find(auth, { name: "state" }); 526 | const pkceKey = _.find(auth, { name: "pkce" }); 527 | const credentialsPlacementKey = _.find(auth, { 528 | name: "credentials_placement", 529 | }); 530 | const credentialsIdKey = _.find(auth, { name: "credentials_id" }); 531 | const tokenPlacementKey = _.find(auth, { name: "token_placement" }); 532 | const tokenHeaderPrefixKey = _.find(auth, { name: "token_header_prefix" }); 533 | const tokenQueryKeyKey = _.find(auth, { name: "token_query_key" }); 534 | const autoFetchTokenKey = _.find(auth, { name: "auto_fetch_token" }); 535 | const autoRefreshTokenKey = _.find(auth, { name: "auto_refresh_token" }); 536 | return { 537 | auth: { 538 | oauth2: 539 | grantTypeKey?.value && grantTypeKey?.value == "password" 540 | ? { 541 | grantType: grantTypeKey ? grantTypeKey.value : "", 542 | accessTokenUrl: accessTokenUrlKey 543 | ? accessTokenUrlKey.value 544 | : "", 545 | refreshTokenUrl: refreshTokenUrlKey 546 | ? refreshTokenUrlKey.value 547 | : "", 548 | username: usernameKey ? usernameKey.value : "", 549 | password: passwordKey ? passwordKey.value : "", 550 | clientId: clientIdKey ? clientIdKey.value : "", 551 | clientSecret: clientSecretKey ? clientSecretKey.value : "", 552 | scope: scopeKey ? scopeKey.value : "", 553 | credentialsPlacement: credentialsPlacementKey?.value 554 | ? credentialsPlacementKey.value 555 | : "body", 556 | credentialsId: credentialsIdKey?.value 557 | ? credentialsIdKey.value 558 | : "credentials", 559 | tokenPlacement: tokenPlacementKey?.value 560 | ? tokenPlacementKey.value 561 | : "header", 562 | tokenHeaderPrefix: tokenHeaderPrefixKey?.value 563 | ? tokenHeaderPrefixKey.value 564 | : "Bearer", 565 | tokenQueryKey: tokenQueryKeyKey?.value 566 | ? tokenQueryKeyKey.value 567 | : "access_token", 568 | autoFetchToken: autoFetchTokenKey 569 | ? JSON.parse(autoFetchTokenKey?.value) 570 | : true, 571 | autoRefreshToken: autoRefreshTokenKey 572 | ? JSON.parse(autoRefreshTokenKey?.value) 573 | : true, 574 | } 575 | : grantTypeKey?.value && grantTypeKey?.value == "authorization_code" 576 | ? { 577 | grantType: grantTypeKey ? grantTypeKey.value : "", 578 | callbackUrl: callbackUrlKey ? callbackUrlKey.value : "", 579 | authorizationUrl: authorizationUrlKey 580 | ? authorizationUrlKey.value 581 | : "", 582 | accessTokenUrl: accessTokenUrlKey 583 | ? accessTokenUrlKey.value 584 | : "", 585 | refreshTokenUrl: refreshTokenUrlKey 586 | ? refreshTokenUrlKey.value 587 | : "", 588 | clientId: clientIdKey ? clientIdKey.value : "", 589 | clientSecret: clientSecretKey ? clientSecretKey.value : "", 590 | scope: scopeKey ? scopeKey.value : "", 591 | state: stateKey ? stateKey.value : "", 592 | pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false, 593 | credentialsPlacement: credentialsPlacementKey?.value 594 | ? credentialsPlacementKey.value 595 | : "body", 596 | credentialsId: credentialsIdKey?.value 597 | ? credentialsIdKey.value 598 | : "credentials", 599 | tokenPlacement: tokenPlacementKey?.value 600 | ? tokenPlacementKey.value 601 | : "header", 602 | tokenHeaderPrefix: tokenHeaderPrefixKey?.value 603 | ? tokenHeaderPrefixKey.value 604 | : "Bearer", 605 | tokenQueryKey: tokenQueryKeyKey?.value 606 | ? tokenQueryKeyKey.value 607 | : "access_token", 608 | autoFetchToken: autoFetchTokenKey 609 | ? JSON.parse(autoFetchTokenKey?.value) 610 | : true, 611 | autoRefreshToken: autoRefreshTokenKey 612 | ? JSON.parse(autoRefreshTokenKey?.value) 613 | : true, 614 | } 615 | : grantTypeKey?.value && grantTypeKey?.value == "client_credentials" 616 | ? { 617 | grantType: grantTypeKey ? grantTypeKey.value : "", 618 | accessTokenUrl: accessTokenUrlKey 619 | ? accessTokenUrlKey.value 620 | : "", 621 | refreshTokenUrl: refreshTokenUrlKey 622 | ? refreshTokenUrlKey.value 623 | : "", 624 | clientId: clientIdKey ? clientIdKey.value : "", 625 | clientSecret: clientSecretKey ? clientSecretKey.value : "", 626 | scope: scopeKey ? scopeKey.value : "", 627 | credentialsPlacement: credentialsPlacementKey?.value 628 | ? credentialsPlacementKey.value 629 | : "body", 630 | credentialsId: credentialsIdKey?.value 631 | ? credentialsIdKey.value 632 | : "credentials", 633 | tokenPlacement: tokenPlacementKey?.value 634 | ? tokenPlacementKey.value 635 | : "header", 636 | tokenHeaderPrefix: tokenHeaderPrefixKey?.value 637 | ? tokenHeaderPrefixKey.value 638 | : "Bearer", 639 | tokenQueryKey: tokenQueryKeyKey?.value 640 | ? tokenQueryKeyKey.value 641 | : "access_token", 642 | autoFetchToken: autoFetchTokenKey 643 | ? JSON.parse(autoFetchTokenKey?.value) 644 | : true, 645 | autoRefreshToken: autoRefreshTokenKey 646 | ? JSON.parse(autoRefreshTokenKey?.value) 647 | : true, 648 | } 649 | : {}, 650 | }, 651 | }; 652 | }, 653 | authwsse(_1, dictionary) { 654 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 655 | 656 | const userKey = _.find(auth, { name: "username" }); 657 | const secretKey = _.find(auth, { name: "password" }); 658 | const username = userKey ? userKey.value : ""; 659 | const password = secretKey ? secretKey.value : ""; 660 | 661 | return { 662 | auth: { 663 | wsse: { 664 | username, 665 | password, 666 | }, 667 | }, 668 | }; 669 | }, 670 | authapikey(_1, dictionary) { 671 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 672 | 673 | const findValueByName = (name) => { 674 | const item = _.find(auth, { name }); 675 | return item ? item.value : ""; 676 | }; 677 | 678 | const key = findValueByName("key"); 679 | const value = findValueByName("value"); 680 | const placement = findValueByName("placement"); 681 | 682 | return { 683 | auth: { 684 | apikey: { 685 | key, 686 | value, 687 | placement, 688 | }, 689 | }, 690 | }; 691 | }, 692 | bodyformurlencoded(_1, dictionary) { 693 | return { 694 | body: { 695 | formUrlEncoded: mapPairListToKeyValPairs(dictionary.ast), 696 | }, 697 | }; 698 | }, 699 | bodymultipart(_1, dictionary) { 700 | return { 701 | body: { 702 | multipartForm: mapPairListToKeyValPairsMultipart(dictionary.ast), 703 | }, 704 | }; 705 | }, 706 | bodyfile(_1, dictionary) { 707 | return { 708 | body: { 709 | file: mapPairListToKeyValPairsFile(dictionary.ast), 710 | }, 711 | }; 712 | }, 713 | body(_1, _2, _3, _4, textblock, _5) { 714 | return { 715 | http: { 716 | body: "json", 717 | }, 718 | body: { 719 | json: outdentString(textblock.sourceString), 720 | }, 721 | }; 722 | }, 723 | bodyjson(_1, _2, _3, _4, textblock, _5) { 724 | return { 725 | body: { 726 | json: outdentString(textblock.sourceString), 727 | }, 728 | }; 729 | }, 730 | bodytext(_1, _2, _3, _4, textblock, _5) { 731 | return { 732 | body: { 733 | text: outdentString(textblock.sourceString), 734 | }, 735 | }; 736 | }, 737 | bodyxml(_1, _2, _3, _4, textblock, _5) { 738 | return { 739 | body: { 740 | xml: outdentString(textblock.sourceString), 741 | }, 742 | }; 743 | }, 744 | bodysparql(_1, _2, _3, _4, textblock, _5) { 745 | return { 746 | body: { 747 | sparql: outdentString(textblock.sourceString), 748 | }, 749 | }; 750 | }, 751 | bodygraphql(_1, _2, _3, _4, textblock, _5) { 752 | return { 753 | body: { 754 | graphql: { 755 | query: outdentString(textblock.sourceString), 756 | }, 757 | }, 758 | }; 759 | }, 760 | bodygraphqlvars(_1, _2, _3, _4, textblock, _5) { 761 | return { 762 | body: { 763 | graphql: { 764 | variables: outdentString(textblock.sourceString), 765 | }, 766 | }, 767 | }; 768 | }, 769 | varsreq(_1, dictionary) { 770 | const vars = mapPairListToKeyValPairs(dictionary.ast); 771 | _.each(vars, (v) => { 772 | let name = v.name; 773 | if (name && name.length && name.charAt(0) === "@") { 774 | v.name = name.slice(1); 775 | v.local = true; 776 | } else { 777 | v.local = false; 778 | } 779 | }); 780 | 781 | return { 782 | vars: { 783 | req: vars, 784 | }, 785 | }; 786 | }, 787 | varsres(_1, dictionary) { 788 | const vars = mapPairListToKeyValPairs(dictionary.ast); 789 | _.each(vars, (v) => { 790 | let name = v.name; 791 | if (name && name.length && name.charAt(0) === "@") { 792 | v.name = name.slice(1); 793 | v.local = true; 794 | } else { 795 | v.local = false; 796 | } 797 | }); 798 | 799 | return { 800 | vars: { 801 | res: vars, 802 | }, 803 | }; 804 | }, 805 | assert(_1, dictionary) { 806 | return { 807 | assertions: mapPairListToKeyValPairs(dictionary.ast), 808 | }; 809 | }, 810 | scriptreq(_1, _2, _3, _4, textblock, _5) { 811 | return { 812 | script: { 813 | req: outdentString(textblock.sourceString), 814 | }, 815 | }; 816 | }, 817 | scriptres(_1, _2, _3, _4, textblock, _5) { 818 | return { 819 | script: { 820 | res: outdentString(textblock.sourceString), 821 | }, 822 | }; 823 | }, 824 | tests(_1, _2, _3, _4, textblock, _5) { 825 | return { 826 | tests: outdentString(textblock.sourceString), 827 | }; 828 | }, 829 | docs(_1, _2, _3, _4, textblock, _5) { 830 | return { 831 | docs: outdentString(textblock.sourceString), 832 | }; 833 | }, 834 | }); 835 | 836 | const parser = (input) => { 837 | const match = grammar.match(input); 838 | 839 | if (match.succeeded()) { 840 | return sem(match).ast; 841 | } else { 842 | throw new Error(match.message); 843 | } 844 | }; 845 | 846 | export default parser; 847 | ```