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.
Files changed (64) hide show
  1. {code_discovery-0.2.0 → code_discovery-0.2.2}/PKG-INFO +1 -1
  2. {code_discovery-0.2.0 → code_discovery-0.2.2}/setup.py +1 -1
  3. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/PKG-INFO +1 -1
  4. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/generators/openapi_generator.py +31 -0
  5. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/main.py +1 -1
  6. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/java_spring_parser.py +278 -20
  7. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/utils/config.py +1 -1
  8. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/utils/state_manager.py +1 -1
  9. {code_discovery-0.2.0 → code_discovery-0.2.2}/.circleci/config.yml +0 -0
  10. {code_discovery-0.2.0 → code_discovery-0.2.2}/.codediscovery.example.yml +0 -0
  11. {code_discovery-0.2.0 → code_discovery-0.2.2}/.github/workflows/api-discovery.yml +0 -0
  12. {code_discovery-0.2.0 → code_discovery-0.2.2}/.gitlab-ci.yml +0 -0
  13. {code_discovery-0.2.0 → code_discovery-0.2.2}/.harness/api-discovery-pipeline.yml +0 -0
  14. {code_discovery-0.2.0 → code_discovery-0.2.2}/CONTRIBUTING.md +0 -0
  15. {code_discovery-0.2.0 → code_discovery-0.2.2}/Dockerfile +0 -0
  16. {code_discovery-0.2.0 → code_discovery-0.2.2}/Jenkinsfile +0 -0
  17. {code_discovery-0.2.0 → code_discovery-0.2.2}/LICENSE +0 -0
  18. {code_discovery-0.2.0 → code_discovery-0.2.2}/MANIFEST.in +0 -0
  19. {code_discovery-0.2.0 → code_discovery-0.2.2}/README.md +0 -0
  20. {code_discovery-0.2.0 → code_discovery-0.2.2}/docker-compose.yml +0 -0
  21. {code_discovery-0.2.0 → code_discovery-0.2.2}/requirements.txt +0 -0
  22. {code_discovery-0.2.0 → code_discovery-0.2.2}/setup.cfg +0 -0
  23. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/SOURCES.txt +0 -0
  24. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/dependency_links.txt +0 -0
  25. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/entry_points.txt +0 -0
  26. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/requires.txt +0 -0
  27. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/code_discovery.egg-info/top_level.txt +0 -0
  28. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/core/__init__.py +0 -0
  29. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/core/models.py +0 -0
  30. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/core/orchestrator.py +0 -0
  31. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/__init__.py +0 -0
  32. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/base.py +0 -0
  33. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/dotnet.py +0 -0
  34. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/java_micronaut.py +0 -0
  35. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/java_spring.py +0 -0
  36. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/python_fastapi.py +0 -0
  37. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/detectors/python_flask.py +0 -0
  38. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/__init__.py +0 -0
  39. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/base.py +0 -0
  40. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/dotnet_enricher.py +0 -0
  41. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/endpoint_enricher.py +0 -0
  42. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/java_micronaut_enricher.py +0 -0
  43. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/java_spring_enricher.py +0 -0
  44. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/python_fastapi_enricher.py +0 -0
  45. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/enrichers/python_flask_enricher.py +0 -0
  46. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/generators/__init__.py +0 -0
  47. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/__init__.py +0 -0
  48. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/base.py +0 -0
  49. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/dotnet_parser.py +0 -0
  50. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/fastapi_parser.py +0 -0
  51. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/flask_parser.py +0 -0
  52. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/parsers/java_micronaut_parser.py +0 -0
  53. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/utils/__init__.py +0 -0
  54. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/utils/api_client.py +0 -0
  55. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/utils/apisec_config.py +0 -0
  56. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/utils/build_parsers.py +0 -0
  57. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/__init__.py +0 -0
  58. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/base.py +0 -0
  59. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/circleci.py +0 -0
  60. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/factory.py +0 -0
  61. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/github.py +0 -0
  62. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/gitlab.py +0 -0
  63. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/harness.py +0 -0
  64. {code_discovery-0.2.0 → code_discovery-0.2.2}/src/vcs/jenkins.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-discovery
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Automatic API discovery system for multiple frameworks and VCS platforms
5
5
  Home-page: https://github.com/yourusername/codediscovery
6
6
  Author: Code Discovery Team
@@ -22,7 +22,7 @@ dev_requirements = [
22
22
 
23
23
  setup(
24
24
  name="code-discovery",
25
- version="0.2.0",
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",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-discovery
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Automatic API discovery system for multiple frameworks and VCS platforms
5
5
  Home-page: https://github.com/yourusername/codediscovery
6
6
  Author: Code Discovery Team
@@ -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]:
@@ -73,7 +73,7 @@ Examples:
73
73
  version = importlib.metadata.version("code-discovery")
74
74
  except Exception:
75
75
  # Fallback if package not installed or metadata not available
76
- version = "0.2.0"
76
+ version = "0.2.2"
77
77
 
78
78
  parser.add_argument(
79
79
  "--version",
@@ -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
- path = match.group(path_group) if match.lastindex and path_group <= match.lastindex else ""
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
- # Create endpoint
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=self._generate_operation_id(full_path, method_info["method"]),
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 _generate_operation_id(self, path: str, method: HTTPMethod) -> str:
582
- """Generate operation ID from path and method."""
583
- # Remove leading slash and convert to camelCase
584
- path_parts = [p for p in path.split('/') if p and not p.startswith('{')]
585
- operation_id = method.value.lower() + ''.join(p.capitalize() for p in path_parts)
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": "openapi-spec.yaml",
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 / ".code-discovery" / "state.yaml"
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