Просмотр исходного кода

feat(generate): support named output paths

ChristianLempa 2 дней назад
Родитель
Сommit
9b47551b7b
3 измененных файлов с 112 добавлено и 2 удалено
  1. 49 0
      cli/core/module/base_commands.py
  2. 9 1
      cli/core/module/base_module.py
  3. 54 1
      tests/test_base_commands.py

+ 49 - 0
cli/core/module/base_commands.py

@@ -53,6 +53,7 @@ class GenerationConfig:
     dry_run: bool = False
     show_files: bool = False
     quiet: bool = False
+    name: str | None = None
 
 
 def list_templates(module_instance, raw: bool = False) -> list:
@@ -322,6 +323,53 @@ def execute_remote_dry_run(
     return total_files, size_str
 
 
+def _validate_output_name(name: str) -> str:
+    """Validate and normalize a generated output name."""
+    normalized_name = name.strip()
+    if not normalized_name:
+        raise ValueError("--name cannot be empty")
+    if "/" in normalized_name or "\\" in normalized_name:
+        raise ValueError("--name must be a file name, not a path")
+    if normalized_name in {".", ".."}:
+        raise ValueError("--name cannot be '.' or '..'")
+    return normalized_name
+
+
+def _prefix_path_part(name: str, part: str) -> str:
+    """Prefix one top-level path part with the generated output name."""
+    path = Path(part)
+    if path.stem == "main" and path.suffix:
+        return f"{name}{path.suffix}"
+    return f"{name}_{part}"
+
+
+def apply_output_name(rendered_files: dict[str, str], name: str | None) -> dict[str, str]:
+    """Rename top-level generated paths with a user-provided output name.
+
+    The top-level entrypoint file named ``main.<ext>`` becomes ``<name>.<ext>``.
+    All other top-level files or directories receive ``<name>_`` as a prefix.
+    Nested path segments are preserved unchanged.
+    """
+    if name is None:
+        return rendered_files
+
+    normalized_name = _validate_output_name(name)
+    renamed_files: dict[str, str] = {}
+    for file_path, content in rendered_files.items():
+        path = Path(file_path)
+        parts = path.parts
+        if not parts:
+            continue
+
+        renamed_top_level = _prefix_path_part(normalized_name, parts[0])
+        renamed_path = Path(renamed_top_level, *parts[1:]).as_posix()
+        if renamed_path in renamed_files:
+            raise ValueError(f"--name creates duplicate generated path: {renamed_path}")
+        renamed_files[renamed_path] = content
+
+    return renamed_files
+
+
 def write_rendered_files(output_dir: Path, rendered_files: dict[str, str]) -> None:
     """Write rendered files to the output directory."""
     output_dir.mkdir(parents=True, exist_ok=True)
@@ -438,6 +486,7 @@ def generate_template(module_instance, config: GenerationConfig) -> None:  # noq
 
     try:
         rendered_files, _variable_values = _render_template(template, config.id, display, config.interactive)
+        rendered_files = apply_output_name(rendered_files, config.name)
 
         if destination is None:
             if config.dry_run:

+ 9 - 1
cli/core/module/base_module.py

@@ -186,10 +186,17 @@ class Module(ABC):
             bool,
             Option(
                 "--interactive/--no-interactive",
-                "-i/-n",
                 help="Enable interactive prompting for variables",
             ),
         ] = True,
+        name: Annotated[
+            str | None,
+            Option(
+                "--name",
+                "-n",
+                help="Rename top-level generated files/directories with this name",
+            ),
+        ] = None,
         var: Annotated[
             list[str] | None,
             Option(
@@ -235,6 +242,7 @@ class Module(ABC):
             interactive=interactive,
             var=var,
             var_file=var_file,
+            name=name,
             dry_run=dry_run,
             show_files=show_files,
             quiet=quiet,

+ 54 - 1
tests/test_base_commands.py

@@ -4,7 +4,7 @@ from __future__ import annotations
 
 from types import SimpleNamespace
 
-from cli.core.module.base_commands import GenerationConfig, generate_template, list_templates
+from cli.core.module.base_commands import GenerationConfig, apply_output_name, generate_template, list_templates
 
 
 def _noop(*_args, **_kwargs) -> None:
@@ -78,6 +78,59 @@ def test_list_templates_raw_outputs_tab_separated_rows() -> None:
     assert display.lines == ["whoami\tWhoami\tdocker,test\t1.0.0\tdefault"]
 
 
+def test_apply_output_name_renames_top_level_paths_only() -> None:
+    """Named generation should rename top-level outputs while preserving nested names."""
+    rendered_files = {
+        "files/test.txt": "nested",
+        "main.tf": "main",
+        "dns.tf": "dns",
+    }
+
+    assert apply_output_name(rendered_files, "servertest1") == {
+        "servertest1_files/test.txt": "nested",
+        "servertest1.tf": "main",
+        "servertest1_dns.tf": "dns",
+    }
+
+
+def test_generate_template_applies_output_name_before_writing(monkeypatch, tmp_path) -> None:
+    """Generate should write renamed paths when --name is provided."""
+    display = _DisplayCapture()
+    template = SimpleNamespace(id="terraform", slug="terraform")
+    module_instance = SimpleNamespace(name="terraform", display=display)
+    written: dict[str, object] = {}
+
+    monkeypatch.setattr("cli.core.module.base_commands._prepare_template", lambda *_args, **_kwargs: template)
+    monkeypatch.setattr(
+        "cli.core.module.base_commands._render_template",
+        lambda *_args, **_kwargs: ({"files/test.txt": "nested", "main.tf": "main", "dns.tf": "dns"}, {}),
+    )
+    monkeypatch.setattr("cli.core.module.base_commands.check_output_directory", lambda *_args, **_kwargs: [])
+
+    def capture_write(output_dir, rendered_files):
+        written["output_dir"] = output_dir
+        written["rendered_files"] = rendered_files
+
+    monkeypatch.setattr("cli.core.module.base_commands.write_rendered_files", capture_write)
+
+    generate_template(
+        module_instance,
+        GenerationConfig(
+            id="terraform",
+            output=str(tmp_path),
+            interactive=False,
+            name="servertest1",
+        ),
+    )
+
+    assert written["output_dir"] == tmp_path
+    assert written["rendered_files"] == {
+        "servertest1_files/test.txt": "nested",
+        "servertest1.tf": "main",
+        "servertest1_dns.tf": "dns",
+    }
+
+
 def test_generate_template_dry_run_skips_destination_prompt_and_overwrite_check(
     monkeypatch,
 ) -> None: