@curl-runner/cli 1.0.3 → 1.1.0
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/dist/cli.js +7 -7
- package/package.json +1 -1
- package/src/executor/request-executor.ts +47 -1
- package/src/parser/yaml.test.ts +176 -0
- package/src/parser/yaml.ts +54 -8
- package/src/types/config.ts +30 -0
- package/src/utils/response-store.test.ts +213 -0
- package/src/utils/response-store.ts +108 -0
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var
|
|
4
|
-
`).filter((
|
|
5
|
-
`);if(
|
|
6
|
-
`),
|
|
7
|
-
`)})}if(this.shouldShowMetrics()&&$.metrics&&w==="detailed"){let z=$.metrics,D=[];if(D.push({label:"Request Duration",value:this.formatDuration(z.duration),color:"cyan"}),z.size!==void 0)D.push({label:"Response Size",value:this.formatSize(z.size),color:"cyan"});if(z.dnsLookup)D.push({label:"DNS Lookup",value:this.formatDuration(z.dnsLookup),color:"cyan"});if(z.tcpConnection)D.push({label:"TCP Connection",value:this.formatDuration(z.tcpConnection),color:"cyan"});if(z.tlsHandshake)D.push({label:"TLS Handshake",value:this.formatDuration(z.tlsHandshake),color:"cyan"});if(z.firstByte)D.push({label:"Time to First Byte",value:this.formatDuration(z.firstByte),color:"cyan"});W.push({label:"Metrics",children:D})}if(Z.render(W),$.error)console.log(),this.logValidationErrors($.error);console.log()}logSummary($,w=!1){if(this.config.format==="raw")return;if(this.config.format==="json"){let z={summary:{total:$.total,successful:$.successful,failed:$.failed,duration:$.duration},results:$.results.map((D)=>({request:{name:D.request.name,url:D.request.url,method:D.request.method||"GET"},success:D.success,status:D.status,...this.shouldShowHeaders()&&D.headers?{headers:D.headers}:{},...this.shouldShowBody()&&D.body?{body:D.body}:{},...D.error?{error:D.error}:{},...this.shouldShowMetrics()&&D.metrics?{metrics:D.metrics}:{}}))};console.log(JSON.stringify(z,null,2));return}if(!this.shouldShowOutput())return;let K=this.config.prettyLevel||"minimal";if(w)console.log();if(K==="minimal"){let z=$.failed===0?"green":"red",D=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`;console.log(`${w?"\u25C6 Global Summary":"Summary"}: ${this.color(D,z)}`);return}let X=($.successful/$.total*100).toFixed(1),Q=$.failed===0?"green":"red",W=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`,Z=w?"\u25C6 Global Summary":"Summary";if(console.log(),console.log(`${Z}: ${this.color(W,Q)} (${this.color(this.formatDuration($.duration),"cyan")})`),$.failed>0&&this.shouldShowRequestDetails())$.results.filter((z)=>!z.success).forEach((z)=>{let D=z.request.name||z.request.url;console.log(` ${this.color("\u2022","red")} ${D}: ${z.error}`)})}logError($){console.error(this.color(`\u2717 ${$}`,"red"))}logWarning($){console.warn(this.color(`\u26A0 ${$}`,"yellow"))}logInfo($){console.log(this.color(`\u2139 ${$}`,"blue"))}logSuccess($){console.log(this.color(`\u2713 ${$}`,"green"))}logFileHeader($,w){if(!this.shouldShowOutput()||this.config.format!=="pretty")return;let K=$.replace(/.*\//,"").replace(".yaml","");console.log(),console.log(this.color(`\u25B6 ${K}.yaml`,"bright")+this.color(` (${w} request${w===1?"":"s"})`,"dim"))}}class E{logger;globalConfig;constructor($={}){this.globalConfig=$,this.logger=new M($.output)}mergeOutputConfig($){return{...this.globalConfig.output,...$.sourceOutputConfig}}async executeRequest($,w=0){let K=performance.now(),X=this.mergeOutputConfig($),Q=new M(X);Q.logRequestStart($,w);let W=T.buildCommand($);Q.logCommand(W);let Z=0,z,D=($.retry?.count||0)+1;while(Z<D){if(Z>0){if(Q.logRetry(Z,D-1),$.retry?.delay)await Bun.sleep($.retry.delay)}let O=await T.executeCurl(W);if(O.success){let J=O.body;try{if(O.headers?.["content-type"]?.includes("application/json")||J&&(J.trim().startsWith("{")||J.trim().startsWith("[")))J=JSON.parse(J)}catch(I){}let G={request:$,success:!0,status:O.status,headers:O.headers,body:J,metrics:{...O.metrics,duration:performance.now()-K}};if($.expect){let I=this.validateResponse(G,$.expect);if(!I.success)G.success=!1,G.error=I.error}return Q.logRequestComplete(G),G}z=O.error,Z++}let U={request:$,success:!1,error:z,metrics:{duration:performance.now()-K}};return Q.logRequestComplete(U),U}validateResponse($,w){if(!w)return{success:!0};let K=[];if(w.status!==void 0){let Q=Array.isArray(w.status)?w.status:[w.status];if(!Q.includes($.status||0))K.push(`Expected status ${Q.join(" or ")}, got ${$.status}`)}if(w.headers)for(let[Q,W]of Object.entries(w.headers)){let Z=$.headers?.[Q]||$.headers?.[Q.toLowerCase()];if(Z!==W)K.push(`Expected header ${Q}="${W}", got "${Z}"`)}if(w.body!==void 0){let Q=this.validateBodyProperties($.body,w.body,"");if(Q.length>0)K.push(...Q)}if(w.responseTime!==void 0&&$.metrics){let Q=$.metrics.duration;if(!this.validateRangePattern(Q,w.responseTime))K.push(`Expected response time to match ${w.responseTime}ms, got ${Q.toFixed(2)}ms`)}let X=K.length>0;if(w.failure===!0){if(X)return{success:!1,error:K.join("; ")};let Q=$.status||0;if(Q>=400)return{success:!0};else return{success:!1,error:`Expected request to fail (4xx/5xx) but got status ${Q}`}}else if(X)return{success:!1,error:K.join("; ")};else return{success:!0}}validateBodyProperties($,w,K){let X=[];if(typeof w!=="object"||w===null){let Q=this.validateValue($,w,K||"body");if(!Q.isValid)X.push(Q.error);return X}if(Array.isArray(w)){let Q=this.validateValue($,w,K||"body");if(!Q.isValid)X.push(Q.error);return X}for(let[Q,W]of Object.entries(w)){let Z=K?`${K}.${Q}`:Q,z;if(Array.isArray($)&&this.isArraySelector(Q))z=this.getArrayValue($,Q);else z=$?.[Q];if(typeof W==="object"&&W!==null&&!Array.isArray(W)){let D=this.validateBodyProperties(z,W,Z);X.push(...D)}else{let D=this.validateValue(z,W,Z);if(!D.isValid)X.push(D.error)}}return X}validateValue($,w,K){if(w==="*")return{isValid:!0};if(Array.isArray(w)){if(!w.some((Q)=>{if(Q==="*")return!0;if(typeof Q==="string"&&this.isRegexPattern(Q))return this.validateRegexPattern($,Q);if(typeof Q==="string"&&this.isRangePattern(Q))return this.validateRangePattern($,Q);return $===Q}))return{isValid:!1,error:`Expected ${K} to match one of ${JSON.stringify(w)}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof w==="string"&&this.isRegexPattern(w)){if(!this.validateRegexPattern($,w))return{isValid:!1,error:`Expected ${K} to match pattern ${w}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof w==="string"&&this.isRangePattern(w)){if(!this.validateRangePattern($,w))return{isValid:!1,error:`Expected ${K} to match range ${w}, got ${JSON.stringify($)}`};return{isValid:!0}}if(w==="null"||w===null){if($!==null)return{isValid:!1,error:`Expected ${K} to be null, got ${JSON.stringify($)}`};return{isValid:!0}}if($!==w)return{isValid:!1,error:`Expected ${K} to be ${JSON.stringify(w)}, got ${JSON.stringify($)}`};return{isValid:!0}}isRegexPattern($){return $.startsWith("^")||$.endsWith("$")||$.includes("\\d")||$.includes("\\w")||$.includes("\\s")||$.includes("[")||$.includes("*")||$.includes("+")||$.includes("?")}validateRegexPattern($,w){let K=String($);try{return new RegExp(w).test(K)}catch{return!1}}isRangePattern($){return/^(>=?|<=?|>|<)\s*[\d.-]+(\s*,\s*(>=?|<=?|>|<)\s*[\d.-]+)*$/.test($)}validateRangePattern($,w){let K=Number($);if(Number.isNaN(K))return!1;let X=w.match(/^([\d.-]+)\s*-\s*([\d.-]+)$/);if(X){let W=Number(X[1]),Z=Number(X[2]);return K>=W&&K<=Z}return w.split(",").map((W)=>W.trim()).every((W)=>{let Z=W.match(/^(>=?|<=?|>|<)\s*([\d.-]+)$/);if(!Z)return!1;let z=Z[1],D=Number(Z[2]);switch(z){case">":return K>D;case">=":return K>=D;case"<":return K<D;case"<=":return K<=D;default:return!1}})}isArraySelector($){return/^\[.*\]$/.test($)||$==="*"||$.startsWith("slice(")}getArrayValue($,w){if(w==="*")return $;if(w.startsWith("[")&&w.endsWith("]")){let K=w.slice(1,-1);if(K==="*")return $;let X=Number(K);if(!Number.isNaN(X))return X>=0?$[X]:$[$.length+X]}if(w.startsWith("slice(")){let K=w.match(/slice\((\d+)(?:,(\d+))?\)/);if(K){let X=Number(K[1]),Q=K[2]?Number(K[2]):void 0;return $.slice(X,Q)}}return}async executeSequential($){let w=performance.now(),K=[];for(let X=0;X<$.length;X++){let Q=await this.executeRequest($[X],X+1);if(K.push(Q),!Q.success&&!this.globalConfig.continueOnError){this.logger.logError("Stopping execution due to error");break}}return this.createSummary(K,performance.now()-w)}async executeParallel($){let w=performance.now(),K=$.map((Q,W)=>this.executeRequest(Q,W+1)),X=await Promise.all(K);return this.createSummary(X,performance.now()-w)}async execute($){this.logger.logExecutionStart($.length,this.globalConfig.execution||"sequential");let w=this.globalConfig.execution==="parallel"?await this.executeParallel($):await this.executeSequential($);if(this.logger.logSummary(w),this.globalConfig.output?.saveToFile)await this.saveSummaryToFile(w);return w}createSummary($,w){let K=$.filter((Q)=>Q.success).length,X=$.filter((Q)=>!Q.success).length;return{total:$.length,successful:K,failed:X,duration:w,results:$}}async saveSummaryToFile($){let w=this.globalConfig.output?.saveToFile;if(!w)return;let K=JSON.stringify($,null,2);await Bun.write(w,K),this.logger.logInfo(`Results saved to ${w}`)}}var{YAML:N}=globalThis.Bun;class H{static async parseFile($){let K=await Bun.file($).text();return N.parse(K)}static parse($){return N.parse($)}static interpolateVariables($,w){if(typeof $==="string"){let K=$.match(/^\$\{([^}]+)\}$/);if(K){let X=K[1],Q=H.resolveDynamicVariable(X);return Q!==null?Q:w[X]||$}return $.replace(/\$\{([^}]+)\}/g,(X,Q)=>{let W=H.resolveDynamicVariable(Q);return W!==null?W:w[Q]||X})}if(Array.isArray($))return $.map((K)=>H.interpolateVariables(K,w));if($&&typeof $==="object"){let K={};for(let[X,Q]of Object.entries($))K[X]=H.interpolateVariables(Q,w);return K}return $}static resolveDynamicVariable($){if($==="UUID")return crypto.randomUUID();if($==="CURRENT_TIME"||$==="TIMESTAMP")return Date.now().toString();if($.startsWith("DATE:")){let w=$.slice(5);return H.formatDate(new Date,w)}if($.startsWith("TIME:")){let w=$.slice(5);return H.formatTime(new Date,w)}return null}static formatDate($,w){let K=$.getFullYear(),X=String($.getMonth()+1).padStart(2,"0"),Q=String($.getDate()).padStart(2,"0");return w.replace("YYYY",K.toString()).replace("MM",X).replace("DD",Q)}static formatTime($,w){let K=String($.getHours()).padStart(2,"0"),X=String($.getMinutes()).padStart(2,"0"),Q=String($.getSeconds()).padStart(2,"0");return w.replace("HH",K).replace("mm",X).replace("ss",Q)}static mergeConfigs($,w){return{...$,...w,headers:{...$.headers,...w.headers},params:{...$.params,...w.params},variables:{...$.variables,...w.variables}}}}function S(){if(typeof BUILD_VERSION<"u")return BUILD_VERSION;if(process.env.CURL_RUNNER_VERSION)return process.env.CURL_RUNNER_VERSION;try{let $=["../package.json","./package.json","../../package.json"];for(let w of $)try{let K=A(w);if(K.name==="@curl-runner/cli"&&K.version)return K.version}catch{}return"0.0.0"}catch{return"0.0.0"}}var q={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",underscore:"\x1B[4m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgBlack:"\x1B[40m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m",bgBlue:"\x1B[44m",bgMagenta:"\x1B[45m",bgCyan:"\x1B[46m",bgWhite:"\x1B[47m"};function F($,w){return`${q[w]}${$}${q.reset}`}var P=`${process.env.HOME}/.curl-runner-version-cache.json`,y=86400000,f="https://registry.npmjs.org/@curl-runner/cli/latest";class L{async checkForUpdates($=!1){try{if(process.env.CI)return;let w=S();if(w==="0.0.0")return;if(!$){let X=await this.getCachedVersion();if(X&&Date.now()-X.lastCheck<y){this.compareVersions(w,X.latestVersion);return}}let K=await this.fetchLatestVersion();if(K)await this.setCachedVersion(K),this.compareVersions(w,K)}catch{}}async fetchLatestVersion(){try{let $=await fetch(f,{signal:AbortSignal.timeout(3000)});if(!$.ok)return null;return(await $.json()).version}catch{return null}}compareVersions($,w){if(this.isNewerVersion($,w))console.log(),console.log(F("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E","yellow")),console.log(F("\u2502","yellow")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" "+F("\uD83D\uDCE6 New version available!","bright")+` ${F($,"red")} \u2192 ${F(w,"green")} `+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" Update with: "+F("npm install -g @curl-runner/cli","cyan")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" or: "+F("bun install -g @curl-runner/cli","cyan")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" "+F("\u2502","yellow")),console.log(F("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F","yellow")),console.log()}isNewerVersion($,w){try{let K=$.replace(/^v/,""),X=w.replace(/^v/,""),Q=K.split(".").map(Number),W=X.split(".").map(Number);for(let Z=0;Z<Math.max(Q.length,W.length);Z++){let z=Q[Z]||0,D=W[Z]||0;if(D>z)return!0;if(D<z)return!1}return!1}catch{return!1}}async getCachedVersion(){try{let $=Bun.file(P);if(await $.exists())return JSON.parse(await $.text())}catch{}return null}async setCachedVersion($){try{let w={lastCheck:Date.now(),latestVersion:$};await Bun.write(P,JSON.stringify(w))}catch{}}}class h{logger=new M;async loadConfigFile(){let $=["curl-runner.yaml","curl-runner.yml",".curl-runner.yaml",".curl-runner.yml"];for(let w of $)try{if(await Bun.file(w).exists()){let X=await H.parseFile(w),Q=X.global||X;return this.logger.logInfo(`Loaded configuration from ${w}`),Q}}catch(K){this.logger.logWarning(`Failed to load configuration from ${w}: ${K}`)}return{}}loadEnvironmentVariables(){let $={};if(process.env.CURL_RUNNER_TIMEOUT)$.defaults={...$.defaults,timeout:Number.parseInt(process.env.CURL_RUNNER_TIMEOUT,10)};if(process.env.CURL_RUNNER_RETRIES)$.defaults={...$.defaults,retry:{...$.defaults?.retry,count:Number.parseInt(process.env.CURL_RUNNER_RETRIES,10)}};if(process.env.CURL_RUNNER_RETRY_DELAY)$.defaults={...$.defaults,retry:{...$.defaults?.retry,delay:Number.parseInt(process.env.CURL_RUNNER_RETRY_DELAY,10)}};if(process.env.CURL_RUNNER_VERBOSE)$.output={...$.output,verbose:process.env.CURL_RUNNER_VERBOSE.toLowerCase()==="true"};if(process.env.CURL_RUNNER_EXECUTION)$.execution=process.env.CURL_RUNNER_EXECUTION;if(process.env.CURL_RUNNER_CONTINUE_ON_ERROR)$.continueOnError=process.env.CURL_RUNNER_CONTINUE_ON_ERROR.toLowerCase()==="true";if(process.env.CURL_RUNNER_OUTPUT_FORMAT){let w=process.env.CURL_RUNNER_OUTPUT_FORMAT;if(["json","pretty","raw"].includes(w))$.output={...$.output,format:w}}if(process.env.CURL_RUNNER_PRETTY_LEVEL){let w=process.env.CURL_RUNNER_PRETTY_LEVEL;if(["minimal","standard","detailed"].includes(w))$.output={...$.output,prettyLevel:w}}if(process.env.CURL_RUNNER_OUTPUT_FILE)$.output={...$.output,saveToFile:process.env.CURL_RUNNER_OUTPUT_FILE};return $}async run($){try{let{files:w,options:K}=this.parseArguments($);if(!K.version&&!K.help)new L().checkForUpdates().catch(()=>{});if(K.help){this.showHelp();return}if(K.version){console.log(`curl-runner v${S()}`);return}let X=this.loadEnvironmentVariables(),Q=await this.loadConfigFile(),W=await this.findYamlFiles(w,K);if(W.length===0)this.logger.logError("No YAML files found"),process.exit(1);this.logger.logInfo(`Found ${W.length} YAML file(s)`);let Z=this.mergeGlobalConfigs(X,Q),z=[],D=[];for(let J of W){this.logger.logInfo(`Processing: ${J}`);let{requests:G,config:I}=await this.processYamlFile(J),j=I?.output||{},_=G.map((Y)=>({...Y,sourceOutputConfig:j,sourceFile:J}));if(I){let{...Y}=I;Z=this.mergeGlobalConfigs(Z,Y)}D.push({file:J,requests:_,config:I}),z.push(..._)}if(K.execution)Z.execution=K.execution;if(K.continueOnError!==void 0)Z.continueOnError=K.continueOnError;if(K.verbose!==void 0)Z.output={...Z.output,verbose:K.verbose};if(K.quiet!==void 0)Z.output={...Z.output,verbose:!1};if(K.output)Z.output={...Z.output,saveToFile:K.output};if(K.outputFormat)Z.output={...Z.output,format:K.outputFormat};if(K.prettyLevel)Z.output={...Z.output,prettyLevel:K.prettyLevel};if(K.showHeaders!==void 0)Z.output={...Z.output,showHeaders:K.showHeaders};if(K.showBody!==void 0)Z.output={...Z.output,showBody:K.showBody};if(K.showMetrics!==void 0)Z.output={...Z.output,showMetrics:K.showMetrics};if(K.timeout)Z.defaults={...Z.defaults,timeout:K.timeout};if(K.retries||K.noRetry){let J=K.noRetry?0:K.retries||0;Z.defaults={...Z.defaults,retry:{...Z.defaults?.retry,count:J}}}if(K.retryDelay)Z.defaults={...Z.defaults,retry:{...Z.defaults?.retry,delay:K.retryDelay}};if(z.length===0)this.logger.logError("No requests found in YAML files"),process.exit(1);let U=new E(Z),O;if(D.length>1){let J=[],G=0;for(let _=0;_<D.length;_++){let Y=D[_];this.logger.logFileHeader(Y.file,Y.requests.length);let R=await U.execute(Y.requests);if(J.push(...R.results),G+=R.duration,_<D.length-1)console.log()}let I=J.filter((_)=>_.success).length,j=J.filter((_)=>!_.success).length;O={total:J.length,successful:I,failed:j,duration:G,results:J},U.logger.logSummary(O,!0)}else O=await U.execute(z);process.exit(O.failed>0&&!Z.continueOnError?1:0)}catch(w){this.logger.logError(w instanceof Error?w.message:String(w)),process.exit(1)}}parseArguments($){let w={},K=[];for(let X=0;X<$.length;X++){let Q=$[X];if(Q.startsWith("--")){let W=Q.slice(2),Z=$[X+1];if(W==="help"||W==="version")w[W]=!0;else if(W==="no-retry")w.noRetry=!0;else if(W==="quiet")w.quiet=!0;else if(W==="show-headers")w.showHeaders=!0;else if(W==="show-body")w.showBody=!0;else if(W==="show-metrics")w.showMetrics=!0;else if(Z&&!Z.startsWith("--")){if(W==="continue-on-error")w.continueOnError=Z==="true";else if(W==="verbose")w.verbose=Z==="true";else if(W==="timeout")w.timeout=Number.parseInt(Z,10);else if(W==="retries")w.retries=Number.parseInt(Z,10);else if(W==="retry-delay")w.retryDelay=Number.parseInt(Z,10);else if(W==="output-format"){if(["json","pretty","raw"].includes(Z))w.outputFormat=Z}else if(W==="pretty-level"){if(["minimal","standard","detailed"].includes(Z))w.prettyLevel=Z}else w[W]=Z;X++}else w[W]=!0}else if(Q.startsWith("-")){let W=Q.slice(1);for(let Z of W)switch(Z){case"h":w.help=!0;break;case"v":w.verbose=!0;break;case"p":w.execution="parallel";break;case"c":w.continueOnError=!0;break;case"q":w.quiet=!0;break;case"o":{let z=$[X+1];if(z&&!z.startsWith("-"))w.output=z,X++;break}}}else K.push(Q)}return{files:K,options:w}}async findYamlFiles($,w){let K=new Set,X=[];if($.length===0)X=w.all?["**/*.yaml","**/*.yml"]:["*.yaml","*.yml"];else for(let Q of $)try{let Z=await(await import("fs/promises")).stat(Q);if(Z.isDirectory()){if(X.push(`${Q}/*.yaml`,`${Q}/*.yml`),w.all)X.push(`${Q}/**/*.yaml`,`${Q}/**/*.yml`)}else if(Z.isFile())X.push(Q)}catch{X.push(Q)}for(let Q of X){let W=new g(Q);for await(let Z of W.scan("."))if(Z.endsWith(".yaml")||Z.endsWith(".yml"))K.add(Z)}return Array.from(K).sort()}async processYamlFile($){let w=await H.parseFile($),K=[],X;if(w.global)X=w.global;let Q={...w.global?.variables,...w.collection?.variables},W={...w.global?.defaults,...w.collection?.defaults};if(w.request){let Z=this.prepareRequest(w.request,Q,W);K.push(Z)}if(w.requests)for(let Z of w.requests){let z=this.prepareRequest(Z,Q,W);K.push(z)}if(w.collection?.requests)for(let Z of w.collection.requests){let z=this.prepareRequest(Z,Q,W);K.push(z)}return{requests:K,config:X}}prepareRequest($,w,K){let X=H.interpolateVariables($,w);return H.mergeConfigs(K,X)}mergeGlobalConfigs($,w){return{...$,...w,variables:{...$.variables,...w.variables},output:{...$.output,...w.output},defaults:{...$.defaults,...w.defaults}}}showHelp(){console.log(`
|
|
3
|
+
var C=Object.create;var{getPrototypeOf:v,defineProperty:k,getOwnPropertyNames:x}=Object;var y=Object.prototype.hasOwnProperty;var f=($,K,Q)=>{Q=$!=null?C(v($)):{};let X=K||!$||!$.__esModule?k(Q,"default",{value:$,enumerable:!0}):Q;for(let Z of x($))if(!y.call(X,Z))k(X,Z,{get:()=>$[Z],enumerable:!0});return X};var S=import.meta.require;var{Glob:c}=globalThis.Bun;var{YAML:q}=globalThis.Bun;class H{static async parseFile($){let Q=await Bun.file($).text();return q.parse(Q)}static parse($){return q.parse($)}static interpolateVariables($,K,Q){if(typeof $==="string"){let X=$.match(/^\$\{([^}]+)\}$/);if(X){let Z=X[1],z=H.resolveVariable(Z,K,Q);return z!==null?z:$}return $.replace(/\$\{([^}]+)\}/g,(Z,z)=>{let W=H.resolveVariable(z,K,Q);return W!==null?W:Z})}if(Array.isArray($))return $.map((X)=>H.interpolateVariables(X,K,Q));if($&&typeof $==="object"){let X={};for(let[Z,z]of Object.entries($))X[Z]=H.interpolateVariables(z,K,Q);return X}return $}static resolveVariable($,K,Q){if($.startsWith("store.")&&Q){let Z=$.slice(6);if(Z in Q)return Q[Z];return null}let X=H.resolveDynamicVariable($);if(X!==null)return X;if($ in K)return K[$];return null}static resolveDynamicVariable($){if($==="UUID")return crypto.randomUUID();if($==="CURRENT_TIME"||$==="TIMESTAMP")return Date.now().toString();if($.startsWith("DATE:")){let K=$.slice(5);return H.formatDate(new Date,K)}if($.startsWith("TIME:")){let K=$.slice(5);return H.formatTime(new Date,K)}return null}static formatDate($,K){let Q=$.getFullYear(),X=String($.getMonth()+1).padStart(2,"0"),Z=String($.getDate()).padStart(2,"0");return K.replace("YYYY",Q.toString()).replace("MM",X).replace("DD",Z)}static formatTime($,K){let Q=String($.getHours()).padStart(2,"0"),X=String($.getMinutes()).padStart(2,"0"),Z=String($.getSeconds()).padStart(2,"0");return K.replace("HH",Q).replace("mm",X).replace("ss",Z)}static mergeConfigs($,K){return{...$,...K,headers:{...$.headers,...K.headers},params:{...$.params,...K.params},variables:{...$.variables,...K.variables}}}}class M{static buildCommand($){let K=["curl"];if(K.push("-X",$.method||"GET"),K.push("-w",'"\\n__CURL_METRICS_START__%{json}__CURL_METRICS_END__"'),$.headers)for(let[X,Z]of Object.entries($.headers))K.push("-H",`"${X}: ${Z}"`);if($.auth){if($.auth.type==="basic"&&$.auth.username&&$.auth.password)K.push("-u",`"${$.auth.username}:${$.auth.password}"`);else if($.auth.type==="bearer"&&$.auth.token)K.push("-H",`"Authorization: Bearer ${$.auth.token}"`)}if($.body){let X=typeof $.body==="string"?$.body:JSON.stringify($.body);if(K.push("-d",`'${X.replace(/'/g,"'\\''")}'`),!$.headers?.["Content-Type"])K.push("-H",'"Content-Type: application/json"')}if($.timeout)K.push("--max-time",$.timeout.toString());if($.followRedirects!==!1){if(K.push("-L"),$.maxRedirects)K.push("--max-redirs",$.maxRedirects.toString())}if($.proxy)K.push("-x",$.proxy);if($.insecure)K.push("-k");if($.output)K.push("-o",$.output);K.push("-s","-S");let Q=$.url;if($.params&&Object.keys($.params).length>0){let X=new URLSearchParams($.params).toString();Q+=(Q.includes("?")?"&":"?")+X}return K.push(`"${Q}"`),K.join(" ")}static async executeCurl($){try{let K=Bun.spawn(["sh","-c",$],{stdout:"pipe",stderr:"pipe"}),Q=await new Response(K.stdout).text(),X=await new Response(K.stderr).text();if(await K.exited,K.exitCode!==0&&!Q)return{success:!1,error:X||`Command failed with exit code ${K.exitCode}`};let Z=Q,z={},W=Q.match(/__CURL_METRICS_START__(.+?)__CURL_METRICS_END__/);if(W){Z=Q.replace(/__CURL_METRICS_START__.+?__CURL_METRICS_END__/,"").trim();try{z=JSON.parse(W[1])}catch(J){}}let D={};if(z.response_code){let J=X.split(`
|
|
4
|
+
`).filter((w)=>w.includes(":"));for(let w of J){let[O,...U]=w.split(":");if(O&&U.length>0)D[O.trim()]=U.join(":").trim()}}return{success:!0,status:z.response_code||z.http_code,headers:D,body:Z,metrics:{duration:(z.time_total||0)*1000,size:z.size_download,dnsLookup:(z.time_namelookup||0)*1000,tcpConnection:(z.time_connect||0)*1000,tlsHandshake:(z.time_appconnect||0)*1000,firstByte:(z.time_starttransfer||0)*1000,download:(z.time_total||0)*1000}}}catch(K){return{success:!1,error:K instanceof Error?K.message:String(K)}}}}class j{colors;constructor($){this.colors=$}color($,K){if(!K||!this.colors[K])return $;return`${this.colors[K]}${$}${this.colors.reset}`}render($,K=" "){$.forEach((Q,X)=>{let Z=X===$.length-1,z=Z?`${K}\u2514\u2500`:`${K}\u251C\u2500`;if(Q.label&&Q.value){let W=Q.color?this.color(Q.value,Q.color):Q.value,D=W.split(`
|
|
5
|
+
`);if(D.length===1)console.log(`${z} ${Q.label}: ${W}`);else{console.log(`${z} ${Q.label}:`);let J=Z?`${K} `:`${K}\u2502 `;D.forEach((w)=>{console.log(`${J}${w}`)})}}else if(Q.label&&!Q.value)console.log(`${z} ${Q.label}:`);else if(!Q.label&&Q.value){let W=Z?`${K} `:`${K}\u2502 `;console.log(`${W}${Q.value}`)}if(Q.children&&Q.children.length>0){let W=Z?`${K} `:`${K}\u2502 `;this.render(Q.children,W)}})}}class Y{config;colors={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",underscore:"\x1B[4m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgBlack:"\x1B[40m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m",bgBlue:"\x1B[44m",bgMagenta:"\x1B[45m",bgCyan:"\x1B[46m",bgWhite:"\x1B[47m"};constructor($={}){this.config={verbose:!1,showHeaders:!1,showBody:!0,showMetrics:!1,format:"pretty",prettyLevel:"minimal",...$}}color($,K){return`${this.colors[K]}${$}${this.colors.reset}`}getShortFilename($){return $.replace(/.*\//,"").replace(".yaml","")}shouldShowOutput(){if(this.config.format==="raw")return!1;if(this.config.format==="pretty")return!0;return this.config.verbose!==!1}shouldShowHeaders(){if(this.config.format!=="pretty")return this.config.showHeaders||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showHeaders||!1;case"detailed":return!0;default:return this.config.showHeaders||!1}}shouldShowBody(){if(this.config.format!=="pretty")return this.config.showBody!==!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showBody!==!1;case"detailed":return!0;default:return this.config.showBody!==!1}}shouldShowMetrics(){if(this.config.format!=="pretty")return this.config.showMetrics||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showMetrics||!1;case"detailed":return!0;default:return this.config.showMetrics||!1}}shouldShowRequestDetails(){if(this.config.format!=="pretty")return this.config.verbose||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.verbose||!1;case"detailed":return!0;default:return this.config.verbose||!1}}shouldShowSeparators(){if(this.config.format!=="pretty")return!0;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return!0;case"detailed":return!0;default:return!0}}colorStatusCode($){return this.color($,"yellow")}logValidationErrors($){let K=$.split("; ");if(K.length===1){let Q=K[0].trim(),X=Q.match(/^Expected status (.+?), got (.+)$/);if(X){let[,Z,z]=X,W=this.colorStatusCode(Z.replace(" or ","|")),D=this.color(z,"red");console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} Expected ${this.color("status","yellow")} ${W}, got ${D}`)}else console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} ${Q}`)}else{console.log(` ${this.color("\u2717","red")} ${this.color("Validation Errors:","red")}`);for(let Q of K){let X=Q.trim();if(X)if(X.startsWith("Expected ")){let Z=X.match(/^Expected status (.+?), got (.+)$/);if(Z){let[,z,W]=Z,D=this.colorStatusCode(z.replace(" or ","|")),J=this.color(W,"red");console.log(` ${this.color("\u2022","red")} ${this.color("status","yellow")}: expected ${D}, got ${J}`)}else{let z=X.match(/^Expected (.+?) to be (.+?), got (.+)$/);if(z){let[,W,D,J]=z;console.log(` ${this.color("\u2022","red")} ${this.color(W,"yellow")}: expected ${this.color(D,"green")}, got ${this.color(J,"red")}`)}else console.log(` ${this.color("\u2022","red")} ${X}`)}}else console.log(` ${this.color("\u2022","red")} ${X}`)}}}formatJson($){if(this.config.format==="raw")return typeof $==="string"?$:JSON.stringify($);if(this.config.format==="json")return JSON.stringify($);return JSON.stringify($,null,2)}formatDuration($){if($<1000)return`${$.toFixed(0)}ms`;return`${($/1000).toFixed(2)}s`}formatSize($){if(!$)return"0 B";let K=["B","KB","MB","GB"],Q=Math.floor(Math.log($)/Math.log(1024));return`${($/1024**Q).toFixed(2)} ${K[Q]}`}logExecutionStart($,K){if(!this.shouldShowOutput())return;if(this.shouldShowSeparators())console.log(),console.log(this.color(`Executing ${$} request(s) in ${K} mode`,"dim")),console.log();else console.log()}logRequestStart($,K){return}logCommand($){if(this.shouldShowRequestDetails())console.log(this.color(" Command:","dim")),console.log(this.color(` ${$}`,"dim"))}logRetry($,K){console.log(this.color(` \u21BB Retry ${$}/${K}...`,"yellow"))}logRequestComplete($){if(this.config.format==="raw"){if($.success&&this.config.showBody&&$.body){let D=this.formatJson($.body);console.log(D)}return}if(this.config.format==="json"){let D={request:{name:$.request.name,url:$.request.url,method:$.request.method||"GET"},success:$.success,status:$.status,...this.shouldShowHeaders()&&$.headers?{headers:$.headers}:{},...this.shouldShowBody()&&$.body?{body:$.body}:{},...$.error?{error:$.error}:{},...this.shouldShowMetrics()&&$.metrics?{metrics:$.metrics}:{}};console.log(JSON.stringify(D,null,2));return}if(!this.shouldShowOutput())return;let K=this.config.prettyLevel||"minimal",Q=$.success?"green":"red",X=$.success?"\u2713":"x",Z=$.request.name||"Request";if(K==="minimal"){let D=$.request.sourceFile?this.getShortFilename($.request.sourceFile):"inline";console.log(`${this.color(X,Q)} ${this.color(Z,"bright")} [${D}]`);let J=[],w=new j(this.colors);J.push({label:$.request.method||"GET",value:$.request.url,color:"blue"});let O=$.status?`${$.status}`:"ERROR";if(J.push({label:`${X} Status`,value:O,color:Q}),$.metrics){let U=`${this.formatDuration($.metrics.duration)} | ${this.formatSize($.metrics.size)}`;J.push({label:"Duration",value:U,color:"cyan"})}if(w.render(J),$.error)console.log(),this.logValidationErrors($.error);console.log();return}console.log(`${this.color(X,Q)} ${this.color(Z,"bright")}`);let z=[],W=new j(this.colors);if(z.push({label:"URL",value:$.request.url,color:"blue"}),z.push({label:"Method",value:$.request.method||"GET",color:"yellow"}),z.push({label:"Status",value:String($.status||"ERROR"),color:Q}),$.metrics)z.push({label:"Duration",value:this.formatDuration($.metrics.duration),color:"cyan"});if(this.shouldShowHeaders()&&$.headers&&Object.keys($.headers).length>0){let D=Object.entries($.headers).map(([J,w])=>({label:this.color(J,"dim"),value:String(w)}));z.push({label:"Headers",children:D})}if(this.shouldShowBody()&&$.body){let J=this.formatJson($.body).split(`
|
|
6
|
+
`),w=this.shouldShowRequestDetails()?1/0:10,O=J.slice(0,w);if(J.length>w)O.push(this.color(`... (${J.length-w} more lines)`,"dim"));z.push({label:"Response Body",value:O.join(`
|
|
7
|
+
`)})}if(this.shouldShowMetrics()&&$.metrics&&K==="detailed"){let D=$.metrics,J=[];if(J.push({label:"Request Duration",value:this.formatDuration(D.duration),color:"cyan"}),D.size!==void 0)J.push({label:"Response Size",value:this.formatSize(D.size),color:"cyan"});if(D.dnsLookup)J.push({label:"DNS Lookup",value:this.formatDuration(D.dnsLookup),color:"cyan"});if(D.tcpConnection)J.push({label:"TCP Connection",value:this.formatDuration(D.tcpConnection),color:"cyan"});if(D.tlsHandshake)J.push({label:"TLS Handshake",value:this.formatDuration(D.tlsHandshake),color:"cyan"});if(D.firstByte)J.push({label:"Time to First Byte",value:this.formatDuration(D.firstByte),color:"cyan"});z.push({label:"Metrics",children:J})}if(W.render(z),$.error)console.log(),this.logValidationErrors($.error);console.log()}logSummary($,K=!1){if(this.config.format==="raw")return;if(this.config.format==="json"){let D={summary:{total:$.total,successful:$.successful,failed:$.failed,duration:$.duration},results:$.results.map((J)=>({request:{name:J.request.name,url:J.request.url,method:J.request.method||"GET"},success:J.success,status:J.status,...this.shouldShowHeaders()&&J.headers?{headers:J.headers}:{},...this.shouldShowBody()&&J.body?{body:J.body}:{},...J.error?{error:J.error}:{},...this.shouldShowMetrics()&&J.metrics?{metrics:J.metrics}:{}}))};console.log(JSON.stringify(D,null,2));return}if(!this.shouldShowOutput())return;let Q=this.config.prettyLevel||"minimal";if(K)console.log();if(Q==="minimal"){let D=$.failed===0?"green":"red",J=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`;console.log(`${K?"\u25C6 Global Summary":"Summary"}: ${this.color(J,D)}`);return}let X=($.successful/$.total*100).toFixed(1),Z=$.failed===0?"green":"red",z=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`,W=K?"\u25C6 Global Summary":"Summary";if(console.log(),console.log(`${W}: ${this.color(z,Z)} (${this.color(this.formatDuration($.duration),"cyan")})`),$.failed>0&&this.shouldShowRequestDetails())$.results.filter((D)=>!D.success).forEach((D)=>{let J=D.request.name||D.request.url;console.log(` ${this.color("\u2022","red")} ${J}: ${D.error}`)})}logError($){console.error(this.color(`\u2717 ${$}`,"red"))}logWarning($){console.warn(this.color(`\u26A0 ${$}`,"yellow"))}logInfo($){console.log(this.color(`\u2139 ${$}`,"blue"))}logSuccess($){console.log(this.color(`\u2713 ${$}`,"green"))}logFileHeader($,K){if(!this.shouldShowOutput()||this.config.format!=="pretty")return;let Q=$.replace(/.*\//,"").replace(".yaml","");console.log(),console.log(this.color(`\u25B6 ${Q}.yaml`,"bright")+this.color(` (${K} request${K===1?"":"s"})`,"dim"))}}function g($,K){let Q=K.split("."),X=$;for(let Z of Q){if(X===null||X===void 0)return;if(typeof X!=="object")return;let z=Z.match(/^(\w+)\[(\d+)\]$/);if(z){let[,W,D]=z,J=Number.parseInt(D,10);if(X=X[W],Array.isArray(X))X=X[J];else return}else if(/^\d+$/.test(Z)&&Array.isArray(X))X=X[Number.parseInt(Z,10)];else X=X[Z]}return X}function p($){if($===void 0||$===null)return"";if(typeof $==="string")return $;if(typeof $==="number"||typeof $==="boolean")return String($);return JSON.stringify($)}function N($,K){let Q={},X={status:$.status,headers:$.headers||{},body:$.body,metrics:$.metrics};for(let[Z,z]of Object.entries(K)){let W=g(X,z);Q[Z]=p(W)}return Q}function P(){return{}}class B{logger;globalConfig;constructor($={}){this.globalConfig=$,this.logger=new Y($.output)}mergeOutputConfig($){return{...this.globalConfig.output,...$.sourceOutputConfig}}async executeRequest($,K=0){let Q=performance.now(),X=this.mergeOutputConfig($),Z=new Y(X);Z.logRequestStart($,K);let z=M.buildCommand($);Z.logCommand(z);let W=0,D,J=($.retry?.count||0)+1;while(W<J){if(W>0){if(Z.logRetry(W,J-1),$.retry?.delay)await Bun.sleep($.retry.delay)}let O=await M.executeCurl(z);if(O.success){let U=O.body;try{if(O.headers?.["content-type"]?.includes("application/json")||U&&(U.trim().startsWith("{")||U.trim().startsWith("[")))U=JSON.parse(U)}catch(I){}let G={request:$,success:!0,status:O.status,headers:O.headers,body:U,metrics:{...O.metrics,duration:performance.now()-Q}};if($.expect){let I=this.validateResponse(G,$.expect);if(!I.success)G.success=!1,G.error=I.error}return Z.logRequestComplete(G),G}D=O.error,W++}let w={request:$,success:!1,error:D,metrics:{duration:performance.now()-Q}};return Z.logRequestComplete(w),w}validateResponse($,K){if(!K)return{success:!0};let Q=[];if(K.status!==void 0){let Z=Array.isArray(K.status)?K.status:[K.status];if(!Z.includes($.status||0))Q.push(`Expected status ${Z.join(" or ")}, got ${$.status}`)}if(K.headers)for(let[Z,z]of Object.entries(K.headers)){let W=$.headers?.[Z]||$.headers?.[Z.toLowerCase()];if(W!==z)Q.push(`Expected header ${Z}="${z}", got "${W}"`)}if(K.body!==void 0){let Z=this.validateBodyProperties($.body,K.body,"");if(Z.length>0)Q.push(...Z)}if(K.responseTime!==void 0&&$.metrics){let Z=$.metrics.duration;if(!this.validateRangePattern(Z,K.responseTime))Q.push(`Expected response time to match ${K.responseTime}ms, got ${Z.toFixed(2)}ms`)}let X=Q.length>0;if(K.failure===!0){if(X)return{success:!1,error:Q.join("; ")};let Z=$.status||0;if(Z>=400)return{success:!0};else return{success:!1,error:`Expected request to fail (4xx/5xx) but got status ${Z}`}}else if(X)return{success:!1,error:Q.join("; ")};else return{success:!0}}validateBodyProperties($,K,Q){let X=[];if(typeof K!=="object"||K===null){let Z=this.validateValue($,K,Q||"body");if(!Z.isValid)X.push(Z.error);return X}if(Array.isArray(K)){let Z=this.validateValue($,K,Q||"body");if(!Z.isValid)X.push(Z.error);return X}for(let[Z,z]of Object.entries(K)){let W=Q?`${Q}.${Z}`:Z,D;if(Array.isArray($)&&this.isArraySelector(Z))D=this.getArrayValue($,Z);else D=$?.[Z];if(typeof z==="object"&&z!==null&&!Array.isArray(z)){let J=this.validateBodyProperties(D,z,W);X.push(...J)}else{let J=this.validateValue(D,z,W);if(!J.isValid)X.push(J.error)}}return X}validateValue($,K,Q){if(K==="*")return{isValid:!0};if(Array.isArray(K)){if(!K.some((Z)=>{if(Z==="*")return!0;if(typeof Z==="string"&&this.isRegexPattern(Z))return this.validateRegexPattern($,Z);if(typeof Z==="string"&&this.isRangePattern(Z))return this.validateRangePattern($,Z);return $===Z}))return{isValid:!1,error:`Expected ${Q} to match one of ${JSON.stringify(K)}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof K==="string"&&this.isRegexPattern(K)){if(!this.validateRegexPattern($,K))return{isValid:!1,error:`Expected ${Q} to match pattern ${K}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof K==="string"&&this.isRangePattern(K)){if(!this.validateRangePattern($,K))return{isValid:!1,error:`Expected ${Q} to match range ${K}, got ${JSON.stringify($)}`};return{isValid:!0}}if(K==="null"||K===null){if($!==null)return{isValid:!1,error:`Expected ${Q} to be null, got ${JSON.stringify($)}`};return{isValid:!0}}if($!==K)return{isValid:!1,error:`Expected ${Q} to be ${JSON.stringify(K)}, got ${JSON.stringify($)}`};return{isValid:!0}}isRegexPattern($){return $.startsWith("^")||$.endsWith("$")||$.includes("\\d")||$.includes("\\w")||$.includes("\\s")||$.includes("[")||$.includes("*")||$.includes("+")||$.includes("?")}validateRegexPattern($,K){let Q=String($);try{return new RegExp(K).test(Q)}catch{return!1}}isRangePattern($){return/^(>=?|<=?|>|<)\s*[\d.-]+(\s*,\s*(>=?|<=?|>|<)\s*[\d.-]+)*$/.test($)}validateRangePattern($,K){let Q=Number($);if(Number.isNaN(Q))return!1;let X=K.match(/^([\d.-]+)\s*-\s*([\d.-]+)$/);if(X){let z=Number(X[1]),W=Number(X[2]);return Q>=z&&Q<=W}return K.split(",").map((z)=>z.trim()).every((z)=>{let W=z.match(/^(>=?|<=?|>|<)\s*([\d.-]+)$/);if(!W)return!1;let D=W[1],J=Number(W[2]);switch(D){case">":return Q>J;case">=":return Q>=J;case"<":return Q<J;case"<=":return Q<=J;default:return!1}})}isArraySelector($){return/^\[.*\]$/.test($)||$==="*"||$.startsWith("slice(")}getArrayValue($,K){if(K==="*")return $;if(K.startsWith("[")&&K.endsWith("]")){let Q=K.slice(1,-1);if(Q==="*")return $;let X=Number(Q);if(!Number.isNaN(X))return X>=0?$[X]:$[$.length+X]}if(K.startsWith("slice(")){let Q=K.match(/slice\((\d+)(?:,(\d+))?\)/);if(Q){let X=Number(Q[1]),Z=Q[2]?Number(Q[2]):void 0;return $.slice(X,Z)}}return}async executeSequential($){let K=performance.now(),Q=[],X=P();for(let Z=0;Z<$.length;Z++){let z=this.interpolateStoreVariables($[Z],X),W=await this.executeRequest(z,Z+1);if(Q.push(W),W.success&&z.store){let D=N(W,z.store);Object.assign(X,D),this.logStoredValues(D)}if(!W.success&&!this.globalConfig.continueOnError){this.logger.logError("Stopping execution due to error");break}}return this.createSummary(Q,performance.now()-K)}interpolateStoreVariables($,K){if(Object.keys(K).length===0)return $;return H.interpolateVariables($,{},K)}logStoredValues($){if(Object.keys($).length===0)return;let K=Object.entries($);for(let[Q,X]of K){let Z=X.length>50?`${X.substring(0,50)}...`:X;this.logger.logInfo(`Stored: ${Q} = "${Z}"`)}}async executeParallel($){let K=performance.now(),Q=$.map((Z,z)=>this.executeRequest(Z,z+1)),X=await Promise.all(Q);return this.createSummary(X,performance.now()-K)}async execute($){this.logger.logExecutionStart($.length,this.globalConfig.execution||"sequential");let K=this.globalConfig.execution==="parallel"?await this.executeParallel($):await this.executeSequential($);if(this.logger.logSummary(K),this.globalConfig.output?.saveToFile)await this.saveSummaryToFile(K);return K}createSummary($,K){let Q=$.filter((Z)=>Z.success).length,X=$.filter((Z)=>!Z.success).length;return{total:$.length,successful:Q,failed:X,duration:K,results:$}}async saveSummaryToFile($){let K=this.globalConfig.output?.saveToFile;if(!K)return;let Q=JSON.stringify($,null,2);await Bun.write(K,Q),this.logger.logInfo(`Results saved to ${K}`)}}function A(){if(typeof BUILD_VERSION<"u")return BUILD_VERSION;if(process.env.CURL_RUNNER_VERSION)return process.env.CURL_RUNNER_VERSION;try{let $=["../package.json","./package.json","../../package.json"];for(let K of $)try{let Q=S(K);if(Q.name==="@curl-runner/cli"&&Q.version)return Q.version}catch{}return"0.0.0"}catch{return"0.0.0"}}var h={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",underscore:"\x1B[4m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgBlack:"\x1B[40m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m",bgBlue:"\x1B[44m",bgMagenta:"\x1B[45m",bgCyan:"\x1B[46m",bgWhite:"\x1B[47m"};function F($,K){return`${h[K]}${$}${h.reset}`}var b=`${process.env.HOME}/.curl-runner-version-cache.json`,d=86400000,m="https://registry.npmjs.org/@curl-runner/cli/latest";class L{async checkForUpdates($=!1){try{if(process.env.CI)return;let K=A();if(K==="0.0.0")return;if(!$){let X=await this.getCachedVersion();if(X&&Date.now()-X.lastCheck<d){this.compareVersions(K,X.latestVersion);return}}let Q=await this.fetchLatestVersion();if(Q)await this.setCachedVersion(Q),this.compareVersions(K,Q)}catch{}}async fetchLatestVersion(){try{let $=await fetch(m,{signal:AbortSignal.timeout(3000)});if(!$.ok)return null;return(await $.json()).version}catch{return null}}compareVersions($,K){if(this.isNewerVersion($,K))console.log(),console.log(F("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E","yellow")),console.log(F("\u2502","yellow")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" "+F("\uD83D\uDCE6 New version available!","bright")+` ${F($,"red")} \u2192 ${F(K,"green")} `+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" Update with: "+F("npm install -g @curl-runner/cli","cyan")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" or: "+F("bun install -g @curl-runner/cli","cyan")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" "+F("\u2502","yellow")),console.log(F("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F","yellow")),console.log()}isNewerVersion($,K){try{let Q=$.replace(/^v/,""),X=K.replace(/^v/,""),Z=Q.split(".").map(Number),z=X.split(".").map(Number);for(let W=0;W<Math.max(Z.length,z.length);W++){let D=Z[W]||0,J=z[W]||0;if(J>D)return!0;if(J<D)return!1}return!1}catch{return!1}}async getCachedVersion(){try{let $=Bun.file(b);if(await $.exists())return JSON.parse(await $.text())}catch{}return null}async setCachedVersion($){try{let K={lastCheck:Date.now(),latestVersion:$};await Bun.write(b,JSON.stringify(K))}catch{}}}class V{logger=new Y;async loadConfigFile(){let $=["curl-runner.yaml","curl-runner.yml",".curl-runner.yaml",".curl-runner.yml"];for(let K of $)try{if(await Bun.file(K).exists()){let X=await H.parseFile(K),Z=X.global||X;return this.logger.logInfo(`Loaded configuration from ${K}`),Z}}catch(Q){this.logger.logWarning(`Failed to load configuration from ${K}: ${Q}`)}return{}}loadEnvironmentVariables(){let $={};if(process.env.CURL_RUNNER_TIMEOUT)$.defaults={...$.defaults,timeout:Number.parseInt(process.env.CURL_RUNNER_TIMEOUT,10)};if(process.env.CURL_RUNNER_RETRIES)$.defaults={...$.defaults,retry:{...$.defaults?.retry,count:Number.parseInt(process.env.CURL_RUNNER_RETRIES,10)}};if(process.env.CURL_RUNNER_RETRY_DELAY)$.defaults={...$.defaults,retry:{...$.defaults?.retry,delay:Number.parseInt(process.env.CURL_RUNNER_RETRY_DELAY,10)}};if(process.env.CURL_RUNNER_VERBOSE)$.output={...$.output,verbose:process.env.CURL_RUNNER_VERBOSE.toLowerCase()==="true"};if(process.env.CURL_RUNNER_EXECUTION)$.execution=process.env.CURL_RUNNER_EXECUTION;if(process.env.CURL_RUNNER_CONTINUE_ON_ERROR)$.continueOnError=process.env.CURL_RUNNER_CONTINUE_ON_ERROR.toLowerCase()==="true";if(process.env.CURL_RUNNER_OUTPUT_FORMAT){let K=process.env.CURL_RUNNER_OUTPUT_FORMAT;if(["json","pretty","raw"].includes(K))$.output={...$.output,format:K}}if(process.env.CURL_RUNNER_PRETTY_LEVEL){let K=process.env.CURL_RUNNER_PRETTY_LEVEL;if(["minimal","standard","detailed"].includes(K))$.output={...$.output,prettyLevel:K}}if(process.env.CURL_RUNNER_OUTPUT_FILE)$.output={...$.output,saveToFile:process.env.CURL_RUNNER_OUTPUT_FILE};return $}async run($){try{let{files:K,options:Q}=this.parseArguments($);if(!Q.version&&!Q.help)new L().checkForUpdates().catch(()=>{});if(Q.help){this.showHelp();return}if(Q.version){console.log(`curl-runner v${A()}`);return}let X=this.loadEnvironmentVariables(),Z=await this.loadConfigFile(),z=await this.findYamlFiles(K,Q);if(z.length===0)this.logger.logError("No YAML files found"),process.exit(1);this.logger.logInfo(`Found ${z.length} YAML file(s)`);let W=this.mergeGlobalConfigs(X,Z),D=[],J=[];for(let U of z){this.logger.logInfo(`Processing: ${U}`);let{requests:G,config:I}=await this.processYamlFile(U),E=I?.output||{},_=G.map((T)=>({...T,sourceOutputConfig:E,sourceFile:U}));if(I){let{...T}=I;W=this.mergeGlobalConfigs(W,T)}J.push({file:U,requests:_,config:I}),D.push(..._)}if(Q.execution)W.execution=Q.execution;if(Q.continueOnError!==void 0)W.continueOnError=Q.continueOnError;if(Q.verbose!==void 0)W.output={...W.output,verbose:Q.verbose};if(Q.quiet!==void 0)W.output={...W.output,verbose:!1};if(Q.output)W.output={...W.output,saveToFile:Q.output};if(Q.outputFormat)W.output={...W.output,format:Q.outputFormat};if(Q.prettyLevel)W.output={...W.output,prettyLevel:Q.prettyLevel};if(Q.showHeaders!==void 0)W.output={...W.output,showHeaders:Q.showHeaders};if(Q.showBody!==void 0)W.output={...W.output,showBody:Q.showBody};if(Q.showMetrics!==void 0)W.output={...W.output,showMetrics:Q.showMetrics};if(Q.timeout)W.defaults={...W.defaults,timeout:Q.timeout};if(Q.retries||Q.noRetry){let U=Q.noRetry?0:Q.retries||0;W.defaults={...W.defaults,retry:{...W.defaults?.retry,count:U}}}if(Q.retryDelay)W.defaults={...W.defaults,retry:{...W.defaults?.retry,delay:Q.retryDelay}};if(D.length===0)this.logger.logError("No requests found in YAML files"),process.exit(1);let w=new B(W),O;if(J.length>1){let U=[],G=0;for(let _=0;_<J.length;_++){let T=J[_];this.logger.logFileHeader(T.file,T.requests.length);let R=await w.execute(T.requests);if(U.push(...R.results),G+=R.duration,_<J.length-1)console.log()}let I=U.filter((_)=>_.success).length,E=U.filter((_)=>!_.success).length;O={total:U.length,successful:I,failed:E,duration:G,results:U},w.logger.logSummary(O,!0)}else O=await w.execute(D);process.exit(O.failed>0&&!W.continueOnError?1:0)}catch(K){this.logger.logError(K instanceof Error?K.message:String(K)),process.exit(1)}}parseArguments($){let K={},Q=[];for(let X=0;X<$.length;X++){let Z=$[X];if(Z.startsWith("--")){let z=Z.slice(2),W=$[X+1];if(z==="help"||z==="version")K[z]=!0;else if(z==="no-retry")K.noRetry=!0;else if(z==="quiet")K.quiet=!0;else if(z==="show-headers")K.showHeaders=!0;else if(z==="show-body")K.showBody=!0;else if(z==="show-metrics")K.showMetrics=!0;else if(W&&!W.startsWith("--")){if(z==="continue-on-error")K.continueOnError=W==="true";else if(z==="verbose")K.verbose=W==="true";else if(z==="timeout")K.timeout=Number.parseInt(W,10);else if(z==="retries")K.retries=Number.parseInt(W,10);else if(z==="retry-delay")K.retryDelay=Number.parseInt(W,10);else if(z==="output-format"){if(["json","pretty","raw"].includes(W))K.outputFormat=W}else if(z==="pretty-level"){if(["minimal","standard","detailed"].includes(W))K.prettyLevel=W}else K[z]=W;X++}else K[z]=!0}else if(Z.startsWith("-")){let z=Z.slice(1);for(let W of z)switch(W){case"h":K.help=!0;break;case"v":K.verbose=!0;break;case"p":K.execution="parallel";break;case"c":K.continueOnError=!0;break;case"q":K.quiet=!0;break;case"o":{let D=$[X+1];if(D&&!D.startsWith("-"))K.output=D,X++;break}}}else Q.push(Z)}return{files:Q,options:K}}async findYamlFiles($,K){let Q=new Set,X=[];if($.length===0)X=K.all?["**/*.yaml","**/*.yml"]:["*.yaml","*.yml"];else for(let Z of $)try{let W=await(await import("fs/promises")).stat(Z);if(W.isDirectory()){if(X.push(`${Z}/*.yaml`,`${Z}/*.yml`),K.all)X.push(`${Z}/**/*.yaml`,`${Z}/**/*.yml`)}else if(W.isFile())X.push(Z)}catch{X.push(Z)}for(let Z of X){let z=new c(Z);for await(let W of z.scan("."))if(W.endsWith(".yaml")||W.endsWith(".yml"))Q.add(W)}return Array.from(Q).sort()}async processYamlFile($){let K=await H.parseFile($),Q=[],X;if(K.global)X=K.global;let Z={...K.global?.variables,...K.collection?.variables},z={...K.global?.defaults,...K.collection?.defaults};if(K.request){let W=this.prepareRequest(K.request,Z,z);Q.push(W)}if(K.requests)for(let W of K.requests){let D=this.prepareRequest(W,Z,z);Q.push(D)}if(K.collection?.requests)for(let W of K.collection.requests){let D=this.prepareRequest(W,Z,z);Q.push(D)}return{requests:Q,config:X}}prepareRequest($,K,Q){let X=H.interpolateVariables($,K);return H.mergeConfigs(Q,X)}mergeGlobalConfigs($,K){return{...$,...K,variables:{...$.variables,...K.variables},output:{...$.output,...K.output},defaults:{...$.defaults,...K.defaults}}}showHelp(){console.log(`
|
|
8
8
|
${this.logger.color("\uD83D\uDE80 CURL RUNNER","bright")}
|
|
9
9
|
|
|
10
10
|
${this.logger.color("USAGE:","yellow")}
|
|
@@ -79,7 +79,7 @@ ${this.logger.color("YAML STRUCTURE:","yellow")}
|
|
|
79
79
|
requests:
|
|
80
80
|
- url: \${BASE_URL}/users
|
|
81
81
|
method: GET
|
|
82
|
-
`)}}var
|
|
82
|
+
`)}}var i=new V;i.run(process.argv.slice(2));
|
|
83
83
|
|
|
84
|
-
//# debugId=
|
|
84
|
+
//# debugId=8CFAFA63D966D23664756E2164756E21
|
|
85
85
|
//# sourceMappingURL=cli.js.map
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { YamlParser } from '../parser/yaml';
|
|
1
2
|
import type {
|
|
2
3
|
ExecutionResult,
|
|
3
4
|
ExecutionSummary,
|
|
4
5
|
GlobalConfig,
|
|
5
6
|
JsonValue,
|
|
6
7
|
RequestConfig,
|
|
8
|
+
ResponseStoreContext,
|
|
7
9
|
} from '../types/config';
|
|
8
10
|
import { CurlBuilder } from '../utils/curl-builder';
|
|
9
11
|
import { Logger } from '../utils/logger';
|
|
12
|
+
import { createStoreContext, extractStoreValues } from '../utils/response-store';
|
|
10
13
|
|
|
11
14
|
export class RequestExecutor {
|
|
12
15
|
private logger: Logger;
|
|
@@ -423,11 +426,21 @@ export class RequestExecutor {
|
|
|
423
426
|
async executeSequential(requests: RequestConfig[]): Promise<ExecutionSummary> {
|
|
424
427
|
const startTime = performance.now();
|
|
425
428
|
const results: ExecutionResult[] = [];
|
|
429
|
+
const storeContext = createStoreContext();
|
|
426
430
|
|
|
427
431
|
for (let i = 0; i < requests.length; i++) {
|
|
428
|
-
|
|
432
|
+
// Interpolate store variables before execution
|
|
433
|
+
const interpolatedRequest = this.interpolateStoreVariables(requests[i], storeContext);
|
|
434
|
+
const result = await this.executeRequest(interpolatedRequest, i + 1);
|
|
429
435
|
results.push(result);
|
|
430
436
|
|
|
437
|
+
// Store values from successful responses
|
|
438
|
+
if (result.success && interpolatedRequest.store) {
|
|
439
|
+
const storedValues = extractStoreValues(result, interpolatedRequest.store);
|
|
440
|
+
Object.assign(storeContext, storedValues);
|
|
441
|
+
this.logStoredValues(storedValues);
|
|
442
|
+
}
|
|
443
|
+
|
|
431
444
|
if (!result.success && !this.globalConfig.continueOnError) {
|
|
432
445
|
this.logger.logError('Stopping execution due to error');
|
|
433
446
|
break;
|
|
@@ -437,6 +450,39 @@ export class RequestExecutor {
|
|
|
437
450
|
return this.createSummary(results, performance.now() - startTime);
|
|
438
451
|
}
|
|
439
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Interpolates store variables (${store.variableName}) in a request config.
|
|
455
|
+
* This is called at execution time to resolve values from previous responses.
|
|
456
|
+
*/
|
|
457
|
+
private interpolateStoreVariables(
|
|
458
|
+
request: RequestConfig,
|
|
459
|
+
storeContext: ResponseStoreContext,
|
|
460
|
+
): RequestConfig {
|
|
461
|
+
// Only interpolate if there are stored values
|
|
462
|
+
if (Object.keys(storeContext).length === 0) {
|
|
463
|
+
return request;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Re-interpolate the request with store context
|
|
467
|
+
// We pass empty variables since static variables were already resolved
|
|
468
|
+
return YamlParser.interpolateVariables(request, {}, storeContext) as RequestConfig;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Logs stored values for debugging purposes.
|
|
473
|
+
*/
|
|
474
|
+
private logStoredValues(values: ResponseStoreContext): void {
|
|
475
|
+
if (Object.keys(values).length === 0) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const entries = Object.entries(values);
|
|
480
|
+
for (const [key, value] of entries) {
|
|
481
|
+
const displayValue = value.length > 50 ? `${value.substring(0, 50)}...` : value;
|
|
482
|
+
this.logger.logInfo(`Stored: ${key} = "${displayValue}"`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
440
486
|
async executeParallel(requests: RequestConfig[]): Promise<ExecutionSummary> {
|
|
441
487
|
const startTime = performance.now();
|
|
442
488
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { YamlParser } from './yaml';
|
|
3
|
+
|
|
4
|
+
describe('YamlParser.interpolateVariables with store context', () => {
|
|
5
|
+
test('should resolve store variables', () => {
|
|
6
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
|
|
7
|
+
const obj = { url: 'https://api.example.com/users/${store.userId}' };
|
|
8
|
+
const variables = {};
|
|
9
|
+
const storeContext = { userId: '123' };
|
|
10
|
+
|
|
11
|
+
const result = YamlParser.interpolateVariables(obj, variables, storeContext);
|
|
12
|
+
expect(result).toEqual({ url: 'https://api.example.com/users/123' });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('should resolve multiple store variables in one string', () => {
|
|
16
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
|
|
17
|
+
const obj = { url: 'https://api.example.com/users/${store.userId}/posts/${store.postId}' };
|
|
18
|
+
const variables = {};
|
|
19
|
+
const storeContext = { userId: '123', postId: '456' };
|
|
20
|
+
|
|
21
|
+
const result = YamlParser.interpolateVariables(obj, variables, storeContext);
|
|
22
|
+
expect(result).toEqual({ url: 'https://api.example.com/users/123/posts/456' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should resolve store variables in nested objects', () => {
|
|
26
|
+
const obj = {
|
|
27
|
+
headers: {
|
|
28
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
|
|
29
|
+
Authorization: 'Bearer ${store.token}',
|
|
30
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
|
|
31
|
+
'X-User-Id': '${store.userId}',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const variables = {};
|
|
35
|
+
const storeContext = { token: 'jwt-token', userId: '123' };
|
|
36
|
+
|
|
37
|
+
const result = YamlParser.interpolateVariables(obj, variables, storeContext);
|
|
38
|
+
expect(result).toEqual({
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: 'Bearer jwt-token',
|
|
41
|
+
'X-User-Id': '123',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should resolve store variables in arrays', () => {
|
|
47
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
|
|
48
|
+
const obj = { ids: ['${store.id1}', '${store.id2}'] };
|
|
49
|
+
const variables = {};
|
|
50
|
+
const storeContext = { id1: '1', id2: '2' };
|
|
51
|
+
|
|
52
|
+
const result = YamlParser.interpolateVariables(obj, variables, storeContext);
|
|
53
|
+
expect(result).toEqual({ ids: ['1', '2'] });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should keep unresolved store variables as-is', () => {
|
|
57
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
|
|
58
|
+
const obj = { url: 'https://api.example.com/users/${store.missing}' };
|
|
59
|
+
const variables = {};
|
|
60
|
+
const storeContext = {};
|
|
61
|
+
|
|
62
|
+
const result = YamlParser.interpolateVariables(obj, variables, storeContext);
|
|
63
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
|
|
64
|
+
expect(result).toEqual({ url: 'https://api.example.com/users/${store.missing}' });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should mix store variables with static variables', () => {
|
|
68
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
|
|
69
|
+
const obj = { url: '${BASE_URL}/users/${store.userId}' };
|
|
70
|
+
const variables = { BASE_URL: 'https://api.example.com' };
|
|
71
|
+
const storeContext = { userId: '123' };
|
|
72
|
+
|
|
73
|
+
const result = YamlParser.interpolateVariables(obj, variables, storeContext);
|
|
74
|
+
expect(result).toEqual({ url: 'https://api.example.com/users/123' });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should work without store context (backwards compatibility)', () => {
|
|
78
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
|
|
79
|
+
const obj = { url: '${BASE_URL}/users' };
|
|
80
|
+
const variables = { BASE_URL: 'https://api.example.com' };
|
|
81
|
+
|
|
82
|
+
const result = YamlParser.interpolateVariables(obj, variables);
|
|
83
|
+
expect(result).toEqual({ url: 'https://api.example.com/users' });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('should resolve single store variable as exact value', () => {
|
|
87
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
|
|
88
|
+
const obj = { userId: '${store.userId}' };
|
|
89
|
+
const variables = {};
|
|
90
|
+
const storeContext = { userId: '123' };
|
|
91
|
+
|
|
92
|
+
const result = YamlParser.interpolateVariables(obj, variables, storeContext);
|
|
93
|
+
expect(result).toEqual({ userId: '123' });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('should mix store variables with dynamic variables', () => {
|
|
97
|
+
const obj = {
|
|
98
|
+
headers: {
|
|
99
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
|
|
100
|
+
'X-Request-ID': '${UUID}',
|
|
101
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
|
|
102
|
+
Authorization: 'Bearer ${store.token}',
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
const variables = {};
|
|
106
|
+
const storeContext = { token: 'my-token' };
|
|
107
|
+
|
|
108
|
+
const result = YamlParser.interpolateVariables(obj, variables, storeContext) as typeof obj;
|
|
109
|
+
// UUID should be a valid UUID string
|
|
110
|
+
expect(result.headers['X-Request-ID']).toMatch(
|
|
111
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
112
|
+
);
|
|
113
|
+
expect(result.headers.Authorization).toBe('Bearer my-token');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('should resolve store variables in request body', () => {
|
|
117
|
+
const obj = {
|
|
118
|
+
body: {
|
|
119
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
|
|
120
|
+
userId: '${store.userId}',
|
|
121
|
+
name: 'Test User',
|
|
122
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
|
|
123
|
+
parentId: '${store.parentId}',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
const variables = {};
|
|
127
|
+
const storeContext = { userId: '123', parentId: '456' };
|
|
128
|
+
|
|
129
|
+
const result = YamlParser.interpolateVariables(obj, variables, storeContext);
|
|
130
|
+
expect(result).toEqual({
|
|
131
|
+
body: {
|
|
132
|
+
userId: '123',
|
|
133
|
+
name: 'Test User',
|
|
134
|
+
parentId: '456',
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('YamlParser.resolveVariable', () => {
|
|
141
|
+
test('should resolve store variable', () => {
|
|
142
|
+
const storeContext = { userId: '123' };
|
|
143
|
+
const result = YamlParser.resolveVariable('store.userId', {}, storeContext);
|
|
144
|
+
expect(result).toBe('123');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('should return null for missing store variable', () => {
|
|
148
|
+
const storeContext = { other: 'value' };
|
|
149
|
+
const result = YamlParser.resolveVariable('store.missing', {}, storeContext);
|
|
150
|
+
expect(result).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('should resolve dynamic variable', () => {
|
|
154
|
+
const result = YamlParser.resolveVariable('UUID', {}, {});
|
|
155
|
+
expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('should resolve static variable', () => {
|
|
159
|
+
const variables = { BASE_URL: 'https://api.example.com' };
|
|
160
|
+
const result = YamlParser.resolveVariable('BASE_URL', variables, {});
|
|
161
|
+
expect(result).toBe('https://api.example.com');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('should return null for unknown variable', () => {
|
|
165
|
+
const result = YamlParser.resolveVariable('UNKNOWN', {}, {});
|
|
166
|
+
expect(result).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('should prioritize store context over static variables', () => {
|
|
170
|
+
// This test ensures store.X prefix is properly handled
|
|
171
|
+
const variables = { 'store.userId': 'static-value' };
|
|
172
|
+
const storeContext = { userId: 'store-value' };
|
|
173
|
+
const result = YamlParser.resolveVariable('store.userId', variables, storeContext);
|
|
174
|
+
expect(result).toBe('store-value');
|
|
175
|
+
});
|
|
176
|
+
});
|
package/src/parser/yaml.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { YAML } from 'bun';
|
|
2
|
-
import type { RequestConfig, YamlFile } from '../types/config';
|
|
2
|
+
import type { RequestConfig, ResponseStoreContext, YamlFile } from '../types/config';
|
|
3
3
|
|
|
4
4
|
// Using class for organization, but could be refactored to functions
|
|
5
5
|
export class YamlParser {
|
|
@@ -13,31 +13,45 @@ export class YamlParser {
|
|
|
13
13
|
return YAML.parse(content) as YamlFile;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Interpolates variables in an object, supporting:
|
|
18
|
+
* - Static variables: ${VAR_NAME}
|
|
19
|
+
* - Dynamic variables: ${UUID}, ${TIMESTAMP}, ${DATE:format}, ${TIME:format}
|
|
20
|
+
* - Stored response values: ${store.variableName}
|
|
21
|
+
*
|
|
22
|
+
* @param obj - The object to interpolate
|
|
23
|
+
* @param variables - Static variables map
|
|
24
|
+
* @param storeContext - Optional stored response values from previous requests
|
|
25
|
+
*/
|
|
26
|
+
static interpolateVariables(
|
|
27
|
+
obj: unknown,
|
|
28
|
+
variables: Record<string, string>,
|
|
29
|
+
storeContext?: ResponseStoreContext,
|
|
30
|
+
): unknown {
|
|
17
31
|
if (typeof obj === 'string') {
|
|
18
32
|
// Check if it's a single variable like ${VAR} (no other characters)
|
|
19
33
|
const singleVarMatch = obj.match(/^\$\{([^}]+)\}$/);
|
|
20
34
|
if (singleVarMatch) {
|
|
21
35
|
const varName = singleVarMatch[1];
|
|
22
|
-
const
|
|
23
|
-
return
|
|
36
|
+
const resolvedValue = YamlParser.resolveVariable(varName, variables, storeContext);
|
|
37
|
+
return resolvedValue !== null ? resolvedValue : obj;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
// Handle multiple variables in the string using regex replacement
|
|
27
41
|
return obj.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
|
28
|
-
const
|
|
29
|
-
return
|
|
42
|
+
const resolvedValue = YamlParser.resolveVariable(varName, variables, storeContext);
|
|
43
|
+
return resolvedValue !== null ? resolvedValue : match;
|
|
30
44
|
});
|
|
31
45
|
}
|
|
32
46
|
|
|
33
47
|
if (Array.isArray(obj)) {
|
|
34
|
-
return obj.map((item) => YamlParser.interpolateVariables(item, variables));
|
|
48
|
+
return obj.map((item) => YamlParser.interpolateVariables(item, variables, storeContext));
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
if (obj && typeof obj === 'object') {
|
|
38
52
|
const result: Record<string, unknown> = {};
|
|
39
53
|
for (const [key, value] of Object.entries(obj)) {
|
|
40
|
-
result[key] = YamlParser.interpolateVariables(value, variables);
|
|
54
|
+
result[key] = YamlParser.interpolateVariables(value, variables, storeContext);
|
|
41
55
|
}
|
|
42
56
|
return result;
|
|
43
57
|
}
|
|
@@ -45,6 +59,38 @@ export class YamlParser {
|
|
|
45
59
|
return obj;
|
|
46
60
|
}
|
|
47
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Resolves a single variable reference.
|
|
64
|
+
* Priority: store context > dynamic variables > static variables
|
|
65
|
+
*/
|
|
66
|
+
static resolveVariable(
|
|
67
|
+
varName: string,
|
|
68
|
+
variables: Record<string, string>,
|
|
69
|
+
storeContext?: ResponseStoreContext,
|
|
70
|
+
): string | null {
|
|
71
|
+
// Check for store variable (${store.variableName})
|
|
72
|
+
if (varName.startsWith('store.') && storeContext) {
|
|
73
|
+
const storeVarName = varName.slice(6); // Remove 'store.' prefix
|
|
74
|
+
if (storeVarName in storeContext) {
|
|
75
|
+
return storeContext[storeVarName];
|
|
76
|
+
}
|
|
77
|
+
return null; // Store variable not found, return null to keep original
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for dynamic variable
|
|
81
|
+
const dynamicValue = YamlParser.resolveDynamicVariable(varName);
|
|
82
|
+
if (dynamicValue !== null) {
|
|
83
|
+
return dynamicValue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for static variable
|
|
87
|
+
if (varName in variables) {
|
|
88
|
+
return variables[varName];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
48
94
|
static resolveDynamicVariable(varName: string): string | null {
|
|
49
95
|
// UUID generation
|
|
50
96
|
if (varName === 'UUID') {
|
package/src/types/config.ts
CHANGED
|
@@ -4,6 +4,18 @@ export interface JsonObject {
|
|
|
4
4
|
}
|
|
5
5
|
export interface JsonArray extends Array<JsonValue> {}
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Configuration for storing response values as variables for subsequent requests.
|
|
9
|
+
* Maps a variable name to a JSON path in the response.
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* - `{ "userId": "body.id" }` - Store response body's id field as ${store.userId}
|
|
13
|
+
* - `{ "token": "body.data.token" }` - Store nested field
|
|
14
|
+
* - `{ "statusCode": "status" }` - Store HTTP status code
|
|
15
|
+
* - `{ "contentType": "headers.content-type" }` - Store response header
|
|
16
|
+
*/
|
|
17
|
+
export type StoreConfig = Record<string, string>;
|
|
18
|
+
|
|
7
19
|
export interface RequestConfig {
|
|
8
20
|
name?: string;
|
|
9
21
|
url: string;
|
|
@@ -29,6 +41,18 @@ export interface RequestConfig {
|
|
|
29
41
|
delay?: number;
|
|
30
42
|
};
|
|
31
43
|
variables?: Record<string, string>;
|
|
44
|
+
/**
|
|
45
|
+
* Store response values as variables for subsequent requests.
|
|
46
|
+
* Use JSON path syntax to extract values from the response.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* store:
|
|
50
|
+
* userId: body.id
|
|
51
|
+
* token: body.data.accessToken
|
|
52
|
+
* statusCode: status
|
|
53
|
+
* contentType: headers.content-type
|
|
54
|
+
*/
|
|
55
|
+
store?: StoreConfig;
|
|
32
56
|
expect?: {
|
|
33
57
|
failure?: boolean; // If true, expect the request to fail (for negative testing)
|
|
34
58
|
status?: number | number[];
|
|
@@ -104,3 +128,9 @@ export interface ExecutionSummary {
|
|
|
104
128
|
duration: number;
|
|
105
129
|
results: ExecutionResult[];
|
|
106
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Context for storing response values between sequential requests.
|
|
134
|
+
* Values are stored as strings and can be referenced using ${store.variableName} syntax.
|
|
135
|
+
*/
|
|
136
|
+
export type ResponseStoreContext = Record<string, string>;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { ExecutionResult } from '../types/config';
|
|
3
|
+
import {
|
|
4
|
+
createStoreContext,
|
|
5
|
+
extractStoreValues,
|
|
6
|
+
getValueByPath,
|
|
7
|
+
mergeStoreContext,
|
|
8
|
+
valueToString,
|
|
9
|
+
} from './response-store';
|
|
10
|
+
|
|
11
|
+
describe('getValueByPath', () => {
|
|
12
|
+
const testObj = {
|
|
13
|
+
status: 200,
|
|
14
|
+
body: {
|
|
15
|
+
id: 123,
|
|
16
|
+
user: {
|
|
17
|
+
name: 'John',
|
|
18
|
+
email: 'john@example.com',
|
|
19
|
+
},
|
|
20
|
+
items: [
|
|
21
|
+
{ id: 1, name: 'Item 1' },
|
|
22
|
+
{ id: 2, name: 'Item 2' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
headers: {
|
|
26
|
+
'content-type': 'application/json',
|
|
27
|
+
'x-request-id': 'abc123',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
test('should get top-level value', () => {
|
|
32
|
+
expect(getValueByPath(testObj, 'status')).toBe(200);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should get nested value', () => {
|
|
36
|
+
expect(getValueByPath(testObj, 'body.id')).toBe(123);
|
|
37
|
+
expect(getValueByPath(testObj, 'body.user.name')).toBe('John');
|
|
38
|
+
expect(getValueByPath(testObj, 'body.user.email')).toBe('john@example.com');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('should get header value', () => {
|
|
42
|
+
expect(getValueByPath(testObj, 'headers.content-type')).toBe('application/json');
|
|
43
|
+
expect(getValueByPath(testObj, 'headers.x-request-id')).toBe('abc123');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should get array element by index', () => {
|
|
47
|
+
expect(getValueByPath(testObj, 'body.items.0.id')).toBe(1);
|
|
48
|
+
expect(getValueByPath(testObj, 'body.items.1.name')).toBe('Item 2');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should get array element using bracket notation', () => {
|
|
52
|
+
expect(getValueByPath(testObj, 'body.items[0].id')).toBe(1);
|
|
53
|
+
expect(getValueByPath(testObj, 'body.items[1].name')).toBe('Item 2');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should return undefined for non-existent path', () => {
|
|
57
|
+
expect(getValueByPath(testObj, 'body.nonexistent')).toBeUndefined();
|
|
58
|
+
expect(getValueByPath(testObj, 'body.user.age')).toBeUndefined();
|
|
59
|
+
expect(getValueByPath(testObj, 'nonexistent.path')).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should return undefined for null or undefined object', () => {
|
|
63
|
+
expect(getValueByPath(null, 'any.path')).toBeUndefined();
|
|
64
|
+
expect(getValueByPath(undefined, 'any.path')).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should handle primitive values correctly', () => {
|
|
68
|
+
expect(getValueByPath('string', 'length')).toBeUndefined();
|
|
69
|
+
expect(getValueByPath(123, 'toString')).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('valueToString', () => {
|
|
74
|
+
test('should convert string to string', () => {
|
|
75
|
+
expect(valueToString('hello')).toBe('hello');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should convert number to string', () => {
|
|
79
|
+
expect(valueToString(123)).toBe('123');
|
|
80
|
+
expect(valueToString(45.67)).toBe('45.67');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should convert boolean to string', () => {
|
|
84
|
+
expect(valueToString(true)).toBe('true');
|
|
85
|
+
expect(valueToString(false)).toBe('false');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('should convert null and undefined to empty string', () => {
|
|
89
|
+
expect(valueToString(null)).toBe('');
|
|
90
|
+
expect(valueToString(undefined)).toBe('');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('should JSON stringify objects', () => {
|
|
94
|
+
expect(valueToString({ a: 1 })).toBe('{"a":1}');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('should JSON stringify arrays', () => {
|
|
98
|
+
expect(valueToString([1, 2, 3])).toBe('[1,2,3]');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('extractStoreValues', () => {
|
|
103
|
+
const mockResult: ExecutionResult = {
|
|
104
|
+
request: {
|
|
105
|
+
url: 'https://api.example.com/users',
|
|
106
|
+
method: 'POST',
|
|
107
|
+
},
|
|
108
|
+
success: true,
|
|
109
|
+
status: 201,
|
|
110
|
+
headers: {
|
|
111
|
+
'content-type': 'application/json',
|
|
112
|
+
'x-request-id': 'req-12345',
|
|
113
|
+
},
|
|
114
|
+
body: {
|
|
115
|
+
id: 456,
|
|
116
|
+
data: {
|
|
117
|
+
token: 'jwt-token-here',
|
|
118
|
+
user: {
|
|
119
|
+
id: 789,
|
|
120
|
+
name: 'Test User',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
metrics: {
|
|
125
|
+
duration: 150,
|
|
126
|
+
size: 1024,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
test('should extract status', () => {
|
|
131
|
+
const result = extractStoreValues(mockResult, {
|
|
132
|
+
statusCode: 'status',
|
|
133
|
+
});
|
|
134
|
+
expect(result.statusCode).toBe('201');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should extract body fields', () => {
|
|
138
|
+
const result = extractStoreValues(mockResult, {
|
|
139
|
+
userId: 'body.id',
|
|
140
|
+
token: 'body.data.token',
|
|
141
|
+
userName: 'body.data.user.name',
|
|
142
|
+
});
|
|
143
|
+
expect(result.userId).toBe('456');
|
|
144
|
+
expect(result.token).toBe('jwt-token-here');
|
|
145
|
+
expect(result.userName).toBe('Test User');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('should extract header values', () => {
|
|
149
|
+
const result = extractStoreValues(mockResult, {
|
|
150
|
+
contentType: 'headers.content-type',
|
|
151
|
+
requestId: 'headers.x-request-id',
|
|
152
|
+
});
|
|
153
|
+
expect(result.contentType).toBe('application/json');
|
|
154
|
+
expect(result.requestId).toBe('req-12345');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('should extract metrics', () => {
|
|
158
|
+
const result = extractStoreValues(mockResult, {
|
|
159
|
+
duration: 'metrics.duration',
|
|
160
|
+
});
|
|
161
|
+
expect(result.duration).toBe('150');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('should handle non-existent paths', () => {
|
|
165
|
+
const result = extractStoreValues(mockResult, {
|
|
166
|
+
missing: 'body.nonexistent',
|
|
167
|
+
});
|
|
168
|
+
expect(result.missing).toBe('');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should extract multiple values', () => {
|
|
172
|
+
const result = extractStoreValues(mockResult, {
|
|
173
|
+
id: 'body.id',
|
|
174
|
+
status: 'status',
|
|
175
|
+
contentType: 'headers.content-type',
|
|
176
|
+
});
|
|
177
|
+
expect(result.id).toBe('456');
|
|
178
|
+
expect(result.status).toBe('201');
|
|
179
|
+
expect(result.contentType).toBe('application/json');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('createStoreContext', () => {
|
|
184
|
+
test('should create empty context', () => {
|
|
185
|
+
const context = createStoreContext();
|
|
186
|
+
expect(context).toEqual({});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('mergeStoreContext', () => {
|
|
191
|
+
test('should merge contexts', () => {
|
|
192
|
+
const existing = { a: '1', b: '2' };
|
|
193
|
+
const newValues = { c: '3', d: '4' };
|
|
194
|
+
const merged = mergeStoreContext(existing, newValues);
|
|
195
|
+
expect(merged).toEqual({ a: '1', b: '2', c: '3', d: '4' });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('should override existing values', () => {
|
|
199
|
+
const existing = { a: '1', b: '2' };
|
|
200
|
+
const newValues = { b: 'new', c: '3' };
|
|
201
|
+
const merged = mergeStoreContext(existing, newValues);
|
|
202
|
+
expect(merged).toEqual({ a: '1', b: 'new', c: '3' });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('should not mutate original contexts', () => {
|
|
206
|
+
const existing = { a: '1' };
|
|
207
|
+
const newValues = { b: '2' };
|
|
208
|
+
const merged = mergeStoreContext(existing, newValues);
|
|
209
|
+
expect(existing).toEqual({ a: '1' });
|
|
210
|
+
expect(newValues).toEqual({ b: '2' });
|
|
211
|
+
expect(merged).toEqual({ a: '1', b: '2' });
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ExecutionResult, ResponseStoreContext, StoreConfig } from '../types/config';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts a value from an object using a dot-notation path.
|
|
5
|
+
* Supports paths like: "body.id", "body.data.token", "headers.content-type", "status"
|
|
6
|
+
*
|
|
7
|
+
* @param obj - The object to extract from
|
|
8
|
+
* @param path - Dot-notation path to the value
|
|
9
|
+
* @returns The extracted value or undefined if not found
|
|
10
|
+
*/
|
|
11
|
+
export function getValueByPath(obj: unknown, path: string): unknown {
|
|
12
|
+
const parts = path.split('.');
|
|
13
|
+
let current: unknown = obj;
|
|
14
|
+
|
|
15
|
+
for (const part of parts) {
|
|
16
|
+
if (current === null || current === undefined) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof current !== 'object') {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle array index access like "items.0.id" or "items[0].id"
|
|
25
|
+
const arrayMatch = part.match(/^(\w+)\[(\d+)\]$/);
|
|
26
|
+
if (arrayMatch) {
|
|
27
|
+
const [, key, indexStr] = arrayMatch;
|
|
28
|
+
const index = Number.parseInt(indexStr, 10);
|
|
29
|
+
current = (current as Record<string, unknown>)[key];
|
|
30
|
+
if (Array.isArray(current)) {
|
|
31
|
+
current = current[index];
|
|
32
|
+
} else {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
} else if (/^\d+$/.test(part) && Array.isArray(current)) {
|
|
36
|
+
// Direct numeric index for arrays
|
|
37
|
+
current = current[Number.parseInt(part, 10)];
|
|
38
|
+
} else {
|
|
39
|
+
current = (current as Record<string, unknown>)[part];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return current;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Converts a value to a string for storage.
|
|
48
|
+
* Objects and arrays are JSON stringified.
|
|
49
|
+
*/
|
|
50
|
+
export function valueToString(value: unknown): string {
|
|
51
|
+
if (value === undefined || value === null) {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === 'string') {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
58
|
+
return String(value);
|
|
59
|
+
}
|
|
60
|
+
return JSON.stringify(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extracts values from an execution result based on the store configuration.
|
|
65
|
+
*
|
|
66
|
+
* @param result - The execution result to extract from
|
|
67
|
+
* @param storeConfig - Configuration mapping variable names to JSON paths
|
|
68
|
+
* @returns Object containing the extracted values as strings
|
|
69
|
+
*/
|
|
70
|
+
export function extractStoreValues(
|
|
71
|
+
result: ExecutionResult,
|
|
72
|
+
storeConfig: StoreConfig,
|
|
73
|
+
): ResponseStoreContext {
|
|
74
|
+
const extracted: ResponseStoreContext = {};
|
|
75
|
+
|
|
76
|
+
// Build an object that represents the full response structure
|
|
77
|
+
const responseObj: Record<string, unknown> = {
|
|
78
|
+
status: result.status,
|
|
79
|
+
headers: result.headers || {},
|
|
80
|
+
body: result.body,
|
|
81
|
+
metrics: result.metrics,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const [varName, path] of Object.entries(storeConfig)) {
|
|
85
|
+
const value = getValueByPath(responseObj, path);
|
|
86
|
+
extracted[varName] = valueToString(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return extracted;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Creates a new response store context.
|
|
94
|
+
*/
|
|
95
|
+
export function createStoreContext(): ResponseStoreContext {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Merges new values into an existing store context.
|
|
101
|
+
* New values override existing ones with the same key.
|
|
102
|
+
*/
|
|
103
|
+
export function mergeStoreContext(
|
|
104
|
+
existing: ResponseStoreContext,
|
|
105
|
+
newValues: ResponseStoreContext,
|
|
106
|
+
): ResponseStoreContext {
|
|
107
|
+
return { ...existing, ...newValues };
|
|
108
|
+
}
|