UNPKG

apx-toolkit

Version:

Automatically discover APIs and generate complete integration packages: code in 12 languages, TypeScript types, test suites, SDK packages, API documentation, mock servers, performance reports, and contract tests. Saves 2-4 weeks of work in seconds.

463 lines (398 loc) 15.5 kB
/** * SDK Package Generator * Generates complete SDK packages ready to publish * Makes the tool a developer's dream with production-ready SDKs */ import type { DiscoveredAPI } from '../types.js'; // Note: 'url' is used as a variable name in Go code generation // This import is for TypeScript URL parsing only export type SDKLanguage = 'typescript' | 'python' | 'go'; export interface SDKPackage { language: SDKLanguage; packageName: string; files: Record<string, string>; description: string; } /** * Generates complete SDK packages for discovered APIs */ export function generateSDKPackages( apis: DiscoveredAPI[], packageName?: string, baseUrl?: string ): SDKPackage[] { const packages: SDKPackage[] = []; if (apis.length === 0) { return packages; } const url = new URL(apis[0].baseUrl); const defaultPackageName = packageName || `api-client-${url.hostname.replace(/\./g, '-')}`; const apiBaseUrl = baseUrl || `${url.protocol}//${url.host}`; packages.push(generateTypeScriptSDK(apis, defaultPackageName, apiBaseUrl)); packages.push(generatePythonSDK(apis, defaultPackageName, apiBaseUrl)); packages.push(generateGoSDK(apis, defaultPackageName, apiBaseUrl)); return packages; } function generateTypeScriptSDK( apis: DiscoveredAPI[], packageName: string, baseUrl: string ): SDKPackage { const files: Record<string, string> = {}; // package.json files['package.json'] = JSON.stringify( { name: packageName, version: '1.0.0', description: 'Auto-generated API client SDK', main: 'dist/index.js', types: 'dist/index.d.ts', scripts: { build: 'tsc', test: 'jest', 'test:ci': 'jest --ci --coverage', }, keywords: ['api', 'client', 'sdk', 'auto-generated'], author: '', license: 'MIT', dependencies: {}, devDependencies: { '@types/node': '^20.0.0', '@types/jest': '^29.5.0', typescript: '^5.0.0', jest: '^29.5.0', 'ts-jest': '^29.1.0', }, }, null, 2 ); // tsconfig.json files['tsconfig.json'] = JSON.stringify( { compilerOptions: { target: 'ES2020', module: 'commonjs', lib: ['ES2020'], outDir: './dist', rootDir: './src', strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, declaration: true, declarationMap: true, }, include: ['src/**/*'], exclude: ['node_modules', 'dist'], }, null, 2 ); // src/index.ts let indexCode = `// Auto-generated API Client SDK\n`; indexCode += `// Generated by APX\n\n`; indexCode += `export class ApiClient {\n`; indexCode += ` private baseUrl: string;\n`; indexCode += ` private headers: Record<string, string>;\n\n`; indexCode += ` constructor(baseUrl: string = '${baseUrl}', headers: Record<string, string> = {}) {\n`; indexCode += ` this.baseUrl = baseUrl;\n`; indexCode += ` this.headers = headers;\n`; indexCode += ` }\n\n`; // Generate methods for each API for (const api of apis) { const url = new URL(api.baseUrl); const methodName = generateMethodName(url.pathname, api.method); indexCode += generateTypeScriptMethod(api, methodName); } indexCode += `}\n\n`; indexCode += `export default ApiClient;\n`; files['src/index.ts'] = indexCode; // README.md files['README.md'] = `# ${packageName}\n\nAuto-generated API client SDK.\n\n## Installation\n\n\`\`\`bash\nnpm install ${packageName}\n\`\`\`\n\n## Usage\n\n\`\`\`typescript\nimport ApiClient from '${packageName}';\n\nconst client = new ApiClient();\nconst data = await client.getData();\n\`\`\`\n`; return { language: 'typescript', packageName, files, description: 'TypeScript SDK with npm package files', }; } function generatePythonSDK( apis: DiscoveredAPI[], packageName: string, baseUrl: string ): SDKPackage { const files: Record<string, string> = {}; // pyproject.toml files['pyproject.toml'] = `[build-system]\nrequires = ["setuptools>=61.0"]\nbuild-backend = "setuptools.build_meta"\n\n[project]\nname = "${packageName}"\nversion = "1.0.0"\ndescription = "Auto-generated API client SDK"\nrequires-python = ">=3.8"\ndependencies = [\n "requests>=2.28.0",\n]\n\n[project.optional-dependencies]\ndev = [\n "pytest>=7.0.0",\n]\n`; // src/__init__.py let initCode = `# Auto-generated API Client SDK\n`; initCode += `# Generated by APX\n\n`; initCode += `import requests\nfrom typing import Dict, Any, Optional\n\n`; initCode += `class ApiClient:\n`; initCode += ` def __init__(self, base_url: str = '${baseUrl}', headers: Optional[Dict[str, str]] = None):\n`; initCode += ` self.base_url = base_url\n`; initCode += ` self.headers = headers or {}\n\n`; // Generate methods for each API for (const api of apis) { const url = new URL(api.baseUrl); const methodName = generateMethodName(url.pathname, api.method); initCode += generatePythonMethod(api, methodName); } files['src/__init__.py'] = initCode; // README.md files['README.md'] = `# ${packageName}\n\nAuto-generated API client SDK.\n\n## Installation\n\n\`\`\`bash\npip install ${packageName}\n\`\`\`\n\n## Usage\n\n\`\`\`python\nfrom ${packageName} import ApiClient\n\nclient = ApiClient()\ndata = client.get_data()\n\`\`\`\n\n## CI/CD\n\nThis package includes GitHub Actions workflows for automated testing and publishing.\n`; // GitHub Actions workflow files['.github/workflows/ci.yml'] = `name: CI on: push: branches: [ main, master ] pull_request: branches: [ main, master ] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 - name: Set up Python \${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: \${{ matrix.python-version }} cache: 'pip' - name: Install dependencies run: | pip install -e ".[dev]" - name: Test with pytest run: | pytest --cov=. --cov-report=xml - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage.xml fail_ci_if_error: false publish: needs: test runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install build tools run: pip install build twine - name: Build package run: python -m build - name: Publish to PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: \${{ secrets.PYPI_API_TOKEN }} run: twine upload dist/* `; return { language: 'python', packageName, files, description: 'Python SDK with PyPI-ready package files and CI/CD', }; } function generateGoSDK( apis: DiscoveredAPI[], packageName: string, baseUrl: string ): SDKPackage { const files: Record<string, string> = {}; // go.mod files['go.mod'] = `module ${packageName}\n\ngo 1.21\n\nrequire (\n\tgithub.com/stretchr/testify v1.8.4\n)\n`; // client.go let clientCode = `// Auto-generated API Client SDK\n`; clientCode += `// Generated by Smart API Finder & Documenter\n\n`; clientCode += `package ${packageName.replace(/-/g, '')}\n\n`; clientCode += `import (\n`; clientCode += `\t"bytes"\n`; clientCode += `\t"encoding/json"\n`; clientCode += `\t"fmt"\n`; clientCode += `\t"net/http"\n`; clientCode += `\t"net/url"\n`; clientCode += `)\n\n`; clientCode += `type ApiClient struct {\n`; clientCode += `\tBaseURL string\n`; clientCode += `\tHeaders map[string]string\n`; clientCode += `\tClient *http.Client\n`; clientCode += `}\n\n`; clientCode += `func NewApiClient(baseURL string, headers map[string]string) *ApiClient {\n`; clientCode += `\tif baseURL == "" {\n`; clientCode += `\t\tbaseURL = "${baseUrl}"\n`; clientCode += `\t}\n`; clientCode += `\treturn &ApiClient{\n`; clientCode += `\t\tBaseURL: baseURL,\n`; clientCode += `\t\tHeaders: headers,\n`; clientCode += `\t\tClient: &http.Client{},\n`; clientCode += `\t}\n`; clientCode += `}\n\n`; // Generate methods for each API for (const api of apis) { const url = new URL(api.baseUrl); const methodName = generateMethodName(url.pathname, api.method); clientCode += generateGoMethod(api, methodName); } files['client.go'] = clientCode; // README.md files['README.md'] = `# ${packageName}\n\nAuto-generated API client SDK.\n\n## Installation\n\n\`\`\`bash\ngo get ${packageName}\n\`\`\`\n\n## Usage\n\n\`\`\`go\nimport "${packageName}"\n\nclient := ${packageName}.NewApiClient("", nil)\ndata, err := client.GetData()\n\`\`\`\n\n## CI/CD\n\nThis package includes GitHub Actions workflows for automated testing.\n`; // GitHub Actions workflow files['.github/workflows/ci.yml'] = `name: CI on: push: branches: [ main, master ] pull_request: branches: [ main, master ] jobs: test: runs-on: ubuntu-latest strategy: matrix: go-version: ["1.19", "1.20", "1.21"] steps: - uses: actions/checkout@v3 - name: Set up Go \${{ matrix.go-version }} uses: actions/setup-go@v4 with: go-version: \${{ matrix.go-version }} cache-dependency-path: go.sum - name: Download dependencies run: go mod download - name: Run tests run: go test -v -coverprofile=coverage.out ./... - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage.out fail_ci_if_error: false `; return { language: 'go', packageName, files, description: 'Go SDK with Go module files and CI/CD', }; } // Helper functions function generateMethodName(path: string, method: string): string { const parts = path.split('/').filter(Boolean); const lastPart = parts[parts.length - 1] || 'data'; const camelCase = lastPart .split(/[-_]/) .map((word, i) => (i === 0 ? word : capitalize(word))) .join(''); return `${method.toLowerCase()}${capitalize(camelCase)}`; } function capitalize(str: string): string { if (!str) return ''; return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); } function generateTypeScriptMethod(api: DiscoveredAPI, methodName: string): string { const url = new URL(api.baseUrl); let code = ` async ${methodName}(params?: Record<string, any>): Promise<any> {\n`; code += ` const url = new URL('${url.pathname}', this.baseUrl);\n`; code += ` if (params) {\n`; code += ` Object.entries(params).forEach(([key, value]) => {\n`; code += ` url.searchParams.set(key, String(value));\n`; code += ` });\n`; code += ` }\n\n`; code += ` const response = await fetch(url.toString(), {\n`; code += ` method: '${api.method}',\n`; code += ` headers: { ...this.headers },\n`; if (api.method === 'POST' && api.body) { code += ` body: JSON.stringify(params || {}),\n`; } code += ` });\n\n`; code += ` if (!response.ok) {\n`; code += ` throw new Error(\`API request failed: \${response.status}\`);\n`; code += ` }\n\n`; code += ` return response.json();\n`; code += ` }\n\n`; return code; } function generatePythonMethod(api: DiscoveredAPI, methodName: string): string { const url = new URL(api.baseUrl); let code = ` def ${methodName}(self, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:\n`; code += ` """${api.method} ${url.pathname}"""\n`; code += ` url = f"{self.base_url}${url.pathname}"\n`; code += ` \n`; code += ` kwargs = {'headers': self.headers}\n`; if (api.method === 'POST') { code += ` if params:\n`; code += ` kwargs['json'] = params\n`; } else { code += ` if params:\n`; code += ` kwargs['params'] = params\n`; } code += ` \n`; code += ` response = requests.${api.method.toLowerCase()}(url, **kwargs)\n`; code += ` response.raise_for_status()\n`; code += ` return response.json()\n\n`; return code; } function generateGoMethod(api: DiscoveredAPI, methodName: string): string { const url = new URL(api.baseUrl); const goMethodName = capitalize(methodName); let code = `func (c *ApiClient) ${goMethodName}(params map[string]string) (map[string]interface{}, error) {\n`; code += `\turlStr := fmt.Sprintf("%s%s", c.BaseURL, "${url.pathname}")\n`; code += `\t\n`; if (api.method === 'POST') { code += `\tvar req *http.Request\n`; code += `\tvar err error\n`; code += `\t\n`; code += `\tif params != nil {\n`; code += `\t\tjsonData, _ := json.Marshal(params)\n`; code += `\t\treq, err = http.NewRequest("${api.method}", urlStr, bytes.NewBuffer(jsonData))\n`; code += `\t} else {\n`; code += `\t\treq, err = http.NewRequest("${api.method}", urlStr, nil)\n`; code += `\t}\n`; } else { code += `\turlObj, err := url.Parse(urlStr)\n`; code += `\tif err != nil {\n`; code += `\t\treturn nil, err\n`; code += `\t}\n`; code += `\t\n`; code += `\tif params != nil {\n`; code += `\t\tq := urlObj.Query()\n`; code += `\t\tfor k, v := range params {\n`; code += `\t\t\tq.Set(k, v)\n`; code += `\t\t}\n`; code += `\t\turlObj.RawQuery = q.Encode()\n`; code += `\t}\n`; code += `\t\n`; code += `\tvar req *http.Request\n`; code += `\treq, err = http.NewRequest("${api.method}", urlObj.String(), nil)\n`; } code += `\tif err != nil {\n`; code += `\t\treturn nil, err\n`; code += `\t}\n`; code += `\tif err != nil {\n`; code += `\t\treturn nil, err\n`; code += `\t}\n`; code += `\t\n`; code += `\tfor k, v := range c.Headers {\n`; code += `\t\treq.Header.Set(k, v)\n`; code += `\t}\n`; code += `\t\n`; code += `\tresp, err := c.Client.Do(req)\n`; code += `\tif err != nil {\n`; code += `\t\treturn nil, err\n`; code += `\t}\n`; code += `\tdefer resp.Body.Close()\n`; code += `\t\n`; code += `\tvar result map[string]interface{}\n`; code += `\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n`; code += `\t\treturn nil, err\n`; code += `\t}\n`; code += `\t\n`; code += `\treturn result, nil\n`; code += `}\n\n`; return code; }