Coverage for src / rhiza / language_validators.py: 100%

76 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-02 07:04 +0000

1"""Language-specific validation for Rhiza projects. 

2 

3This module provides a framework for validating project structure based on 

4the programming language. Different languages have different requirements 

5(e.g., Python needs pyproject.toml, Go needs go.mod). 

6""" 

7 

8from abc import ABC, abstractmethod 

9from pathlib import Path 

10 

11from loguru import logger 

12 

13 

14class LanguageValidator(ABC): 

15 """Abstract base class for language-specific validators.""" 

16 

17 @abstractmethod 

18 def validate_project_structure(self, target: Path) -> bool: 

19 """Validate language-specific project structure. 

20 

21 Args: 

22 target: Path to the project root. 

23 

24 Returns: 

25 True if validation passes, False otherwise. 

26 """ 

27 ... 

28 

29 @abstractmethod 

30 def get_language_name(self) -> str: 

31 """Get the name of the language this validator handles. 

32 

33 Returns: 

34 Language name (e.g., "python", "go"). 

35 """ 

36 ... 

37 

38 

39class PythonValidator(LanguageValidator): 

40 """Validator for Python projects.""" 

41 

42 def get_language_name(self) -> str: 

43 """Get the language name.""" 

44 return "python" 

45 

46 def validate_project_structure(self, target: Path) -> bool: 

47 """Validate Python project structure. 

48 

49 Checks for: 

50 - pyproject.toml (required) 

51 - src directory (warning if missing) 

52 - tests directory (warning if missing) 

53 

54 Args: 

55 target: Path to the project root. 

56 

57 Returns: 

58 True if validation passes, False otherwise. 

59 """ 

60 validation_passed = True 

61 

62 # Check for pyproject.toml (required) 

63 pyproject_file = target / "pyproject.toml" 

64 if not pyproject_file.exists(): 

65 logger.error(f"pyproject.toml not found: {pyproject_file}") 

66 logger.error("pyproject.toml is required for Python projects") 

67 logger.info("Run 'rhiza init' to create a default pyproject.toml") 

68 validation_passed = False 

69 else: 

70 logger.success(f"pyproject.toml exists: {pyproject_file}") 

71 

72 # Check for standard directories (warnings only) 

73 src_dir = target / "src" 

74 tests_dir = target / "tests" 

75 

76 if not src_dir.exists(): 

77 logger.warning(f"Standard 'src' folder not found: {src_dir}") 

78 logger.warning("Consider creating a 'src' directory for source code") 

79 else: 

80 logger.success(f"'src' folder exists: {src_dir}") 

81 

82 if not tests_dir.exists(): 

83 logger.warning(f"Standard 'tests' folder not found: {tests_dir}") 

84 logger.warning("Consider creating a 'tests' directory for test files") 

85 else: 

86 logger.success(f"'tests' folder exists: {tests_dir}") 

87 

88 return validation_passed 

89 

90 

91class GoValidator(LanguageValidator): 

92 """Validator for Go projects.""" 

93 

94 def get_language_name(self) -> str: 

95 """Get the language name.""" 

96 return "go" 

97 

98 def validate_project_structure(self, target: Path) -> bool: 

99 """Validate Go project structure. 

100 

101 Checks for: 

102 - go.mod (required) 

103 - cmd directory (warning if missing) 

104 - pkg or internal directory (warning if missing) 

105 

106 Args: 

107 target: Path to the project root. 

108 

109 Returns: 

110 True if validation passes, False otherwise. 

111 """ 

112 validation_passed = True 

113 

114 # Check for go.mod (required) 

115 go_mod_file = target / "go.mod" 

116 if not go_mod_file.exists(): 

117 logger.error(f"go.mod not found: {go_mod_file}") 

118 logger.error("go.mod is required for Go projects") 

119 logger.info("Run 'go mod init <module-name>' to create go.mod") 

120 validation_passed = False 

121 else: 

122 logger.success(f"go.mod exists: {go_mod_file}") 

123 

124 # Check for standard directories (warnings only) 

125 cmd_dir = target / "cmd" 

126 pkg_dir = target / "pkg" 

127 internal_dir = target / "internal" 

128 

129 if not cmd_dir.exists(): 

130 logger.warning(f"Standard 'cmd' folder not found: {cmd_dir}") 

131 logger.warning("Consider creating a 'cmd' directory for main applications") 

132 else: 

133 logger.success(f"'cmd' folder exists: {cmd_dir}") 

134 

135 if not pkg_dir.exists() and not internal_dir.exists(): 

136 logger.warning("Neither 'pkg' nor 'internal' folder found") 

137 logger.warning("Consider creating 'pkg' for public libraries or 'internal' for private packages") 

138 else: 

139 if pkg_dir.exists(): 

140 logger.success(f"'pkg' folder exists: {pkg_dir}") 

141 if internal_dir.exists(): 

142 logger.success(f"'internal' folder exists: {internal_dir}") 

143 

144 return validation_passed 

145 

146 

147class LanguageValidatorRegistry: 

148 """Registry for language validators.""" 

149 

150 def __init__(self): 

151 """Initialize the registry with default validators.""" 

152 self._validators: dict[str, LanguageValidator] = {} 

153 self._register_defaults() 

154 

155 def _register_defaults(self) -> None: 

156 """Register default language validators.""" 

157 self.register(PythonValidator()) 

158 self.register(GoValidator()) 

159 

160 def register(self, validator: LanguageValidator) -> None: 

161 """Register a language validator. 

162 

163 Args: 

164 validator: The validator to register. 

165 """ 

166 language_name = validator.get_language_name() 

167 self._validators[language_name] = validator 

168 logger.debug(f"Registered validator for language: {language_name}") 

169 

170 def get_validator(self, language: str) -> LanguageValidator | None: 

171 """Get a validator for the specified language. 

172 

173 Args: 

174 language: The language name (e.g., "python", "go"). 

175 

176 Returns: 

177 The validator for the language, or None if not found. 

178 """ 

179 return self._validators.get(language.lower()) 

180 

181 def get_supported_languages(self) -> list[str]: 

182 """Get list of supported languages. 

183 

184 Returns: 

185 List of supported language names. 

186 """ 

187 return list(self._validators.keys()) 

188 

189 

190# Global registry instance 

191_registry = LanguageValidatorRegistry() 

192 

193 

194def get_validator_registry() -> LanguageValidatorRegistry: 

195 """Get the global validator registry. 

196 

197 Returns: 

198 The global validator registry instance. 

199 """ 

200 return _registry