/** * VCR Request Extraction Helpers * * Provides base class and utilities for extracting request data from test specs * for VCR cassette recording and replay. * * @module @wavespec/kit/vcr */ import type { TestSpec } from "@wavespec/types"; import type { OperationData, RequestExtractor, ExtractorConfig, } from "./types.js"; /** * Base request extractor that adapters can extend * * Provides common logic for extracting request data from TestSpec * for VCR cassette recording. * * ## Usage * Extend this class and implement extractOperationData to create * adapter-specific extractors: * * ```typescript * export class MyAdapterExtractor extends BaseRequestExtractor { * extractOperationData(spec: TestSpec): OperationData { * return { * operation: spec.operation, * data: { * myField: spec.params.myField, * // ... adapter-specific fields * } * }; * } * } * ``` * * ## Configuration * Pass options to customize extraction behavior: * * ```typescript * const extractor = new MyAdapterExtractor({ * includeHeaders: ["Authorization"], * excludeFields: ["timestamp"], * normalizeFields: 'kebab-case', * strictMode: true * }); * ``` * * @see RequestExtractor interface for contract * @see OperationData for output format */ export abstract class BaseRequestExtractor implements RequestExtractor { /** * Create a new extractor with optional configuration * * @param config - Extraction configuration options */ constructor(protected config?: ExtractorConfig) {} /** * Extract operation data from test spec * Subclasses must implement protocol-specific extraction * * @param spec - Test specification * @returns Extracted operation data */ abstract extractOperationData(spec: TestSpec): OperationData; /** * Implement RequestExtractor interface by delegating to subclass * * @param spec - Test specification * @returns Extracted operation data */ extract(spec: TestSpec): OperationData { const data = this.extractOperationData(spec); this.validate(data); return data; } /** * Normalize headers (lowercase keys and optionally filter) * * Applies header key normalization and optional filtering. Normalizes * keys to lowercase and optionally includes only specified headers. * * @param headers - Headers to normalize * @returns Normalized headers (lowercase keys) * * @example * ```typescript * const headers = { "Content-Type": "application/json", "Authorization": "..." }; * const normalized = extractor.normalizeHeaders(headers); * // If configured with includeHeaders: ["Content-Type"] * // => { "content-type": "application/json" } * ``` */ protected normalizeHeaders(headers?: Record): Record { if (!headers) return {}; const config = this.config; const result: Record = {}; for (const [key, value] of Object.entries(headers)) { const lowerKey = key.toLowerCase(); // Skip excluded headers if excludeHeaders is set if ( config?.excludeHeaders && config.excludeHeaders.some((h: string) => h.toLowerCase() === lowerKey) ) { continue; } // Include only specified headers if includeHeaders is set if ( config?.includeHeaders && !config.includeHeaders.some((h: string) => h.toLowerCase() === lowerKey) ) { continue; } result[lowerKey] = value; } return result; } /** * Check if a field should be included based on excludeFields config * * Filters fields based on the excludeFields configuration. * Useful for removing non-deterministic or sensitive fields from extracted data. * * @param fieldName - Field name to check * @returns True if field should be included, false if excluded * * @example * ```typescript * extractor.shouldIncludeField("timestamp"); // If timestamp in excludeFields * // => false * ``` */ protected shouldIncludeField(fieldName: string): boolean { if (!this.config?.excludeFields) { return true; } return !this.config.excludeFields.includes(fieldName); } /** * Normalize keys using normalizeKeys config option * * When normalizeKeys is true, converts keys to lowercase for consistency. * * @param key - Key to normalize * @returns Normalized key * * @example * ```typescript * extractor.normalizeKey("MyKey"); // If normalizeKeys configured * // => "mykey" * ``` */ protected normalizeKey(key: string): string { if (this.config?.normalizeKeys) { return key.toLowerCase(); } return key; } /** * Validate extracted data in strict mode * * Only performs validation if strictMode is enabled in config. * Throws errors for missing required fields. * * @param data - Data to validate * @throws Error if validation fails in strict mode * * @example * ```typescript * const extractor = new MyExtractor({ strictMode: true }); * extractor.extract(spec); // Throws if operation or data is missing * ``` */ protected validate(data: OperationData): void { if (!this.config?.strictMode) return; if (!data.operation) { throw new Error("Operation is required in strict mode"); } if (!data.data || Object.keys(data.data).length === 0) { throw new Error("Request data is required in strict mode"); } } } /** * Extract operation data from test spec * * Helper function for simple extraction without subclassing. * Useful for adapters that have straightforward extraction logic. * * @param spec - Test specification * @param dataExtractor - Function to extract data from spec * @returns Extracted operation data with operation from spec * * @example * ```typescript * const data = extractOperationData( * { operation: "get", params: { path: "/users" } }, * (spec) => ({ path: spec.params.path }) * ); * // => { operation: "get", data: { path: "/users" } } * ``` */ export function extractOperationData( spec: TestSpec, dataExtractor: (spec: TestSpec) => Record ): OperationData { return { operation: spec.operation, data: dataExtractor(spec), }; }