Initial monorepo structure
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"""Ordnerstruktur-Manager für das Monorepo.
|
||||
|
||||
Verwaltet die 3-Ebenen-Ordnerhierarchie:
|
||||
Ebene 1 (Kontextordner) → Ebene 2 (Projektordner) → Ebene 3 (Modulordner)
|
||||
|
||||
Verantwortung: Anlegen, Validieren und Verwalten der Monorepo-Ordnerstruktur.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from monorepo.models import Context, ProjectInfo
|
||||
|
||||
|
||||
class StructureManager:
|
||||
"""Verwaltet die 3-Ebenen-Ordnerhierarchie des Monorepos."""
|
||||
|
||||
CONTEXTS = ("privat", "dhive", "bahn", "shared")
|
||||
NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9\-]{0,48}[a-z0-9]$")
|
||||
|
||||
def __init__(self, root_path: Path) -> None:
|
||||
"""Initialisiert den StructureManager.
|
||||
|
||||
Args:
|
||||
root_path: Wurzelverzeichnis des Monorepos.
|
||||
"""
|
||||
self.root_path = root_path
|
||||
|
||||
def validate_name(self, name: str) -> bool:
|
||||
"""Prüft ob ein Name der kebab-case-Konvention entspricht.
|
||||
|
||||
Regeln:
|
||||
- Ausschließlich Kleinbuchstaben, Ziffern und Bindestriche
|
||||
- Zwischen 2 und 50 Zeichen lang
|
||||
- Beginnt und endet nicht mit einem Bindestrich
|
||||
|
||||
Args:
|
||||
name: Der zu prüfende Projektname.
|
||||
|
||||
Returns:
|
||||
True wenn der Name gültig ist, False sonst.
|
||||
"""
|
||||
if not name:
|
||||
return False
|
||||
return self.NAME_PATTERN.match(name) is not None
|
||||
|
||||
def create_project(self, context: str, name: str) -> Path:
|
||||
"""Erstellt ein neues Projekt im gegebenen Kontext.
|
||||
|
||||
Validiert den Namen, prüft auf Namenskonflikte und erstellt
|
||||
den Projektordner im korrekten Kontextordner.
|
||||
|
||||
Args:
|
||||
context: Der Arbeitskontext (privat, dhive, bahn, shared).
|
||||
name: Der Projektname in kebab-case.
|
||||
|
||||
Returns:
|
||||
Pfad zum erstellten Projektordner.
|
||||
|
||||
Raises:
|
||||
ValueError: Wenn der Kontext ungültig ist, der Name die
|
||||
Namenskonvention verletzt, oder ein Projekt mit
|
||||
diesem Namen bereits existiert.
|
||||
"""
|
||||
# Kontext validieren
|
||||
if context not in self.CONTEXTS:
|
||||
raise ValueError(
|
||||
f"Ungültiger Kontext '{context}'. "
|
||||
f"Erlaubte Kontexte: {', '.join(self.CONTEXTS)}"
|
||||
)
|
||||
|
||||
# Name validieren
|
||||
if not self.validate_name(name):
|
||||
raise ValueError(
|
||||
f"Ungültiger Projektname '{name}'. "
|
||||
f"Der Name muss kebab-case sein (Kleinbuchstaben, Ziffern, "
|
||||
f"Bindestriche), zwischen 2 und 50 Zeichen lang, und darf "
|
||||
f"nicht mit einem Bindestrich beginnen oder enden."
|
||||
)
|
||||
|
||||
# Namenskonflikt prüfen
|
||||
project_path = self.root_path / context / name
|
||||
if project_path.exists():
|
||||
raise ValueError(
|
||||
f"Namenskonflikt: Ein Projekt mit dem Namen '{name}' "
|
||||
f"existiert bereits im Kontext '{context}' "
|
||||
f"(Pfad: {project_path})."
|
||||
)
|
||||
|
||||
# Projektordner erstellen
|
||||
project_path.mkdir(parents=True, exist_ok=False)
|
||||
return project_path
|
||||
|
||||
def list_projects(self, context: str | None = None) -> list[ProjectInfo]:
|
||||
"""Listet alle Projekte, optional gefiltert nach Kontext.
|
||||
|
||||
Args:
|
||||
context: Optionaler Kontext-Filter. Wenn None, werden alle
|
||||
Projekte aus allen Kontexten aufgelistet.
|
||||
|
||||
Returns:
|
||||
Liste von ProjectInfo-Objekten für alle gefundenen Projekte.
|
||||
|
||||
Raises:
|
||||
ValueError: Wenn ein ungültiger Kontext angegeben wird.
|
||||
"""
|
||||
if context is not None and context not in self.CONTEXTS:
|
||||
raise ValueError(
|
||||
f"Ungültiger Kontext '{context}'. "
|
||||
f"Erlaubte Kontexte: {', '.join(self.CONTEXTS)}"
|
||||
)
|
||||
|
||||
contexts_to_scan = (context,) if context else self.CONTEXTS
|
||||
projects: list[ProjectInfo] = []
|
||||
|
||||
for ctx in contexts_to_scan:
|
||||
context_dir = self.root_path / ctx
|
||||
if not context_dir.exists():
|
||||
continue
|
||||
|
||||
for entry in sorted(context_dir.iterdir()):
|
||||
if entry.is_dir() and not entry.name.startswith("."):
|
||||
projects.append(
|
||||
ProjectInfo(
|
||||
name=entry.name,
|
||||
context=ctx,
|
||||
path=entry,
|
||||
)
|
||||
)
|
||||
|
||||
return projects
|
||||
|
||||
def resolve_context(self, project_path: Path) -> str:
|
||||
"""Ermittelt den Arbeitskontext eines Projekts anhand seines Pfads.
|
||||
|
||||
Analysiert den Pfad relativ zum Monorepo-Root und extrahiert
|
||||
den Kontextordner (erste Ebene).
|
||||
|
||||
Args:
|
||||
project_path: Absoluter oder relativer Pfad zum Projekt.
|
||||
|
||||
Returns:
|
||||
Der ermittelte Arbeitskontext als String.
|
||||
|
||||
Raises:
|
||||
ValueError: Wenn der Pfad nicht innerhalb eines bekannten
|
||||
Kontextordners liegt.
|
||||
"""
|
||||
# Pfad auflösen (absolut machen)
|
||||
resolved = project_path.resolve()
|
||||
root_resolved = self.root_path.resolve()
|
||||
|
||||
# Prüfen ob der Pfad innerhalb des Monorepos liegt
|
||||
try:
|
||||
relative = resolved.relative_to(root_resolved)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Der Pfad '{project_path}' liegt nicht innerhalb "
|
||||
f"des Monorepos ({self.root_path})."
|
||||
)
|
||||
|
||||
# Ersten Pfadteil (Kontextordner) extrahieren
|
||||
parts = relative.parts
|
||||
if not parts:
|
||||
raise ValueError(
|
||||
f"Der Pfad '{project_path}' zeigt auf das Monorepo-Root, "
|
||||
f"nicht auf ein Projekt in einem Kontext."
|
||||
)
|
||||
|
||||
context_name = parts[0]
|
||||
if context_name not in self.CONTEXTS:
|
||||
raise ValueError(
|
||||
f"Der Pfad '{project_path}' liegt nicht in einem bekannten "
|
||||
f"Kontextordner. Gefunden: '{context_name}', "
|
||||
f"erwartet: {', '.join(self.CONTEXTS)}."
|
||||
)
|
||||
|
||||
return context_name
|
||||
Reference in New Issue
Block a user