github-to-mcp-monorepo 1.0.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/.env.example +8 -0
- package/.github/CODEOWNERS +6 -0
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/.prettierignore +5 -0
- package/.prettierrc +7 -0
- package/.vscode/settings.json +4 -0
- package/ARCHITECTURE.md +1429 -0
- package/CHANGELOG.md +167 -0
- package/CONTRIBUTING.md +327 -0
- package/LICENSE +201 -0
- package/README.md +1028 -0
- package/SECURITY.md +248 -0
- package/VISUAL_GUIDE.md +437 -0
- package/apps/vscode/IMPLEMENTATION.md +480 -0
- package/apps/vscode/README.md +248 -0
- package/apps/vscode/package.json +381 -0
- package/apps/vscode/resources/icon.png +0 -0
- package/apps/vscode/resources/icon.svg +5 -0
- package/apps/vscode/src/commands/browseRegistry.ts +211 -0
- package/apps/vscode/src/commands/configureClaudeDesktop.ts +332 -0
- package/apps/vscode/src/commands/convert.ts +82 -0
- package/apps/vscode/src/commands/convertCurrentRepo.ts +109 -0
- package/apps/vscode/src/commands/convertFromUrl.ts +138 -0
- package/apps/vscode/src/commands/index.ts +121 -0
- package/apps/vscode/src/commands/validate.ts +197 -0
- package/apps/vscode/src/extension.ts +464 -0
- package/apps/vscode/src/global.d.ts +36 -0
- package/apps/vscode/src/test/extension.test.ts +73 -0
- package/apps/vscode/src/utils/file-generator.ts +529 -0
- package/apps/vscode/src/utils/github-api.ts +335 -0
- package/apps/vscode/src/utils/index.ts +29 -0
- package/apps/vscode/src/utils/mcp-config.ts +334 -0
- package/apps/vscode/src/utils/storage.ts +87 -0
- package/apps/vscode/src/views/McpServersTreeView.ts +160 -0
- package/apps/vscode/src/views/OutputChannelView.ts +195 -0
- package/apps/vscode/src/views/StatusBarItem.ts +251 -0
- package/apps/vscode/src/views/ToolsExplorerView.ts +314 -0
- package/apps/vscode/src/views/historyProvider.ts +75 -0
- package/apps/vscode/src/views/index.ts +12 -0
- package/apps/vscode/src/views/resultsPanel.ts +330 -0
- package/apps/vscode/src/webviews/ConversionPanel.ts +350 -0
- package/apps/vscode/src/webviews/ToolDetailsPanel.ts +448 -0
- package/apps/vscode/src/webviews/index.ts +9 -0
- package/apps/vscode/src/webviews/webview-ui/styles.ts +492 -0
- package/apps/vscode/tsconfig.json +20 -0
- package/apps/web/PLAYGROUND_GUIDE.md +499 -0
- package/apps/web/README.md +505 -0
- package/apps/web/app/api/convert/route.ts +100 -0
- package/apps/web/app/api/convert/stream/route.ts +198 -0
- package/apps/web/app/api/deploy/route.ts +157 -0
- package/apps/web/app/api/edge/route.ts +308 -0
- package/apps/web/app/api/export-docker/route.ts +284 -0
- package/apps/web/app/api/generate-openapi/route.ts +119 -0
- package/apps/web/app/api/mcp/[serverId]/route.ts +263 -0
- package/apps/web/app/api/playground/connect/route.ts +143 -0
- package/apps/web/app/api/playground/disconnect/route.ts +78 -0
- package/apps/web/app/api/playground/execute/route.ts +135 -0
- package/apps/web/app/api/playground/sessions/route.ts +103 -0
- package/apps/web/app/api/playground/tools/route.ts +117 -0
- package/apps/web/app/api/playground/v2/connect/route.ts +96 -0
- package/apps/web/app/api/playground/v2/disconnect/route.ts +88 -0
- package/apps/web/app/api/playground/v2/health/route.ts +80 -0
- package/apps/web/app/api/playground/v2/prompts/route.ts +160 -0
- package/apps/web/app/api/playground/v2/resources/route.ts +159 -0
- package/apps/web/app/api/playground/v2/sessions/route.ts +184 -0
- package/apps/web/app/api/playground/v2/tools/route.ts +167 -0
- package/apps/web/app/api/stream/route.ts +232 -0
- package/apps/web/app/batch/BatchConvertClient.tsx +190 -0
- package/apps/web/app/batch/page.tsx +37 -0
- package/apps/web/app/convert/page.tsx +269 -0
- package/apps/web/app/dashboard/page.tsx +380 -0
- package/apps/web/app/globals.css +622 -0
- package/apps/web/app/layout.tsx +120 -0
- package/apps/web/app/manifest.ts +31 -0
- package/apps/web/app/opengraph-image.tsx +112 -0
- package/apps/web/app/page.old.tsx +924 -0
- package/apps/web/app/page.tsx +77 -0
- package/apps/web/app/playground/page.tsx +306 -0
- package/apps/web/app/playground/v2/error.tsx +163 -0
- package/apps/web/app/playground/v2/layout.tsx +58 -0
- package/apps/web/app/playground/v2/loading.tsx +152 -0
- package/apps/web/app/playground/v2/page.tsx +644 -0
- package/apps/web/app/playground/v2/providers.tsx +214 -0
- package/apps/web/app/playground/v2/use-shortcuts.ts +209 -0
- package/apps/web/app/playground/v2/use-url-state.ts +296 -0
- package/apps/web/app/providers.tsx +22 -0
- package/apps/web/app/sitemap.ts +32 -0
- package/apps/web/app/twitter-image.tsx +112 -0
- package/apps/web/components/BranchSelector.tsx +401 -0
- package/apps/web/components/ClaudeConfigExport.tsx +226 -0
- package/apps/web/components/Features.tsx +84 -0
- package/apps/web/components/Footer.tsx +119 -0
- package/apps/web/components/GenerationProgress.tsx +248 -0
- package/apps/web/components/GithubUrlInput.tsx +483 -0
- package/apps/web/components/Header.tsx +175 -0
- package/apps/web/components/Hero.tsx +117 -0
- package/apps/web/components/HowItWorks.tsx +119 -0
- package/apps/web/components/InstallBanner.tsx +158 -0
- package/apps/web/components/Logo.tsx +116 -0
- package/apps/web/components/ParticleBackground.tsx +105 -0
- package/apps/web/components/Playground.tsx +472 -0
- package/apps/web/components/PlaygroundToolTester.tsx +410 -0
- package/apps/web/components/ProductCards.tsx +179 -0
- package/apps/web/components/SplitView.tsx +194 -0
- package/apps/web/components/ToolFilter.tsx +260 -0
- package/apps/web/components/ToolList.tsx +325 -0
- package/apps/web/components/batch/BatchConvert.tsx +785 -0
- package/apps/web/components/batch/index.ts +7 -0
- package/apps/web/components/convert/ConfigTabs.tsx +230 -0
- package/apps/web/components/convert/ConversionResult.tsx +482 -0
- package/apps/web/components/convert/InlinePlayground.tsx +259 -0
- package/apps/web/components/convert/LoadingSteps.tsx +311 -0
- package/apps/web/components/convert/OneClickInstall.tsx +224 -0
- package/apps/web/components/convert/ToolCard.tsx +189 -0
- package/apps/web/components/convert/TryInPlayground.tsx +242 -0
- package/apps/web/components/convert/index.ts +12 -0
- package/apps/web/components/deploy/DeployButton.tsx +369 -0
- package/apps/web/components/deploy/index.ts +7 -0
- package/apps/web/components/docker/DockerExport.tsx +690 -0
- package/apps/web/components/docker/index.ts +7 -0
- package/apps/web/components/install/OneClickInstall.tsx +676 -0
- package/apps/web/components/install/index.ts +7 -0
- package/apps/web/components/playground/CapabilityTabs.tsx +150 -0
- package/apps/web/components/playground/ConnectionStatusV2.tsx +322 -0
- package/apps/web/components/playground/EmptyStates.tsx +305 -0
- package/apps/web/components/playground/ExecutionLog.tsx +260 -0
- package/apps/web/components/playground/ExecutionLogV2.tsx +378 -0
- package/apps/web/components/playground/JsonViewer.tsx +388 -0
- package/apps/web/components/playground/PlaygroundLayout.tsx +244 -0
- package/apps/web/components/playground/PromptsPanel.tsx +385 -0
- package/apps/web/components/playground/ResourcesPanel.tsx +378 -0
- package/apps/web/components/playground/SchemaForm.tsx +477 -0
- package/apps/web/components/playground/ServerStatus.tsx +151 -0
- package/apps/web/components/playground/ShareButton.tsx +239 -0
- package/apps/web/components/playground/ToolsPanel.tsx +309 -0
- package/apps/web/components/playground/TransportConfigurator.tsx +563 -0
- package/apps/web/components/playground/index.ts +74 -0
- package/apps/web/components/playground/types.ts +202 -0
- package/apps/web/components/streaming/StreamingProgress.tsx +441 -0
- package/apps/web/components/streaming/index.ts +7 -0
- package/apps/web/components/ui/badge.tsx +42 -0
- package/apps/web/components/ui/button.tsx +88 -0
- package/apps/web/components/ui/card.tsx +75 -0
- package/apps/web/components/ui/code-block.tsx +122 -0
- package/apps/web/components/ui/index.ts +12 -0
- package/apps/web/components/ui/input.tsx +55 -0
- package/apps/web/components/ui/tabs.tsx +61 -0
- package/apps/web/hooks/index.ts +85 -0
- package/apps/web/hooks/types.ts +1173 -0
- package/apps/web/hooks/use-conversion.ts +133 -0
- package/apps/web/hooks/use-execution-history.ts +376 -0
- package/apps/web/hooks/use-generation-progress.ts +147 -0
- package/apps/web/hooks/use-local-storage.ts +88 -0
- package/apps/web/hooks/use-mcp-client.ts +623 -0
- package/apps/web/hooks/use-mcp-connection.ts +500 -0
- package/apps/web/hooks/use-mcp-execution.ts +282 -0
- package/apps/web/hooks/use-mcp-prompts.ts +441 -0
- package/apps/web/hooks/use-mcp-resources.ts +430 -0
- package/apps/web/hooks/use-mcp-tools.ts +540 -0
- package/apps/web/hooks/use-playground-store.ts +299 -0
- package/apps/web/hooks/use-playground.ts +184 -0
- package/apps/web/hooks/use-streaming-conversion.ts +227 -0
- package/apps/web/hooks/useBatchConversion.ts +271 -0
- package/apps/web/hooks/useDockerConfig.ts +161 -0
- package/apps/web/hooks/usePlatformDetection.ts +80 -0
- package/apps/web/hooks/useStreaming.ts +199 -0
- package/apps/web/lib/api/errors.ts +386 -0
- package/apps/web/lib/api/index.ts +137 -0
- package/apps/web/lib/api/logger.ts +187 -0
- package/apps/web/lib/api/middleware.ts +364 -0
- package/apps/web/lib/api/openapi.ts +977 -0
- package/apps/web/lib/api/session-manager.ts +594 -0
- package/apps/web/lib/api/types.ts +433 -0
- package/apps/web/lib/api/validation.ts +523 -0
- package/apps/web/lib/constants.ts +114 -0
- package/apps/web/lib/mcp/client.ts +1137 -0
- package/apps/web/lib/mcp/events.ts +651 -0
- package/apps/web/lib/mcp/index.ts +347 -0
- package/apps/web/lib/mcp/logger.ts +428 -0
- package/apps/web/lib/mcp/metrics.ts +703 -0
- package/apps/web/lib/mcp/retry.ts +616 -0
- package/apps/web/lib/mcp/session-manager.ts +779 -0
- package/apps/web/lib/mcp/transports.ts +988 -0
- package/apps/web/lib/mcp/types.ts +594 -0
- package/apps/web/lib/mcp-client-enhanced.ts +871 -0
- package/apps/web/lib/mcp-client.ts +778 -0
- package/apps/web/lib/mcp-errors.ts +489 -0
- package/apps/web/lib/mcp-sandbox.ts +593 -0
- package/apps/web/lib/mcp-testing.ts +428 -0
- package/apps/web/lib/mcp-types.ts +448 -0
- package/apps/web/lib/playground-store.tsx +1147 -0
- package/apps/web/lib/utils.ts +439 -0
- package/apps/web/next-env.d.ts +5 -0
- package/apps/web/next.config.js +23 -0
- package/apps/web/package.json +55 -0
- package/apps/web/postcss.config.js +6 -0
- package/apps/web/public/.well-known/ai-plugin.json +17 -0
- package/apps/web/public/logo.svg +6 -0
- package/apps/web/public/robots.txt +22 -0
- package/apps/web/public/schema.json +27 -0
- package/apps/web/tailwind.config.js +26 -0
- package/apps/web/tailwind.config.ts +123 -0
- package/apps/web/tsconfig.json +20 -0
- package/apps/web/types/deploy.ts +139 -0
- package/apps/web/types/index.ts +247 -0
- package/apps/web/vercel.json +39 -0
- package/eslint.config.mjs +23 -0
- package/llms.txt +102 -0
- package/mkdocs/docs/api/core.md +318 -0
- package/mkdocs/docs/api/index.md +128 -0
- package/mkdocs/docs/api/mcp-server.md +301 -0
- package/mkdocs/docs/api/openapi-parser.md +254 -0
- package/mkdocs/docs/assets/logo.svg +7 -0
- package/mkdocs/docs/changelog.md +118 -0
- package/mkdocs/docs/cli/generate.md +148 -0
- package/mkdocs/docs/cli/index.md +52 -0
- package/mkdocs/docs/cli/inspect.md +164 -0
- package/mkdocs/docs/cli/serve.md +136 -0
- package/mkdocs/docs/concepts/classification.md +254 -0
- package/mkdocs/docs/concepts/how-it-works.md +299 -0
- package/mkdocs/docs/concepts/index.md +77 -0
- package/mkdocs/docs/concepts/mcp-protocol.md +362 -0
- package/mkdocs/docs/concepts/tool-types.md +382 -0
- package/mkdocs/docs/contributing/architecture.md +262 -0
- package/mkdocs/docs/contributing/development.md +245 -0
- package/mkdocs/docs/contributing/index.md +73 -0
- package/mkdocs/docs/contributing/testing.md +320 -0
- package/mkdocs/docs/getting-started/configuration.md +235 -0
- package/mkdocs/docs/getting-started/index.md +54 -0
- package/mkdocs/docs/getting-started/installation.md +145 -0
- package/mkdocs/docs/getting-started/quickstart.md +160 -0
- package/mkdocs/docs/guides/batch.md +375 -0
- package/mkdocs/docs/guides/claude-desktop.md +227 -0
- package/mkdocs/docs/guides/cursor.md +188 -0
- package/mkdocs/docs/guides/custom-tools.md +367 -0
- package/mkdocs/docs/guides/index.md +78 -0
- package/mkdocs/docs/guides/private-repos.md +221 -0
- package/mkdocs/docs/guides/vscode.md +247 -0
- package/mkdocs/docs/index.md +175 -0
- package/mkdocs/docs/reference/config.md +223 -0
- package/mkdocs/docs/reference/env.md +192 -0
- package/mkdocs/docs/reference/index.md +102 -0
- package/mkdocs/docs/reference/tools.md +309 -0
- package/mkdocs/docs/stylesheets/extra.css +231 -0
- package/mkdocs/mkdocs.yml +204 -0
- package/mkdocs/overrides/.gitkeep +1 -0
- package/mkdocs/overrides/main.html +7 -0
- package/mkdocs/python-deps.txt +7 -0
- package/mkdocs/vercel.json +11 -0
- package/package.json +63 -0
- package/packages/core/package.json +61 -0
- package/packages/core/src/__tests__/bitbucket-client.test.ts +366 -0
- package/packages/core/src/__tests__/cli.test.ts +235 -0
- package/packages/core/src/__tests__/code-extractor.test.ts +378 -0
- package/packages/core/src/__tests__/docker-generator.test.ts +255 -0
- package/packages/core/src/__tests__/github-client.test.ts +390 -0
- package/packages/core/src/__tests__/gitlab-client.test.ts +319 -0
- package/packages/core/src/__tests__/go-extractor.test.ts +351 -0
- package/packages/core/src/__tests__/graphql-extractor.test.ts +330 -0
- package/packages/core/src/__tests__/java-extractor.test.ts +497 -0
- package/packages/core/src/__tests__/plugins.test.ts +467 -0
- package/packages/core/src/__tests__/readme-extractor.test.ts +258 -0
- package/packages/core/src/__tests__/redis-cache.test.ts +307 -0
- package/packages/core/src/__tests__/rust-extractor.test.ts +252 -0
- package/packages/core/src/__tests__/streaming.test.ts +251 -0
- package/packages/core/src/additional-extractors.ts +333 -0
- package/packages/core/src/cache/cache-interface.ts +179 -0
- package/packages/core/src/cache/index.ts +210 -0
- package/packages/core/src/cache/redis-cache.ts +291 -0
- package/packages/core/src/cache/upstash-cache.ts +379 -0
- package/packages/core/src/cache.ts +251 -0
- package/packages/core/src/cli.ts +822 -0
- package/packages/core/src/code-extractor.ts +696 -0
- package/packages/core/src/docker-generator.ts +470 -0
- package/packages/core/src/edge-compatible.ts +491 -0
- package/packages/core/src/extractors/go-extractor.ts +791 -0
- package/packages/core/src/extractors/index.ts +9 -0
- package/packages/core/src/extractors/java-extractor.ts +937 -0
- package/packages/core/src/extractors/rust-extractor.ts +744 -0
- package/packages/core/src/github-client.ts +319 -0
- package/packages/core/src/go-generator.ts +356 -0
- package/packages/core/src/graphql-extractor.ts +358 -0
- package/packages/core/src/index.ts +797 -0
- package/packages/core/src/langchain-exporter.ts +617 -0
- package/packages/core/src/language-parsers.ts +1114 -0
- package/packages/core/src/mcp-introspector.ts +279 -0
- package/packages/core/src/monorepo-detector.ts +378 -0
- package/packages/core/src/plugins/index.ts +370 -0
- package/packages/core/src/plugins/registry.ts +404 -0
- package/packages/core/src/plugins/types.ts +215 -0
- package/packages/core/src/providers/base-provider.ts +246 -0
- package/packages/core/src/providers/bitbucket-client.ts +464 -0
- package/packages/core/src/providers/gitlab-client.ts +388 -0
- package/packages/core/src/providers/index.ts +176 -0
- package/packages/core/src/python-generator.ts +260 -0
- package/packages/core/src/queue/index.ts +100 -0
- package/packages/core/src/queue/memory-queue.ts +445 -0
- package/packages/core/src/queue/redis-queue.ts +578 -0
- package/packages/core/src/queue/types.ts +251 -0
- package/packages/core/src/readme-extractor.ts +409 -0
- package/packages/core/src/schema-generator.ts +638 -0
- package/packages/core/src/streaming.ts +999 -0
- package/packages/core/src/types.ts +289 -0
- package/packages/core/tsconfig.json +9 -0
- package/packages/core/tsup.config.ts +25 -0
- package/packages/mcp-server/README.md +297 -0
- package/packages/mcp-server/package.json +55 -0
- package/packages/mcp-server/src/__tests__/mcp-server.test.ts +177 -0
- package/packages/mcp-server/src/__tests__/tools.test.ts +217 -0
- package/packages/mcp-server/src/index.ts +1206 -0
- package/packages/mcp-server/src/prompts/index.ts +601 -0
- package/packages/mcp-server/src/tools/export-docker.ts +362 -0
- package/packages/mcp-server/src/tools/generate-openapi.ts +162 -0
- package/packages/mcp-server/src/tools/monitor-mcp-server.ts +448 -0
- package/packages/mcp-server/src/tools/stream-convert.ts +398 -0
- package/packages/mcp-server/src/tools/test-mcp-tool.ts +531 -0
- package/packages/mcp-server/tsconfig.json +12 -0
- package/packages/mcp-server/tsup.config.ts +14 -0
- package/packages/openapi-parser/package-lock.json +3028 -0
- package/packages/openapi-parser/package.json +41 -0
- package/packages/openapi-parser/src/analyzer.ts +700 -0
- package/packages/openapi-parser/src/asyncapi-parser.ts +475 -0
- package/packages/openapi-parser/src/cli.ts +302 -0
- package/packages/openapi-parser/src/generator.ts +570 -0
- package/packages/openapi-parser/src/generators/express-analyzer.ts +649 -0
- package/packages/openapi-parser/src/generators/fastapi-analyzer.ts +960 -0
- package/packages/openapi-parser/src/generators/index.ts +200 -0
- package/packages/openapi-parser/src/generators/nextjs-analyzer.ts +768 -0
- package/packages/openapi-parser/src/generators/openapi-builder.ts +527 -0
- package/packages/openapi-parser/src/generators/types.ts +298 -0
- package/packages/openapi-parser/src/graphql-parser.ts +462 -0
- package/packages/openapi-parser/src/grpc-parser.ts +649 -0
- package/packages/openapi-parser/src/har-parser.ts +723 -0
- package/packages/openapi-parser/src/index.ts +635 -0
- package/packages/openapi-parser/src/insomnia-parser.ts +614 -0
- package/packages/openapi-parser/src/parser.ts +231 -0
- package/packages/openapi-parser/src/postman-parser.ts +611 -0
- package/packages/openapi-parser/src/ref-resolver.ts +313 -0
- package/packages/openapi-parser/src/transformer.ts +459 -0
- package/packages/openapi-parser/tests/generators/express.test.ts +209 -0
- package/packages/openapi-parser/tests/generators/fastapi.test.ts +236 -0
- package/packages/openapi-parser/tests/generators/nextjs.test.ts +273 -0
- package/packages/openapi-parser/tests/parsers.test.ts +847 -0
- package/packages/openapi-parser/tsconfig.json +9 -0
- package/packages/openapi-parser/tsup.config.ts +11 -0
- package/packages/registry/package.json +59 -0
- package/packages/registry/src/cli.ts +456 -0
- package/packages/registry/src/index.ts +44 -0
- package/packages/registry/src/popular/github.json +47 -0
- package/packages/registry/src/popular/index.ts +55 -0
- package/packages/registry/src/popular/linear.json +42 -0
- package/packages/registry/src/popular/notion.json +42 -0
- package/packages/registry/src/popular/openai.json +40 -0
- package/packages/registry/src/popular/resend.json +38 -0
- package/packages/registry/src/popular/slack.json +42 -0
- package/packages/registry/src/popular/stripe.json +163 -0
- package/packages/registry/src/popular/supabase.json +42 -0
- package/packages/registry/src/popular/twilio.json +40 -0
- package/packages/registry/src/popular/vercel.json +40 -0
- package/packages/registry/src/registry.ts +492 -0
- package/packages/registry/src/storage.ts +334 -0
- package/packages/registry/src/types.ts +275 -0
- package/packages/registry/src/updater.ts +208 -0
- package/packages/registry/tsconfig.json +10 -0
- package/packages/registry/tsup.config.ts +11 -0
- package/pnpm-workspace.yaml +3 -0
- package/scripts/build-docs.sh +16 -0
- package/server.json +9 -0
- package/templates/Dockerfile.python.template +60 -0
- package/templates/Dockerfile.typescript.template +60 -0
- package/templates/docker-compose.template.yml +68 -0
- package/tests/fixtures/express-app/index.js +34 -0
- package/tests/fixtures/express-app/routes/posts.js +43 -0
- package/tests/fixtures/express-app/routes/users.js +58 -0
- package/tests/fixtures/fastapi-app/main.py +125 -0
- package/tests/fixtures/fastapi-app/routes/admin.py +42 -0
- package/tests/fixtures/graphql/simple-schema.graphql +65 -0
- package/tests/fixtures/mocks/github-api-responses.json +63 -0
- package/tests/fixtures/nextjs-app/app/api/posts/route.ts +55 -0
- package/tests/fixtures/nextjs-app/app/api/users/[id]/route.ts +63 -0
- package/tests/fixtures/nextjs-app/app/api/users/route.ts +44 -0
- package/tests/fixtures/nextjs-app/pages/api/health.ts +28 -0
- package/tests/fixtures/openapi/petstore.yaml +179 -0
- package/tests/integration/langchain-export.test.ts +405 -0
- package/tests/integration/openapi-conversion.test.ts +221 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +32 -0
|
@@ -0,0 +1,960 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview FastAPI/Flask route analyzer
|
|
3
|
+
* Parses Python web framework routes and extracts API information
|
|
4
|
+
* @copyright Copyright (c) 2024-2026 nirholas
|
|
5
|
+
* @license MIT
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
RouteAnalyzer,
|
|
10
|
+
FileContent,
|
|
11
|
+
AnalysisResult,
|
|
12
|
+
AnalyzedRoute,
|
|
13
|
+
RouteParameter,
|
|
14
|
+
RouteBody,
|
|
15
|
+
RouteResponse,
|
|
16
|
+
JsonSchemaDefinition,
|
|
17
|
+
HttpMethod,
|
|
18
|
+
} from './types.js';
|
|
19
|
+
import { OpenAPIV3_1 } from 'openapi-types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Regular expressions for FastAPI/Flask route detection
|
|
23
|
+
*/
|
|
24
|
+
const ROUTE_PATTERNS = {
|
|
25
|
+
// FastAPI decorators: @app.get('/path')
|
|
26
|
+
fastapiDecorator: /@(?:app|router)\s*\.\s*(get|post|put|patch|delete|head|options)\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
27
|
+
|
|
28
|
+
// Flask decorators: @app.route('/path', methods=['GET'])
|
|
29
|
+
flaskRoute: /@(?:app|blueprint|bp)\s*\.\s*route\s*\(\s*['"]([^'"]+)['"](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?/gi,
|
|
30
|
+
|
|
31
|
+
// FastAPI path parameters
|
|
32
|
+
fastapiPathParam: /\{(\w+)(?::\s*\w+)?\}/g,
|
|
33
|
+
|
|
34
|
+
// Flask path parameters
|
|
35
|
+
flaskPathParam: /<(?:(\w+):)?(\w+)>/g,
|
|
36
|
+
|
|
37
|
+
// Pydantic model
|
|
38
|
+
pydanticModel: /class\s+(\w+)\s*\(\s*(?:BaseModel|Base)\s*\)\s*:\s*([\s\S]*?)(?=\nclass\s|\n\n\n|$)/gi,
|
|
39
|
+
|
|
40
|
+
// Type hints
|
|
41
|
+
typeHint: /(\w+)\s*:\s*(\w+(?:\[[\w\[\],\s]+\])?)/g,
|
|
42
|
+
|
|
43
|
+
// Default values
|
|
44
|
+
defaultValue: /(\w+)\s*:\s*\w+\s*=\s*([^,\n]+)/g,
|
|
45
|
+
|
|
46
|
+
// Query parameter
|
|
47
|
+
queryParam: /(\w+)\s*:\s*(\w+)\s*=\s*Query\s*\(/gi,
|
|
48
|
+
|
|
49
|
+
// Path parameter
|
|
50
|
+
pathParam: /(\w+)\s*:\s*(\w+)\s*=\s*Path\s*\(/gi,
|
|
51
|
+
|
|
52
|
+
// Body parameter
|
|
53
|
+
bodyParam: /(\w+)\s*:\s*(\w+)\s*=\s*Body\s*\(/gi,
|
|
54
|
+
|
|
55
|
+
// Header parameter
|
|
56
|
+
headerParam: /(\w+)\s*:\s*(\w+)\s*=\s*Header\s*\(/gi,
|
|
57
|
+
|
|
58
|
+
// Docstring
|
|
59
|
+
docstring: /"""([\s\S]*?)"""|'''([\s\S]*?)'''/g,
|
|
60
|
+
|
|
61
|
+
// Function definition
|
|
62
|
+
functionDef: /(?:async\s+)?def\s+(\w+)\s*\([^)]*\)(?:\s*->\s*[\w\[\],\s]+)?:\s*/g,
|
|
63
|
+
|
|
64
|
+
// Return type annotation
|
|
65
|
+
returnType: /->\s*([\w\[\],\s]+)/,
|
|
66
|
+
|
|
67
|
+
// Response model
|
|
68
|
+
responseModel: /response_model\s*=\s*(\w+)/gi,
|
|
69
|
+
|
|
70
|
+
// Status code
|
|
71
|
+
statusCode: /status_code\s*=\s*(\d+)/gi,
|
|
72
|
+
|
|
73
|
+
// Tags
|
|
74
|
+
tags: /tags\s*=\s*\[([^\]]+)\]/gi,
|
|
75
|
+
|
|
76
|
+
// Summary/description
|
|
77
|
+
summary: /summary\s*=\s*['"]([^'"]+)['"]/gi,
|
|
78
|
+
description: /description\s*=\s*['"]([^'"]+)['"]/gi,
|
|
79
|
+
|
|
80
|
+
// Deprecated
|
|
81
|
+
deprecated: /deprecated\s*=\s*True/gi,
|
|
82
|
+
|
|
83
|
+
// Optional type
|
|
84
|
+
optionalType: /Optional\[(\w+)\]/,
|
|
85
|
+
|
|
86
|
+
// List type
|
|
87
|
+
listType: /List\[(\w+)\]|list\[(\w+)\]/,
|
|
88
|
+
|
|
89
|
+
// Dict type
|
|
90
|
+
dictType: /Dict\[(\w+),\s*(\w+)\]|dict\[(\w+),\s*(\w+)\]/,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* FastAPI/Flask route analyzer implementation
|
|
95
|
+
*/
|
|
96
|
+
export class FastAPIAnalyzer implements RouteAnalyzer {
|
|
97
|
+
name = 'fastapi';
|
|
98
|
+
|
|
99
|
+
private schemas: Record<string, JsonSchemaDefinition> = {};
|
|
100
|
+
private warnings: string[] = [];
|
|
101
|
+
private errors: string[] = [];
|
|
102
|
+
private framework: 'fastapi' | 'flask' = 'fastapi';
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if files contain FastAPI or Flask code
|
|
106
|
+
*/
|
|
107
|
+
canAnalyze(files: FileContent[]): boolean {
|
|
108
|
+
return files.some(file => {
|
|
109
|
+
const content = file.content;
|
|
110
|
+
return (
|
|
111
|
+
content.includes('from fastapi import') ||
|
|
112
|
+
content.includes('import fastapi') ||
|
|
113
|
+
content.includes('from flask import') ||
|
|
114
|
+
content.includes('import flask') ||
|
|
115
|
+
content.includes('FastAPI()') ||
|
|
116
|
+
content.includes('Flask(__name__)') ||
|
|
117
|
+
/@(?:app|router)\s*\.\s*(get|post|put|patch|delete)/.test(content)
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Analyze FastAPI/Flask files and extract routes
|
|
124
|
+
*/
|
|
125
|
+
async analyze(files: FileContent[]): Promise<AnalysisResult> {
|
|
126
|
+
this.schemas = {};
|
|
127
|
+
this.warnings = [];
|
|
128
|
+
this.errors = [];
|
|
129
|
+
|
|
130
|
+
const routes: AnalyzedRoute[] = [];
|
|
131
|
+
const filesAnalyzed: string[] = [];
|
|
132
|
+
|
|
133
|
+
// Detect framework
|
|
134
|
+
this.detectFramework(files);
|
|
135
|
+
|
|
136
|
+
// First pass: extract Pydantic models
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
this.extractPydanticModels(file);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Second pass: extract routes
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
if (this.isPythonApiFile(file)) {
|
|
144
|
+
filesAnalyzed.push(file.path);
|
|
145
|
+
const fileRoutes = this.extractRoutes(file);
|
|
146
|
+
routes.push(...fileRoutes);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
routes,
|
|
152
|
+
schemas: this.schemas,
|
|
153
|
+
securitySchemes: this.detectSecuritySchemes(files),
|
|
154
|
+
warnings: this.warnings,
|
|
155
|
+
errors: this.errors,
|
|
156
|
+
framework: this.framework,
|
|
157
|
+
filesAnalyzed,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Detect whether it's FastAPI or Flask
|
|
163
|
+
*/
|
|
164
|
+
private detectFramework(files: FileContent[]): void {
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
if (file.content.includes('fastapi') || file.content.includes('FastAPI')) {
|
|
167
|
+
this.framework = 'fastapi';
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (file.content.includes('flask') || file.content.includes('Flask')) {
|
|
171
|
+
this.framework = 'flask';
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if a file contains Python API routes
|
|
179
|
+
*/
|
|
180
|
+
private isPythonApiFile(file: FileContent): boolean {
|
|
181
|
+
const content = file.content;
|
|
182
|
+
return (
|
|
183
|
+
file.path.endsWith('.py') &&
|
|
184
|
+
(ROUTE_PATTERNS.fastapiDecorator.test(content) ||
|
|
185
|
+
ROUTE_PATTERNS.flaskRoute.test(content))
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Extract routes from a file
|
|
191
|
+
*/
|
|
192
|
+
private extractRoutes(file: FileContent): AnalyzedRoute[] {
|
|
193
|
+
const routes: AnalyzedRoute[] = [];
|
|
194
|
+
const content = file.content;
|
|
195
|
+
const lines = content.split('\n');
|
|
196
|
+
|
|
197
|
+
// Reset regex lastIndex
|
|
198
|
+
ROUTE_PATTERNS.fastapiDecorator.lastIndex = 0;
|
|
199
|
+
ROUTE_PATTERNS.flaskRoute.lastIndex = 0;
|
|
200
|
+
|
|
201
|
+
if (this.framework === 'fastapi') {
|
|
202
|
+
// Extract FastAPI routes
|
|
203
|
+
let match;
|
|
204
|
+
while ((match = ROUTE_PATTERNS.fastapiDecorator.exec(content)) !== null) {
|
|
205
|
+
const method = match[1].toLowerCase() as HttpMethod;
|
|
206
|
+
const path = match[2];
|
|
207
|
+
const line = this.getLineNumber(content, match.index);
|
|
208
|
+
|
|
209
|
+
const decoratorLine = lines[line - 1];
|
|
210
|
+
const route = this.createFastAPIRoute(file, method, path, line, content, decoratorLine);
|
|
211
|
+
routes.push(route);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
// Extract Flask routes
|
|
215
|
+
let match;
|
|
216
|
+
while ((match = ROUTE_PATTERNS.flaskRoute.exec(content)) !== null) {
|
|
217
|
+
const path = match[1];
|
|
218
|
+
const methodsStr = match[2] || "'GET'";
|
|
219
|
+
const methods = methodsStr
|
|
220
|
+
.replace(/['"]/g, '')
|
|
221
|
+
.split(',')
|
|
222
|
+
.map(m => m.trim().toLowerCase() as HttpMethod);
|
|
223
|
+
|
|
224
|
+
const line = this.getLineNumber(content, match.index);
|
|
225
|
+
|
|
226
|
+
for (const method of methods) {
|
|
227
|
+
const route = this.createFlaskRoute(file, method, path, line, content);
|
|
228
|
+
routes.push(route);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return routes;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create a FastAPI route
|
|
238
|
+
*/
|
|
239
|
+
private createFastAPIRoute(
|
|
240
|
+
file: FileContent,
|
|
241
|
+
method: HttpMethod,
|
|
242
|
+
path: string,
|
|
243
|
+
line: number,
|
|
244
|
+
content: string,
|
|
245
|
+
decoratorLine: string
|
|
246
|
+
): AnalyzedRoute {
|
|
247
|
+
const openApiPath = this.convertToOpenApiPath(path);
|
|
248
|
+
const pathParams = this.extractFastAPIPathParameters(path);
|
|
249
|
+
|
|
250
|
+
// Extract metadata from decorator
|
|
251
|
+
const { tags, summary, description, statusCode, responseModel, deprecated } =
|
|
252
|
+
this.parseDecoratorMetadata(decoratorLine, content, line);
|
|
253
|
+
|
|
254
|
+
// Find the function definition and extract parameters
|
|
255
|
+
const funcInfo = this.findFunctionInfo(content, line);
|
|
256
|
+
const { queryParams, headerParams, bodyParam } = this.parseFunctionParameters(funcInfo?.params || '');
|
|
257
|
+
|
|
258
|
+
const route: AnalyzedRoute = {
|
|
259
|
+
method,
|
|
260
|
+
path,
|
|
261
|
+
openApiPath,
|
|
262
|
+
operationId: funcInfo?.name || this.generateOperationId(method, path),
|
|
263
|
+
summary: summary || funcInfo?.docSummary,
|
|
264
|
+
description: description || funcInfo?.docDescription,
|
|
265
|
+
tags,
|
|
266
|
+
pathParameters: pathParams,
|
|
267
|
+
queryParameters: queryParams,
|
|
268
|
+
headerParameters: headerParams,
|
|
269
|
+
responses: this.buildResponses(statusCode, responseModel, method),
|
|
270
|
+
deprecated,
|
|
271
|
+
sourceFile: file.path,
|
|
272
|
+
sourceLine: line,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
if (bodyParam) {
|
|
276
|
+
route.requestBody = bodyParam;
|
|
277
|
+
} else if (['post', 'put', 'patch'].includes(method)) {
|
|
278
|
+
// Try to infer body from Pydantic type hint
|
|
279
|
+
const bodyType = this.inferBodyType(funcInfo?.params || '');
|
|
280
|
+
if (bodyType) {
|
|
281
|
+
route.requestBody = {
|
|
282
|
+
contentType: 'application/json',
|
|
283
|
+
required: true,
|
|
284
|
+
schema: { $ref: `#/components/schemas/${bodyType}` },
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return route;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Create a Flask route
|
|
294
|
+
*/
|
|
295
|
+
private createFlaskRoute(
|
|
296
|
+
file: FileContent,
|
|
297
|
+
method: HttpMethod,
|
|
298
|
+
path: string,
|
|
299
|
+
line: number,
|
|
300
|
+
content: string
|
|
301
|
+
): AnalyzedRoute {
|
|
302
|
+
const openApiPath = this.convertFlaskToOpenApiPath(path);
|
|
303
|
+
const pathParams = this.extractFlaskPathParameters(path);
|
|
304
|
+
|
|
305
|
+
const funcInfo = this.findFunctionInfo(content, line);
|
|
306
|
+
|
|
307
|
+
const route: AnalyzedRoute = {
|
|
308
|
+
method,
|
|
309
|
+
path,
|
|
310
|
+
openApiPath,
|
|
311
|
+
operationId: funcInfo?.name || this.generateOperationId(method, path),
|
|
312
|
+
summary: funcInfo?.docSummary,
|
|
313
|
+
description: funcInfo?.docDescription,
|
|
314
|
+
tags: this.inferTags(path),
|
|
315
|
+
pathParameters: pathParams,
|
|
316
|
+
queryParameters: this.extractFlaskQueryParams(content, line),
|
|
317
|
+
headerParameters: [],
|
|
318
|
+
responses: this.buildResponses(200, undefined, method),
|
|
319
|
+
sourceFile: file.path,
|
|
320
|
+
sourceLine: line,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
if (['post', 'put', 'patch'].includes(method)) {
|
|
324
|
+
route.requestBody = {
|
|
325
|
+
contentType: 'application/json',
|
|
326
|
+
required: true,
|
|
327
|
+
schema: { type: 'object' },
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return route;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Convert FastAPI path to OpenAPI format
|
|
336
|
+
*/
|
|
337
|
+
private convertToOpenApiPath(path: string): string {
|
|
338
|
+
// FastAPI already uses {param} format
|
|
339
|
+
return path;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Convert Flask path to OpenAPI format
|
|
344
|
+
*/
|
|
345
|
+
private convertFlaskToOpenApiPath(path: string): string {
|
|
346
|
+
// Convert <type:param> to {param}
|
|
347
|
+
return path.replace(/<(?:\w+:)?(\w+)>/g, '{$1}');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Extract path parameters from FastAPI path
|
|
352
|
+
*/
|
|
353
|
+
private extractFastAPIPathParameters(path: string): RouteParameter[] {
|
|
354
|
+
const params: RouteParameter[] = [];
|
|
355
|
+
ROUTE_PATTERNS.fastapiPathParam.lastIndex = 0;
|
|
356
|
+
|
|
357
|
+
let match;
|
|
358
|
+
while ((match = ROUTE_PATTERNS.fastapiPathParam.exec(path)) !== null) {
|
|
359
|
+
params.push({
|
|
360
|
+
name: match[1],
|
|
361
|
+
location: 'path',
|
|
362
|
+
required: true,
|
|
363
|
+
type: 'string',
|
|
364
|
+
description: `Path parameter: ${match[1]}`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return params;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Extract path parameters from Flask path
|
|
373
|
+
*/
|
|
374
|
+
private extractFlaskPathParameters(path: string): RouteParameter[] {
|
|
375
|
+
const params: RouteParameter[] = [];
|
|
376
|
+
ROUTE_PATTERNS.flaskPathParam.lastIndex = 0;
|
|
377
|
+
|
|
378
|
+
let match;
|
|
379
|
+
while ((match = ROUTE_PATTERNS.flaskPathParam.exec(path)) !== null) {
|
|
380
|
+
const type = match[1] || 'string';
|
|
381
|
+
const name = match[2];
|
|
382
|
+
params.push({
|
|
383
|
+
name,
|
|
384
|
+
location: 'path',
|
|
385
|
+
required: true,
|
|
386
|
+
type: this.mapFlaskType(type),
|
|
387
|
+
description: `Path parameter: ${name}`,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return params;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Map Flask type to JSON Schema type
|
|
396
|
+
*/
|
|
397
|
+
private mapFlaskType(flaskType: string): 'string' | 'number' | 'integer' {
|
|
398
|
+
const typeMap: Record<string, 'string' | 'number' | 'integer'> = {
|
|
399
|
+
string: 'string',
|
|
400
|
+
int: 'integer',
|
|
401
|
+
float: 'number',
|
|
402
|
+
path: 'string',
|
|
403
|
+
uuid: 'string',
|
|
404
|
+
};
|
|
405
|
+
return typeMap[flaskType] || 'string';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Parse FastAPI decorator metadata
|
|
410
|
+
*/
|
|
411
|
+
private parseDecoratorMetadata(
|
|
412
|
+
decoratorLine: string,
|
|
413
|
+
content: string,
|
|
414
|
+
line: number
|
|
415
|
+
): {
|
|
416
|
+
tags: string[];
|
|
417
|
+
summary?: string;
|
|
418
|
+
description?: string;
|
|
419
|
+
statusCode?: number;
|
|
420
|
+
responseModel?: string;
|
|
421
|
+
deprecated: boolean;
|
|
422
|
+
} {
|
|
423
|
+
// Get the full decorator including multiple lines
|
|
424
|
+
const lines = content.split('\n');
|
|
425
|
+
let fullDecorator = decoratorLine;
|
|
426
|
+
let i = line;
|
|
427
|
+
|
|
428
|
+
// Handle multi-line decorators
|
|
429
|
+
while (!fullDecorator.includes(')') && i < lines.length) {
|
|
430
|
+
fullDecorator += lines[i];
|
|
431
|
+
i++;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const result = {
|
|
435
|
+
tags: [] as string[],
|
|
436
|
+
summary: undefined as string | undefined,
|
|
437
|
+
description: undefined as string | undefined,
|
|
438
|
+
statusCode: undefined as number | undefined,
|
|
439
|
+
responseModel: undefined as string | undefined,
|
|
440
|
+
deprecated: false,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// Extract tags
|
|
444
|
+
const tagsMatch = fullDecorator.match(/tags\s*=\s*\[([^\]]+)\]/);
|
|
445
|
+
if (tagsMatch) {
|
|
446
|
+
result.tags = tagsMatch[1]
|
|
447
|
+
.split(',')
|
|
448
|
+
.map(t => t.trim().replace(/['"]/g, ''))
|
|
449
|
+
.filter(t => t);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Extract summary
|
|
453
|
+
const summaryMatch = fullDecorator.match(/summary\s*=\s*['"]([^'"]+)['"]/);
|
|
454
|
+
if (summaryMatch) {
|
|
455
|
+
result.summary = summaryMatch[1];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Extract description
|
|
459
|
+
const descMatch = fullDecorator.match(/description\s*=\s*['"]([^'"]+)['"]/);
|
|
460
|
+
if (descMatch) {
|
|
461
|
+
result.description = descMatch[1];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Extract status code
|
|
465
|
+
const statusMatch = fullDecorator.match(/status_code\s*=\s*(\d+)/);
|
|
466
|
+
if (statusMatch) {
|
|
467
|
+
result.statusCode = parseInt(statusMatch[1], 10);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Extract response model
|
|
471
|
+
const modelMatch = fullDecorator.match(/response_model\s*=\s*(\w+)/);
|
|
472
|
+
if (modelMatch) {
|
|
473
|
+
result.responseModel = modelMatch[1];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Check for deprecated
|
|
477
|
+
if (fullDecorator.includes('deprecated=True')) {
|
|
478
|
+
result.deprecated = true;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Find function info after a route decorator
|
|
486
|
+
*/
|
|
487
|
+
private findFunctionInfo(content: string, decoratorLine: number): {
|
|
488
|
+
name: string;
|
|
489
|
+
params: string;
|
|
490
|
+
returnType?: string;
|
|
491
|
+
docSummary?: string;
|
|
492
|
+
docDescription?: string;
|
|
493
|
+
} | undefined {
|
|
494
|
+
const lines = content.split('\n');
|
|
495
|
+
|
|
496
|
+
// Find the function definition after the decorator
|
|
497
|
+
for (let i = decoratorLine; i < Math.min(decoratorLine + 10, lines.length); i++) {
|
|
498
|
+
const line = lines[i];
|
|
499
|
+
const funcMatch = line.match(/(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([\w\[\],\s]+))?/);
|
|
500
|
+
|
|
501
|
+
if (funcMatch) {
|
|
502
|
+
const name = funcMatch[1];
|
|
503
|
+
const params = funcMatch[2];
|
|
504
|
+
const returnType = funcMatch[3];
|
|
505
|
+
|
|
506
|
+
// Look for docstring
|
|
507
|
+
const docstring = this.extractDocstring(lines, i + 1);
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
name,
|
|
511
|
+
params,
|
|
512
|
+
returnType,
|
|
513
|
+
docSummary: docstring?.summary,
|
|
514
|
+
docDescription: docstring?.description,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Extract docstring from function
|
|
524
|
+
*/
|
|
525
|
+
private extractDocstring(lines: string[], startLine: number): {
|
|
526
|
+
summary?: string;
|
|
527
|
+
description?: string;
|
|
528
|
+
} | undefined {
|
|
529
|
+
// Look for docstring start
|
|
530
|
+
for (let i = startLine; i < Math.min(startLine + 3, lines.length); i++) {
|
|
531
|
+
const line = lines[i].trim();
|
|
532
|
+
if (line.startsWith('"""') || line.startsWith("'''")) {
|
|
533
|
+
const quote = line.startsWith('"""') ? '"""' : "'''";
|
|
534
|
+
let docContent = '';
|
|
535
|
+
|
|
536
|
+
// Single line docstring
|
|
537
|
+
if (line.endsWith(quote) && line.length > 6) {
|
|
538
|
+
docContent = line.slice(3, -3);
|
|
539
|
+
} else {
|
|
540
|
+
// Multi-line docstring
|
|
541
|
+
docContent = line.slice(3);
|
|
542
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
543
|
+
const docLine = lines[j];
|
|
544
|
+
if (docLine.includes(quote)) {
|
|
545
|
+
docContent += '\n' + docLine.split(quote)[0];
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
docContent += '\n' + docLine;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const parts = docContent.trim().split('\n\n');
|
|
553
|
+
return {
|
|
554
|
+
summary: parts[0]?.trim(),
|
|
555
|
+
description: parts.length > 1 ? parts.slice(1).join('\n\n').trim() : undefined,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Parse function parameters
|
|
565
|
+
*/
|
|
566
|
+
private parseFunctionParameters(params: string): {
|
|
567
|
+
queryParams: RouteParameter[];
|
|
568
|
+
headerParams: RouteParameter[];
|
|
569
|
+
bodyParam?: RouteBody;
|
|
570
|
+
} {
|
|
571
|
+
const queryParams: RouteParameter[] = [];
|
|
572
|
+
const headerParams: RouteParameter[] = [];
|
|
573
|
+
let bodyParam: RouteBody | undefined;
|
|
574
|
+
|
|
575
|
+
// Split parameters
|
|
576
|
+
const paramList = params.split(',').map(p => p.trim()).filter(p => p);
|
|
577
|
+
|
|
578
|
+
for (const param of paramList) {
|
|
579
|
+
// Skip self, request, etc.
|
|
580
|
+
if (['self', 'request', 'db', 'session'].some(skip => param.startsWith(skip))) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Check for Query parameter
|
|
585
|
+
if (param.includes('= Query(')) {
|
|
586
|
+
const match = param.match(/(\w+)\s*:\s*(\w+)/);
|
|
587
|
+
if (match) {
|
|
588
|
+
queryParams.push({
|
|
589
|
+
name: match[1],
|
|
590
|
+
location: 'query',
|
|
591
|
+
required: !param.includes('None'),
|
|
592
|
+
type: this.mapPythonType(match[2]),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// Check for Header parameter
|
|
597
|
+
else if (param.includes('= Header(')) {
|
|
598
|
+
const match = param.match(/(\w+)\s*:\s*(\w+)/);
|
|
599
|
+
if (match) {
|
|
600
|
+
headerParams.push({
|
|
601
|
+
name: match[1],
|
|
602
|
+
location: 'header',
|
|
603
|
+
required: !param.includes('None'),
|
|
604
|
+
type: this.mapPythonType(match[2]),
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Check for Body parameter
|
|
609
|
+
else if (param.includes('= Body(')) {
|
|
610
|
+
const match = param.match(/(\w+)\s*:\s*(\w+)/);
|
|
611
|
+
if (match) {
|
|
612
|
+
bodyParam = {
|
|
613
|
+
contentType: 'application/json',
|
|
614
|
+
required: !param.includes('None'),
|
|
615
|
+
schema: this.schemas[match[2]]
|
|
616
|
+
? { $ref: `#/components/schemas/${match[2]}` }
|
|
617
|
+
: { type: 'object' },
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// Regular type-hinted parameter (likely Pydantic model for body)
|
|
622
|
+
else if (param.includes(':') && !param.includes('=')) {
|
|
623
|
+
const match = param.match(/(\w+)\s*:\s*(\w+)/);
|
|
624
|
+
if (match && this.schemas[match[2]]) {
|
|
625
|
+
// This is likely a request body
|
|
626
|
+
bodyParam = {
|
|
627
|
+
contentType: 'application/json',
|
|
628
|
+
required: true,
|
|
629
|
+
schema: { $ref: `#/components/schemas/${match[2]}` },
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return { queryParams, headerParams, bodyParam };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Infer body type from function parameters
|
|
640
|
+
*/
|
|
641
|
+
private inferBodyType(params: string): string | undefined {
|
|
642
|
+
const paramList = params.split(',').map(p => p.trim()).filter(p => p);
|
|
643
|
+
|
|
644
|
+
for (const param of paramList) {
|
|
645
|
+
if (['self', 'request', 'db', 'session'].some(skip => param.startsWith(skip))) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const match = param.match(/\w+\s*:\s*(\w+)/);
|
|
650
|
+
if (match && this.schemas[match[1]]) {
|
|
651
|
+
return match[1];
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return undefined;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Map Python type to JSON Schema type
|
|
660
|
+
*/
|
|
661
|
+
private mapPythonType(pythonType: string): 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' {
|
|
662
|
+
const typeMap: Record<string, 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object'> = {
|
|
663
|
+
str: 'string',
|
|
664
|
+
int: 'integer',
|
|
665
|
+
float: 'number',
|
|
666
|
+
bool: 'boolean',
|
|
667
|
+
list: 'array',
|
|
668
|
+
List: 'array',
|
|
669
|
+
dict: 'object',
|
|
670
|
+
Dict: 'object',
|
|
671
|
+
Any: 'object',
|
|
672
|
+
};
|
|
673
|
+
return typeMap[pythonType] || 'object';
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Extract Flask query parameters
|
|
678
|
+
*/
|
|
679
|
+
private extractFlaskQueryParams(content: string, routeLine: number): RouteParameter[] {
|
|
680
|
+
const params: RouteParameter[] = [];
|
|
681
|
+
const lines = content.split('\n');
|
|
682
|
+
|
|
683
|
+
// Look for request.args usage in the function
|
|
684
|
+
for (let i = routeLine; i < Math.min(routeLine + 50, lines.length); i++) {
|
|
685
|
+
const line = lines[i];
|
|
686
|
+
|
|
687
|
+
// Check for request.args.get('param')
|
|
688
|
+
const argsMatch = line.match(/request\.args\.get\s*\(\s*['"](\w+)['"]/g);
|
|
689
|
+
if (argsMatch) {
|
|
690
|
+
for (const match of argsMatch) {
|
|
691
|
+
const paramMatch = match.match(/['"](\w+)['"]/);
|
|
692
|
+
if (paramMatch) {
|
|
693
|
+
params.push({
|
|
694
|
+
name: paramMatch[1],
|
|
695
|
+
location: 'query',
|
|
696
|
+
required: false,
|
|
697
|
+
type: 'string',
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Stop at next function definition
|
|
704
|
+
if (i > routeLine && /^def\s+\w+/.test(line)) {
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return params;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Build response definitions
|
|
714
|
+
*/
|
|
715
|
+
private buildResponses(
|
|
716
|
+
statusCode?: number,
|
|
717
|
+
responseModel?: string,
|
|
718
|
+
method?: HttpMethod
|
|
719
|
+
): RouteResponse[] {
|
|
720
|
+
const responses: RouteResponse[] = [];
|
|
721
|
+
|
|
722
|
+
const defaultStatus = statusCode || this.getDefaultStatus(method);
|
|
723
|
+
|
|
724
|
+
responses.push({
|
|
725
|
+
statusCode: defaultStatus,
|
|
726
|
+
description: this.getStatusDescription(defaultStatus),
|
|
727
|
+
contentType: 'application/json',
|
|
728
|
+
schema: responseModel && this.schemas[responseModel]
|
|
729
|
+
? { $ref: `#/components/schemas/${responseModel}` }
|
|
730
|
+
: { type: 'object' },
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// Add error responses
|
|
734
|
+
responses.push({
|
|
735
|
+
statusCode: 422,
|
|
736
|
+
description: 'Validation Error',
|
|
737
|
+
contentType: 'application/json',
|
|
738
|
+
schema: {
|
|
739
|
+
type: 'object',
|
|
740
|
+
properties: {
|
|
741
|
+
detail: {
|
|
742
|
+
type: 'array',
|
|
743
|
+
items: {
|
|
744
|
+
type: 'object',
|
|
745
|
+
properties: {
|
|
746
|
+
loc: { type: 'array', items: { type: 'string' } },
|
|
747
|
+
msg: { type: 'string' },
|
|
748
|
+
type: { type: 'string' },
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
return responses;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Get default status code for method
|
|
761
|
+
*/
|
|
762
|
+
private getDefaultStatus(method?: HttpMethod): number {
|
|
763
|
+
switch (method) {
|
|
764
|
+
case 'post': return 201;
|
|
765
|
+
case 'delete': return 204;
|
|
766
|
+
default: return 200;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Get status description
|
|
772
|
+
*/
|
|
773
|
+
private getStatusDescription(status: number): string {
|
|
774
|
+
const descriptions: Record<number, string> = {
|
|
775
|
+
200: 'Successful Response',
|
|
776
|
+
201: 'Created',
|
|
777
|
+
204: 'No Content',
|
|
778
|
+
400: 'Bad Request',
|
|
779
|
+
401: 'Unauthorized',
|
|
780
|
+
403: 'Forbidden',
|
|
781
|
+
404: 'Not Found',
|
|
782
|
+
422: 'Validation Error',
|
|
783
|
+
500: 'Internal Server Error',
|
|
784
|
+
};
|
|
785
|
+
return descriptions[status] || 'Response';
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Extract Pydantic models
|
|
790
|
+
*/
|
|
791
|
+
private extractPydanticModels(file: FileContent): void {
|
|
792
|
+
const content = file.content;
|
|
793
|
+
ROUTE_PATTERNS.pydanticModel.lastIndex = 0;
|
|
794
|
+
|
|
795
|
+
let match;
|
|
796
|
+
while ((match = ROUTE_PATTERNS.pydanticModel.exec(content)) !== null) {
|
|
797
|
+
const name = match[1];
|
|
798
|
+
const body = match[2];
|
|
799
|
+
this.schemas[name] = this.parsePydanticModel(body);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Parse Pydantic model body to JSON Schema
|
|
805
|
+
*/
|
|
806
|
+
private parsePydanticModel(body: string): JsonSchemaDefinition {
|
|
807
|
+
const properties: Record<string, JsonSchemaDefinition> = {};
|
|
808
|
+
const required: string[] = [];
|
|
809
|
+
|
|
810
|
+
const lines = body.split('\n').filter(l => l.trim() && !l.trim().startsWith('#'));
|
|
811
|
+
|
|
812
|
+
for (const line of lines) {
|
|
813
|
+
const match = line.match(/^\s*(\w+)\s*:\s*([^=]+)(?:=\s*(.+))?/);
|
|
814
|
+
if (match) {
|
|
815
|
+
const propName = match[1];
|
|
816
|
+
const propType = match[2].trim();
|
|
817
|
+
const defaultValue = match[3]?.trim();
|
|
818
|
+
|
|
819
|
+
properties[propName] = this.pythonTypeToJsonSchema(propType);
|
|
820
|
+
|
|
821
|
+
// If no default value and not Optional, it's required
|
|
822
|
+
if (!defaultValue && !propType.includes('Optional')) {
|
|
823
|
+
required.push(propName);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return {
|
|
829
|
+
type: 'object',
|
|
830
|
+
properties,
|
|
831
|
+
required: required.length > 0 ? required : undefined,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Convert Python type annotation to JSON Schema
|
|
837
|
+
*/
|
|
838
|
+
private pythonTypeToJsonSchema(pythonType: string): JsonSchemaDefinition {
|
|
839
|
+
const type = pythonType.trim();
|
|
840
|
+
|
|
841
|
+
// Check for Optional
|
|
842
|
+
const optionalMatch = type.match(/Optional\[(.+)\]/);
|
|
843
|
+
if (optionalMatch) {
|
|
844
|
+
const innerSchema = this.pythonTypeToJsonSchema(optionalMatch[1]);
|
|
845
|
+
return { ...innerSchema, nullable: true };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Check for List
|
|
849
|
+
const listMatch = type.match(/(?:List|list)\[(.+)\]/);
|
|
850
|
+
if (listMatch) {
|
|
851
|
+
return {
|
|
852
|
+
type: 'array',
|
|
853
|
+
items: this.pythonTypeToJsonSchema(listMatch[1]),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Check for Dict
|
|
858
|
+
const dictMatch = type.match(/(?:Dict|dict)\[(\w+),\s*(.+)\]/);
|
|
859
|
+
if (dictMatch) {
|
|
860
|
+
return {
|
|
861
|
+
type: 'object',
|
|
862
|
+
additionalProperties: this.pythonTypeToJsonSchema(dictMatch[2]),
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Basic types
|
|
867
|
+
const typeMap: Record<string, JsonSchemaDefinition> = {
|
|
868
|
+
str: { type: 'string' },
|
|
869
|
+
int: { type: 'integer' },
|
|
870
|
+
float: { type: 'number' },
|
|
871
|
+
bool: { type: 'boolean' },
|
|
872
|
+
Any: { type: 'object' },
|
|
873
|
+
None: { type: 'null' },
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
if (typeMap[type]) {
|
|
877
|
+
return typeMap[type];
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Check if it's a reference to a known schema
|
|
881
|
+
if (this.schemas[type]) {
|
|
882
|
+
return { $ref: `#/components/schemas/${type}` };
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return { type: 'object' };
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Generate operation ID
|
|
890
|
+
*/
|
|
891
|
+
private generateOperationId(method: HttpMethod, path: string): string {
|
|
892
|
+
const cleanPath = path
|
|
893
|
+
.replace(/[/{}]/g, '_')
|
|
894
|
+
.replace(/_+/g, '_')
|
|
895
|
+
.replace(/^_|_$/g, '');
|
|
896
|
+
return `${method}_${cleanPath || 'root'}`;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Infer tags from path
|
|
901
|
+
*/
|
|
902
|
+
private inferTags(path: string): string[] {
|
|
903
|
+
const parts = path.split('/').filter(p => p && !p.startsWith('{'));
|
|
904
|
+
if (parts.length > 0) {
|
|
905
|
+
return [parts[0].charAt(0).toUpperCase() + parts[0].slice(1)];
|
|
906
|
+
}
|
|
907
|
+
return ['Default'];
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Get line number from character index
|
|
912
|
+
*/
|
|
913
|
+
private getLineNumber(content: string, index: number): number {
|
|
914
|
+
return content.slice(0, index).split('\n').length;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Detect security schemes
|
|
919
|
+
*/
|
|
920
|
+
private detectSecuritySchemes(files: FileContent[]): Record<string, OpenAPIV3_1.SecuritySchemeObject> {
|
|
921
|
+
const schemes: Record<string, OpenAPIV3_1.SecuritySchemeObject> = {};
|
|
922
|
+
|
|
923
|
+
for (const file of files) {
|
|
924
|
+
const content = file.content;
|
|
925
|
+
|
|
926
|
+
// OAuth2
|
|
927
|
+
if (content.includes('OAuth2PasswordBearer') || content.includes('oauth2_scheme')) {
|
|
928
|
+
schemes.oauth2 = {
|
|
929
|
+
type: 'oauth2',
|
|
930
|
+
flows: {
|
|
931
|
+
password: {
|
|
932
|
+
tokenUrl: '/token',
|
|
933
|
+
scopes: {},
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// API Key
|
|
940
|
+
if (content.includes('APIKeyHeader') || content.includes('api_key')) {
|
|
941
|
+
schemes.apiKey = {
|
|
942
|
+
type: 'apiKey',
|
|
943
|
+
in: 'header',
|
|
944
|
+
name: 'X-API-Key',
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// JWT/Bearer
|
|
949
|
+
if (content.includes('HTTPBearer') || content.includes('jwt')) {
|
|
950
|
+
schemes.bearerAuth = {
|
|
951
|
+
type: 'http',
|
|
952
|
+
scheme: 'bearer',
|
|
953
|
+
bearerFormat: 'JWT',
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return schemes;
|
|
959
|
+
}
|
|
960
|
+
}
|