@@ -62,6 +62,14 @@ export class ChainExecutor {
6262 output = await this . rsaDecrypt ( input , step ) ;
6363 break ;
6464
65+ case 'http-parse' :
66+ output = await this . httpParse ( input , step ) ;
67+ break ;
68+
69+ case 'http-build' :
70+ output = await this . httpBuild ( input , step ) ;
71+ break ;
72+
6573 default :
6674 throw new Error ( `Unsupported step type: ${ step . type } ` ) ;
6775 }
@@ -149,6 +157,205 @@ export class ChainExecutor {
149157 }
150158 }
151159
160+ private async httpParse ( input : string , step : ChainStep ) : Promise < string > {
161+ try {
162+ const url = new URL ( input . trim ( ) ) ;
163+ const pathTemplate = step . params ?. pathTemplate || '' ;
164+ const queryTemplate = step . params ?. queryTemplate || '' ;
165+ const outputField = step . params ?. outputField || 'full' ;
166+
167+ // Parse path parameters
168+ const pathParams : Record < string , string > = { } ;
169+ if ( pathTemplate ) {
170+ const templateParts = pathTemplate . split ( '/' ) . filter ( part => part ) ;
171+ const urlParts = url . pathname . split ( '/' ) . filter ( part => part ) ;
172+
173+ templateParts . forEach ( ( templatePart , index ) => {
174+ if ( templatePart . startsWith ( ':' ) ) {
175+ const paramName = templatePart . substring ( 1 ) ;
176+ if ( urlParts [ index ] ) {
177+ pathParams [ paramName ] = decodeURIComponent ( urlParts [ index ] ) ;
178+ }
179+ } else if ( templatePart . startsWith ( '{' ) && templatePart . endsWith ( '}' ) ) {
180+ const paramName = templatePart . slice ( 1 , - 1 ) ;
181+ if ( urlParts [ index ] ) {
182+ pathParams [ paramName ] = decodeURIComponent ( urlParts [ index ] ) ;
183+ }
184+ }
185+ } ) ;
186+ }
187+
188+ // Parse query parameters
189+ const queryParams : Record < string , string > = { } ;
190+ if ( queryTemplate ) {
191+ try {
192+ // Parse query template as JSON array
193+ const queryKeys = JSON . parse ( queryTemplate ) ;
194+ if ( Array . isArray ( queryKeys ) ) {
195+ const urlParams = new URLSearchParams ( url . search ) ;
196+ queryKeys . forEach ( key => {
197+ const value = urlParams . get ( key ) ;
198+ if ( value !== null ) {
199+ queryParams [ key ] = value ;
200+ }
201+ } ) ;
202+ }
203+ } catch {
204+ // Fallback: get all query parameters
205+ url . searchParams . forEach ( ( value , key ) => {
206+ queryParams [ key ] = value ;
207+ } ) ;
208+ }
209+ } else {
210+ // No template: get all query parameters
211+ url . searchParams . forEach ( ( value , key ) => {
212+ queryParams [ key ] = value ;
213+ } ) ;
214+ }
215+
216+ // Return specified output field
217+ switch ( outputField ) {
218+ case 'host' :
219+ return url . host ;
220+ case 'pathname' :
221+ return url . pathname ;
222+ case 'pathParams' :
223+ return JSON . stringify ( pathParams ) ;
224+ case 'queryParams' :
225+ return JSON . stringify ( queryParams ) ;
226+ case 'full' :
227+ default :
228+ return JSON . stringify ( {
229+ protocol : url . protocol ,
230+ host : url . host ,
231+ pathname : url . pathname ,
232+ search : url . search ,
233+ hash : url . hash ,
234+ pathParams,
235+ queryParams
236+ } ) ;
237+ }
238+ } catch ( error ) {
239+ throw new Error ( `HTTP parsing failed: ${ error instanceof Error ? error . message : 'Invalid URL' } ` ) ;
240+ }
241+ }
242+
243+ private async httpBuild ( input : string , step : ChainStep ) : Promise < string > {
244+ try {
245+ const baseUrl = step . params ?. baseUrl || '' ;
246+ const pathTemplate = step . params ?. pathTemplate || '' ;
247+ const queryTemplate = step . params ?. queryTemplate || '' ;
248+ const inputMapping = step . params ?. inputMapping || 'auto' ;
249+
250+ if ( ! baseUrl ) {
251+ throw new Error ( 'Base URL is required for HTTP build' ) ;
252+ }
253+
254+ // Parse input based on mapping strategy
255+ let pathParams : Record < string , string > = { } ;
256+ let queryParams : Record < string , string > = { } ;
257+
258+ if ( inputMapping === 'auto' ) {
259+ // Try to parse input as JSON
260+ try {
261+ const inputData = JSON . parse ( input ) ;
262+ if ( typeof inputData === 'object' && inputData !== null ) {
263+ pathParams = inputData ;
264+ queryParams = inputData ;
265+ }
266+ } catch {
267+ // If not JSON, treat as single value
268+ throw new Error ( 'Auto mapping requires JSON input' ) ;
269+ }
270+ } else if ( inputMapping . startsWith ( 'pathParam.' ) ) {
271+ // Map input to specific path parameter
272+ const paramName = inputMapping . substring ( 10 ) ;
273+ pathParams [ paramName ] = input ;
274+ } else if ( inputMapping . startsWith ( 'queryParam.' ) ) {
275+ // Map input to specific query parameter
276+ const paramName = inputMapping . substring ( 11 ) ;
277+ queryParams [ paramName ] = input ;
278+ } else if ( inputMapping === 'json' ) {
279+ // Parse input as full parameter object
280+ const inputData = JSON . parse ( input ) ;
281+ pathParams = inputData . pathParams || { } ;
282+ queryParams = inputData . queryParams || { } ;
283+ }
284+
285+ // Build URL
286+ let fullUrl = baseUrl ;
287+
288+ // Build path
289+ if ( pathTemplate ) {
290+ let pathPart = pathTemplate ;
291+
292+ // Replace path parameters
293+ Object . entries ( pathParams ) . forEach ( ( [ key , value ] ) => {
294+ pathPart = pathPart
295+ . replace ( `:${ key } ` , encodeURIComponent ( value ) )
296+ . replace ( `{${ key } }` , encodeURIComponent ( value ) ) ;
297+ } ) ;
298+
299+ // Ensure proper URL joining
300+ if ( ! fullUrl . endsWith ( '/' ) && ! pathPart . startsWith ( '/' ) ) {
301+ fullUrl += '/' ;
302+ }
303+ if ( fullUrl . endsWith ( '/' ) && pathPart . startsWith ( '/' ) ) {
304+ pathPart = pathPart . substring ( 1 ) ;
305+ }
306+
307+ fullUrl += pathPart ;
308+ }
309+
310+ // Build query string
311+ let queryString = '' ;
312+ if ( queryTemplate ) {
313+ try {
314+ // Parse query template as JSON array
315+ const queryKeys = JSON . parse ( queryTemplate ) ;
316+ if ( Array . isArray ( queryKeys ) ) {
317+ const queryParts : string [ ] = [ ] ;
318+ queryKeys . forEach ( key => {
319+ const paramValue = queryParams [ key ] ;
320+ if ( paramValue && paramValue . trim ( ) !== '' ) {
321+ queryParts . push ( `${ encodeURIComponent ( key ) } =${ encodeURIComponent ( paramValue ) } ` ) ;
322+ }
323+ } ) ;
324+ queryString = queryParts . join ( '&' ) ;
325+ }
326+ } catch {
327+ // Fallback: use all query parameters
328+ const queryEntries = Object . entries ( queryParams ) . filter ( ( [ _ , value ] ) => value . trim ( ) !== '' ) ;
329+ if ( queryEntries . length > 0 ) {
330+ const searchParams = new URLSearchParams ( ) ;
331+ queryEntries . forEach ( ( [ key , value ] ) => {
332+ searchParams . set ( key , value ) ;
333+ } ) ;
334+ queryString = searchParams . toString ( ) ;
335+ }
336+ }
337+ } else {
338+ // No template: use all query parameters
339+ const queryEntries = Object . entries ( queryParams ) . filter ( ( [ _ , value ] ) => value . trim ( ) !== '' ) ;
340+ if ( queryEntries . length > 0 ) {
341+ const searchParams = new URLSearchParams ( ) ;
342+ queryEntries . forEach ( ( [ key , value ] ) => {
343+ searchParams . set ( key , value ) ;
344+ } ) ;
345+ queryString = searchParams . toString ( ) ;
346+ }
347+ }
348+
349+ if ( queryString ) {
350+ fullUrl += `?${ queryString } ` ;
351+ }
352+
353+ return fullUrl ;
354+ } catch ( error ) {
355+ throw new Error ( `HTTP build failed: ${ error instanceof Error ? error . message : 'Unknown error' } ` ) ;
356+ }
357+ }
358+
152359 public async executeChain (
153360 steps : ChainStep [ ] ,
154361 inputText : string ,
@@ -258,6 +465,18 @@ export class ChainExecutor {
258465 category : 'crypto' ,
259466 requiredParams : [ 'keyId' ] ,
260467 } ,
468+ 'http-parse' : {
469+ name : 'HTTP 파싱' ,
470+ description : 'URL을 분석하여 구성 요소를 추출합니다' ,
471+ category : 'http' ,
472+ requiredParams : [ 'outputField' ] ,
473+ } ,
474+ 'http-build' : {
475+ name : 'HTTP 생성' ,
476+ description : '파라미터로부터 URL을 생성합니다' ,
477+ category : 'http' ,
478+ requiredParams : [ 'baseUrl' ] ,
479+ } ,
261480 } ;
262481 }
263482}
0 commit comments