Skip to content

Commit 60685c4

Browse files
committed
feat: HTTP 파싱 및 생성 기능 추가
- HTTP 파싱 및 생성 기능을 ChainExecutor에 추가하여 URL 분석 및 생성 지원 - HTTP 파싱 기능은 URL의 구성 요소를 추출하고, 다양한 출력 필드를 지원 - HTTP 생성 기능은 주어진 파라미터로부터 URL을 생성하며, 경로 및 쿼리 템플릿을 지원 - ChainBuilderPage에서 HTTP 관련 설정 UI를 추가하여 사용자 편의성 향상 - 관련 상수 및 타입 정의를 업데이트하여 코드 일관성 유지
1 parent 53cdadc commit 60685c4

4 files changed

Lines changed: 363 additions & 15 deletions

File tree

src/main/chainExecutor.ts

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)