archapi 0.3.0__py3-none-any.whl
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.
- archapi/__init__.py +3 -0
- archapi/core.py +295 -0
- archapi/frameworks/__init__.py +0 -0
- archapi/frameworks/base.py +56 -0
- archapi/frameworks/detector.py +113 -0
- archapi/frameworks/express_ts/__init__.py +0 -0
- archapi/frameworks/express_ts/adapter.py +237 -0
- archapi/frameworks/fastapi_adapter.py +184 -0
- archapi/frameworks/generic.py +211 -0
- archapi/frameworks/registry.py +28 -0
- archapi/generation/__init__.py +0 -0
- archapi/genome/__init__.py +0 -0
- archapi/indexing/__init__.py +0 -0
- archapi/indexing/cache.py +141 -0
- archapi/mapping/__init__.py +0 -0
- archapi/planning/__init__.py +0 -0
- archapi/planning/intent_planner.py +175 -0
- archapi/planning/task_dag.py +81 -0
- archapi/scanner/__init__.py +0 -0
- archapi/security/__init__.py +0 -0
- archapi/security/context_redactor.py +38 -0
- archapi/security/policy_gate.py +70 -0
- archapi/security/secret_scanner.py +90 -0
- archapi/types.py +103 -0
- archapi/validation/__init__.py +0 -0
- archapi/validation/architecture_score.py +77 -0
- archapi/validation/basic_validators.py +38 -0
- archapi/validation/command_validator.py +146 -0
- archapi-0.3.0.dist-info/METADATA +79 -0
- archapi-0.3.0.dist-info/RECORD +33 -0
- archapi-0.3.0.dist-info/WHEEL +5 -0
- archapi-0.3.0.dist-info/licenses/LICENSE +21 -0
- archapi-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
from archapi.frameworks.generic import GenericAdapter
|
|
7
|
+
from archapi.types import APIPlan, APIGenome, GeneratedFile, ScanResult, ValidationReport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExpressTypeScriptAdapter(GenericAdapter):
|
|
11
|
+
name = "express-typescript"
|
|
12
|
+
|
|
13
|
+
def scan(self, project_path: Path) -> ScanResult:
|
|
14
|
+
result = super().scan(project_path)
|
|
15
|
+
result.framework = self.name
|
|
16
|
+
return result
|
|
17
|
+
|
|
18
|
+
def extract_genome(self, maps: Dict[str, Any], scan_result: ScanResult) -> APIGenome:
|
|
19
|
+
genome = super().extract_genome(maps, scan_result)
|
|
20
|
+
genome.framework = self.name
|
|
21
|
+
|
|
22
|
+
package_json = scan_result.project_path / "package.json"
|
|
23
|
+
package_text = ""
|
|
24
|
+
if package_json.exists():
|
|
25
|
+
package_text = package_json.read_text(
|
|
26
|
+
encoding="utf-8",
|
|
27
|
+
errors="ignore",
|
|
28
|
+
).lower()
|
|
29
|
+
|
|
30
|
+
if "zod" in package_text:
|
|
31
|
+
genome.schema_style = "zod"
|
|
32
|
+
elif "joi" in package_text:
|
|
33
|
+
genome.schema_style = "joi"
|
|
34
|
+
|
|
35
|
+
if "jest" in package_text and "supertest" in package_text:
|
|
36
|
+
genome.test_style = "jest-supertest"
|
|
37
|
+
|
|
38
|
+
genome.route_style = "express-router" if scan_result.routes else "unknown"
|
|
39
|
+
genome.controller_style = "express-controller" if scan_result.controllers else "unknown"
|
|
40
|
+
genome.service_style = "service-layer" if scan_result.services else "unknown"
|
|
41
|
+
genome.metadata["language"] = "typescript"
|
|
42
|
+
genome.metadata["project_path"] = str(scan_result.project_path)
|
|
43
|
+
|
|
44
|
+
required_layers = [
|
|
45
|
+
bool(scan_result.routes),
|
|
46
|
+
bool(scan_result.controllers),
|
|
47
|
+
bool(scan_result.services),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
has_express_package = "express" in package_text
|
|
51
|
+
|
|
52
|
+
if not any(required_layers):
|
|
53
|
+
genome.confidence = min(genome.confidence, 0.10)
|
|
54
|
+
elif not has_express_package:
|
|
55
|
+
# If route/controller/service layers are present but package.json is missing
|
|
56
|
+
# at project root, allow generation with warning-level confidence.
|
|
57
|
+
# This supports config-hint mode for monorepos or unusual layouts.
|
|
58
|
+
genome.confidence = min(genome.confidence, 0.65)
|
|
59
|
+
genome.metadata["package_json_warning"] = "Express package.json not found at project root."
|
|
60
|
+
|
|
61
|
+
return genome
|
|
62
|
+
|
|
63
|
+
def generate_code(
|
|
64
|
+
self,
|
|
65
|
+
plan: APIPlan,
|
|
66
|
+
genome: APIGenome,
|
|
67
|
+
maps: Dict[str, Any],
|
|
68
|
+
) -> List[GeneratedFile]:
|
|
69
|
+
if not plan.generation_allowed:
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
entity = plan.entities[-1] if plan.entities else "Resource"
|
|
73
|
+
entity_pascal = entity[0].upper() + entity[1:]
|
|
74
|
+
entity_lower = entity_pascal[0].lower() + entity_pascal[1:]
|
|
75
|
+
entity_file = entity_lower.lower()
|
|
76
|
+
|
|
77
|
+
route_dir = self._output_dir(maps, "route_map", "src/routes")
|
|
78
|
+
controller_dir = self._output_dir(maps, "controller_map", "src/controllers")
|
|
79
|
+
service_dir = self._output_dir(maps, "service_map", "src/services")
|
|
80
|
+
schema_dir = self._output_dir(maps, "schema_map", "src/schemas")
|
|
81
|
+
test_dir = self._output_dir(maps, "test_map", "tests")
|
|
82
|
+
|
|
83
|
+
route_path = route_dir / f"{entity_file}.routes.ts"
|
|
84
|
+
controller_path = controller_dir / f"{entity_file}.controller.ts"
|
|
85
|
+
service_path = service_dir / f"{entity_file}.service.ts"
|
|
86
|
+
schema_path = schema_dir / f"{entity_file}.schema.ts"
|
|
87
|
+
test_path = test_dir / f"{entity_file}.test.ts"
|
|
88
|
+
|
|
89
|
+
express_path = (
|
|
90
|
+
plan.path
|
|
91
|
+
.replace("{user_id}", ":userId")
|
|
92
|
+
.replace("{product_id}", ":productId")
|
|
93
|
+
.replace("{id}", ":id")
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
status_code = int(plan.metadata.get("response_status", 200))
|
|
97
|
+
action = plan.metadata.get("action", "unknown")
|
|
98
|
+
|
|
99
|
+
route_content = f"""import {{ Router, Request, Response, NextFunction }} from "express";
|
|
100
|
+
import {{ {entity_lower}Controller }} from "../controllers/{entity_file}.controller";
|
|
101
|
+
import {{ {entity_lower}RequestSchema }} from "../schemas/{entity_file}.schema";
|
|
102
|
+
|
|
103
|
+
const router = Router();
|
|
104
|
+
|
|
105
|
+
function validate{entity_pascal}Request(req: Request, res: Response, next: NextFunction) {{
|
|
106
|
+
const parsed = {entity_lower}RequestSchema.safeParse({{
|
|
107
|
+
params: req.params,
|
|
108
|
+
query: req.query,
|
|
109
|
+
body: req.body,
|
|
110
|
+
}});
|
|
111
|
+
|
|
112
|
+
if (!parsed.success) {{
|
|
113
|
+
return res.status(400).json({{ errors: parsed.error.flatten() }});
|
|
114
|
+
}}
|
|
115
|
+
|
|
116
|
+
return next();
|
|
117
|
+
}}
|
|
118
|
+
|
|
119
|
+
router.{plan.method.lower()}("{express_path}", validate{entity_pascal}Request, {entity_lower}Controller.handle);
|
|
120
|
+
|
|
121
|
+
export default router;
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
controller_content = f"""import {{ Request, Response, NextFunction }} from "express";
|
|
125
|
+
import {{ {entity_lower}Service }} from "../services/{entity_file}.service";
|
|
126
|
+
|
|
127
|
+
export const {entity_lower}Controller = {{
|
|
128
|
+
async handle(req: Request, res: Response, next: NextFunction) {{
|
|
129
|
+
try {{
|
|
130
|
+
const result = await {entity_lower}Service.execute({{
|
|
131
|
+
params: req.params,
|
|
132
|
+
query: req.query,
|
|
133
|
+
body: req.body,
|
|
134
|
+
}});
|
|
135
|
+
|
|
136
|
+
return res.status({status_code}).json({{ data: result }});
|
|
137
|
+
}} catch (error) {{
|
|
138
|
+
return next(error);
|
|
139
|
+
}}
|
|
140
|
+
}},
|
|
141
|
+
}};
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
service_content = f"""type {entity_pascal}ServiceInput = {{
|
|
145
|
+
params: Record<string, unknown>;
|
|
146
|
+
query: Record<string, unknown>;
|
|
147
|
+
body: unknown;
|
|
148
|
+
}};
|
|
149
|
+
|
|
150
|
+
export const {entity_lower}Service = {{
|
|
151
|
+
async execute(input: {entity_pascal}ServiceInput) {{
|
|
152
|
+
// TODO: Replace this placeholder with project-specific business logic.
|
|
153
|
+
return {{
|
|
154
|
+
message: "{entity_pascal} API placeholder response",
|
|
155
|
+
action: "{action}",
|
|
156
|
+
params: input.params,
|
|
157
|
+
query: input.query,
|
|
158
|
+
body: input.body,
|
|
159
|
+
}};
|
|
160
|
+
}},
|
|
161
|
+
}};
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
schema_content = f"""import {{ z }} from "zod";
|
|
165
|
+
|
|
166
|
+
export const {entity_lower}RequestSchema = z.object({{
|
|
167
|
+
params: z.record(z.unknown()).optional(),
|
|
168
|
+
query: z.record(z.unknown()).optional(),
|
|
169
|
+
body: z.unknown().optional(),
|
|
170
|
+
}});
|
|
171
|
+
|
|
172
|
+
export type {entity_pascal}Request = z.infer<typeof {entity_lower}RequestSchema>;
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
test_content = f"""describe("{entity_pascal} API", () => {{
|
|
176
|
+
it("should have a generated placeholder test", () => {{
|
|
177
|
+
// TODO: Replace this placeholder with a real Supertest request.
|
|
178
|
+
expect(true).toBe(true);
|
|
179
|
+
}});
|
|
180
|
+
}});
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
return [
|
|
184
|
+
GeneratedFile(route_path, route_content),
|
|
185
|
+
GeneratedFile(controller_path, controller_content),
|
|
186
|
+
GeneratedFile(service_path, service_content),
|
|
187
|
+
GeneratedFile(schema_path, schema_content),
|
|
188
|
+
GeneratedFile(test_path, test_content),
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
def _output_dir(self, maps: Dict[str, Any], map_key: str, fallback: str) -> Path:
|
|
192
|
+
values = maps.get(map_key, {})
|
|
193
|
+
project_path = Path(maps.get("_project_path", "."))
|
|
194
|
+
|
|
195
|
+
if isinstance(values, dict) and values:
|
|
196
|
+
first_path = Path(next(iter(values.values()))).resolve()
|
|
197
|
+
parent = first_path.parent
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
return parent.relative_to(project_path)
|
|
201
|
+
except ValueError:
|
|
202
|
+
return Path(fallback)
|
|
203
|
+
|
|
204
|
+
return Path(fallback)
|
|
205
|
+
|
|
206
|
+
def validate_generated_code(
|
|
207
|
+
self,
|
|
208
|
+
files: List[GeneratedFile],
|
|
209
|
+
plan: APIPlan,
|
|
210
|
+
genome: APIGenome,
|
|
211
|
+
) -> ValidationReport:
|
|
212
|
+
errors: List[str] = []
|
|
213
|
+
warnings: List[str] = []
|
|
214
|
+
|
|
215
|
+
if not plan.generation_allowed:
|
|
216
|
+
errors.append(plan.reason or "Generation not allowed.")
|
|
217
|
+
|
|
218
|
+
required_layers = ["routes", "controllers", "services", "schemas", "tests"]
|
|
219
|
+
generated_paths = [str(file.path) for file in files]
|
|
220
|
+
|
|
221
|
+
for layer in required_layers:
|
|
222
|
+
if not any(layer in path for path in generated_paths):
|
|
223
|
+
errors.append(f"Missing generated {layer} layer.")
|
|
224
|
+
|
|
225
|
+
for file in files:
|
|
226
|
+
if not file.content.strip():
|
|
227
|
+
errors.append(f"Generated file is empty: {file.path}")
|
|
228
|
+
|
|
229
|
+
if Path(file.path).exists():
|
|
230
|
+
warnings.append(
|
|
231
|
+
f"Generated file path already exists relative to current directory: {file.path}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if genome.confidence < 0.75:
|
|
235
|
+
warnings.append("Architecture confidence is moderate; review generated files before applying.")
|
|
236
|
+
|
|
237
|
+
return ValidationReport(success=not errors, errors=errors, warnings=warnings)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
from archapi.frameworks.generic import GenericAdapter
|
|
7
|
+
from archapi.types import APIPlan, APIGenome, GeneratedFile, ScanResult, ValidationReport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FastAPIAdapter(GenericAdapter):
|
|
11
|
+
name = "fastapi"
|
|
12
|
+
|
|
13
|
+
def scan(self, project_path: Path) -> ScanResult:
|
|
14
|
+
result = super().scan(project_path)
|
|
15
|
+
result.framework = self.name
|
|
16
|
+
return result
|
|
17
|
+
|
|
18
|
+
def extract_genome(self, maps: Dict[str, Any], scan_result: ScanResult) -> APIGenome:
|
|
19
|
+
genome = super().extract_genome(maps, scan_result)
|
|
20
|
+
genome.framework = self.name
|
|
21
|
+
|
|
22
|
+
req = scan_result.project_path / "requirements.txt"
|
|
23
|
+
pyproject = scan_result.project_path / "pyproject.toml"
|
|
24
|
+
|
|
25
|
+
dep_text = ""
|
|
26
|
+
if req.exists():
|
|
27
|
+
dep_text += req.read_text(encoding="utf-8", errors="ignore").lower()
|
|
28
|
+
if pyproject.exists():
|
|
29
|
+
dep_text += pyproject.read_text(encoding="utf-8", errors="ignore").lower()
|
|
30
|
+
|
|
31
|
+
genome.route_style = "fastapi-apirouter" if scan_result.routes else "unknown"
|
|
32
|
+
genome.controller_style = "fastapi-endpoint" if scan_result.controllers or scan_result.routes else "unknown"
|
|
33
|
+
genome.service_style = "service-layer" if scan_result.services else "unknown"
|
|
34
|
+
genome.schema_style = "pydantic" if "pydantic" in dep_text or scan_result.schemas else "unknown"
|
|
35
|
+
genome.test_style = "pytest" if "pytest" in dep_text or scan_result.tests else "unknown"
|
|
36
|
+
genome.metadata["language"] = "python"
|
|
37
|
+
genome.metadata["project_path"] = str(scan_result.project_path)
|
|
38
|
+
|
|
39
|
+
has_fastapi_dependency = "fastapi" in dep_text
|
|
40
|
+
|
|
41
|
+
if not scan_result.routes and not scan_result.services:
|
|
42
|
+
genome.confidence = min(genome.confidence, 0.10)
|
|
43
|
+
elif not has_fastapi_dependency:
|
|
44
|
+
genome.confidence = min(genome.confidence, 0.65)
|
|
45
|
+
genome.metadata["dependency_warning"] = "FastAPI dependency not found at project root."
|
|
46
|
+
|
|
47
|
+
return genome
|
|
48
|
+
|
|
49
|
+
def generate_code(
|
|
50
|
+
self,
|
|
51
|
+
plan: APIPlan,
|
|
52
|
+
genome: APIGenome,
|
|
53
|
+
maps: Dict[str, Any],
|
|
54
|
+
) -> List[GeneratedFile]:
|
|
55
|
+
if not plan.generation_allowed:
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
entity = plan.entities[-1] if plan.entities else "Resource"
|
|
59
|
+
entity_pascal = entity[0].upper() + entity[1:]
|
|
60
|
+
entity_lower = entity_pascal[0].lower() + entity_pascal[1:]
|
|
61
|
+
entity_file = entity_lower.lower()
|
|
62
|
+
|
|
63
|
+
router_dir = self._output_dir(maps, "route_map", "app/routers")
|
|
64
|
+
service_dir = self._output_dir(maps, "service_map", "app/services")
|
|
65
|
+
schema_dir = self._output_dir(maps, "schema_map", "app/schemas")
|
|
66
|
+
test_dir = self._output_dir(maps, "test_map", "tests")
|
|
67
|
+
|
|
68
|
+
router_path = router_dir / f"{entity_file}_router.py"
|
|
69
|
+
service_path = service_dir / f"{entity_file}_service.py"
|
|
70
|
+
schema_path = schema_dir / f"{entity_file}_schema.py"
|
|
71
|
+
test_path = test_dir / f"test_{entity_file}.py"
|
|
72
|
+
|
|
73
|
+
method = plan.method.lower()
|
|
74
|
+
status_code = int(plan.metadata.get("response_status", 200))
|
|
75
|
+
action = plan.metadata.get("action", "unknown")
|
|
76
|
+
|
|
77
|
+
router_content = f'''from fastapi import APIRouter, HTTPException
|
|
78
|
+
from app.schemas.{entity_file}_schema import {entity_pascal}Request, {entity_pascal}Response
|
|
79
|
+
from app.services.{entity_file}_service import {entity_lower}_service
|
|
80
|
+
|
|
81
|
+
router = APIRouter()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@router.{method}("{plan.path}", response_model={entity_pascal}Response, status_code={status_code})
|
|
85
|
+
async def handle_{entity_lower}(request: {entity_pascal}Request = None):
|
|
86
|
+
try:
|
|
87
|
+
result = await {entity_lower}_service.execute(request)
|
|
88
|
+
return result
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
raise HTTPException(status_code=500, detail=str(exc))
|
|
91
|
+
'''
|
|
92
|
+
|
|
93
|
+
service_content = f'''from app.schemas.{entity_file}_schema import {entity_pascal}Request, {entity_pascal}Response
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class {entity_pascal}Service:
|
|
97
|
+
async def execute(self, request: {entity_pascal}Request | None) -> {entity_pascal}Response:
|
|
98
|
+
# TODO: Replace this placeholder with project-specific business logic.
|
|
99
|
+
return {entity_pascal}Response(
|
|
100
|
+
message="{entity_pascal} API placeholder response",
|
|
101
|
+
action="{action}",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
{entity_lower}_service = {entity_pascal}Service()
|
|
106
|
+
'''
|
|
107
|
+
|
|
108
|
+
schema_content = f'''from pydantic import BaseModel
|
|
109
|
+
from typing import Optional, Any, Dict
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class {entity_pascal}Request(BaseModel):
|
|
113
|
+
params: Optional[Dict[str, Any]] = None
|
|
114
|
+
query: Optional[Dict[str, Any]] = None
|
|
115
|
+
body: Optional[Any] = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class {entity_pascal}Response(BaseModel):
|
|
119
|
+
message: str
|
|
120
|
+
action: str
|
|
121
|
+
'''
|
|
122
|
+
|
|
123
|
+
test_content = f'''def test_{entity_lower}_generated_placeholder():
|
|
124
|
+
# TODO: Replace this placeholder with a real TestClient request.
|
|
125
|
+
assert True
|
|
126
|
+
'''
|
|
127
|
+
|
|
128
|
+
return [
|
|
129
|
+
GeneratedFile(router_path, router_content),
|
|
130
|
+
GeneratedFile(service_path, service_content),
|
|
131
|
+
GeneratedFile(schema_path, schema_content),
|
|
132
|
+
GeneratedFile(test_path, test_content),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
def _output_dir(self, maps: Dict[str, Any], map_key: str, fallback: str) -> Path:
|
|
136
|
+
values = maps.get(map_key, {})
|
|
137
|
+
project_path = Path(maps.get("_project_path", "."))
|
|
138
|
+
|
|
139
|
+
if isinstance(values, dict) and values:
|
|
140
|
+
first_path = Path(next(iter(values.values()))).resolve()
|
|
141
|
+
parent = first_path.parent
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
return parent.relative_to(project_path)
|
|
145
|
+
except ValueError:
|
|
146
|
+
return Path(fallback)
|
|
147
|
+
|
|
148
|
+
return Path(fallback)
|
|
149
|
+
|
|
150
|
+
def validate_generated_code(
|
|
151
|
+
self,
|
|
152
|
+
files: List[GeneratedFile],
|
|
153
|
+
plan: APIPlan,
|
|
154
|
+
genome: APIGenome,
|
|
155
|
+
) -> ValidationReport:
|
|
156
|
+
errors: List[str] = []
|
|
157
|
+
warnings: List[str] = []
|
|
158
|
+
|
|
159
|
+
if not plan.generation_allowed:
|
|
160
|
+
errors.append(plan.reason or "Generation not allowed.")
|
|
161
|
+
|
|
162
|
+
required_suffixes = ["_router.py", "_service.py", "_schema.py"]
|
|
163
|
+
generated_paths = [str(file.path) for file in files]
|
|
164
|
+
|
|
165
|
+
for suffix in required_suffixes:
|
|
166
|
+
if not any(path.endswith(suffix) for path in generated_paths):
|
|
167
|
+
errors.append(f"Missing generated FastAPI layer: {suffix}")
|
|
168
|
+
|
|
169
|
+
if not any("/test_" in path or path.startswith("tests/test_") for path in generated_paths):
|
|
170
|
+
errors.append("Missing generated FastAPI test layer.")
|
|
171
|
+
|
|
172
|
+
for file in files:
|
|
173
|
+
if not file.content.strip():
|
|
174
|
+
errors.append(f"Generated file is empty: {file.path}")
|
|
175
|
+
|
|
176
|
+
if Path(file.path).exists():
|
|
177
|
+
warnings.append(
|
|
178
|
+
f"Generated file path already exists relative to current directory: {file.path}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if genome.confidence < 0.75:
|
|
182
|
+
warnings.append("Architecture confidence is moderate; review generated files before applying.")
|
|
183
|
+
|
|
184
|
+
return ValidationReport(success=not errors, errors=errors, warnings=warnings)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
from archapi.frameworks.base import FrameworkAdapter
|
|
7
|
+
from archapi.planning.intent_planner import IntentPlanner
|
|
8
|
+
from archapi.types import (
|
|
9
|
+
APIPlan,
|
|
10
|
+
APIGenome,
|
|
11
|
+
DetectionResult,
|
|
12
|
+
GeneratedFile,
|
|
13
|
+
ScanResult,
|
|
14
|
+
ValidationReport,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
IGNORED_DIRS = {
|
|
19
|
+
"node_modules",
|
|
20
|
+
".git",
|
|
21
|
+
"dist",
|
|
22
|
+
"build",
|
|
23
|
+
"coverage",
|
|
24
|
+
".venv",
|
|
25
|
+
"__pycache__",
|
|
26
|
+
"vendor",
|
|
27
|
+
"target",
|
|
28
|
+
".archapi",
|
|
29
|
+
"sample_projects",
|
|
30
|
+
"archapi.egg-info",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GenericAdapter(FrameworkAdapter):
|
|
35
|
+
name = "generic"
|
|
36
|
+
|
|
37
|
+
def detect(self, project_path: Path) -> DetectionResult:
|
|
38
|
+
return DetectionResult("generic", 0.10, ["Fallback generic adapter"])
|
|
39
|
+
|
|
40
|
+
def scan(self, project_path: Path) -> ScanResult:
|
|
41
|
+
result = ScanResult(framework=self.name, project_path=project_path)
|
|
42
|
+
|
|
43
|
+
for path in self._walk_files(project_path):
|
|
44
|
+
lower = str(path).lower()
|
|
45
|
+
|
|
46
|
+
if "route" in lower or "url" in lower:
|
|
47
|
+
result.routes.append(path)
|
|
48
|
+
elif "controller" in lower or "handler" in lower or "view" in lower:
|
|
49
|
+
result.controllers.append(path)
|
|
50
|
+
elif "service" in lower:
|
|
51
|
+
result.services.append(path)
|
|
52
|
+
elif "model" in lower or "entity" in lower:
|
|
53
|
+
result.models.append(path)
|
|
54
|
+
elif "schema" in lower or "serializer" in lower or "dto" in lower:
|
|
55
|
+
result.schemas.append(path)
|
|
56
|
+
elif "middleware" in lower or "permission" in lower or "auth" in lower:
|
|
57
|
+
result.middleware.append(path)
|
|
58
|
+
elif "test" in lower or "spec" in lower:
|
|
59
|
+
result.tests.append(path)
|
|
60
|
+
elif path.name in {
|
|
61
|
+
"package.json",
|
|
62
|
+
"pyproject.toml",
|
|
63
|
+
"requirements.txt",
|
|
64
|
+
"pom.xml",
|
|
65
|
+
"build.gradle",
|
|
66
|
+
"go.mod",
|
|
67
|
+
"composer.json",
|
|
68
|
+
"Gemfile",
|
|
69
|
+
}:
|
|
70
|
+
result.config_files.append(path)
|
|
71
|
+
else:
|
|
72
|
+
result.unknown.append(path)
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
def build_maps(self, scan_result: ScanResult) -> Dict[str, Any]:
|
|
77
|
+
return {
|
|
78
|
+
"_project_path": str(scan_result.project_path),
|
|
79
|
+
"file_map": {
|
|
80
|
+
str(path.relative_to(scan_result.project_path)): str(path)
|
|
81
|
+
for path in (
|
|
82
|
+
scan_result.routes
|
|
83
|
+
+ scan_result.controllers
|
|
84
|
+
+ scan_result.services
|
|
85
|
+
+ scan_result.models
|
|
86
|
+
+ scan_result.schemas
|
|
87
|
+
+ scan_result.middleware
|
|
88
|
+
+ scan_result.tests
|
|
89
|
+
)
|
|
90
|
+
},
|
|
91
|
+
"route_map": self._name_map(scan_result.routes),
|
|
92
|
+
"controller_map": self._name_map(scan_result.controllers),
|
|
93
|
+
"service_map": self._name_map(scan_result.services),
|
|
94
|
+
"model_map": self._name_map(scan_result.models),
|
|
95
|
+
"schema_map": self._name_map(scan_result.schemas),
|
|
96
|
+
"middleware_map": self._name_map(scan_result.middleware),
|
|
97
|
+
"test_map": self._name_map(scan_result.tests),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
def extract_genome(self, maps: Dict[str, Any], scan_result: ScanResult) -> APIGenome:
|
|
101
|
+
confidence = 0.0
|
|
102
|
+
confidence += 0.20 if scan_result.routes else 0
|
|
103
|
+
confidence += 0.20 if scan_result.controllers else 0
|
|
104
|
+
confidence += 0.20 if scan_result.services else 0
|
|
105
|
+
confidence += 0.15 if scan_result.models else 0
|
|
106
|
+
confidence += 0.15 if scan_result.schemas else 0
|
|
107
|
+
confidence += 0.10 if scan_result.tests else 0
|
|
108
|
+
|
|
109
|
+
return APIGenome(
|
|
110
|
+
framework=self.name,
|
|
111
|
+
route_style="detected" if scan_result.routes else "unknown",
|
|
112
|
+
controller_style="detected" if scan_result.controllers else "unknown",
|
|
113
|
+
service_style="detected" if scan_result.services else "unknown",
|
|
114
|
+
model_style="detected" if scan_result.models else "unknown",
|
|
115
|
+
schema_style="detected" if scan_result.schemas else "unknown",
|
|
116
|
+
auth_style="detected" if scan_result.middleware else "unknown",
|
|
117
|
+
test_style="detected" if scan_result.tests else "unknown",
|
|
118
|
+
confidence=round(confidence, 2),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def plan_api(self, request: str, genome: APIGenome, maps: Dict[str, Any]) -> APIPlan:
|
|
122
|
+
intent = IntentPlanner().plan(request)
|
|
123
|
+
|
|
124
|
+
generation_allowed = genome.confidence >= 0.45
|
|
125
|
+
reason = None if generation_allowed else "Architecture confidence too low; returning plan only."
|
|
126
|
+
|
|
127
|
+
return APIPlan(
|
|
128
|
+
request=request,
|
|
129
|
+
method=intent.method,
|
|
130
|
+
path=intent.path,
|
|
131
|
+
entities=intent.entities,
|
|
132
|
+
layers=["route", "controller", "service", "schema", "test"],
|
|
133
|
+
generation_allowed=generation_allowed,
|
|
134
|
+
reason=reason,
|
|
135
|
+
metadata={
|
|
136
|
+
"adapter": self.name,
|
|
137
|
+
"resource": intent.resource,
|
|
138
|
+
"action": intent.action,
|
|
139
|
+
"response_status": intent.response_status,
|
|
140
|
+
**intent.metadata,
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def generate_code(
|
|
145
|
+
self,
|
|
146
|
+
plan: APIPlan,
|
|
147
|
+
genome: APIGenome,
|
|
148
|
+
maps: Dict[str, Any],
|
|
149
|
+
) -> List[GeneratedFile]:
|
|
150
|
+
if not plan.generation_allowed:
|
|
151
|
+
return []
|
|
152
|
+
|
|
153
|
+
entity = plan.entities[-1] if plan.entities else "Resource"
|
|
154
|
+
lower = entity.lower()
|
|
155
|
+
|
|
156
|
+
return [
|
|
157
|
+
GeneratedFile(
|
|
158
|
+
path=Path(f"generated/{lower}_api.txt"),
|
|
159
|
+
content=(
|
|
160
|
+
"# Generated API plan\n"
|
|
161
|
+
f"method: {plan.method}\n"
|
|
162
|
+
f"path: {plan.path}\n"
|
|
163
|
+
f"entity: {entity}\n"
|
|
164
|
+
"note: Generic framework fallback was used.\n"
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
def validate_generated_code(
|
|
170
|
+
self,
|
|
171
|
+
files: List[GeneratedFile],
|
|
172
|
+
plan: APIPlan,
|
|
173
|
+
genome: APIGenome,
|
|
174
|
+
) -> ValidationReport:
|
|
175
|
+
errors = []
|
|
176
|
+
warnings = []
|
|
177
|
+
|
|
178
|
+
if not plan.generation_allowed:
|
|
179
|
+
errors.append(plan.reason or "Generation not allowed.")
|
|
180
|
+
|
|
181
|
+
if plan.generation_allowed and not files:
|
|
182
|
+
errors.append("No files generated.")
|
|
183
|
+
|
|
184
|
+
for file in files:
|
|
185
|
+
if not file.content.strip():
|
|
186
|
+
errors.append(f"Generated file is empty: {file.path}")
|
|
187
|
+
|
|
188
|
+
return ValidationReport(success=not errors, errors=errors, warnings=warnings)
|
|
189
|
+
|
|
190
|
+
def _walk_files(self, root: Path) -> List[Path]:
|
|
191
|
+
files = []
|
|
192
|
+
root = Path(root)
|
|
193
|
+
|
|
194
|
+
for path in root.rglob("*"):
|
|
195
|
+
if path.is_dir():
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
rel_parts = path.relative_to(root).parts
|
|
200
|
+
except ValueError:
|
|
201
|
+
rel_parts = path.parts
|
|
202
|
+
|
|
203
|
+
if any(part in IGNORED_DIRS for part in rel_parts):
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
files.append(path)
|
|
207
|
+
|
|
208
|
+
return files
|
|
209
|
+
|
|
210
|
+
def _name_map(self, paths: List[Path]) -> Dict[str, str]:
|
|
211
|
+
return {path.stem: str(path) for path in paths}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from archapi.frameworks.generic import GenericAdapter
|
|
4
|
+
from archapi.frameworks.express_ts.adapter import ExpressTypeScriptAdapter
|
|
5
|
+
from archapi.frameworks.fastapi_adapter import FastAPIAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FrameworkRegistry:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
generic = GenericAdapter()
|
|
11
|
+
|
|
12
|
+
self._adapters = {
|
|
13
|
+
"generic": generic,
|
|
14
|
+
"express-typescript": ExpressTypeScriptAdapter(),
|
|
15
|
+
"nestjs": generic,
|
|
16
|
+
"fastapi": FastAPIAdapter(),
|
|
17
|
+
"django-drf": generic,
|
|
18
|
+
"flask": generic,
|
|
19
|
+
"spring-boot": generic,
|
|
20
|
+
"dotnet-core": generic,
|
|
21
|
+
"laravel": generic,
|
|
22
|
+
"rails": generic,
|
|
23
|
+
"go-api": generic,
|
|
24
|
+
"node-unknown": generic,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def get(self, framework: str):
|
|
28
|
+
return self._adapters.get(framework, self._adapters["generic"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|