code-discovery 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- {code_discovery-0.2.0 → code_discovery-0.2.2}/PKG-INFO +1 -1
- {code_discovery-0.2.0 → code_discovery-0.2.2}/setup.py +1 -1
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/PKG-INFO +1 -1
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/generators/openapi_generator.py +31 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/main.py +1 -1
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/java_spring_parser.py +278 -20
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/utils/config.py +1 -1
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/utils/state_manager.py +1 -1
- {code_discovery-0.2.0 → code_discovery-0.2.2}/.circleci/config.yml +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/.codediscovery.example.yml +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/.github/workflows/api-discovery.yml +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/.gitlab-ci.yml +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/.harness/api-discovery-pipeline.yml +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/CONTRIBUTING.md +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/Dockerfile +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/Jenkinsfile +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/LICENSE +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/MANIFEST.in +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/README.md +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/docker-compose.yml +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/requirements.txt +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/setup.cfg +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/SOURCES.txt +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/dependency_links.txt +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/entry_points.txt +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/requires.txt +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/top_level.txt +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/core/__init__.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/core/models.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/core/orchestrator.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/__init__.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/base.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/dotnet.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/java_micronaut.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/java_spring.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/python_fastapi.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/python_flask.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/__init__.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/base.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/dotnet_enricher.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/endpoint_enricher.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/java_micronaut_enricher.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/java_spring_enricher.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/python_fastapi_enricher.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/python_flask_enricher.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/generators/__init__.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/__init__.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/base.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/dotnet_parser.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/fastapi_parser.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/flask_parser.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/java_micronaut_parser.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/utils/__init__.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/utils/api_client.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/utils/apisec_config.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/utils/build_parsers.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/__init__.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/base.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/circleci.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/factory.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/github.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/gitlab.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/harness.py +0 -0
- {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/jenkins.py +0 -0
|
@@ -22,7 +22,7 @@ dev_requirements = [
|
|
|
22
22
|
|
|
23
23
|
setup(
|
|
24
24
|
name="code-discovery",
|
|
25
|
-
version="0.2.
|
|
25
|
+
version="0.2.2",
|
|
26
26
|
author="Code Discovery Team",
|
|
27
27
|
author_email="team@codediscovery.dev",
|
|
28
28
|
description="Automatic API discovery system for multiple frameworks and VCS platforms",
|
|
@@ -151,6 +151,8 @@ class OpenAPIGenerator:
|
|
|
151
151
|
def _generate_paths(self, endpoints: List[APIEndpoint]) -> Dict[str, Any]:
|
|
152
152
|
"""Generate the paths section."""
|
|
153
153
|
paths = {}
|
|
154
|
+
seen_operations = {} # Track (path, method) combinations
|
|
155
|
+
duplicate_warnings = []
|
|
154
156
|
|
|
155
157
|
for endpoint in endpoints:
|
|
156
158
|
path = endpoint.path
|
|
@@ -159,8 +161,37 @@ class OpenAPIGenerator:
|
|
|
159
161
|
|
|
160
162
|
# Add operation for this HTTP method
|
|
161
163
|
method_lower = endpoint.method.value.lower()
|
|
164
|
+
operation_key = (path, method_lower)
|
|
165
|
+
|
|
166
|
+
# Check for duplicates
|
|
167
|
+
if operation_key in seen_operations:
|
|
168
|
+
duplicate_warnings.append(
|
|
169
|
+
f"{endpoint.method.value} {path} (duplicate #{seen_operations[operation_key]['count'] + 1})"
|
|
170
|
+
)
|
|
171
|
+
# Make operationId unique by appending a counter
|
|
172
|
+
if endpoint.operation_id:
|
|
173
|
+
endpoint.operation_id = f"{endpoint.operation_id}_{seen_operations[operation_key]['count']}"
|
|
174
|
+
else:
|
|
175
|
+
endpoint.operation_id = f"{method_lower}_{seen_operations[operation_key]['count']}"
|
|
176
|
+
seen_operations[operation_key]['count'] += 1
|
|
177
|
+
else:
|
|
178
|
+
seen_operations[operation_key] = {'count': 1}
|
|
179
|
+
|
|
162
180
|
paths[path][method_lower] = self._generate_operation(endpoint)
|
|
163
181
|
|
|
182
|
+
# Warn about duplicates
|
|
183
|
+
if duplicate_warnings:
|
|
184
|
+
print(f"\n⚠️ Warning: Found {len(duplicate_warnings)} duplicate path+method combinations.")
|
|
185
|
+
print(" These endpoints have been made unique by modifying their operationId.")
|
|
186
|
+
print(" Consider adding unique paths or tags to differentiate them.")
|
|
187
|
+
if len(duplicate_warnings) <= 10:
|
|
188
|
+
for warning in duplicate_warnings:
|
|
189
|
+
print(f" - {warning}")
|
|
190
|
+
else:
|
|
191
|
+
for warning in duplicate_warnings[:10]:
|
|
192
|
+
print(f" - {warning}")
|
|
193
|
+
print(f" ... and {len(duplicate_warnings) - 10} more")
|
|
194
|
+
|
|
164
195
|
return paths
|
|
165
196
|
|
|
166
197
|
def _generate_operation(self, endpoint: APIEndpoint) -> Dict[str, Any]:
|
|
@@ -59,24 +59,28 @@ class SpringBootParser(BaseParser):
|
|
|
59
59
|
endpoints = []
|
|
60
60
|
|
|
61
61
|
# Extract class-level @RequestMapping
|
|
62
|
-
class_path = self._extract_class_request_mapping(content)
|
|
62
|
+
class_path = self._extract_class_request_mapping(content, file_path)
|
|
63
|
+
|
|
64
|
+
# Extract controller class name for tags and unique operationIds
|
|
65
|
+
controller_name = self._extract_controller_name(content, file_path)
|
|
63
66
|
|
|
64
67
|
# Find all method annotations
|
|
65
|
-
methods = self._extract_methods(content)
|
|
68
|
+
methods = self._extract_methods(content, file_path)
|
|
66
69
|
|
|
67
70
|
for method_info in methods:
|
|
68
71
|
endpoint = self._create_endpoint(
|
|
69
72
|
method_info,
|
|
70
73
|
class_path,
|
|
71
74
|
file_path,
|
|
75
|
+
controller_name,
|
|
72
76
|
)
|
|
73
77
|
if endpoint:
|
|
74
78
|
endpoints.append(endpoint)
|
|
75
79
|
|
|
76
80
|
return endpoints
|
|
77
81
|
|
|
78
|
-
def _extract_class_request_mapping(self, content: str) -> str:
|
|
79
|
-
"""Extract class-level @RequestMapping path."""
|
|
82
|
+
def _extract_class_request_mapping(self, content: str, file_path: Path) -> str:
|
|
83
|
+
"""Extract class-level @RequestMapping path, resolving constants if needed."""
|
|
80
84
|
# Match @RequestMapping("/path") or @RequestMapping(value = "/path")
|
|
81
85
|
patterns = [
|
|
82
86
|
r'@RequestMapping\s*\(\s*"([^"]+)"\s*\)',
|
|
@@ -89,9 +93,31 @@ class SpringBootParser(BaseParser):
|
|
|
89
93
|
if match:
|
|
90
94
|
return match.group(1)
|
|
91
95
|
|
|
96
|
+
# Check for constant references: @RequestMapping(ClassName.CONSTANT_NAME)
|
|
97
|
+
constant_patterns = [
|
|
98
|
+
r'@RequestMapping\s*\(\s*(\w+)\.(\w+)\s*\)',
|
|
99
|
+
r'@RequestMapping\s*\(\s*value\s*=\s*(\w+)\.(\w+)\s*\)',
|
|
100
|
+
r'@RequestMapping\s*\(\s*path\s*=\s*(\w+)\.(\w+)\s*\)',
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
for pattern in constant_patterns:
|
|
104
|
+
match = re.search(pattern, content)
|
|
105
|
+
if match:
|
|
106
|
+
class_name = match.group(1)
|
|
107
|
+
constant_name = match.group(2)
|
|
108
|
+
# Resolve the constant value
|
|
109
|
+
constant_value = self._resolve_constant(class_name, constant_name, file_path)
|
|
110
|
+
if constant_value:
|
|
111
|
+
return constant_value
|
|
112
|
+
|
|
113
|
+
# Check for inheritance - if controller extends a base class with @RequestMapping
|
|
114
|
+
base_class_path = self._extract_inherited_request_mapping(content, file_path)
|
|
115
|
+
if base_class_path:
|
|
116
|
+
return base_class_path
|
|
117
|
+
|
|
92
118
|
return ""
|
|
93
119
|
|
|
94
|
-
def _extract_methods(self, content: str) -> List[Dict[str, Any]]:
|
|
120
|
+
def _extract_methods(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
|
|
95
121
|
"""Extract method information from controller (Spring Boot 2.7.5 through 3.5.x)."""
|
|
96
122
|
methods = []
|
|
97
123
|
|
|
@@ -129,22 +155,27 @@ class SpringBootParser(BaseParser):
|
|
|
129
155
|
# Patterns: @RequestMapping(value = "/path", method = RequestMethod.GET)
|
|
130
156
|
# @RequestMapping(path = "/path", method = RequestMethod.POST)
|
|
131
157
|
# @RequestMapping("/path", method = RequestMethod.PUT)
|
|
158
|
+
# @RequestMapping(value = ClassName.CONSTANT, method = RequestMethod.GET)
|
|
132
159
|
request_mapping_patterns = [
|
|
133
160
|
# @RequestMapping(value = "/path", method = RequestMethod.GET)
|
|
134
|
-
(r'@RequestMapping\s*\(\s*value\s*=\s*"([^"]+)"\s*,\s*method\s*=\s*RequestMethod\.(\w+)\s*\)', 1, 2),
|
|
161
|
+
(r'@RequestMapping\s*\(\s*value\s*=\s*"([^"]+)"\s*,\s*method\s*=\s*RequestMethod\.(\w+)\s*\)', 1, 2, True),
|
|
135
162
|
# @RequestMapping(path = "/path", method = RequestMethod.GET)
|
|
136
|
-
(r'@RequestMapping\s*\(\s*path\s*=\s*"([^"]+)"\s*,\s*method\s*=\s*RequestMethod\.(\w+)\s*\)', 1, 2),
|
|
163
|
+
(r'@RequestMapping\s*\(\s*path\s*=\s*"([^"]+)"\s*,\s*method\s*=\s*RequestMethod\.(\w+)\s*\)', 1, 2, True),
|
|
137
164
|
# @RequestMapping("/path", method = RequestMethod.GET)
|
|
138
|
-
(r'@RequestMapping\s*\(\s*"([^"]+)"\s*,\s*method\s*=\s*RequestMethod\.(\w+)\s*\)', 1, 2),
|
|
165
|
+
(r'@RequestMapping\s*\(\s*"([^"]+)"\s*,\s*method\s*=\s*RequestMethod\.(\w+)\s*\)', 1, 2, True),
|
|
139
166
|
# @RequestMapping(method = RequestMethod.GET, value = "/path")
|
|
140
|
-
(r'@RequestMapping\s*\(\s*method\s*=\s*RequestMethod\.(\w+)\s*,\s*value\s*=\s*"([^"]+)"\s*\)', 2, 1),
|
|
167
|
+
(r'@RequestMapping\s*\(\s*method\s*=\s*RequestMethod\.(\w+)\s*,\s*value\s*=\s*"([^"]+)"\s*\)', 2, 1, True),
|
|
141
168
|
# @RequestMapping(method = RequestMethod.GET, path = "/path")
|
|
142
|
-
(r'@RequestMapping\s*\(\s*method\s*=\s*RequestMethod\.(\w+)\s*,\s*path\s*=\s*"([^"]+)"\s*\)', 2, 1),
|
|
169
|
+
(r'@RequestMapping\s*\(\s*method\s*=\s*RequestMethod\.(\w+)\s*,\s*path\s*=\s*"([^"]+)"\s*\)', 2, 1, True),
|
|
170
|
+
# @RequestMapping(value = ClassName.CONSTANT, method = RequestMethod.GET)
|
|
171
|
+
(r'@RequestMapping\s*\(\s*value\s*=\s*(\w+)\.(\w+)\s*,\s*method\s*=\s*RequestMethod\.(\w+)\s*\)', None, 3, False),
|
|
172
|
+
# @RequestMapping(path = ClassName.CONSTANT, method = RequestMethod.GET)
|
|
173
|
+
(r'@RequestMapping\s*\(\s*path\s*=\s*(\w+)\.(\w+)\s*,\s*method\s*=\s*RequestMethod\.(\w+)\s*\)', None, 3, False),
|
|
143
174
|
# @RequestMapping(method = RequestMethod.GET) - no path
|
|
144
|
-
(r'@RequestMapping\s*\(\s*method\s*=\s*RequestMethod\.(\w+)\s*\)', None, 1),
|
|
175
|
+
(r'@RequestMapping\s*\(\s*method\s*=\s*RequestMethod\.(\w+)\s*\)', None, 1, True),
|
|
145
176
|
]
|
|
146
177
|
|
|
147
|
-
for pattern, path_group, method_group in request_mapping_patterns:
|
|
178
|
+
for pattern, path_group, method_group, is_string_literal in request_mapping_patterns:
|
|
148
179
|
for match in re.finditer(pattern, content):
|
|
149
180
|
http_method_name = match.group(method_group).upper()
|
|
150
181
|
try:
|
|
@@ -154,7 +185,13 @@ class SpringBootParser(BaseParser):
|
|
|
154
185
|
|
|
155
186
|
# Extract path if specified
|
|
156
187
|
if path_group:
|
|
157
|
-
|
|
188
|
+
if is_string_literal:
|
|
189
|
+
path = match.group(path_group) if match.lastindex and path_group <= match.lastindex else ""
|
|
190
|
+
else:
|
|
191
|
+
# Constant reference: extract class and constant name
|
|
192
|
+
const_class = match.group(1)
|
|
193
|
+
const_name = match.group(2)
|
|
194
|
+
path = self._resolve_constant(const_class, const_name, file_path) or ""
|
|
158
195
|
else:
|
|
159
196
|
path = ""
|
|
160
197
|
|
|
@@ -249,6 +286,7 @@ class SpringBootParser(BaseParser):
|
|
|
249
286
|
method_info: Dict[str, Any],
|
|
250
287
|
class_path: str,
|
|
251
288
|
file_path: Path,
|
|
289
|
+
controller_name: str,
|
|
252
290
|
) -> Optional[APIEndpoint]:
|
|
253
291
|
"""Create an APIEndpoint from method information."""
|
|
254
292
|
# Combine class path and method path
|
|
@@ -264,11 +302,23 @@ class SpringBootParser(BaseParser):
|
|
|
264
302
|
# Extract response schema from return type
|
|
265
303
|
response_schema = self._extract_response_schema(method_info["signature"], file_path)
|
|
266
304
|
|
|
267
|
-
#
|
|
305
|
+
# Extract method name for unique operationId
|
|
306
|
+
method_name = self._extract_method_name(method_info["signature"])
|
|
307
|
+
|
|
308
|
+
# Generate unique operationId using controller + method name
|
|
309
|
+
operation_id = self._generate_operation_id(
|
|
310
|
+
full_path,
|
|
311
|
+
method_info["method"],
|
|
312
|
+
controller_name=controller_name,
|
|
313
|
+
method_name=method_name,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Create endpoint with controller name as tag
|
|
268
317
|
endpoint = APIEndpoint(
|
|
269
318
|
path=full_path,
|
|
270
319
|
method=method_info["method"],
|
|
271
|
-
operation_id=
|
|
320
|
+
operation_id=operation_id,
|
|
321
|
+
tags=[controller_name] if controller_name else [],
|
|
272
322
|
parameters=parameters,
|
|
273
323
|
request_body=request_body,
|
|
274
324
|
responses=[
|
|
@@ -578,10 +628,218 @@ class SpringBootParser(BaseParser):
|
|
|
578
628
|
|
|
579
629
|
return None
|
|
580
630
|
|
|
581
|
-
def
|
|
582
|
-
"""
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
631
|
+
def _resolve_constant(self, class_name: str, constant_name: str, current_file: Path) -> Optional[str]:
|
|
632
|
+
"""
|
|
633
|
+
Resolve a constant value from a class.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
class_name: Name of the class containing the constant (e.g., "BaseController")
|
|
637
|
+
constant_name: Name of the constant (e.g., "PROJECTS_BASE")
|
|
638
|
+
current_file: Path to the current file (for relative imports)
|
|
639
|
+
|
|
640
|
+
Returns:
|
|
641
|
+
Resolved constant value or None if not found
|
|
642
|
+
"""
|
|
643
|
+
# Try to find the class file
|
|
644
|
+
class_file = self._find_class_file(class_name, current_file)
|
|
645
|
+
if not class_file:
|
|
646
|
+
return None
|
|
647
|
+
|
|
648
|
+
# Read the class file
|
|
649
|
+
class_content = self.read_file(class_file)
|
|
650
|
+
if not class_content:
|
|
651
|
+
return None
|
|
652
|
+
|
|
653
|
+
# Extract constant value
|
|
654
|
+
# Pattern: public static final String CONSTANT_NAME = "value";
|
|
655
|
+
# Or: public static final String CONSTANT_NAME = OTHER_CONSTANT + "/path";
|
|
656
|
+
constant_pattern = rf'public\s+static\s+final\s+String\s+{constant_name}\s*=\s*([^;]+);'
|
|
657
|
+
match = re.search(constant_pattern, class_content)
|
|
658
|
+
if not match:
|
|
659
|
+
return None
|
|
660
|
+
|
|
661
|
+
constant_value_expr = match.group(1).strip()
|
|
662
|
+
|
|
663
|
+
# Handle string literals: "value"
|
|
664
|
+
if constant_value_expr.startswith('"') and constant_value_expr.endswith('"'):
|
|
665
|
+
return constant_value_expr.strip('"')
|
|
666
|
+
|
|
667
|
+
# Handle string concatenation: CONSTANT1 + "/path" or "path" + CONSTANT2
|
|
668
|
+
parts = []
|
|
669
|
+
# Split by + and process each part
|
|
670
|
+
concat_parts = re.split(r'\s*\+\s*', constant_value_expr)
|
|
671
|
+
for part in concat_parts:
|
|
672
|
+
part = part.strip()
|
|
673
|
+
# String literal
|
|
674
|
+
if part.startswith('"') and part.endswith('"'):
|
|
675
|
+
parts.append(part.strip('"'))
|
|
676
|
+
# Another constant reference
|
|
677
|
+
elif '.' in part:
|
|
678
|
+
# Format: ClassName.CONSTANT_NAME
|
|
679
|
+
const_match = re.match(r'(\w+)\.(\w+)', part)
|
|
680
|
+
if const_match:
|
|
681
|
+
const_class = const_match.group(1)
|
|
682
|
+
const_name = const_match.group(2)
|
|
683
|
+
# Recursively resolve
|
|
684
|
+
resolved = self._resolve_constant(const_class, const_name, class_file)
|
|
685
|
+
if resolved:
|
|
686
|
+
parts.append(resolved)
|
|
687
|
+
else:
|
|
688
|
+
return None # Can't resolve dependency
|
|
689
|
+
else:
|
|
690
|
+
return None # Invalid format
|
|
691
|
+
else:
|
|
692
|
+
# Might be a simple constant name in the same class
|
|
693
|
+
same_class_match = re.search(
|
|
694
|
+
rf'public\s+static\s+final\s+String\s+{part}\s*=\s*"([^"]+)"',
|
|
695
|
+
class_content
|
|
696
|
+
)
|
|
697
|
+
if same_class_match:
|
|
698
|
+
parts.append(same_class_match.group(1))
|
|
699
|
+
else:
|
|
700
|
+
return None # Can't resolve
|
|
701
|
+
|
|
702
|
+
return ''.join(parts)
|
|
703
|
+
|
|
704
|
+
def _find_class_file(self, class_name: str, reference_file: Path) -> Optional[Path]:
|
|
705
|
+
"""
|
|
706
|
+
Find the file containing a class definition.
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
class_name: Name of the class to find
|
|
710
|
+
reference_file: File that references this class (for package resolution)
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
Path to the class file or None if not found
|
|
714
|
+
"""
|
|
715
|
+
# Try to find in cached files first
|
|
716
|
+
for file_path, content in self._file_content_cache.items():
|
|
717
|
+
# Check if file contains the class definition
|
|
718
|
+
class_pattern = rf'(?:public\s+)?(?:abstract\s+)?(?:class|interface|enum)\s+{class_name}\b'
|
|
719
|
+
if re.search(class_pattern, content):
|
|
720
|
+
return file_path
|
|
721
|
+
|
|
722
|
+
# Try to find by filename (common convention: ClassName.java)
|
|
723
|
+
for file_path in self.source_paths:
|
|
724
|
+
if file_path.name == f"{class_name}.java":
|
|
725
|
+
return file_path
|
|
726
|
+
|
|
727
|
+
# Try to find in same package (extract package from reference file)
|
|
728
|
+
try:
|
|
729
|
+
ref_content = self.read_file(reference_file)
|
|
730
|
+
if ref_content:
|
|
731
|
+
package_match = re.search(r'package\s+([\w.]+);', ref_content)
|
|
732
|
+
if package_match:
|
|
733
|
+
package = package_match.group(1)
|
|
734
|
+
# Search for class in same package directory
|
|
735
|
+
ref_dir = reference_file.parent
|
|
736
|
+
candidate = ref_dir / f"{class_name}.java"
|
|
737
|
+
if candidate.exists():
|
|
738
|
+
return candidate
|
|
739
|
+
except:
|
|
740
|
+
pass
|
|
741
|
+
|
|
742
|
+
# Search all Java files
|
|
743
|
+
for file_path in self.find_files("*.java"):
|
|
744
|
+
try:
|
|
745
|
+
content = self.read_file(file_path)
|
|
746
|
+
if content:
|
|
747
|
+
class_pattern = rf'(?:public\s+)?(?:abstract\s+)?(?:class|interface|enum)\s+{class_name}\b'
|
|
748
|
+
if re.search(class_pattern, content):
|
|
749
|
+
return file_path
|
|
750
|
+
except:
|
|
751
|
+
continue
|
|
752
|
+
|
|
753
|
+
return None
|
|
754
|
+
|
|
755
|
+
def _extract_inherited_request_mapping(self, content: str, file_path: Path) -> Optional[str]:
|
|
756
|
+
"""
|
|
757
|
+
Extract @RequestMapping from a base class if the controller extends it.
|
|
758
|
+
|
|
759
|
+
Args:
|
|
760
|
+
content: Controller file content
|
|
761
|
+
file_path: Path to controller file
|
|
762
|
+
|
|
763
|
+
Returns:
|
|
764
|
+
Base class @RequestMapping path or None
|
|
765
|
+
"""
|
|
766
|
+
# Check if class extends another class
|
|
767
|
+
extends_match = re.search(r'class\s+\w+\s+extends\s+(\w+)', content)
|
|
768
|
+
if not extends_match:
|
|
769
|
+
return None
|
|
770
|
+
|
|
771
|
+
base_class_name = extends_match.group(1)
|
|
772
|
+
base_class_file = self._find_class_file(base_class_name, file_path)
|
|
773
|
+
|
|
774
|
+
if base_class_file:
|
|
775
|
+
base_content = self.read_file(base_class_file)
|
|
776
|
+
if base_content:
|
|
777
|
+
# Extract @RequestMapping from base class
|
|
778
|
+
base_path = self._extract_class_request_mapping(base_content, base_class_file)
|
|
779
|
+
if base_path:
|
|
780
|
+
return base_path
|
|
781
|
+
|
|
782
|
+
return None
|
|
783
|
+
|
|
784
|
+
def _extract_controller_name(self, content: str, file_path: Path) -> str:
|
|
785
|
+
"""Extract controller class name from file content or file path."""
|
|
786
|
+
# Try to extract from class declaration first
|
|
787
|
+
class_match = re.search(r'public\s+class\s+(\w+)', content)
|
|
788
|
+
if class_match:
|
|
789
|
+
class_name = class_match.group(1)
|
|
790
|
+
# Remove common suffixes for cleaner tag names
|
|
791
|
+
for suffix in ['Controller', 'Resource', 'Endpoint', 'Api']:
|
|
792
|
+
if class_name.endswith(suffix):
|
|
793
|
+
return class_name[:-len(suffix)]
|
|
794
|
+
return class_name
|
|
795
|
+
|
|
796
|
+
# Fallback to file name
|
|
797
|
+
file_name = file_path.stem # Gets filename without extension
|
|
798
|
+
# Remove common suffixes
|
|
799
|
+
for suffix in ['Controller', 'Resource', 'Endpoint', 'Api']:
|
|
800
|
+
if file_name.endswith(suffix):
|
|
801
|
+
return file_name[:-len(suffix)]
|
|
802
|
+
return file_name
|
|
803
|
+
|
|
804
|
+
def _generate_operation_id(
|
|
805
|
+
self,
|
|
806
|
+
path: str,
|
|
807
|
+
method: HTTPMethod,
|
|
808
|
+
controller_name: Optional[str] = None,
|
|
809
|
+
method_name: Optional[str] = None,
|
|
810
|
+
) -> str:
|
|
811
|
+
"""Generate unique operation ID from path, method, controller, and method name."""
|
|
812
|
+
# Build operation ID components
|
|
813
|
+
parts = []
|
|
814
|
+
|
|
815
|
+
# Start with HTTP method
|
|
816
|
+
parts.append(method.value.lower())
|
|
817
|
+
|
|
818
|
+
# Add controller name if available (makes it unique)
|
|
819
|
+
if controller_name:
|
|
820
|
+
# Convert to camelCase: "PrimaryTransaction" -> "primaryTransaction"
|
|
821
|
+
controller_camel = controller_name[0].lower() + controller_name[1:] if controller_name else ""
|
|
822
|
+
parts.append(controller_camel)
|
|
823
|
+
|
|
824
|
+
# Add method name if available (further uniqueness)
|
|
825
|
+
if method_name:
|
|
826
|
+
parts.append(method_name)
|
|
827
|
+
|
|
828
|
+
# Add path parts as fallback if no method name
|
|
829
|
+
if not method_name:
|
|
830
|
+
path_parts = [p for p in path.split('/') if p and not p.startswith('{')]
|
|
831
|
+
parts.extend(p.capitalize() for p in path_parts)
|
|
832
|
+
|
|
833
|
+
# Join parts: e.g., "getPrimaryTransactionGetById"
|
|
834
|
+
operation_id = ''.join(parts)
|
|
835
|
+
|
|
836
|
+
# If still empty or too generic, use path-based fallback
|
|
837
|
+
if not operation_id or operation_id == method.value.lower():
|
|
838
|
+
path_parts = [p for p in path.split('/') if p and not p.startswith('{')]
|
|
839
|
+
if path_parts:
|
|
840
|
+
operation_id = method.value.lower() + ''.join(p.capitalize() for p in path_parts)
|
|
841
|
+
else:
|
|
842
|
+
operation_id = method.value.lower() + "Endpoint"
|
|
843
|
+
|
|
586
844
|
return operation_id
|
|
587
845
|
|
|
@@ -16,7 +16,7 @@ class Config:
|
|
|
16
16
|
"frameworks": [], # Empty means auto-detect all
|
|
17
17
|
"openapi": {
|
|
18
18
|
"version": "3.0.0",
|
|
19
|
-
"output_path": "
|
|
19
|
+
"output_path": "apisec-bolt-code-discovery/openapi_spec.yaml",
|
|
20
20
|
"output_format": "yaml", # yaml or json
|
|
21
21
|
"include_examples": True,
|
|
22
22
|
},
|
|
@@ -32,7 +32,7 @@ class DiscoveryStateManager:
|
|
|
32
32
|
repo_path: Path to the repository root.
|
|
33
33
|
"""
|
|
34
34
|
self.repo_path = Path(repo_path)
|
|
35
|
-
self.state_file = self.repo_path / "
|
|
35
|
+
self.state_file = self.repo_path / "apisec-bolt-code-discovery" / "state.yaml"
|
|
36
36
|
self.state_dir = self.state_file.parent
|
|
37
37
|
|
|
38
38
|
def load_state(self) -> Optional[Dict[str, Any]]:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|