Skip to content

Commit 5831edf

Browse files
stepzen import commands (#94)
* feat: implement comprehensive import enhancement system Add complete foundation for StepZen CLI import integration with VS Code extension. Implements all four import types with robust validation and error handling. ## New Features ### Import Service Architecture - Generalized ImportService with command builder pattern - Automatic import type detection and validation - Service registry integration with dependency injection - Comprehensive error handling and logging ### Import Types Supported - **cURL Import**: Parse cURL commands, extract headers, handle authentication - **OpenAPI Import**: Support for OpenAPI specs (files and URLs) - **GraphQL Import**: GraphQL endpoint introspection with authentication - **Database Import**: All database types (PostgreSQL, MySQL, Snowflake, etc.) ### Type System - Complete TypeScript interfaces for all import configurations - Proper type discrimination and validation - Support for all CLI options and parameters ### Testing - 213 passing tests with comprehensive coverage - Unit tests for all import types and validation logic - Error handling scenarios and edge cases - Query name generation and configuration building ## Technical Implementation - Command builder pattern for extensibility - Robust validation with proper error bubbling - Integration with existing service registry - Follows established architecture patterns - ESLint compliant with TypeScript strict mode ## Files Added/Modified - `src/types/import.ts` - Complete type definitions - `src/services/importService.ts` - Core import service implementation - `src/test/unit/services/importService.test.ts` - Comprehensive test suite - `src/test/unit/commands/importCurl.test.ts` - cURL parsing logic tests - `src/services/index.ts` - Service registry integration - `src/extension.ts` - Command registration This establishes the foundation for intelligent import enhancement features including cURL parsing, guided configuration, and schema optimization. * bug fixes
1 parent 9e08744 commit 5831edf

File tree

16 files changed

+3341
-191
lines changed

16 files changed

+3341
-191
lines changed

README.md

Lines changed: 160 additions & 186 deletions
Large diffs are not rendered by default.

docs/import-enhancement.md

Lines changed: 426 additions & 0 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,26 @@
7070
"title": "Add Tool Directive",
7171
"category": "StepZen"
7272
},
73+
{
74+
"command": "stepzen.importCurl",
75+
"title": "Import cURL",
76+
"category": "StepZen"
77+
},
78+
{
79+
"command": "stepzen.importOpenapi",
80+
"title": "Import OpenAPI",
81+
"category": "StepZen"
82+
},
83+
{
84+
"command": "stepzen.importGraphql",
85+
"title": "Import GraphQL",
86+
"category": "StepZen"
87+
},
88+
{
89+
"command": "stepzen.importDatabase",
90+
"title": "Import Database",
91+
"category": "StepZen"
92+
},
7393
{
7494
"command": "stepzen.openSchemaVisualizer",
7595
"title": "Open Schema Visualizer",

src/commands/importCurl.ts

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/**
2+
* Copyright IBM Corp. 2025
3+
* Assisted by CursorAI
4+
*/
5+
6+
import * as vscode from "vscode";
7+
import { services } from "../services";
8+
import { handleError } from "../errors";
9+
import { CurlImportConfig } from "../types/import";
10+
11+
/**
12+
* Command to import a REST endpoint using cURL syntax
13+
*/
14+
export async function importCurl(): Promise<void> {
15+
try {
16+
services.logger.info("Starting cURL import");
17+
18+
// 1. Check workspace trust
19+
if (!vscode.workspace.isTrusted) {
20+
vscode.window.showWarningMessage(
21+
"Import features not available in untrusted workspaces"
22+
);
23+
return;
24+
}
25+
26+
// 2. Collect cURL configuration from user
27+
const config = await collectCurlConfiguration();
28+
if (!config) {
29+
services.logger.info("cURL import cancelled by user");
30+
return;
31+
}
32+
33+
// 3. Execute the import using the generalized import service
34+
const result = await services.import.executeImport(config);
35+
36+
// 4. Handle results
37+
if (result.success) {
38+
vscode.window.showInformationMessage(
39+
`Schema imported successfully to ${result.targetDir}/${result.schemaName}`
40+
);
41+
42+
// TODO: Offer Phase 2 functional enhancements
43+
// await offerFunctionalEnhancements(result);
44+
} else {
45+
vscode.window.showErrorMessage(`Import failed: ${result.error}`);
46+
}
47+
48+
services.logger.info("cURL import completed");
49+
} catch (err) {
50+
handleError(err);
51+
}
52+
}
53+
54+
/**
55+
* Collect cURL import configuration from the user through VS Code UI
56+
*/
57+
async function collectCurlConfiguration(): Promise<CurlImportConfig | undefined> {
58+
// Step 1: Get cURL command or endpoint
59+
const curlInput = await vscode.window.showInputBox({
60+
prompt: "Paste your cURL command or enter the endpoint URL",
61+
placeHolder: "curl -H 'Authorization: Bearer token' https://api.example.com/users",
62+
ignoreFocusOut: true,
63+
});
64+
65+
if (!curlInput) {
66+
return undefined;
67+
}
68+
69+
// Step 2: Parse cURL command or use as endpoint
70+
const parsedConfig = parseCurlCommand(curlInput);
71+
72+
// Step 3: Collect additional configuration
73+
const name = await vscode.window.showInputBox({
74+
prompt: "Schema name (folder name for generated files)",
75+
value: parsedConfig.suggestedName,
76+
placeHolder: "api_example_com",
77+
ignoreFocusOut: true,
78+
});
79+
80+
if (!name) {
81+
return undefined;
82+
}
83+
84+
const queryName = await vscode.window.showInputBox({
85+
prompt: "Query field name (GraphQL field name)",
86+
value: parsedConfig.suggestedQueryName,
87+
placeHolder: "users",
88+
ignoreFocusOut: true,
89+
});
90+
91+
if (!queryName) {
92+
return undefined;
93+
}
94+
95+
// Step 4: Advanced options (optional)
96+
const showAdvanced = await vscode.window.showQuickPick(
97+
["No", "Yes"],
98+
{
99+
placeHolder: "Configure advanced options?",
100+
ignoreFocusOut: true,
101+
}
102+
);
103+
104+
let advancedConfig = {};
105+
if (showAdvanced === "Yes") {
106+
advancedConfig = await collectAdvancedOptions();
107+
}
108+
109+
// Step 5: Build final configuration
110+
const config: CurlImportConfig = {
111+
endpoint: parsedConfig.endpoint || curlInput, // Fallback to original input
112+
name,
113+
queryName,
114+
nonInteractive: true, // Always use non-interactive mode
115+
...parsedConfig,
116+
...advancedConfig,
117+
};
118+
119+
return config;
120+
}
121+
122+
/**
123+
* Parse a cURL command or endpoint URL into configuration
124+
*/
125+
function parseCurlCommand(input: string): Partial<CurlImportConfig> {
126+
const trimmed = input.trim();
127+
128+
// Simple URL case
129+
if (trimmed.startsWith('http')) {
130+
return parseSimpleUrl(trimmed);
131+
}
132+
133+
// cURL command case
134+
if (trimmed.startsWith('curl')) {
135+
return parseFullCurlCommand(trimmed);
136+
}
137+
138+
// Assume it's a URL if it doesn't start with curl
139+
return parseSimpleUrl(trimmed);
140+
}
141+
142+
/**
143+
* Parse a simple URL into configuration
144+
*/
145+
function parseSimpleUrl(url: string): Partial<CurlImportConfig> {
146+
try {
147+
const parsed = new URL(url);
148+
const hostname = parsed.hostname.replace(/\./g, '_');
149+
const pathSegments = parsed.pathname.split('/').filter(Boolean);
150+
151+
return {
152+
endpoint: url,
153+
suggestedName: hostname,
154+
suggestedQueryName: generateQueryName(pathSegments),
155+
};
156+
} catch (err) {
157+
services.logger.warn(`Failed to parse URL: ${url}`, err);
158+
return {
159+
endpoint: url,
160+
suggestedName: 'imported_api',
161+
suggestedQueryName: 'data',
162+
};
163+
}
164+
}
165+
166+
/**
167+
* Parse a full cURL command into configuration
168+
*/
169+
function parseFullCurlCommand(curlCommand: string): Partial<CurlImportConfig> {
170+
// Basic parsing - extract URL and headers
171+
const urlMatch = curlCommand.match(/https?:\/\/[^\s"']+/);
172+
let url = urlMatch ? urlMatch[0] : '';
173+
174+
// Remove any trailing quotes that might have been captured
175+
url = url.replace(/["']$/, '');
176+
177+
// Extract headers
178+
const headerMatches = curlCommand.matchAll(/(?:-H|--header)\s+['"]([^'"]+)['"]/g);
179+
const headers: Array<{ name: string; value: string }> = [];
180+
const secrets: string[] = [];
181+
182+
for (const match of headerMatches) {
183+
const headerString = match[1];
184+
const colonIndex = headerString.indexOf(':');
185+
if (colonIndex > 0) {
186+
const name = headerString.substring(0, colonIndex).trim();
187+
const value = headerString.substring(colonIndex + 1).trim();
188+
189+
headers.push({ name, value });
190+
191+
// Auto-detect secrets
192+
if (isSecretHeader(name)) {
193+
secrets.push(name);
194+
}
195+
}
196+
}
197+
198+
// Extract data/body for POST requests
199+
let data: string | undefined;
200+
let method: string | undefined;
201+
202+
// Check for --data, -d, --data-raw, --data-ascii, --data-binary flags
203+
// Use a more robust regex that handles nested quotes and complex JSON
204+
const dataPattern = /(?:--data|--data-raw|--data-ascii|--data-binary|-d)\s+(['"])((?:(?!\1)[^\\]|\\.)*)(\1)/g;
205+
const dataMatch = dataPattern.exec(curlCommand);
206+
if (dataMatch) {
207+
data = dataMatch[2]; // The content between the quotes
208+
method = 'POST'; // Default to POST when data is present
209+
}
210+
211+
// Check for explicit method specification (-X or --request)
212+
const methodMatch = curlCommand.match(/(?:-X|--request)\s+([A-Z]+)/);
213+
if (methodMatch) {
214+
method = methodMatch[1];
215+
}
216+
217+
const baseConfig = parseSimpleUrl(url);
218+
219+
return {
220+
...baseConfig,
221+
headers: headers.length > 0 ? headers : undefined,
222+
secrets: secrets.length > 0 ? secrets : undefined,
223+
data,
224+
method,
225+
};
226+
}
227+
228+
/**
229+
* Generate a query name from URL path segments
230+
*/
231+
function generateQueryName(pathSegments: string[]): string {
232+
if (pathSegments.length === 0) {
233+
return 'data';
234+
}
235+
236+
// Use the last meaningful segment
237+
const lastSegment = pathSegments[pathSegments.length - 1];
238+
239+
// Remove common REST patterns
240+
const cleaned = lastSegment
241+
.replace(/\{[^}]+\}/g, '') // Remove {id} patterns
242+
.replace(/[^a-zA-Z]/g, ''); // Remove non-letters
243+
244+
return cleaned || 'data';
245+
}
246+
247+
/**
248+
* Check if a header name indicates it contains secrets
249+
*/
250+
function isSecretHeader(headerName: string): boolean {
251+
const secretPatterns = [
252+
'authorization',
253+
'x-api-key',
254+
'api-key',
255+
'token',
256+
'auth',
257+
'secret',
258+
];
259+
260+
const lowerName = headerName.toLowerCase();
261+
return secretPatterns.some(pattern => lowerName.includes(pattern));
262+
}
263+
264+
/**
265+
* Collect advanced configuration options
266+
*/
267+
async function collectAdvancedOptions(): Promise<Partial<CurlImportConfig>> {
268+
const options: Partial<CurlImportConfig> = {};
269+
270+
// Query type
271+
const queryType = await vscode.window.showInputBox({
272+
prompt: "Custom return type name (optional)",
273+
placeHolder: "User",
274+
ignoreFocusOut: true,
275+
});
276+
277+
if (queryType) {
278+
options.queryType = queryType;
279+
}
280+
281+
// Prefix
282+
const prefix = await vscode.window.showInputBox({
283+
prompt: "Type prefix (optional)",
284+
placeHolder: "Api",
285+
ignoreFocusOut: true,
286+
});
287+
288+
if (prefix) {
289+
options.prefix = prefix;
290+
}
291+
292+
// Path parameters
293+
const pathParams = await vscode.window.showInputBox({
294+
prompt: "Path parameters (optional)",
295+
placeHolder: "/users/$userId",
296+
ignoreFocusOut: true,
297+
});
298+
299+
if (pathParams) {
300+
options.pathParams = pathParams;
301+
}
302+
303+
return options;
304+
}

0 commit comments

Comments
 (0)