CtrlK
BlogDocsLog inGet started
Tessl Logo

giuseppe-trisciuoglio/developer-kit

Comprehensive developer toolkit providing reusable skills for Java/Spring Boot, TypeScript/NestJS/React/Next.js, Python, PHP, AWS CloudFormation, AI/RAG, DevOps, and more.

82

Quality

82%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Risky

Do not use without reviewing

Validation failed for skills in this tile
One or more skills have errors that need to be fixed before they can move to Implementation and Discovery review.
Overview
Quality
Evals
Security
Files

generate_crud_boilerplate.pyplugins/developer-kit-java/skills/spring-boot-crud-patterns/scripts/

#!/usr/bin/env python3
"""
Spring Boot CRUD boilerplate generator

Given an entity spec, scaffold a feature-based CRUD template aligned with the
"Spring Boot CRUD Patterns" skill (domain/application/presentation/infrastructure).

Usage:
  python skills/spring-boot-crud-patterns/scripts/generate_crud_boilerplate.py \
    --spec entity.json --package com.example.product --output ./generated

Spec format (JSON preferred; YAML supported if PyYAML is installed and file ends with .yml/.yaml):
{
  "entity": "Product",
  "id": {"name": "id", "type": "Long", "generated": true},
  "fields": [
    {"name": "name", "type": "String"},
    {"name": "price", "type": "BigDecimal"},
    {"name": "inStock", "type": "Boolean"}
  ]
}

Notes:
- Generates a feature folder with domain/application/presentation/infrastructure subpackages
- Uses Java records for DTOs, constructor injection, @Transactional, and standard REST codes
- Keep output as a starting point; adapt to your conventions
"""

import argparse
import json
import os
import re
import sys
from textwrap import dedent
from string import Template

try:
    import yaml  # type: ignore
    _HAS_YAML = True
except Exception:
    _HAS_YAML = False

# ------------------------- Helpers -------------------------

JAVA_TYPE_IMPORTS = {
    "BigDecimal": "import java.math.BigDecimal;",
    "UUID": "import java.util.UUID;",
    "LocalDate": "import java.time.LocalDate;",
    "LocalDateTime": "import java.time.LocalDateTime;",
}

JPA_IMPORTS = dedent(
    """
    import jakarta.persistence.*;
    import jakarta.validation.constraints.*;
    """
).strip()

COLLECTION_IMPORTS = "import java.util.Set;"

SPRING_IMPORTS = dedent(
    """
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    """
).strip()

CONTROLLER_IMPORTS = dedent(
    """
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    import jakarta.validation.Valid;
    """
).strip()

REPOSITORY_IMPORTS = "import org.springframework.data.jpa.repository.JpaRepository;"

SUPPORTED_SIMPLE_TYPES = {
    # primitive/object pairs default to wrapper types for null-safety in DTOs
    "String": "String",
    "Long": "Long",
    "Integer": "Integer",
    "Boolean": "Boolean",
    "BigDecimal": "BigDecimal",
    "UUID": "UUID",
    "LocalDate": "LocalDate",
    "LocalDateTime": "LocalDateTime",
}


def load_spec(spec_path: str) -> dict:
    with open(spec_path, "r", encoding="utf-8") as f:
        text = f.read()
    if spec_path.endswith('.yml') or spec_path.endswith('.yaml'):
        if not _HAS_YAML:
            raise SystemExit("PyYAML not installed. Install with `pip install pyyaml` or provide JSON spec.")
        return yaml.safe_load(text)
    return json.loads(text)


def camel_to_snake(name: str) -> str:
    import re as _re
    s1 = _re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
    return _re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower()


def lower_first(s: str) -> str:
    return s[:1].lower() + s[1:] if s else s


def ensure_dir(path: str) -> None:
    os.makedirs(path, exist_ok=True)


def write_file(path: str, content: str) -> None:
    ensure_dir(os.path.dirname(path))
    with open(path, "w", encoding="utf-8") as f:
        f.write(content.rstrip() + "\n")

def write_file_if_absent(path: str, content: str) -> None:
    if os.path.exists(path):
        return
    write_file(path, content)


def qualify_imports(types: list[str]) -> str:
    imports = []
    for t in types:
        imp = JAVA_TYPE_IMPORTS.get(t)
        if imp and imp not in imports:
            imports.append(imp)
    return "\n".join(imports)


def indent_block(s: str, n: int = 4) -> str:
    prefix = " " * n
    return "\n".join((prefix + line if line.strip() else line) for line in (s or "").splitlines())


# ------------------------- Template loading -------------------------

def load_template_text(templates_dir: str | None, filename: str) -> str | None:
    if not templates_dir:
        return None
    candidate = os.path.join(templates_dir, filename)
    if os.path.isfile(candidate):
        with open(candidate, "r", encoding="utf-8") as f:
            return f.read()
    return None


def render_template_file(templates_dir: str | None, filename: str, placeholders: dict) -> str | None:
    text = load_template_text(templates_dir, filename)
    if text is None:
        return None
    try:
        return Template(text).safe_substitute(placeholders).rstrip() + "\n"
    except Exception:
        # On any template error, fall back to defaults
        return None

# ------------------------- Templates -------------------------

def tmpl_domain_model(pkg: str, entity: str, id: dict, fields: list[dict], use_lombok: bool = False) -> str:
    used_types = {id["type"]} | {f["type"] for f in fields}
    extra_imports = qualify_imports(sorted(used_types))
    fields_src = []
    for f in [id, *fields]:
        fields_src.append(f"    private final {f['type']} {f['name']};")
    ctor_params = ", ".join([f"{f['type']} {f['name']}" for f in [id, *fields]])
    assigns = "\n".join([f"        this.{f['name']} = {f['name']};" for f in [id, *fields]])
    lombok_import = "import lombok.Getter;" if use_lombok else ""
    extra_imports_full = "\n".join(filter(None, [extra_imports, lombok_import]))
    class_annot = "@Getter\n" if use_lombok else ""
    getters = "\n".join([f"    public {f['type']} {('get' + f['name'][0].upper() + f['name'][1:])}() {{ return {f['name']}; }}" for f in [id, *fields]]) if not use_lombok else ""

    return dedent(f"""
package {pkg}.domain.model;

{extra_imports_full}

/**
 * Domain aggregate for {entity}.
 * Keep framework-free; capture invariants in factories/methods.
 */
{class_annot}public class {entity} {{
    {os.linesep.join(fields_src)}

        private {entity}({ctor_params}) {{
    {assigns}
        }}

        public static {entity} create({ctor_params}) {{
            // TODO: add invariant checks
            return new {entity}({', '.join([f['name'] for f in [id, *fields]])});
        }}

    {getters}
    }}
    """)


def tmpl_domain_repository(pkg: str, entity: str, id_type: str) -> str:
    return dedent(f"""
    package {pkg}.domain.repository;

    import java.util.Optional;
    import java.util.List;
    import {pkg}.domain.model.{entity};

    public interface {entity}Repository {{
        {entity} save({entity} aggregate);
        Optional<{entity}> findById({id_type} id);
        List<{entity}> findAll(int page, int size);
        void deleteById({id_type} id);
        boolean existsById({id_type} id);
        long count();
    }}
    """)


def tmpl_jpa_entity(pkg: str, entity: str, id: dict, fields: list[dict], relationships: list[dict], use_lombok: bool = False) -> str:
    used_types = {id["type"]} | {f["type"] for f in fields}
    extra_imports = qualify_imports(sorted(used_types))
    id_ann = "@GeneratedValue(strategy = GenerationType.IDENTITY)" if id["type"] == "Long" and id.get("generated", True) else ""
    all_fields = [id, *fields]
    fields_src = [
        f"    private {f['type']} {f['name']};" if f is id else f"    @Column(nullable = false)\n    private {f['type']} {f['name']};"
        for f in all_fields
    ]

    # Relationship fields and imports
    rel_fields_src = []
    target_imports = []
    need_set = False
    for r in relationships or []:
        rtype = (r.get("type") or "").upper()
        name = r.get("name")
        target = r.get("target")
        if not name or not target or rtype not in {"ONE_TO_ONE", "ONE_TO_MANY", "MANY_TO_MANY"}:
            continue
        target_import = f"import {pkg}.infrastructure.persistence.{target}Entity;"
        if target_import not in target_imports:
            target_imports.append(target_import)
        annotations = []
        if rtype == "ONE_TO_ONE":
            mapped_by = r.get("mappedBy")
            optional = r.get("optional", True)
            if mapped_by:
                annotations.append(f"    @OneToOne(mappedBy = \"{mapped_by}\", fetch = FetchType.LAZY)")
            else:
                annotations.append(f"    @OneToOne(fetch = FetchType.LAZY, optional = {str(optional).lower()})")
                join_col = r.get("joinColumn")
                if join_col:
                    annotations.append(f"    @JoinColumn(name = \"{join_col}\")")
            field_type = f"{target}Entity"
            init = ""
        elif rtype == "ONE_TO_MANY":
            need_set = True
            mapped_by = r.get("mappedBy")
            if mapped_by:
                annotations.append(f"    @OneToMany(mappedBy = \"{mapped_by}\", fetch = FetchType.LAZY)")
            else:
                annotations.append("    @OneToMany(fetch = FetchType.LAZY)")
                join_col = r.get("joinColumn")
                if join_col:
                    annotations.append(f"    @JoinColumn(name = \"{join_col}\")")
            field_type = f"Set<{target}Entity>"
            init = " = new java.util.LinkedHashSet<>()"
        else:  # MANY_TO_MANY
            need_set = True
            annotations.append("    @ManyToMany(fetch = FetchType.LAZY)")
            jt = r.get("joinTable") or {}
            jt_name = jt.get("name")
            join_col = jt.get("joinColumn")
            inv_join_col = jt.get("inverseJoinColumn")
            if jt_name and join_col and inv_join_col:
                annotations.append(
                    f"    @JoinTable(name = \"{jt_name}\", joinColumns = @JoinColumn(name = \"{join_col}\"), inverseJoinColumns = @JoinColumn(name = \"{inv_join_col}\"))"
                )
            field_type = f"Set<{target}Entity>"
            init = " = new java.util.LinkedHashSet<>()"
        rel_fields_src.append("\n".join(annotations + [f"    private {field_type} {name}{init};"]))

    rel_block = ("\n" + os.linesep.join(rel_fields_src)) if rel_fields_src else ""

    lombok_imports = ("\n".join([
        "import lombok.Getter;",
        "import lombok.Setter;",
        "import lombok.NoArgsConstructor;",
        "import lombok.AccessLevel;",
    ]) if use_lombok else "")
    imports_block = "\n".join(filter(None, [JPA_IMPORTS, extra_imports, COLLECTION_IMPORTS if need_set else "", "\n".join(target_imports), lombok_imports]))
    imports_block_indented = indent_block(imports_block)

    rel_getters = os.linesep.join([
        f"        public {('Set<' + r['target'] + 'Entity>' if r['type'].upper() != 'ONE_TO_ONE' else r['target'] + 'Entity')} get{r['name'][0].upper() + r['name'][1:]}() {{ return {r['name']}; }}"
        for r in (relationships or []) if r.get('name') and r.get('target') and r.get('type', '').upper() in {"ONE_TO_ONE", "ONE_TO_MANY", "MANY_TO_MANY"}
    ]) if not use_lombok else ""
    rel_setters = os.linesep.join([
        f"        public void set{r['name'][0].upper() + r['name'][1:]}({('Set<' + r['target'] + 'Entity>' if r['type'].upper() != 'ONE_TO_ONE' else r['target'] + 'Entity')} {r['name']}) {{ this.{r['name']} = {r['name']}; }}"
        for r in (relationships or []) if r.get('name') and r.get('target') and r.get('type', '').upper() in {"ONE_TO_ONE", "ONE_TO_MANY", "MANY_TO_MANY"}
    ]) if not use_lombok else ""

    class_annots = ("\n".join([
        "@Getter",
        "@Setter",
        "@NoArgsConstructor(access = AccessLevel.PROTECTED)",
    ]) + "\n") if use_lombok else ""
    class_annots_block = indent_block(class_annots.strip()) if use_lombok else ""

    fields_getters = os.linesep.join([f"        public {f['type']} {('get' + f['name'][0].upper() + f['name'][1:])}() {{ return {f['name']}; }}" for f in all_fields]) if not use_lombok else ""
    fields_setters = os.linesep.join([f"        public void set{f['name'][0].upper() + f['name'][1:]}({f['type']} {f['name']}) {{ this.{f['name']} = {f['name']}; }}" for f in all_fields]) if not use_lombok else ""

    return dedent(f"""
package {pkg}.infrastructure.persistence;

{imports_block}

@Entity
@Table(name = "{camel_to_snake(entity)}")
{class_annots}public class {entity}Entity {{
    @Id
    {id_ann}
    private {id['type']} {id['name']};
{os.linesep.join(fields_src[1:])}
{rel_block}

        {'' if use_lombok else f'protected {entity}Entity() {{ /* for JPA */ }}'}

        public {entity}Entity({', '.join([f['type'] + ' ' + f['name'] for f in all_fields])}) {{
{os.linesep.join([f"            this.{f['name']} = {f['name']};" for f in all_fields])}
        }}

{fields_getters}
{fields_setters}
{rel_getters}
{rel_setters}
    }}
    """)


def tmpl_spring_data_repo(pkg: str, entity: str, id_type: str) -> str:
    return dedent(f"""
    package {pkg}.infrastructure.persistence;

{indent_block(REPOSITORY_IMPORTS)}

    public interface {entity}JpaRepository extends JpaRepository<{entity}Entity, {id_type}> {{}}
    """)


def tmpl_persistence_adapter(pkg: str, entity: str, id: dict, fields: list[dict], use_lombok: bool = False) -> str:
    return dedent(f"""
package {pkg}.infrastructure.persistence;

import java.util.Optional;
import java.util.List;
import java.util.stream.Collectors;

import {pkg}.domain.model.{entity};
import {pkg}.domain.repository.{entity}Repository;

import org.springframework.stereotype.Component;
{('import lombok.RequiredArgsConstructor;' if use_lombok else '')}

@Component
{('@RequiredArgsConstructor' if use_lombok else '')}
public class {entity}RepositoryAdapter implements {entity}Repository {{

        private final {entity}JpaRepository jpa;

        {'' if use_lombok else f'public {entity}RepositoryAdapter({entity}JpaRepository jpa) {{\n            this.jpa = jpa;\n        }}'}

        @Override
        public {entity} save({entity} aggregate) {{
            {entity}Entity e = toEntity(aggregate);
            e = jpa.save(e);
            return toDomain(e);
        }}

        @Override
        public Optional<{entity}> findById({id['type']} id) {{
            return jpa.findById(id).map(this::toDomain);
        }}

        @Override
        public List<{entity}> findAll(int page, int size) {{
            return jpa.findAll(org.springframework.data.domain.PageRequest.of(page, size))
                    .stream().map(this::toDomain).collect(java.util.stream.Collectors.toList());
        }}

        @Override
        public void deleteById({id['type']} id) {{
            jpa.deleteById(id);
        }}

        @Override
        public boolean existsById({id['type']} id) {{
            return jpa.existsById(id);
        }}

        @Override
        public long count() {{
            return jpa.count();
        }}

        private {entity}Entity toEntity({entity} a) {{
            return new {entity}Entity({', '.join(['a.get' + id['name'][0].upper() + id['name'][1:] + '()'] + ['a.get' + f['name'][0].upper() + f['name'][1:] + '()' for f in fields])});
        }}

        private {entity} toDomain({entity}Entity e) {{
            return {entity}.create({', '.join(['e.get' + id['name'][0].upper() + id['name'][1:] + '()'] + ['e.get' + f['name'][0].upper() + f['name'][1:] + '()' for f in fields])});
        }}
    }}
    """)


def tmpl_application_service(pkg: str, entity: str, id: dict, fields: list[dict], use_lombok: bool = False) -> str:
    lc_entity = lower_first(entity)
    dto_req = f"{entity}Request"
    dto_res = f"{entity}Response"
    params = ", ".join([f"request.{f['name']}()" for f in [id, *fields]])
    update_params = ", ".join([f"request.{f['name']}()" for f in [*fields]])

    return dedent(f"""
    package {pkg}.application.service;

{indent_block(SPRING_IMPORTS)}

    import java.util.List;

    import {pkg}.domain.model.{entity};
    import {pkg}.domain.repository.{entity}Repository;
    import {pkg}.presentation.dto.{dto_req};
    import {pkg}.presentation.dto.{dto_res};

    @Service
    @Transactional
    public class {entity}Service {{

        private final {entity}Repository repository;

        public {entity}Service({entity}Repository repository) {{
            this.repository = repository;
        }}

        public {dto_res} create({dto_req} request) {{
            {entity} {lc_entity} = {entity}.create({params});
            {lc_entity} = repository.save({lc_entity});
            return {dto_res}.from({lc_entity});
        }}

        @Transactional(readOnly = true)
        public {dto_res} get({id['type']} id) {{
            return repository.findById(id).map({dto_res}::from)
                    .orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND));
        }}

        public {dto_res} update({dto_req} request) {{
            // In a real app, load existing aggregate and apply changes
            {entity} updated = {entity}.create(request.{id['name']}(), {update_params});
            updated = repository.save(updated);
            return {dto_res}.from(updated);
        }}

        public void delete({id['type']} id) {{
            repository.deleteById(id);
        }}

        @Transactional(readOnly = true)
        public java.util.List<{dto_res}> list(int page, int size) {{
            return repository.findAll(page, size).stream().map({dto_res}::from).collect(java.util.stream.Collectors.toList());
        }}
    }}
    """)


def tmpl_dto_request(pkg: str, entity: str, id: dict, fields: list[dict]) -> str:
    used_types = {id["type"]} | {f["type"] for f in fields}
    extra_imports = qualify_imports(sorted(used_types))
    comps = ", ".join([f"{f['type']} {f['name']}" for f in [id, *fields]])
    return dedent(f"""
package {pkg}.presentation.dto;

{extra_imports}

public record {entity}Request({comps}) {{ }}
    """)


def tmpl_dto_response(pkg: str, entity: str, id: dict, fields: list[dict]) -> str:
    used_types = {id["type"]} | {f["type"] for f in fields}
    extra_imports = qualify_imports(sorted(used_types))
    comps = ", ".join([f"{f['type']} {f['name']}" for f in [id, *fields]])
    getters = ", ".join([f"aggregate.get{f['name'][0].upper() + f['name'][1:]}()" for f in [id, *fields]])
    return dedent(f"""
    package {pkg}.presentation.dto;

{indent_block(extra_imports)}

    import {pkg}.domain.model.{entity};

    public record {entity}Response({comps}) {{
        public static {entity}Response from({entity} aggregate) {{
            return new {entity}Response({getters});
        }}
    }}
    """)


def tmpl_controller(pkg: str, entity: str, id: dict, use_lombok: bool = False) -> str:
    base = f"/api/{camel_to_snake(entity)}s"  # naive pluralization with 's'
    dto_req = f"{entity}Request"
    dto_res = f"{entity}Response"
    var_name = lower_first(id['name'])
    path_seg = "/{" + var_name + "}"

    return dedent(f"""
    package {pkg}.presentation.rest;

{indent_block(CONTROLLER_IMPORTS)}

    import {pkg}.application.service.{entity}Service;
    import {pkg}.presentation.dto.{dto_req};
    import {pkg}.presentation.dto.{dto_res};

    @RestController
    @RequestMapping("{base}")
    public class {entity}Controller {{

        private final {entity}Service service;

        public {entity}Controller({entity}Service service) {{
            this.service = service;
        }}

        @PostMapping
        public ResponseEntity<{dto_res}> create(@RequestBody @Valid {dto_req} request) {{
            var created = service.create(request);
            return ResponseEntity.status(201).body(created);
        }}

        @GetMapping("{path_seg}")
        public ResponseEntity<{dto_res}> get(@PathVariable {id['type']} {var_name}) {{
            return ResponseEntity.ok(service.get({var_name}));
        }}

        @PutMapping("{path_seg}")
        public ResponseEntity<{dto_res}> update(@PathVariable {id['type']} {var_name},
                                                @RequestBody @Valid {dto_req} request) {{
            // In a real app: ensure path id == request id
            return ResponseEntity.ok(service.update(request));
        }}

        @DeleteMapping("{path_seg}")
        public ResponseEntity<Void> delete(@PathVariable {id['type']} {var_name}) {{
            service.delete({var_name});
            return ResponseEntity.noContent().build();
        }}

        @GetMapping
        public ResponseEntity<java.util.List<{dto_res}>> list(@RequestParam(defaultValue = "0") int page,
                                                               @RequestParam(defaultValue = "20") int size) {{
            return ResponseEntity.ok(service.list(page, size));
        }}
    }}
    """)


# ------------------------- Main -------------------------

def main():
    parser = argparse.ArgumentParser(description="Generate Spring Boot CRUD boilerplate from entity spec")
    parser.add_argument("--spec", required=True, help="Path to entity spec (JSON or YAML)")
    parser.add_argument("--package", required=True, help="Base package, e.g., com.example.product")
    parser.add_argument("--output", default="./generated", help="Output root directory")
    parser.add_argument("--lombok", action="store_true", help="Use Lombok annotations for getters/setters where applicable")
    parser.add_argument("--templates-dir", help="Directory with override templates (*.tpl). If omitted, auto-detects ../templates relative to this script if present.")
    args = parser.parse_args()

    spec = load_spec(args.spec)

    entity = spec.get("entity")
    if not entity or not re.match(r"^[A-Z][A-Za-z0-9_]*$", entity):
        raise SystemExit("Spec 'entity' must be a PascalCase identifier, e.g., 'Product'")

    id_spec = spec.get("id") or {"name": "id", "type": "Long", "generated": True}
    if id_spec["type"] not in SUPPORTED_SIMPLE_TYPES:
        raise SystemExit(f"Unsupported id type: {id_spec['type']}")

    fields = spec.get("fields", [])
    relationships = spec.get("relationships", [])
    for f in fields:
        if f["type"] not in SUPPORTED_SIMPLE_TYPES:
            raise SystemExit(f"Unsupported field type: {f['name']} -> {f['type']}")

    feature_name = entity.lower()
    base_pkg = args.package

    out_root = os.path.abspath(args.output)
    java_root = os.path.join(out_root, "src/main/java", base_pkg.replace(".", "/"))

    # Paths
    paths = {
        "domain_model": os.path.join(java_root, "domain/model", f"{entity}.java"),
        "domain_repo": os.path.join(java_root, "domain/repository", f"{entity}Repository.java"),
        "domain_service": os.path.join(java_root, "domain/service", f"{entity}Service.java"),
        "jpa_entity": os.path.join(java_root, "infrastructure/persistence", f"{entity}Entity.java"),
        "spring_data_repo": os.path.join(java_root, "infrastructure/persistence", f"{entity}JpaRepository.java"),
        "persistence_adapter": os.path.join(java_root, "infrastructure/persistence", f"{entity}RepositoryAdapter.java"),
        "app_service_create": os.path.join(java_root, "application/service", f"Create{entity}Service.java"),
        "app_service_get": os.path.join(java_root, "application/service", f"Get{entity}Service.java"),
        "app_service_update": os.path.join(java_root, "application/service", f"Update{entity}Service.java"),
        "app_service_delete": os.path.join(java_root, "application/service", f"Delete{entity}Service.java"),
        "app_service_list": os.path.join(java_root, "application/service", f"List{entity}Service.java"),
        "dto_req": os.path.join(java_root, "presentation/dto", f"{entity}Request.java"),
        "dto_res": os.path.join(java_root, "presentation/dto", f"{entity}Response.java"),
        "controller": os.path.join(java_root, "presentation/rest", f"{entity}Controller.java"),
        "ex_not_found": os.path.join(java_root, "application/exception", f"{entity}NotFoundException.java"),
        "ex_exist": os.path.join(java_root, "application/exception", f"{entity}ExistException.java"),
        "entity_exception_handler": os.path.join(java_root, "presentation/rest", f"{entity}ExceptionHandler.java"),
    }

    # Resolve templates directory (required; no fallback to built-ins)
    script_dir = os.path.dirname(os.path.abspath(__file__))
    default_templates_dir = os.path.normpath(os.path.join(script_dir, "..", "templates"))
    templates_dir = args.templates_dir or (default_templates_dir if os.path.isdir(default_templates_dir) else None)
    if not templates_dir or not os.path.isdir(templates_dir):
        raise SystemExit("Templates directory not found. Provide --templates-dir or create: " + default_templates_dir)

    required_templates = [
        "DomainModel.java.tpl",
        "DomainRepository.java.tpl",
        "DomainService.java.tpl",
        "JpaEntity.java.tpl",
        "SpringDataRepository.java.tpl",
        "PersistenceAdapter.java.tpl",
        "CreateService.java.tpl",
        "GetService.java.tpl",
        "UpdateService.java.tpl",
        "DeleteService.java.tpl",
        "ListService.java.tpl",
        "DtoRequest.java.tpl",
        "DtoResponse.java.tpl",
        "Controller.java.tpl",
        "NotFoundException.java.tpl",
        "ExistException.java.tpl",
        "EntityExceptionHandler.java.tpl",
    ]
    missing = [name for name in required_templates if load_template_text(templates_dir, name) is None]
    if missing:
        raise SystemExit("Missing required templates: " + ", ".join(missing) + " in " + templates_dir)

    # Build dynamic code fragments for templates
    all_fields = [id_spec, *fields]
    used_types = {f["type"] for f in all_fields}
    extra_imports = qualify_imports(sorted(used_types))

    def cap(s: str) -> str:
        return s[:1].upper() + s[1:] if s else s

    # Domain fragments
    final_kw = "" if args.lombok else "final "
    domain_fields_decls = "\n".join([f"    private {final_kw}{f['type']} {f['name']};" for f in all_fields])
    domain_ctor_params = ", ".join([f"{f['type']} {f['name']}" for f in all_fields])
    domain_assigns = "\n".join([f"        this.{f['name']} = {f['name']};" for f in all_fields])
    domain_getters = ("\n".join([f"    public {f['type']} get{cap(f['name'])}() {{ return {f['name']}; }}" for f in all_fields]) if not args.lombok else "")
    model_constructor_block = (f"    private {entity}({domain_ctor_params}) {{\n{domain_assigns}\n    }}" if not args.lombok else "")
    all_names_csv = ", ".join([f["name"] for f in all_fields])

    # JPA fragments
    def _jpa_field_decl(f: dict) -> str:
        if f is id_spec:
            lines = ["    @Id"]
            if bool(id_spec.get("generated", False)) and id_spec["type"] in ("Long", "Integer"):
                lines.append("    @GeneratedValue(strategy = GenerationType.IDENTITY)")
            lines.append(f"    private {f['type']} {f['name']};")
            return "\n".join(lines)
        return f"    @Column(nullable = false)\n    private {f['type']} {f['name']};"
    jpa_fields_decls = "\n".join([_jpa_field_decl(f) for f in all_fields])
    jpa_ctor_params = domain_ctor_params
    jpa_assigns = "\n".join([f"        this.{f['name']} = {f['name']};" for f in all_fields])
    jpa_getters_setters = "\n".join([
        f"    public {f['type']} get{cap(f['name'])}() {{ return {f['name']}; }}\n    public void set{cap(f['name'])}({f['type']} {f['name']}) {{ this.{f['name']} = {f['name']}; }}" for f in all_fields
    ])

    # DTO components
    id_generated = bool(id_spec.get("generated", False))
    dto_response_components = ", ".join([f"{f['type']} {f['name']}" for f in all_fields])
    dto_request_fields = (fields if id_generated else all_fields)
    dto_request_components = ", ".join([f"{f['type']} {f['name']}" for f in dto_request_fields])

    # Mapping fragments
    adapter_to_entity_args = ", ".join([f"a.get{cap(f['name'])}()" for f in all_fields])
    adapter_to_domain_args = ", ".join([f"e.get{cap(f['name'])}()" for f in all_fields])
    if id_generated:
        request_all_args = ", ".join(["null", *[f"request.{f['name']}()" for f in fields]])
    else:
        request_all_args = ", ".join([f"request.{id_spec['name']}()", *[f"request.{f['name']}()" for f in fields]])
    response_from_agg_args = ", ".join([f"agg.get{cap(f['name'])}()" for f in all_fields])
    list_map_response_args = ", ".join([f"a.get{cap(f['name'])}()" for f in all_fields])
    update_create_args = ", ".join([id_spec["name"], *[f"request.{f['name']}()" for f in fields]])
    mapper_create_args = ", ".join(["id", *[f"request.{f['name']}()" for f in fields]])
    create_id_arg = ("null" if id_generated else f"request.{id_spec['name']}()")

    table_name = camel_to_snake(entity)
    base_path = f"/api/{table_name}"

    # Common placeholders for external templates
    # Lombok-related placeholders
    # Domain model should only have @Getter for DDD immutability
    lombok_domain_imports = "import lombok.Getter;" if args.lombok else ""
    lombok_domain_annotations = "@Getter" if args.lombok else ""
    lombok_domain_annotations_block = ("\n" + lombok_domain_annotations) if lombok_domain_annotations else ""

    lombok_model_imports = "import lombok.Getter;\nimport lombok.Setter;\nimport lombok.AllArgsConstructor;" if args.lombok else ""
    lombok_common_imports = "import lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;" if args.lombok else ""
    model_annotations = "@Getter\n@Setter\n@AllArgsConstructor" if args.lombok else ""
    service_annotations = "@RequiredArgsConstructor\n@Slf4j" if args.lombok else ""
    controller_annotations = "@RequiredArgsConstructor\n@Slf4j" if args.lombok else ""
    adapter_annotations = "@RequiredArgsConstructor\n@Slf4j" if args.lombok else ""
    # annotation blocks that include a leading newline when present to avoid empty lines
    service_annotations_block = ("\n" + service_annotations) if service_annotations else ""
    controller_annotations_block = ("\n" + controller_annotations) if controller_annotations else ""
    adapter_annotations_block = ("\n" + adapter_annotations) if adapter_annotations else ""
    model_annotations_block = ("\n" + model_annotations) if model_annotations else ""

    

    # Common placeholders for external templates
    placeholders = {
        "entity": entity,
        "Entity": entity,
        "EntityRequest": f"{entity}Request",
        "EntityResponse": f"{entity}Response",
        "entity_lower": lower_first(entity),
        "package": base_pkg,
        "Package": base_pkg.replace(".", "/"),
        "table_name": table_name,
        "base_path": base_path,
        "id_type": id_spec["type"],
        "id_name": id_spec["name"],
        "id_name_lower": lower_first(id_spec["name"]),
        "id_generated": str(id_generated).lower(),
        "fields": fields,
        "all_fields": all_fields,
        "extra_imports": extra_imports,
        "final_kw": final_kw,
        "domain_fields_decls": domain_fields_decls,
        "domain_ctor_params": domain_ctor_params,
        "domain_assigns": domain_assigns,
        "domain_getters": domain_getters,
        "model_constructor_block": model_constructor_block,
        "all_names_csv": all_names_csv,
        "jpa_fields_decls": jpa_fields_decls,
        "jpa_ctor_params": jpa_ctor_params,
        "jpa_assigns": jpa_assigns,
        "jpa_getters_setters": jpa_getters_setters,
        "dto_response_components": dto_response_components,
        "dto_request_components": dto_request_components,
        "adapter_to_entity_args": adapter_to_entity_args,
        "adapter_to_domain_args": adapter_to_domain_args,
        "request_all_args": request_all_args,
        "response_from_agg_args": response_from_agg_args,
        "list_map_response_args": list_map_response_args,
        "update_create_args": update_create_args,
        "mapper_create_args": mapper_create_args,
        "create_id_arg": create_id_arg,
        # Domain-specific Lombok placeholders (DDD-compliant)
        "lombok_domain_imports": lombok_domain_imports,
        "lombok_domain_annotations_block": lombok_domain_annotations_block,
        # Infrastructure/infrastructure Lombok placeholders
        "lombok_model_imports": lombok_model_imports,
        "lombok_common_imports": lombok_common_imports,
        "model_annotations": model_annotations,
        "service_annotations": service_annotations,
        "controller_annotations": controller_annotations,
        "adapter_annotations": adapter_annotations,
        "service_annotations_block": service_annotations_block,
        "controller_annotations_block": controller_annotations_block,
        "adapter_annotations_block": adapter_annotations_block,
        "model_annotations_block": model_annotations_block,
        # Constructor placeholders
        "controller_constructor": "",
        "adapter_constructor": "",
        "create_constructor": "",
        "update_constructor": "",
        "get_constructor": "",
        "list_constructor": "",
        "delete_constructor": "",
        "domain_service_constructor": "",
    }

    def _render(name, placeholders_dict):
        c = render_template_file(templates_dir, name, placeholders_dict)
        if c is None: raise SystemExit(f"Template render failed: {name}")
        c = (c.replace("$controller_constructor", placeholders_dict.get("controller_constructor", ""))
               .replace("$adapter_constructor", placeholders_dict.get("adapter_constructor", ""))
               .replace("$create_constructor", placeholders_dict.get("create_constructor", ""))
               .replace("$update_constructor", placeholders_dict.get("update_constructor", ""))
               .replace("$get_constructor", placeholders_dict.get("get_constructor", ""))
               .replace("$list_constructor", placeholders_dict.get("list_constructor", ""))
               .replace("$delete_constructor", placeholders_dict.get("delete_constructor", ""))
               .replace("$domain_service_constructor", placeholders_dict.get("domain_service_constructor", "")))
        return c

    # Write files (templates only, fail on error)
    content = _render("DomainModel.java.tpl", placeholders)
    write_file(paths["domain_model"], content)

    content = _render("DomainRepository.java.tpl", placeholders)
    write_file(paths["domain_repo"], content)

    content = _render("DomainService.java.tpl", placeholders)
    write_file(paths["domain_service"], content)

    content = _render("JpaEntity.java.tpl", placeholders)
    write_file(paths["jpa_entity"], content)

    content = _render("SpringDataRepository.java.tpl", placeholders)
    write_file(paths["spring_data_repo"], content)

    content = _render("PersistenceAdapter.java.tpl", placeholders)
    write_file(paths["persistence_adapter"], content)

    content = _render("CreateService.java.tpl", placeholders)
    write_file(paths["app_service_create"], content)

    content = _render("GetService.java.tpl", placeholders)
    write_file(paths["app_service_get"], content)

    content = _render("UpdateService.java.tpl", placeholders)
    write_file(paths["app_service_update"], content)

    content = _render("DeleteService.java.tpl", placeholders)
    write_file(paths["app_service_delete"], content)

    content = _render("ListService.java.tpl", placeholders)
    write_file(paths["app_service_list"], content)

    content = _render("DtoRequest.java.tpl", placeholders)
    write_file(paths["dto_req"], content)

    content = _render("DtoResponse.java.tpl", placeholders)
    write_file(paths["dto_res"], content)

    content = _render("Controller.java.tpl", placeholders)
    write_file(paths["controller"], content)

    # Exceptions
    content = _render("NotFoundException.java.tpl", placeholders)
    write_file(paths["ex_not_found"], content)

    content = _render("ExistException.java.tpl", placeholders)
    write_file(paths["ex_exist"], content)

    content = _render("EntityExceptionHandler.java.tpl", placeholders)
    write_file(paths["entity_exception_handler"], content)

    # Helpful README
    readme = dedent(f"""
    # Generated CRUD Feature: {entity}

    Base package: {base_pkg}

    Structure:
    - domain/model/{entity}.java
    - domain/repository/{entity}Repository.java
    - infrastructure/persistence/{entity}Entity.java
    - infrastructure/persistence/{entity}JpaRepository.java
    - infrastructure/persistence/{entity}RepositoryAdapter.java
    - application/service/Create{entity}Service.java
    - application/service/Get{entity}Service.java
    - application/service/Update{entity}Service.java
    - application/service/Delete{entity}Service.java
    - application/service/List{entity}Service.java
    - presentation/dto/{entity}Request.java
    - presentation/dto/{entity}Response.java
    - presentation/rest/{entity}Controller.java

    Next steps:
    - Add validation and invariants in domain aggregate
    - Secure endpoints and add tests (unit + @DataJpaTest + Testcontainers)
    - Wire into your Spring Boot app (component scan should pick up beans)
    """)
    write_file(os.path.join(out_root, "README-GENERATED.md"), readme)

    print(f"CRUD boilerplate generated under: {out_root}")



if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

plugins

developer-kit-java

skills

README.md

tile.json