@curl-runner/cli 1.0.2 → 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 -6
- package/package.json +2 -2
- 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/logger.ts +252 -105
- package/src/utils/response-store.test.ts +213 -0
- package/src/utils/response-store.ts +108 -0
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var
|
|
4
|
-
`).filter((
|
|
5
|
-
`))console.log(`
|
|
6
|
-
`),Z=this.shouldShowRequestDetails()?1/0:10;for(let W of Q.slice(0,Z))console.log(` ${W}`);if(Q.length>Z)console.log(this.color(` ... (${Q.length-Z} more lines)`,"dim"))}console.log()}logSummary($,w=!1){if(this.config.format==="raw")return;if(this.config.format==="json"){let Q={summary:{total:$.total,successful:$.successful,failed:$.failed,duration:$.duration},results:$.results.map((Z)=>({request:{name:Z.request.name,url:Z.request.url,method:Z.request.method||"GET"},success:Z.success,status:Z.status,...this.shouldShowHeaders()&&Z.headers?{headers:Z.headers}:{},...this.shouldShowBody()&&Z.body?{body:Z.body}:{},...Z.error?{error:Z.error}:{},...this.shouldShowMetrics()&&Z.metrics?{metrics:Z.metrics}:{}}))};console.log(JSON.stringify(Q,null,2));return}if(!this.shouldShowOutput())return;if(this.shouldShowSeparators()){this.printSeparator("\u2550");let Q=w?"\uD83C\uDFAF OVERALL SUMMARY":"\uD83D\uDCCA EXECUTION SUMMARY";console.log(this.color(Q,"bright")),this.printSeparator()}let K=($.successful/$.total*100).toFixed(1),X=$.failed===0?"green":$.successful===0?"red":"yellow";if(console.log(` Total Requests: ${this.color(String($.total),"cyan")}`),console.log(` Successful: ${this.color(String($.successful),"green")}`),console.log(` Failed: ${this.color(String($.failed),"red")}`),console.log(` Success Rate: ${this.color(`${K}%`,X)}`),console.log(` Total Duration: ${this.color(this.formatDuration($.duration),"cyan")}`),$.failed>0&&this.shouldShowRequestDetails())console.log(),console.log(this.color(" Failed Requests:","red")),$.results.filter((Q)=>!Q.success).forEach((Q)=>{let Z=Q.request.name||Q.request.url;console.log(` \u2022 ${Z}: ${Q.error}`)});if(this.shouldShowSeparators())this.printSeparator("\u2550")}logError($){console.error(this.color(`\u274C ${$}`,"red"))}logWarning($){console.warn(this.color(`\u26A0\uFE0F ${$}`,"yellow"))}logInfo($){console.log(this.color(`\u2139\uFE0F ${$}`,"blue"))}logSuccess($){console.log(this.color(`\u2705 ${$}`,"green"))}logFileHeader($,w){if(!this.shouldShowOutput()||this.config.format!=="pretty")return;let K=$.replace(/.*\//,"").replace(".yaml","");console.log(),this.printSeparator("\u2500"),console.log(this.color(`\uD83D\uDCC4 ${K}.yaml`,"bright")+this.color(` (${w} request${w===1?"":"s"})`,"dim")),this.printSeparator("\u2500")}}class A{logger;globalConfig;constructor($={}){this.globalConfig=$,this.logger=new Y($.output)}mergeOutputConfig($){return{...this.globalConfig.output,...$.sourceOutputConfig}}async executeRequest($,w=0){let K=performance.now(),X=this.mergeOutputConfig($),Q=new Y(X);Q.logRequestStart($,w);let Z=L.buildCommand($);Q.logCommand(Z);let W=0,z,D=($.retry?.count||0)+1;while(W<D){if(W>0){if(Q.logRetry(W,D-1),$.retry?.delay)await Bun.sleep($.retry.delay)}let O=await L.executeCurl(Z);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(T){}let _={request:$,success:!0,status:O.status,headers:O.headers,body:J,metrics:{...O.metrics,duration:performance.now()-K}};if($.expect){let T=this.validateResponse(_,$.expect);if(!T.success)_.success=!1,_.error=T.error}return Q.logRequestComplete(_),_}z=O.error,W++}let G={request:$,success:!1,error:z,metrics:{duration:performance.now()-K}};return Q.logRequestComplete(G),G}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,Z]of Object.entries(w.headers)){let W=$.headers?.[Q]||$.headers?.[Q.toLowerCase()];if(W!==Z)K.push(`Expected header ${Q}="${Z}", got "${W}"`)}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,Z]of Object.entries(w)){let W=K?`${K}.${Q}`:Q,z;if(Array.isArray($)&&this.isArraySelector(Q))z=this.getArrayValue($,Q);else z=$?.[Q];if(typeof Z==="object"&&Z!==null&&!Array.isArray(Z)){let D=this.validateBodyProperties(z,Z,W);X.push(...D)}else{let D=this.validateValue(z,Z,W);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 Z=Number(X[1]),W=Number(X[2]);return K>=Z&&K<=W}return w.split(",").map((Z)=>Z.trim()).every((Z)=>{let W=Z.match(/^(>=?|<=?|>|<)\s*([\d.-]+)$/);if(!W)return!1;let z=W[1],D=Number(W[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,Z)=>this.executeRequest(Q,Z+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:R}=globalThis.Bun;class H{static async parseFile($){let K=await Bun.file($).text();return R.parse(K)}static parse($){return R.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 Z=H.resolveDynamicVariable(Q);return Z!==null?Z: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 M(){if(typeof BUILD_VERSION!=="undefined")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=j(w);if(K.name==="@curl-runner/cli"&&K.version)return K.version}catch{}return"0.0.0"}catch{return"0.0.0"}}var P={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 U($,w){return`${P[w]}${$}${P.reset}`}var k=`${process.env.HOME}/.curl-runner-version-cache.json`,x=86400000,y="https://registry.npmjs.org/@curl-runner/cli/latest";class B{async checkForUpdates($=!1){try{if(process.env.CI)return;let w=M();if(w==="0.0.0")return;if(!$){let X=await this.getCachedVersion();if(X&&Date.now()-X.lastCheck<x){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(y,{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(U("\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(U("\u2502","yellow")+" "+U("\u2502","yellow")),console.log(U("\u2502","yellow")+" "+U("\uD83D\uDCE6 New version available!","bright")+` ${U($,"red")} \u2192 ${U(w,"green")} `+U("\u2502","yellow")),console.log(U("\u2502","yellow")+" "+U("\u2502","yellow")),console.log(U("\u2502","yellow")+" Update with: "+U("npm install -g @curl-runner/cli","cyan")+" "+U("\u2502","yellow")),console.log(U("\u2502","yellow")+" or: "+U("bun install -g @curl-runner/cli","cyan")+" "+U("\u2502","yellow")),console.log(U("\u2502","yellow")+" "+U("\u2502","yellow")),console.log(U("\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),Z=X.split(".").map(Number);for(let W=0;W<Math.max(Q.length,Z.length);W++){let z=Q[W]||0,D=Z[W]||0;if(D>z)return!0;if(D<z)return!1}return!1}catch{return!1}}async getCachedVersion(){try{let $=Bun.file(k);if(await $.exists())return JSON.parse(await $.text())}catch{}return null}async setCachedVersion($){try{let w={lastCheck:Date.now(),latestVersion:$};await Bun.write(k,JSON.stringify(w))}catch{}}}class q{logger=new Y;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 B().checkForUpdates().catch(()=>{});if(K.help){this.showHelp();return}if(K.version){console.log(`curl-runner v${M()}`);return}let X=this.loadEnvironmentVariables(),Q=await this.loadConfigFile(),Z=await this.findYamlFiles(w,K);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,Q),z=[],D=[];for(let J of Z){this.logger.logInfo(`Processing: ${J}`);let{requests:_,config:T}=await this.processYamlFile(J),S=T?.output||{},F=_.map((I)=>({...I,sourceOutputConfig:S,sourceFile:J}));if(T){let{...I}=T;W=this.mergeGlobalConfigs(W,I)}D.push({file:J,requests:F,config:T}),z.push(...F)}if(K.execution)W.execution=K.execution;if(K.continueOnError!==void 0)W.continueOnError=K.continueOnError;if(K.verbose!==void 0)W.output={...W.output,verbose:K.verbose};if(K.quiet!==void 0)W.output={...W.output,verbose:!1};if(K.output)W.output={...W.output,saveToFile:K.output};if(K.outputFormat)W.output={...W.output,format:K.outputFormat};if(K.prettyLevel)W.output={...W.output,prettyLevel:K.prettyLevel};if(K.showHeaders!==void 0)W.output={...W.output,showHeaders:K.showHeaders};if(K.showBody!==void 0)W.output={...W.output,showBody:K.showBody};if(K.showMetrics!==void 0)W.output={...W.output,showMetrics:K.showMetrics};if(K.timeout)W.defaults={...W.defaults,timeout:K.timeout};if(K.retries||K.noRetry){let J=K.noRetry?0:K.retries||0;W.defaults={...W.defaults,retry:{...W.defaults?.retry,count:J}}}if(K.retryDelay)W.defaults={...W.defaults,retry:{...W.defaults?.retry,delay:K.retryDelay}};if(z.length===0)this.logger.logError("No requests found in YAML files"),process.exit(1);let G=new A(W),O;if(D.length>1){let J=[],_=0;for(let F=0;F<D.length;F++){let I=D[F];this.logger.logFileHeader(I.file,I.requests.length);let E=await G.execute(I.requests);if(J.push(...E.results),_+=E.duration,F<D.length-1)console.log()}let T=J.filter((F)=>F.success).length,S=J.filter((F)=>!F.success).length;O={total:J.length,successful:T,failed:S,duration:_,results:J},G.logger.logSummary(O,!0)}else O=await G.execute(z);process.exit(O.failed>0&&!W.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 Z=Q.slice(2),W=$[X+1];if(Z==="help"||Z==="version")w[Z]=!0;else if(Z==="no-retry")w.noRetry=!0;else if(Z==="quiet")w.quiet=!0;else if(Z==="show-headers")w.showHeaders=!0;else if(Z==="show-body")w.showBody=!0;else if(Z==="show-metrics")w.showMetrics=!0;else if(W&&!W.startsWith("--")){if(Z==="continue-on-error")w.continueOnError=W==="true";else if(Z==="verbose")w.verbose=W==="true";else if(Z==="timeout")w.timeout=Number.parseInt(W,10);else if(Z==="retries")w.retries=Number.parseInt(W,10);else if(Z==="retry-delay")w.retryDelay=Number.parseInt(W,10);else if(Z==="output-format"){if(["json","pretty","raw"].includes(W))w.outputFormat=W}else if(Z==="pretty-level"){if(["minimal","standard","detailed"].includes(W))w.prettyLevel=W}else w[Z]=W;X++}else w[Z]=!0}else if(Q.startsWith("-")){let Z=Q.slice(1);for(let W of Z)switch(W){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 W=await(await import("fs/promises")).stat(Q);if(W.isDirectory()){if(X.push(`${Q}/*.yaml`,`${Q}/*.yml`),w.all)X.push(`${Q}/**/*.yaml`,`${Q}/**/*.yml`)}else if(W.isFile())X.push(Q)}catch{X.push(Q)}for(let Q of X){let Z=new d(Q);for await(let W of Z.scan("."))if(W.endsWith(".yaml")||W.endsWith(".yml"))K.add(W)}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},Z={...w.global?.defaults,...w.collection?.defaults};if(w.request){let W=this.prepareRequest(w.request,Q,Z);K.push(W)}if(w.requests)for(let W of w.requests){let z=this.prepareRequest(W,Q,Z);K.push(z)}if(w.collection?.requests)for(let W of w.collection.requests){let z=this.prepareRequest(W,Q,Z);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(`
|
|
7
8
|
${this.logger.color("\uD83D\uDE80 CURL RUNNER","bright")}
|
|
8
9
|
|
|
9
10
|
${this.logger.color("USAGE:","yellow")}
|
|
@@ -78,7 +79,7 @@ ${this.logger.color("YAML STRUCTURE:","yellow")}
|
|
|
78
79
|
requests:
|
|
79
80
|
- url: \${BASE_URL}/users
|
|
80
81
|
method: GET
|
|
81
|
-
`)}}var
|
|
82
|
+
`)}}var i=new V;i.run(process.argv.slice(2));
|
|
82
83
|
|
|
83
|
-
//# debugId=
|
|
84
|
+
//# debugId=8CFAFA63D966D23664756E2164756E21
|
|
84
85
|
//# sourceMappingURL=cli.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@curl-runner/cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A powerful CLI tool for HTTP request management using YAML configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cli.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "bun run dist/cli.js",
|
|
12
|
-
"dev": "bun
|
|
12
|
+
"dev": "bun run src/cli.ts",
|
|
13
13
|
"build": "bun run scripts/build-with-version.ts",
|
|
14
14
|
"format": "biome format --write .",
|
|
15
15
|
"lint": "biome lint .",
|
|
@@ -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>;
|