mirror of
https://github.com/ChristianLempa/boilerplates.git
synced 2026-08-24 10:04:44 -05:00
feat(generate): support named output paths
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user