Skip to main content
Glama
ValidationService.ts7.83 kB
/** * Service for validating Bruno collections and requests * Handles request detail parsing and collection validation */ import * as fs from 'fs/promises'; import * as path from 'path'; export interface RequestDetails { name: string; method: string; url: string; headers: Record<string, string>; body?: { type: string; content: string; }; auth: string; tests?: string[]; metadata: { type: string; seq?: number; }; } export interface CollectionValidationResult { valid: boolean; errors: string[]; warnings: string[]; summary: { hasBrunoJson: boolean; totalRequests: number; validRequests: number; invalidRequests: number; environments: number; }; } /** * Service responsible for validation operations * Single Responsibility: Validate collections and parse request details */ export class ValidationService { /** * Get detailed information about a request */ async getRequestDetails( requestFilePath: string, requestName: string ): Promise<RequestDetails> { // Read and parse the .bru file const content = await fs.readFile(requestFilePath, 'utf-8'); const details: RequestDetails = { name: requestName, method: 'GET', url: '', headers: {}, body: undefined, auth: 'none', tests: [], metadata: { type: 'http', seq: undefined } }; // Parse metadata block const metaMatch = content.match(/meta\s*\{([\s\S]*?)\n\}/s); if (metaMatch) { const metaContent = metaMatch[1]; const nameMatch = metaContent.match(/name:\s*(.+)/); if (nameMatch) details.name = nameMatch[1].trim(); const typeMatch = metaContent.match(/type:\s*(.+)/); if (typeMatch) details.metadata.type = typeMatch[1].trim(); const seqMatch = metaContent.match(/seq:\s*(\d+)/); if (seqMatch) details.metadata.seq = parseInt(seqMatch[1]); } // Parse method block (get, post, put, patch, delete, etc.) const methodBlocks = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']; for (const method of methodBlocks) { const methodRegex = new RegExp(`${method}\\s*\\{([\\s\\S]*?)\\n\\}`, 's'); const methodMatch = content.match(methodRegex); if (methodMatch) { details.method = method.toUpperCase(); const methodContent = methodMatch[1]; const urlMatch = methodContent.match(/url:\s*(.+)/); if (urlMatch) details.url = urlMatch[1].trim(); const authMatch = methodContent.match(/auth:\s*(.+)/); if (authMatch) details.auth = authMatch[1].trim(); break; } } // Parse headers block const headersMatch = content.match(/headers\s*\{([\s\S]*?)\n\}/s); if (headersMatch) { const headersContent = headersMatch[1]; const headerLines = headersContent.split('\n').filter(line => line.trim()); for (const line of headerLines) { const colonIndex = line.indexOf(':'); if (colonIndex > 0) { const key = line.substring(0, colonIndex).trim(); const value = line.substring(colonIndex + 1).trim(); details.headers[key] = value; } } } // Parse body block const bodyTypeMatch = content.match(/body:(json|text|xml|formUrlEncoded|multipartForm|graphql|sparql|none)\s*\{/); if (bodyTypeMatch) { const bodyType = bodyTypeMatch[1]; const bodyRegex = new RegExp(`body:${bodyType}\\s*\\{([\\s\\S]*?)\\n\\}`, 's'); const bodyMatch = content.match(bodyRegex); if (bodyMatch) { details.body = { type: bodyType, content: bodyMatch[1].trim() }; } } // Parse tests block const testsMatch = content.match(/tests\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/s); if (testsMatch) { const testsContent = testsMatch[1]; // Extract test function calls const testRegex = /test\s*\(\s*["']([^"']+)["']/g; let testMatch; while ((testMatch = testRegex.exec(testsContent)) !== null) { details.tests!.push(testMatch[1]); } } return details; } /** * Validate a Bruno collection */ async validateCollection( collectionPath: string, listRequests: () => Promise<any[]>, listEnvironments: () => Promise<any[]>, validateEnvironment: (envName: string) => Promise<any>, getRequestDetails: (reqName: string) => Promise<any> ): Promise<CollectionValidationResult> { const result: CollectionValidationResult = { valid: true, errors: [], warnings: [], summary: { hasBrunoJson: false, totalRequests: 0, validRequests: 0, invalidRequests: 0, environments: 0 } }; // Check if collection directory exists try { await fs.access(collectionPath); } catch { result.valid = false; result.errors.push(`Collection directory not found: ${collectionPath}`); return result; } // Check for bruno.json const brunoJsonPath = path.join(collectionPath, 'bruno.json'); try { await fs.access(brunoJsonPath); result.summary.hasBrunoJson = true; // Validate bruno.json structure const brunoJsonContent = await fs.readFile(brunoJsonPath, 'utf-8'); try { const brunoJson = JSON.parse(brunoJsonContent); // Check required fields if (!brunoJson.version) { result.warnings.push('bruno.json missing "version" field'); } if (!brunoJson.name) { result.warnings.push('bruno.json missing "name" field'); } if (!brunoJson.type) { result.warnings.push('bruno.json missing "type" field'); } else if (brunoJson.type !== 'collection') { result.errors.push(`Invalid type in bruno.json: expected "collection", got "${brunoJson.type}"`); result.valid = false; } } catch (error) { result.errors.push(`Invalid JSON in bruno.json: ${error}`); result.valid = false; } } catch { result.errors.push('bruno.json not found in collection root'); result.valid = false; return result; } // Validate requests try { const requests = await listRequests(); result.summary.totalRequests = requests.length; if (requests.length === 0) { result.warnings.push('Collection contains no requests'); } // Validate each request file for (const req of requests) { try { await getRequestDetails(req.name); result.summary.validRequests++; } catch (error) { result.summary.invalidRequests++; result.errors.push(`Invalid request "${req.name}": ${error}`); result.valid = false; } } } catch (error) { result.errors.push(`Failed to list requests: ${error}`); result.valid = false; } // Check for environments try { const environments = await listEnvironments(); result.summary.environments = environments.length; if (environments.length === 0) { result.warnings.push('No environments found in collection'); } // Validate each environment for (const env of environments) { const validation = await validateEnvironment(env.name); if (!validation.valid) { result.warnings.push(`Environment "${env.name}" has issues: ${validation.errors.join(', ')}`); } if (validation.warnings.length > 0) { validation.warnings.forEach((w: string) => result.warnings.push(`Environment "${env.name}": ${w}`)); } } } catch (error) { // Environments are optional, so this is just a warning result.warnings.push('Could not validate environments'); } return result; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jcr82/bruno-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server