fast-clean-architecture 1.0.0__py3-none-any.whl → 1.1.2__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.
Files changed (27) hide show
  1. fast_clean_architecture/__init__.py +5 -6
  2. fast_clean_architecture/analytics.py +260 -0
  3. fast_clean_architecture/cli.py +563 -46
  4. fast_clean_architecture/config.py +47 -23
  5. fast_clean_architecture/error_tracking.py +201 -0
  6. fast_clean_architecture/exceptions.py +432 -12
  7. fast_clean_architecture/generators/__init__.py +11 -1
  8. fast_clean_architecture/generators/component_generator.py +407 -103
  9. fast_clean_architecture/generators/config_updater.py +186 -38
  10. fast_clean_architecture/generators/generator_factory.py +223 -0
  11. fast_clean_architecture/generators/package_generator.py +9 -7
  12. fast_clean_architecture/generators/template_validator.py +109 -9
  13. fast_clean_architecture/generators/validation_config.py +5 -3
  14. fast_clean_architecture/generators/validation_metrics.py +10 -6
  15. fast_clean_architecture/health.py +169 -0
  16. fast_clean_architecture/logging_config.py +52 -0
  17. fast_clean_architecture/metrics.py +108 -0
  18. fast_clean_architecture/protocols.py +406 -0
  19. fast_clean_architecture/templates/external.py.j2 +109 -32
  20. fast_clean_architecture/utils.py +50 -31
  21. fast_clean_architecture/validation.py +302 -0
  22. {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.2.dist-info}/METADATA +131 -64
  23. fast_clean_architecture-1.1.2.dist-info/RECORD +38 -0
  24. fast_clean_architecture-1.0.0.dist-info/RECORD +0 -30
  25. {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.2.dist-info}/WHEEL +0 -0
  26. {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.2.dist-info}/entry_points.txt +0 -0
  27. {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,46 +1,71 @@
1
1
  """Component generator for creating individual component files."""
2
2
 
3
+ import functools
3
4
  import os
4
5
  import re
5
- import functools
6
- import logging
6
+ from datetime import datetime
7
7
  from pathlib import Path
8
- from typing import Dict, Any, Optional, Union, List
8
+ from typing import (
9
+ Any,
10
+ Callable,
11
+ Dict,
12
+ Generic,
13
+ List,
14
+ Optional,
15
+ Set,
16
+ Tuple,
17
+ TypeVar,
18
+ Union,
19
+ )
9
20
 
10
21
  import jinja2
11
22
  from jinja2 import TemplateSyntaxError
12
- from jinja2.exceptions import SecurityError
23
+ from jinja2.exceptions import SecurityError as JinjaSecurityError
13
24
  from jinja2.sandbox import SandboxedEnvironment
14
25
  from rich.console import Console
15
26
 
27
+ from ..config import Config
28
+ from ..error_tracking import track_error, track_exception
29
+ from ..exceptions import (
30
+ ComponentError,
31
+ FileConflictError,
32
+ SecurityError,
33
+ TemplateError,
34
+ ValidationError,
35
+ create_secure_error,
36
+ )
37
+ from ..logging_config import get_logger
38
+ from ..metrics import PerformanceTracker, measure_execution_time
39
+ from ..protocols import (
40
+ ComponentGeneratorProtocol,
41
+ ComponentValidationStrategy,
42
+ SecurePathHandler,
43
+ TemplateValidatorProtocol,
44
+ )
16
45
  from ..templates import TEMPLATES_DIR
17
46
  from ..utils import (
47
+ ensure_directory,
18
48
  get_template_variables,
49
+ sanitize_error_message,
50
+ sanitize_name,
51
+ secure_file_operation,
19
52
  to_pascal_case,
20
53
  to_snake_case,
21
- ensure_directory,
22
- sanitize_name,
23
- validate_python_identifier,
24
54
  validate_name,
25
- secure_file_operation,
26
- create_secure_error,
27
- sanitize_error_message,
28
- )
29
- from ..exceptions import (
30
- TemplateError,
31
- ValidationError,
32
- FileConflictError,
33
- ComponentError,
55
+ validate_python_identifier,
34
56
  )
35
- from ..config import Config
36
57
  from .template_validator import TemplateValidatorFactory
37
58
 
38
- # Set up logger for security events
39
- logger = logging.getLogger(__name__)
59
+ # Set up structured logger
60
+ logger = get_logger(__name__)
40
61
 
41
62
 
42
- class ComponentGenerator:
43
- """Generator for creating individual component files from templates."""
63
+ class ComponentGenerator(ComponentGeneratorProtocol):
64
+ """Generator for creating individual component files from templates.
65
+
66
+ This class implements the ComponentGeneratorProtocol to ensure type safety
67
+ and provides enhanced security through protocol-based design patterns.
68
+ """
44
69
 
45
70
  # Mapping of component types to their template files
46
71
  TEMPLATE_MAPPING = {
@@ -59,9 +84,24 @@ class ComponentGenerator:
59
84
  # Infrastructure repositories use a different template
60
85
  INFRASTRUCTURE_REPOSITORY_TEMPLATE = "infrastructure_repository.py.j2"
61
86
 
62
- def __init__(self, config: Config, console: Optional[Console] = None):
87
+ def __init__(
88
+ self,
89
+ config: Config,
90
+ template_validator: Optional[TemplateValidatorProtocol] = None,
91
+ path_handler: Optional[SecurePathHandler[Union[str, Path]]] = None,
92
+ console: Optional[Console] = None,
93
+ ):
94
+ """Initialize ComponentGenerator with dependency injection.
95
+
96
+ Args:
97
+ config: Configuration object
98
+ template_validator: Template validator (injected dependency)
99
+ path_handler: Secure path handler (injected dependency)
100
+ console: Console for output
101
+ """
63
102
  self.config = config
64
103
  self.console = console or Console()
104
+
65
105
  # Use SandboxedEnvironment for security
66
106
  self.template_env = SandboxedEnvironment(
67
107
  loader=jinja2.FileSystemLoader(TEMPLATES_DIR),
@@ -73,19 +113,38 @@ class ComponentGenerator:
73
113
  # Define allowed filters and functions
74
114
  self._setup_sandbox_security()
75
115
 
76
- # Initialize template validator with simplified validation config
77
- from .validation_config import ValidationConfig
116
+ # Use injected template validator or create default
117
+ if template_validator is not None:
118
+ self.template_validator = template_validator
119
+ else:
120
+ # Fallback to factory creation for backward compatibility
121
+ from .validation_config import ValidationConfig
122
+
123
+ validation_config = ValidationConfig(
124
+ sandbox_mode=True,
125
+ max_template_size_bytes=64 * 1024, # 64KB limit
126
+ render_timeout_seconds=10,
127
+ max_variable_nesting_depth=10,
128
+ )
129
+ self.template_validator = TemplateValidatorFactory().create(
130
+ self.template_env, validation_config
131
+ )
78
132
 
79
- validation_config = ValidationConfig(
80
- sandbox_mode=True,
81
- max_template_size_bytes=64 * 1024, # 64KB limit
82
- render_timeout_seconds=10,
83
- max_variable_nesting_depth=10,
84
- )
85
- self.template_validator = TemplateValidatorFactory().create(
86
- self.template_env, validation_config
87
- )
133
+ # Use injected path handler or create default
134
+ if path_handler is not None:
135
+ self.path_handler: SecurePathHandler[Union[str, Path]] = path_handler
136
+ else:
137
+ # Fallback to default creation for backward compatibility
138
+ self.path_handler = SecurePathHandler[Union[str, Path]](
139
+ max_path_length=4096,
140
+ allowed_extensions=[".py", ".j2", ".yaml", ".yml", ".json"],
141
+ )
88
142
 
143
+ # Initialize component validation strategies
144
+ self._validation_strategies = self._setup_validation_strategies()
145
+
146
+ @measure_execution_time("create_component")
147
+ @track_exception(operation="create_component") # type: ignore[misc]
89
148
  def create_component(
90
149
  self,
91
150
  base_path: Path,
@@ -97,7 +156,27 @@ class ComponentGenerator:
97
156
  dry_run: bool = False,
98
157
  force: bool = False,
99
158
  ) -> Path:
100
- """Create a single component file."""
159
+ """Create a single component file with enhanced type safety.
160
+
161
+ This method implements the ComponentGeneratorProtocol interface
162
+ and uses secure path handling for enhanced security.
163
+ """
164
+ # Log component creation start
165
+ logger.info(
166
+ "Starting component creation",
167
+ operation="create_component",
168
+ system_name=system_name,
169
+ module_name=module_name,
170
+ layer=layer,
171
+ component_type=component_type,
172
+ component_name=component_name,
173
+ dry_run=dry_run,
174
+ force=force,
175
+ )
176
+
177
+ # Validate and secure the base path using type-safe path handler
178
+ secure_base_path = Path(self.path_handler.process(base_path))
179
+
101
180
  # Validate inputs
102
181
  self._validate_component_inputs(
103
182
  system_name, module_name, layer, component_type, component_name
@@ -108,26 +187,36 @@ class ComponentGenerator:
108
187
  if not validate_python_identifier(sanitized_name):
109
188
  raise ValidationError(f"Invalid component name: {component_name}")
110
189
 
111
- # Determine file path and name
190
+ # Determine file path and name with secure base path
112
191
  file_path, file_name = self._get_component_file_path(
113
- base_path, system_name, module_name, layer, component_type, sanitized_name
192
+ secure_base_path,
193
+ system_name,
194
+ module_name,
195
+ layer,
196
+ component_type,
197
+ sanitized_name,
114
198
  )
115
199
 
116
- # Check for conflicts
117
- if file_path.exists() and not force:
200
+ # Additional security validation for the final file path
201
+ secure_file_path = Path(self.path_handler.process(file_path))
202
+
203
+ # Check for conflicts using secure file path
204
+ if secure_file_path.exists() and not force:
118
205
  raise FileConflictError(
119
- f"Component file already exists: {file_path}. Use --force to overwrite."
206
+ f"Component file already exists: {secure_file_path}. Use --force to overwrite."
120
207
  )
121
208
 
122
209
  if dry_run:
123
- self.console.print(f"[yellow]DRY RUN:[/yellow] Would create {file_path}")
124
- return file_path
210
+ self.console.print(
211
+ f"[yellow]DRY RUN:[/yellow] Would create {secure_file_path}"
212
+ )
213
+ return secure_file_path
125
214
 
126
215
  # Validate file system permissions and space
127
- self._validate_file_system(file_path)
216
+ self._validate_file_system(secure_file_path)
128
217
 
129
218
  # Ensure directory exists
130
- ensure_directory(file_path.parent)
219
+ ensure_directory(secure_file_path.parent)
131
220
 
132
221
  # Generate template variables
133
222
  template_vars = get_template_variables(
@@ -148,49 +237,97 @@ class ComponentGenerator:
148
237
  if custom_template:
149
238
  template_name = custom_template
150
239
 
151
- # Render and write file
152
- content = self._render_template(template_name, template_vars)
240
+ # Render and write file with performance tracking
241
+ with PerformanceTracker(
242
+ "template_rendering",
243
+ template_name=template_name,
244
+ component_type=component_type,
245
+ ):
246
+ content = self._render_template(template_name, template_vars)
153
247
 
154
- # Write file with atomic operations and error handling
248
+ # Write file with atomic operations and error handling using secure path
155
249
  backup_path = None
156
- lock_file = file_path.with_suffix(f"{file_path.suffix}.lock")
250
+ lock_file = secure_file_path.with_suffix(f"{secure_file_path.suffix}.lock")
157
251
 
158
252
  try:
159
253
  # Use file locking to prevent race conditions
160
- import fcntl
161
254
  import tempfile
162
255
 
256
+ import portalocker
257
+
163
258
  # Create lock file for atomic operations
164
259
  with open(lock_file, "w") as lock:
165
260
  try:
166
- fcntl.flock(lock.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
167
- except (IOError, OSError):
261
+ portalocker.lock(lock, portalocker.LOCK_EX | portalocker.LOCK_NB)
262
+ except portalocker.LockException:
168
263
  raise ValidationError(
169
- f"File {file_path} is locked by another process"
264
+ f"File {secure_file_path} is locked by another process"
170
265
  )
171
266
 
172
267
  # Create backup if file exists (within lock)
173
- if file_path.exists():
174
- backup_path = file_path.with_suffix(f"{file_path.suffix}.backup")
268
+ if secure_file_path.exists():
269
+ backup_path = secure_file_path.with_suffix(
270
+ f"{secure_file_path.suffix}.backup"
271
+ )
175
272
  # Use atomic copy to prevent corruption
176
- with open(file_path, "rb") as src, open(backup_path, "wb") as dst:
273
+ with (
274
+ open(secure_file_path, "rb") as src,
275
+ open(backup_path, "wb") as dst,
276
+ ):
177
277
  dst.write(src.read())
178
278
 
179
279
  # Atomic write operation to prevent TOCTOU vulnerabilities
180
- self._atomic_write_file(file_path, content)
280
+ self._atomic_write_file(secure_file_path, content)
181
281
 
182
282
  # Clean up backup on success
183
283
  if backup_path and backup_path.exists():
184
284
  backup_path.unlink()
185
285
 
286
+ # Success - print message and log
287
+ self.console.print(f"[green]✓[/green] Created {secure_file_path}")
288
+ logger.info(
289
+ "Component created successfully",
290
+ operation="create_component",
291
+ file_path=str(secure_file_path),
292
+ component_name=component_name,
293
+ component_type=component_type,
294
+ layer=layer,
295
+ )
296
+ # secure_file_path is already guaranteed to be a Path object
297
+ # Return will happen after finally block
298
+
186
299
  except Exception as e:
300
+ # Log the error with context
301
+ logger.error(
302
+ "Component creation failed",
303
+ operation="create_component",
304
+ error=str(e),
305
+ error_type=type(e).__name__,
306
+ component_name=component_name,
307
+ component_type=component_type,
308
+ file_path=(
309
+ str(secure_file_path) if "secure_file_path" in locals() else None
310
+ ),
311
+ )
312
+
187
313
  # Rollback on failure
188
314
  if backup_path and backup_path.exists():
189
315
  try:
190
- if file_path.exists():
191
- file_path.unlink()
192
- backup_path.rename(file_path)
316
+ if secure_file_path.exists():
317
+ secure_file_path.unlink()
318
+ backup_path.rename(secure_file_path)
319
+ logger.info(
320
+ "Rollback completed successfully",
321
+ operation="rollback",
322
+ file_path=str(secure_file_path),
323
+ )
193
324
  except Exception as rollback_error:
325
+ logger.error(
326
+ "Rollback failed",
327
+ operation="rollback",
328
+ error=str(rollback_error),
329
+ file_path=str(secure_file_path),
330
+ )
194
331
  self.console.print(
195
332
  f"[red]Error during rollback: {rollback_error}[/red]"
196
333
  )
@@ -203,14 +340,11 @@ class ComponentGenerator:
203
340
  if lock_file.exists():
204
341
  try:
205
342
  lock_file.unlink()
206
- except Exception:
343
+ except (OSError, FileNotFoundError):
207
344
  pass # Ignore cleanup errors
208
345
 
209
- self.console.print(f"[green]✓[/green] Created {file_path}")
210
- # Ensure we're returning a valid Path object
211
- if not isinstance(file_path, Path):
212
- file_path = Path(file_path)
213
- return file_path
346
+ # Return the secure file path after successful completion
347
+ return secure_file_path
214
348
 
215
349
  def _atomic_write_file(self, file_path: Path, content: str) -> None:
216
350
  """Write file atomically to prevent corruption and TOCTOU vulnerabilities.
@@ -225,19 +359,14 @@ class ComponentGenerator:
225
359
  file_path = file_path.resolve()
226
360
 
227
361
  # Use secure file operation with locking
228
- return secure_file_operation(
229
- file_path, self._perform_atomic_write, file_path, content
230
- )
362
+ secure_file_operation(file_path, self._perform_atomic_write, file_path, content)
231
363
 
232
364
  def _perform_atomic_write(self, file_path: Path, content: str) -> None:
233
365
  """Perform the actual atomic write operation."""
234
- import tempfile
235
366
  import os
367
+ import tempfile
236
368
 
237
- # Validate file_path and ensure it's a proper Path object
238
- if not isinstance(file_path, Path):
239
- file_path = Path(file_path)
240
-
369
+ # file_path is already guaranteed to be a Path object by type annotation
241
370
  # Ensure we have a valid parent directory
242
371
  parent_dir = file_path.parent
243
372
  if parent_dir is None or str(parent_dir) == ".":
@@ -263,12 +392,11 @@ class ComponentGenerator:
263
392
  temp_file.write(content)
264
393
  temp_file.flush()
265
394
  os.fsync(temp_file.fileno()) # Ensure data is written to disk
266
- temp_fd = None # File descriptor is now closed
395
+ # File descriptor is now closed by context manager
267
396
 
268
397
  # Atomically move temporary file to target location
269
398
  temp_path_obj = Path(temp_path)
270
399
  temp_path_obj.replace(file_path)
271
- temp_path = None # Successfully moved, don't clean up
272
400
 
273
401
  # Verify file was written correctly
274
402
  if not file_path.exists() or file_path.stat().st_size == 0:
@@ -288,28 +416,24 @@ class ComponentGenerator:
288
416
  )
289
417
  else:
290
418
  raise create_secure_error("file_write", "write file", str(e))
291
- finally:
292
- # Clean up on failure
293
- if temp_fd is not None:
294
- try:
295
- os.close(temp_fd)
296
- except OSError:
297
- pass
419
+ except Exception:
420
+ # Clean up temporary file on any other failure
298
421
  if temp_path and Path(temp_path).exists():
299
422
  try:
300
423
  Path(temp_path).unlink()
301
424
  except OSError:
302
425
  pass
426
+ raise
303
427
 
304
428
  def create_multiple_components(
305
429
  self,
306
430
  base_path: Path,
307
431
  system_name: str,
308
432
  module_name: str,
309
- components_spec: Dict[str, Dict[str, list]],
433
+ components_spec: Dict[str, Dict[str, List[str]]],
310
434
  dry_run: bool = False,
311
435
  force: bool = False,
312
- ) -> list[Path]:
436
+ ) -> List[Path]:
313
437
  """Create multiple components from specification.
314
438
 
315
439
  Args:
@@ -380,7 +504,7 @@ class ComponentGenerator:
380
504
  layer_components = {
381
505
  "domain": ["entities", "repositories", "value_objects"],
382
506
  "application": ["services", "commands", "queries"],
383
- "infrastructure": ["models", "repositories", "external", "internal"],
507
+ "infrastructure": ["models", "repositories", "external"],
384
508
  "presentation": ["api", "schemas"],
385
509
  }
386
510
 
@@ -401,7 +525,7 @@ class ComponentGenerator:
401
525
  layer: str,
402
526
  component_type: str,
403
527
  component_name: str,
404
- ) -> tuple[Path, str]:
528
+ ) -> Tuple[Path, str]:
405
529
  """Get the file path and name for a component."""
406
530
  # Determine file name based on component type
407
531
  file_name = self._get_component_file_name(component_type, component_name)
@@ -432,7 +556,6 @@ class ComponentGenerator:
432
556
  "queries": f"{snake_name}.py",
433
557
  "models": f"{snake_name}_model.py",
434
558
  "external": f"{snake_name}_client.py",
435
- "internal": f"{snake_name}.py",
436
559
  "api": f"{snake_name}_router.py",
437
560
  "schemas": f"{snake_name}_schemas.py",
438
561
  }
@@ -463,8 +586,8 @@ class ComponentGenerator:
463
586
 
464
587
  def _validate_file_system(self, file_path: Path) -> None:
465
588
  """Validate file system security and basic requirements."""
466
- import shutil
467
589
  import os
590
+ import shutil
468
591
 
469
592
  try:
470
593
  # Prevent symlink attacks by checking the entire path
@@ -533,7 +656,14 @@ class ComponentGenerator:
533
656
  while current_path != current_path.parent:
534
657
  if current_path.exists() and current_path.is_symlink():
535
658
  # Instead of hardcoded allowlist, use more sophisticated validation
536
- symlink_target = current_path.readlink()
659
+ # Use os.readlink for Python 3.8 compatibility
660
+ import os
661
+
662
+ try:
663
+ symlink_target = Path(os.readlink(current_path))
664
+ except (OSError, AttributeError):
665
+ # If readlink fails, treat as unsafe
666
+ raise ValidationError(f"Cannot resolve symlink: {current_path}")
537
667
 
538
668
  # Check if symlink points outside the project or to dangerous locations
539
669
  if symlink_target.is_absolute():
@@ -597,7 +727,7 @@ class ComponentGenerator:
597
727
  safe_patterns = [
598
728
  # Unix/Linux system symlinks
599
729
  ("/var", "/private/var"), # macOS /var -> /private/var
600
- ("/tmp", "/private/tmp"), # macOS /tmp -> /private/tmp
730
+ ("/tmp", "/private/tmp"), # macOS /tmp -> /private/tmp # nosec B108
601
731
  ("/etc", "/private/etc"), # macOS /etc -> /private/etc
602
732
  # Other common system symlinks
603
733
  ("/usr/bin", "/bin"),
@@ -668,8 +798,8 @@ class ComponentGenerator:
668
798
  @functools.lru_cache(maxsize=128)
669
799
  def _is_temp_path_cached(self, path_str: str) -> bool:
670
800
  """Cached implementation of temporary path detection."""
671
- import tempfile
672
801
  import os
802
+ import tempfile
673
803
 
674
804
  path = Path(path_str)
675
805
 
@@ -679,8 +809,15 @@ class ComponentGenerator:
679
809
 
680
810
  # Check if path is relative to system temp directory
681
811
  try:
682
- if path.resolve().is_relative_to(system_temp):
812
+ # Use compatibility method for Python 3.8
813
+ resolved_path = path.resolve()
814
+ # Check if path is under system temp using relative_to
815
+ try:
816
+ resolved_path.relative_to(system_temp)
683
817
  return True
818
+ except ValueError:
819
+ # Path is not relative to system temp
820
+ pass
684
821
  except (AttributeError, ValueError):
685
822
  # Fallback for older Python versions or path resolution issues
686
823
  if str(path.resolve()).startswith(str(system_temp)):
@@ -725,10 +862,10 @@ class ComponentGenerator:
725
862
 
726
863
  # Check for common temp directory patterns
727
864
  temp_patterns = [
728
- "/tmp/",
865
+ "/tmp/", # nosec B108
729
866
  "/temp/",
730
867
  "/temporary/",
731
- "/var/tmp/",
868
+ "/var/tmp/", # nosec B108
732
869
  "/var/temp/",
733
870
  "/private/var/folders/", # macOS temp
734
871
  "appdata/local/temp/", # Windows temp
@@ -818,8 +955,8 @@ class ComponentGenerator:
818
955
  raise ValidationError(
819
956
  f"Template variable nesting too deep (max depth: 10, current: {depth})"
820
957
  )
821
- sanitized_vars = {}
822
- suspicious_patterns_found = []
958
+ sanitized_vars: Dict[str, Any] = {}
959
+ suspicious_patterns_found: List[str] = []
823
960
 
824
961
  for key, value in template_vars.items():
825
962
  if isinstance(value, str):
@@ -834,7 +971,7 @@ class ComponentGenerator:
834
971
  sanitized_vars[key] = value
835
972
  elif isinstance(value, (list, tuple)):
836
973
  # Recursively sanitize list/tuple items
837
- sanitized_list = []
974
+ sanitized_list: List[Any] = []
838
975
  for item in value:
839
976
  if isinstance(item, dict):
840
977
  sanitized_list.append(
@@ -860,21 +997,21 @@ class ComponentGenerator:
860
997
 
861
998
  return sanitized_vars
862
999
 
863
- def _sanitize_string_value(self, value: str) -> str:
1000
+ def _sanitize_string_value(self, value: Any) -> str:
864
1001
  """Enhanced sanitization for string values to prevent various injection attacks."""
1002
+ import unicodedata
1003
+ import urllib.parse
1004
+
865
1005
  if not isinstance(value, str):
866
1006
  value = str(value)
867
1007
 
868
1008
  # URL decode to catch encoded injection attempts
869
- import urllib.parse
870
-
871
1009
  try:
872
1010
  decoded_value = urllib.parse.unquote(value)
873
- except Exception:
1011
+ except (ValueError, UnicodeDecodeError):
874
1012
  decoded_value = value
875
1013
 
876
1014
  # Unicode normalization to prevent Unicode-based attacks
877
- import unicodedata
878
1015
 
879
1016
  normalized_value = unicodedata.normalize("NFKC", decoded_value)
880
1017
 
@@ -908,8 +1045,8 @@ class ComponentGenerator:
908
1045
  # Remove control characters and dangerous Unicode categories
909
1046
  sanitized = re.sub(r"[\x00-\x1f\x7f-\x9f]", "", sanitized)
910
1047
 
911
- # Remove potentially dangerous characters
912
- sanitized = re.sub(r'[<>"\'\\\/`$]', "", sanitized)
1048
+ # Remove potentially dangerous characters (but preserve forward slashes for URLs)
1049
+ sanitized = re.sub(r'[<>"\'\\`$]', "", sanitized)
913
1050
 
914
1051
  # Limit length to prevent DoS
915
1052
  if len(sanitized) > 1000:
@@ -1037,3 +1174,170 @@ class ComponentGenerator:
1037
1174
  raise TemplateError(f"Error rendering template {template_name}: {e}")
1038
1175
  except Exception as e:
1039
1176
  raise TemplateError(f"Unexpected error rendering template: {e}")
1177
+
1178
+ def validate_component(self, component: Dict[str, Any]) -> bool:
1179
+ """Validate component configuration and structure.
1180
+
1181
+ This method implements the ComponentGeneratorProtocol interface
1182
+ and provides comprehensive validation using type-safe strategies.
1183
+
1184
+ Args:
1185
+ component: Component configuration dictionary
1186
+
1187
+ Returns:
1188
+ True if component is valid
1189
+
1190
+ Raises:
1191
+ ValidationError: If component is invalid
1192
+ """
1193
+ if not isinstance(component, dict):
1194
+ raise ValidationError(
1195
+ f"Component must be a dictionary, got {type(component)}"
1196
+ )
1197
+
1198
+ # Extract component type for validation strategy selection
1199
+ component_type = component.get("type", "unknown")
1200
+
1201
+ # Use appropriate validation strategy
1202
+ if component_type in self._validation_strategies:
1203
+ strategy = self._validation_strategies[component_type]
1204
+ return strategy.validate(component)
1205
+
1206
+ # Fallback to basic validation
1207
+ required_fields = ["name", "type"]
1208
+ for field in required_fields:
1209
+ if field not in component:
1210
+ raise ValidationError(f"Missing required field '{field}' in component")
1211
+
1212
+ # Validate component name
1213
+ try:
1214
+ validate_name(component["name"])
1215
+ except (ValueError, TypeError) as e:
1216
+ raise ValidationError(f"Invalid component name: {e}")
1217
+ except SecurityError:
1218
+ # Re-raise SecurityError as-is for proper handling
1219
+ raise
1220
+
1221
+ return True
1222
+
1223
+ def _setup_validation_strategies(
1224
+ self,
1225
+ ) -> Dict[str, ComponentValidationStrategy[str]]:
1226
+ """Setup type-safe validation strategies for different component types.
1227
+
1228
+ Returns:
1229
+ Dictionary mapping component types to their validation strategies
1230
+ """
1231
+ strategies = {}
1232
+
1233
+ # Entity validation strategy
1234
+ entity_rules = {
1235
+ "required_fields": ["name", "type"],
1236
+ "field_types": {
1237
+ "name": str,
1238
+ "type": str,
1239
+ "attributes": (list, type(None)),
1240
+ "methods": (list, type(None)),
1241
+ },
1242
+ }
1243
+ strategies["entity"] = ComponentValidationStrategy("entity", entity_rules)
1244
+
1245
+ # Service validation strategy
1246
+ service_rules = {
1247
+ "required_fields": ["name", "type"],
1248
+ "field_types": {
1249
+ "name": str,
1250
+ "type": str,
1251
+ "dependencies": (list, type(None)),
1252
+ "methods": (list, type(None)),
1253
+ },
1254
+ }
1255
+ strategies["service"] = ComponentValidationStrategy("service", service_rules)
1256
+
1257
+ # Repository validation strategy
1258
+ repository_rules = {
1259
+ "required_fields": ["name", "type"],
1260
+ "field_types": {
1261
+ "name": str,
1262
+ "type": str,
1263
+ "entity_type": (str, type(None)),
1264
+ "methods": (list, type(None)),
1265
+ },
1266
+ }
1267
+ strategies["repository"] = ComponentValidationStrategy(
1268
+ "repository", repository_rules
1269
+ )
1270
+
1271
+ # Add more strategies for other component types
1272
+ for component_type in [
1273
+ "value_object",
1274
+ "command",
1275
+ "query",
1276
+ "model",
1277
+ "external",
1278
+ "api",
1279
+ "schema",
1280
+ ]:
1281
+ basic_rules = {
1282
+ "required_fields": ["name", "type"],
1283
+ "field_types": {
1284
+ "name": str,
1285
+ "type": str,
1286
+ },
1287
+ }
1288
+ strategies[component_type] = ComponentValidationStrategy(
1289
+ component_type, basic_rules
1290
+ )
1291
+
1292
+ return strategies
1293
+
1294
+ def create_component_with_validation(
1295
+ self,
1296
+ component_config: Dict[str, Any],
1297
+ base_path: Path,
1298
+ dry_run: bool = False,
1299
+ force: bool = False,
1300
+ ) -> Path:
1301
+ """Create a component with enhanced type-safe validation.
1302
+
1303
+ This method provides an alternative interface that uses the new
1304
+ validation strategies and secure path handling.
1305
+
1306
+ Args:
1307
+ component_config: Component configuration dictionary
1308
+ base_path: Base directory for component creation
1309
+ dry_run: If True, only simulate the operation
1310
+ force: If True, overwrite existing files
1311
+
1312
+ Returns:
1313
+ Path to the created component file
1314
+
1315
+ Raises:
1316
+ ValidationError: If component configuration is invalid
1317
+ SecurityError: If security constraints are violated
1318
+ """
1319
+ # Validate component configuration
1320
+ self.validate_component(component_config)
1321
+
1322
+ # Validate and secure the base path
1323
+ secure_base_path = Path(self.path_handler.process(base_path))
1324
+
1325
+ # Extract component details
1326
+ system_name = component_config.get("system_name", "default")
1327
+ module_name = component_config.get("module_name", "default")
1328
+ layer = component_config.get("layer", "domain")
1329
+ component_type = component_config["type"]
1330
+ component_name = component_config["name"]
1331
+
1332
+ # Use the existing create_component method with validated inputs
1333
+ result = self.create_component(
1334
+ base_path=secure_base_path,
1335
+ system_name=system_name,
1336
+ module_name=module_name,
1337
+ layer=layer,
1338
+ component_type=component_type,
1339
+ component_name=component_name,
1340
+ dry_run=dry_run,
1341
+ force=force,
1342
+ )
1343
+ return Path(result)