check_project.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. #!/usr/bin/env python3
  2. """
  3. DDD Project Structure Validation Script
  4. This script analyzes an existing project structure and:
  5. 1. Identifies the project type (single-module, multi-module, microservices)
  6. 2. Validates directory structure compliance
  7. 3. Checks package naming conventions
  8. 4. Verifies layer dependencies
  9. 5. Generates a validation report
  10. """
  11. import os
  12. import sys
  13. import json
  14. from pathlib import Path
  15. from typing import Dict, List, Optional, Tuple
  16. class DDDProjectChecker:
  17. """DDD Project Structure Checker"""
  18. def __init__(self, project_path: str):
  19. self.project_path = Path(project_path)
  20. self.issues = []
  21. self.warnings = []
  22. self.project_type = None
  23. self.modules = []
  24. self.architecture = None
  25. def check(self) -> Dict:
  26. """Check project structure and return report"""
  27. if not self.project_path.exists():
  28. return {
  29. "status": "error",
  30. "message": f"Project path does not exist: {self.project_path}"
  31. }
  32. # Identify project type
  33. self.project_type = self._identify_project_type()
  34. # Check structure based on type
  35. if self.project_type == "single-module":
  36. result = self._check_single_module()
  37. elif self.project_type == "multi-module":
  38. result = self._check_multi_module()
  39. elif self.project_type == "microservices":
  40. result = self._check_microservices()
  41. else:
  42. return {
  43. "status": "error",
  44. "message": "Could not identify project type"
  45. }
  46. # Validate common requirements
  47. self._check_common_files()
  48. self._check_package_naming()
  49. return {
  50. "status": "success",
  51. "projectType": self.project_type,
  52. "architecture": self.architecture,
  53. "modules": self.modules,
  54. "issues": self.issues,
  55. "warnings": self.warnings,
  56. "compliant": len(self.issues) == 0
  57. }
  58. def _identify_project_type(self) -> Optional[str]:
  59. """Identify project type based on structure"""
  60. root_pom = self.project_path / "pom.xml"
  61. if not root_pom.exists():
  62. return None
  63. # Read pom.xml to check for modules
  64. try:
  65. content = root_pom.read_text(encoding="utf-8")
  66. # Check for modules tag
  67. if "<modules>" in content:
  68. # Check if it's microservices (has services/ directory)
  69. services_dir = self.project_path / "services"
  70. if services_dir.exists() and services_dir.is_dir():
  71. return "microservices"
  72. else:
  73. return "multi-module"
  74. else:
  75. return "single-module"
  76. except Exception as e:
  77. self.issues.append(f"Error reading pom.xml: {e}")
  78. return None
  79. def _check_single_module(self) -> Dict:
  80. """Check single-module project structure"""
  81. src_main_java = self.project_path / "src" / "main" / "java"
  82. if not src_main_java.exists():
  83. self.issues.append("Missing src/main/java directory")
  84. return {"valid": False}
  85. # Identify architecture by checking layer directories
  86. layers = self._detect_layers(src_main_java)
  87. self.architecture = self._identify_architecture(layers)
  88. # Check layer structure
  89. for layer in layers:
  90. layer_path = src_main_java
  91. # Find layer directory (could be at any depth)
  92. for part in layer.split("/"):
  93. layer_path = layer_path / part
  94. if not layer_path.exists():
  95. self.warnings.append(f"Layer '{layer}' not found at expected location")
  96. break
  97. # Check package-info.java
  98. self._check_package_info_files(src_main_java)
  99. return {"valid": True, "layers": layers}
  100. def _check_multi_module(self) -> Dict:
  101. """Check multi-module project structure"""
  102. # Find all modules
  103. modules = []
  104. for item in self.project_path.iterdir():
  105. if item.is_dir() and (item / "pom.xml").exists():
  106. # Check if it's a module (has sub-modules or is a business module)
  107. pom_content = (item / "pom.xml").read_text(encoding="utf-8")
  108. if "<modules>" in pom_content or "-" in item.name:
  109. modules.append(item.name)
  110. self.modules = modules
  111. # Check each module
  112. for module_name in modules:
  113. module_path = self.project_path / module_name
  114. if module_path.is_dir():
  115. self._check_module_structure(module_path, module_name)
  116. return {"valid": True, "modules": modules}
  117. def _check_microservices(self) -> Dict:
  118. """Check microservices project structure"""
  119. services_dir = self.project_path / "services"
  120. if not services_dir.exists():
  121. self.issues.append("Missing services/ directory")
  122. return {"valid": False}
  123. # Find all services
  124. services = []
  125. for item in services_dir.iterdir():
  126. if item.is_dir() and (item / "pom.xml").exists():
  127. services.append(item.name)
  128. self._check_service_structure(item, item.name)
  129. self.modules = services
  130. return {"valid": True, "services": services}
  131. def _check_module_structure(self, module_path: Path, module_name: str):
  132. """Check structure of a business module"""
  133. # Check for sub-modules
  134. sub_modules = []
  135. for item in module_path.iterdir():
  136. if item.is_dir() and (item / "pom.xml").exists():
  137. sub_modules.append(item.name)
  138. # Expected sub-modules based on architecture
  139. if len(sub_modules) > 0:
  140. # This is a parent module with sub-modules
  141. layers = self._detect_layers_from_module_names(sub_modules)
  142. self.architecture = self._identify_architecture_from_modules(sub_modules)
  143. # Check each sub-module
  144. for sub_module in sub_modules:
  145. sub_module_path = module_path / sub_module
  146. src_main_java = sub_module_path / "src" / "main" / "java"
  147. if src_main_java.exists():
  148. self._check_package_info_files(src_main_java)
  149. else:
  150. # Single module, check layers directly
  151. src_main_java = module_path / "src" / "main" / "java"
  152. if src_main_java.exists():
  153. layers = self._detect_layers(src_main_java)
  154. self.architecture = self._identify_architecture(layers)
  155. self._check_package_info_files(src_main_java)
  156. def _check_service_structure(self, service_path: Path, service_name: str):
  157. """Check structure of a microservice"""
  158. # Check for service modules
  159. modules = []
  160. for item in service_path.iterdir():
  161. if item.is_dir() and (item / "pom.xml").exists():
  162. modules.append(item.name)
  163. # Expected: api, domain, application, infrastructure, interfaces, start
  164. expected_modules = ["api", "domain", "application", "infrastructure", "interfaces", "start"]
  165. for expected in expected_modules:
  166. module_name = f"{service_name}-{expected}"
  167. if module_name not in modules:
  168. self.warnings.append(f"Service {service_name} missing module: {expected}")
  169. # Check each module
  170. for module in modules:
  171. module_path = service_path / module
  172. src_main_java = module_path / "src" / "main" / "java"
  173. if src_main_java.exists():
  174. self._check_package_info_files(src_main_java)
  175. def _detect_layers(self, java_path: Path) -> List[str]:
  176. """Detect layer structure from directory"""
  177. layers = []
  178. # Common layer names
  179. layer_names = [
  180. "interfaces", "application", "domain", "infrastructure",
  181. "adapter", "app", "usecase", "entity", "valueobject"
  182. ]
  183. def scan_directory(path: Path, depth: int = 0, max_depth: int = 3):
  184. if depth > max_depth:
  185. return
  186. for item in path.iterdir():
  187. if item.is_dir():
  188. if item.name in layer_names:
  189. layers.append(item.name)
  190. scan_directory(item, depth + 1, max_depth)
  191. scan_directory(java_path)
  192. return list(set(layers))
  193. def _detect_layers_from_module_names(self, module_names: List[str]) -> List[str]:
  194. """Detect layers from module names"""
  195. layers = []
  196. for name in module_names:
  197. # Extract layer suffix (e.g., api-adapter -> adapter)
  198. parts = name.split("-")
  199. if len(parts) > 1:
  200. layers.append(parts[-1])
  201. return list(set(layers))
  202. def _identify_architecture(self, layers: List[str]) -> str:
  203. """Identify architecture pattern from layers"""
  204. if "adapter" in layers and "app" in layers:
  205. return "cola-v5"
  206. elif "adapter" in layers and "application" in layers:
  207. return "hexagonal"
  208. elif "interfaces" in layers and "application" in layers:
  209. if "usecase" in layers:
  210. return "clean"
  211. else:
  212. return "ddd-classic"
  213. else:
  214. return "unknown"
  215. def _identify_architecture_from_modules(self, module_names: List[str]) -> str:
  216. """Identify architecture from module names"""
  217. if any("adapter" in name and "app" in name for name in module_names):
  218. return "cola-v5"
  219. elif any("adapter" in name for name in module_names):
  220. return "hexagonal"
  221. elif any("interfaces" in name or "application" in name for name in module_names):
  222. return "ddd-classic"
  223. else:
  224. return "unknown"
  225. def _check_package_info_files(self, java_path: Path):
  226. """Check for package-info.java files"""
  227. def scan_for_package_info(path: Path):
  228. package_info = path / "package-info.java"
  229. if not package_info.exists():
  230. # Check if this is a meaningful package (has .java files or subdirectories)
  231. has_java = any(f.suffix == ".java" for f in path.iterdir() if f.is_file())
  232. has_subdirs = any(d.is_dir() for d in path.iterdir())
  233. if has_java or has_subdirs:
  234. self.warnings.append(f"Missing package-info.java in {path.relative_to(self.project_path)}")
  235. # Recursively check subdirectories
  236. for item in path.iterdir():
  237. if item.is_dir():
  238. scan_for_package_info(item)
  239. scan_for_package_info(java_path)
  240. def _check_common_files(self):
  241. """Check for common required files"""
  242. required_files = [".gitignore", "LICENSE", "README.md"]
  243. for file_name in required_files:
  244. file_path = self.project_path / file_name
  245. if not file_path.exists():
  246. self.warnings.append(f"Missing required file: {file_name}")
  247. # Check for Maven wrapper
  248. mvnw = self.project_path / "mvnw"
  249. mvnw_cmd = self.project_path / "mvnw.cmd"
  250. if not mvnw.exists() and not mvnw_cmd.exists():
  251. self.warnings.append("Missing Maven wrapper files (mvnw, mvnw.cmd)")
  252. def _check_package_naming(self):
  253. """Check package naming conventions"""
  254. src_main_java = self.project_path / "src" / "main" / "java"
  255. if not src_main_java.exists():
  256. return
  257. def check_package_name(path: Path, expected_base: Optional[str] = None):
  258. """Recursively check package naming"""
  259. for item in path.iterdir():
  260. if item.is_dir():
  261. # Check if directory name follows Java package naming (lowercase, no hyphens)
  262. if not item.name.islower() or "-" in item.name:
  263. self.issues.append(
  264. f"Invalid package name: {item.relative_to(src_main_java)} "
  265. f"(should be lowercase, no hyphens)"
  266. )
  267. check_package_name(item)
  268. check_package_name(src_main_java)
  269. def main():
  270. """Main entry point"""
  271. if len(sys.argv) < 2:
  272. print("Usage: check_project.py <project-path>", file=sys.stderr)
  273. sys.exit(1)
  274. project_path = sys.argv[1]
  275. checker = DDDProjectChecker(project_path)
  276. result = checker.check()
  277. print(json.dumps(result, indent=2, ensure_ascii=False))
  278. if __name__ == "__main__":
  279. main()