Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:28:34 +08:00
commit 390afca02b
220 changed files with 86013 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
package $package.presentation.rest;
$lombok_common_imports
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import $package.application.service.Create${entity}Service;
import $package.application.service.Get${entity}Service;
import $package.application.service.Update${entity}Service;
import $package.application.service.Delete${entity}Service;
import $package.application.service.List${entity}Service;
import $package.presentation.dto.$EntityRequest;
import $package.presentation.dto.$EntityResponse;
import $package.presentation.dto.PageResponse;
import org.springframework.data.domain.Pageable;
@RestController$controller_annotations_block
@RequestMapping("$base_path")
public class ${entity}Controller {
private final Create${entity}Service createService;
private final Get${entity}Service getService;
private final Update${entity}Service updateService;
private final Delete${entity}Service deleteService;
private final List${entity}Service listService;
$controller_constructor
@PostMapping
public ResponseEntity<$EntityResponse> create(@RequestBody @Valid $EntityRequest request) {
$EntityResponse created = createService.create(request);
return ResponseEntity.status(HttpStatus.CREATED)
.header("Location", "$base_path/" + created.$id_name())
.body(created);
}
@GetMapping("/{${id_name_lower}}")
public ResponseEntity<$EntityResponse> get(@PathVariable $id_type ${id_name_lower}) {
return ResponseEntity.ok(getService.get(${id_name_lower}));
}
@PutMapping("/{${id_name_lower}}")
public ResponseEntity<$EntityResponse> update(@PathVariable $id_type ${id_name_lower},
@RequestBody @Valid $EntityRequest request) {
return ResponseEntity.ok(updateService.update(${id_name_lower}, request));
}
@DeleteMapping("/{${id_name_lower}}")
public ResponseEntity<Void> delete(@PathVariable $id_type ${id_name_lower}) {
deleteService.delete(${id_name_lower});
return ResponseEntity.noContent().build();
}
@GetMapping
public ResponseEntity<PageResponse<$EntityResponse>> list(Pageable pageable) {
return ResponseEntity.ok(listService.list(pageable.getPageNumber(), pageable.getPageSize()));
}
}

View File

@@ -0,0 +1,32 @@
package $package.application.service;
$lombok_common_imports
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import $package.domain.model.$entity;
import $package.domain.service.${entity}Service;
import $package.application.mapper.${entity}Mapper;
import $package.application.exception.${entity}ExistException;
import org.springframework.dao.DataIntegrityViolationException;
import $package.presentation.dto.$EntityRequest;
import $package.presentation.dto.$EntityResponse;
@Service$service_annotations_block
@Transactional
public class Create${entity}Service {
private final ${entity}Service ${entity_lower}Service;
private final ${entity}Mapper mapper;
$create_constructor
public $EntityResponse create($EntityRequest request) {
try {
$entity agg = mapper.toAggregate($create_id_arg, request);
agg = ${entity_lower}Service.save(agg);
return mapper.toResponse(agg);
} catch (DataIntegrityViolationException ex) {
throw new ${entity}ExistException("Duplicate $entity");
}
}
}

View File

@@ -0,0 +1,23 @@
package $package.application.service;
$lombok_common_imports
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import $package.domain.service.${entity}Service;
import $package.application.exception.${entity}NotFoundException;
@Service$service_annotations_block
@Transactional
public class Delete${entity}Service {
private final ${entity}Service ${entity_lower}Service;
$delete_constructor
public void delete($id_type $id_name) {
if (!${entity_lower}Service.existsById($id_name)) {
throw new ${entity}NotFoundException($id_name);
}
${entity_lower}Service.deleteById($id_name);
}
}

View File

@@ -0,0 +1,20 @@
package $package.domain.model;
$lombok_domain_imports
$extra_imports
$lombok_domain_annotations_block
public class $entity {
$domain_fields_decls
private $entity($domain_ctor_params) {
$domain_assigns
}
public static $entity create($domain_ctor_params) {
// TODO: add invariant checks
return new $entity($all_names_csv);
}
$domain_getters
}

View File

@@ -0,0 +1,14 @@
package $package.domain.repository;
import java.util.Optional;
import java.util.List;
import $package.domain.model.$entity;
public interface ${entity}Repository {
$entity save($entity aggregate);
Optional<$entity> findById($id_type $id_name);
List<$entity> findAll(int page, int size);
void deleteById($id_type $id_name);
boolean existsById($id_type $id_name);
long count();
}

View File

@@ -0,0 +1,42 @@
package $package.domain.service;
import java.util.List;
import java.util.Optional;
$lombok_common_imports
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import $package.domain.model.$entity;
import $package.domain.repository.${entity}Repository;
@Service$service_annotations_block
@Transactional
public class ${entity}Service {
private final ${entity}Repository repository;
$domain_service_constructor
public $entity save($entity aggregate) {
return repository.save(aggregate);
}
public Optional<$entity> findById($id_type $id_name) {
return repository.findById($id_name);
}
public List<$entity> findAll(int page, int size) {
return repository.findAll(page, size);
}
public void deleteById($id_type $id_name) {
repository.deleteById($id_name);
}
public boolean existsById($id_type $id_name) {
return repository.existsById($id_name);
}
public long count() {
return repository.count();
}
}

View File

@@ -0,0 +1,5 @@
package $package.presentation.dto;
$extra_imports
public record $EntityRequest($dto_request_components) { }

View File

@@ -0,0 +1,5 @@
package $package.presentation.dto;
$extra_imports
public record $EntityResponse($dto_response_components) { }

View File

@@ -0,0 +1,35 @@
package $package.presentation.rest;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import $package.application.exception.${entity}NotFoundException;
import $package.application.exception.${entity}ExistException;
import $package.presentation.dto.ErrorResponse;
@RestControllerAdvice
public class ${entity}ExceptionHandler {
@ExceptionHandler(${entity}NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(${entity}NotFoundException ex, org.springframework.web.context.request.WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"Not Found",
ex.getMessage(),
request.getDescription(false).replaceFirst("uri=", "")
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(${entity}ExistException.class)
public ResponseEntity<ErrorResponse> handleExist(${entity}ExistException ex, org.springframework.web.context.request.WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.CONFLICT.value(),
"Conflict",
ex.getMessage(),
request.getDescription(false).replaceFirst("uri=", "")
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
}

View File

@@ -0,0 +1,8 @@
package $package.presentation.dto;
public record ErrorResponse(
int status,
String error,
String message,
String path
) { }

View File

@@ -0,0 +1,10 @@
package $package.application.exception;
public class ${entity}ExistException extends RuntimeException {
public ${entity}ExistException(String message) {
super(message);
}
public ${entity}ExistException($id_type $id_name) {
super("$entity already exists: " + $id_name);
}
}

View File

@@ -0,0 +1,25 @@
package $package.application.service;
$lombok_common_imports
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import $package.domain.service.${entity}Service;
import $package.application.mapper.${entity}Mapper;
import $package.application.exception.${entity}NotFoundException;
import $package.presentation.dto.$EntityResponse;
@Service$service_annotations_block
@Transactional(readOnly = true)
public class Get${entity}Service {
private final ${entity}Service ${entity_lower}Service;
private final ${entity}Mapper mapper;
$get_constructor
public $EntityResponse get($id_type $id_name) {
return ${entity_lower}Service.findById($id_name)
.map(mapper::toResponse)
.orElseThrow(() -> new ${entity}NotFoundException($id_name));
}
}

View File

@@ -0,0 +1,55 @@
package $package.presentation.rest;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.dao.DataIntegrityViolationException;
import $package.presentation.dto.ErrorResponse;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex, org.springframework.web.context.request.WebRequest request) {
String errors = ex.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(java.util.stream.Collectors.joining(", "));
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation Error",
"Validation failed: " + errors,
request.getDescription(false).replaceFirst("uri=", "")
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<ErrorResponse> handleResponseStatusException(
ResponseStatusException ex, org.springframework.web.context.request.WebRequest request) {
ErrorResponse error = new ErrorResponse(
ex.getStatusCode().value(),
ex.getStatusCode().toString(),
ex.getReason(),
request.getDescription(false).replaceFirst("uri=", "")
);
return new ResponseEntity<>(error, ex.getStatusCode());
}
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ErrorResponse> handleDataIntegrityViolation(
DataIntegrityViolationException ex, org.springframework.web.context.request.WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.CONFLICT.value(),
"Conflict",
ex.getMostSpecificCause() != null ? ex.getMostSpecificCause().getMessage() : ex.getMessage(),
request.getDescription(false).replaceFirst("uri=", "")
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
}

View File

@@ -0,0 +1,25 @@
package $package.infrastructure.persistence;
import jakarta.persistence.*;
$extra_imports
$lombok_model_imports
import lombok.NoArgsConstructor;
@Entity
@Table(name = "$table_name")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ${entity}Entity {
$jpa_fields_decls
protected ${entity}Entity() { /* for JPA */ }
// Full constructor (optional, can be removed if not needed)
public ${entity}Entity($jpa_ctor_params) {
$jpa_assigns
}
// Lombok generates getters and setters automatically
}

View File

@@ -0,0 +1,31 @@
package $package.application.service;
$lombok_common_imports
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import $package.domain.service.${entity}Service;
import $package.application.mapper.${entity}Mapper;
import $package.presentation.dto.$EntityResponse;
import $package.presentation.dto.PageResponse;
import java.util.List;
import java.util.stream.Collectors;
@Service$service_annotations_block
@Transactional(readOnly = true)
public class List${entity}Service {
private final ${entity}Service ${entity_lower}Service;
private final ${entity}Mapper mapper;
$list_constructor
public PageResponse<$EntityResponse> list(int page, int size) {
List<$EntityResponse> content = ${entity_lower}Service.findAll(page, size)
.stream()
.map(mapper::toResponse)
.collect(Collectors.toList());
long total = ${entity_lower}Service.count();
int totalPages = (int) Math.ceil(total / (double) size);
return new PageResponse<>(content, page, size, total, totalPages);
}
}

View File

@@ -0,0 +1,16 @@
package $package.application.mapper;
import $package.domain.model.$entity;
import $package.presentation.dto.$dto_request;
import $package.presentation.dto.$dto_response;
public class ${entity}Mapper {
public $entity toAggregate($id_type id, $dto_request request) {
return $entity.create($mapper_create_args);
}
public $dto_response toResponse($entity a) {
return new $dto_response($list_map_response_args);
}
}

View File

@@ -0,0 +1,7 @@
package $package.application.exception;
public class ${entity}NotFoundException extends RuntimeException {
public ${entity}NotFoundException($id_type $id_name) {
super("$entity not found: " + $id_name);
}
}

View File

@@ -0,0 +1,9 @@
package $package.presentation.dto;
public record PageResponse<T>(
java.util.List<T> content,
int page,
int size,
long totalElements,
int totalPages
) { }

View File

@@ -0,0 +1,54 @@
package $package.infrastructure.persistence;
$lombok_common_imports
import java.util.Optional;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.domain.PageRequest;
import $package.domain.model.$entity;
import $package.domain.repository.${entity}Repository;
import org.springframework.stereotype.Component;
@Component$adapter_annotations_block
public class ${entity}RepositoryAdapter implements ${entity}Repository {
private final ${entity}JpaRepository jpa;
$adapter_constructor
@Override
public $entity save($entity a) {
${entity}Entity e = new ${entity}Entity($adapter_to_entity_args);
e = jpa.save(e);
return $entity.create($adapter_to_domain_args);
}
@Override
public Optional<$entity> findById($id_type $id_name) {
return jpa.findById($id_name).map(e -> $entity.create($adapter_to_domain_args));
}
@Override
public List<$entity> findAll(int page, int size) {
return jpa.findAll(PageRequest.of(page, size))
.stream()
.map(e -> $entity.create($adapter_to_domain_args))
.collect(Collectors.toList());
}
@Override
public void deleteById($id_type $id_name) {
jpa.deleteById($id_name);
}
@Override
public boolean existsById($id_type $id_name) {
return jpa.existsById($id_name);
}
@Override
public long count() {
return jpa.count();
}
}

View File

@@ -0,0 +1,39 @@
# CRUD Generator Templates
You must provide all templates (.tpl) required by the generator; there is no fallback.
How it works:
- The generator loads .tpl files from this directory (or a directory passed via --templates-dir).
- It uses Python string.Template placeholders (e.g., $package, $entity, $id_type, $id_name, $id_name_lower, $base_path, $dto_request, $dto_response).
- If any template is missing or fails to render, generation fails.
Required template filenames:
- DomainModel.java.tpl
- DomainRepository.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
- Mapper.java.tpl
- DtoRequest.java.tpl
- DtoResponse.java.tpl
- PageResponse.java.tpl
- ErrorResponse.java.tpl
- Controller.java.tpl
- GlobalExceptionHandler.java.tpl
- EntityExceptionHandler.java.tpl
- NotFoundException.java.tpl
- ExistException.java.tpl
Tip: Start simple and expand over time; these files are your teams baseline.
Conventions:
- Base path is versioned: /v1/{resources}
- POST returns 201 Created and sets Location: /v1/{resources}/{id}
- GET collection supports pagination via Pageable in controller and returns PageResponse<T>
- Application layer uses ${Entity}Mapper for DTO↔Domain and throws ${Entity}ExistException on duplicates
- Exceptions are mapped by GlobalExceptionHandler and ${Entity}ExceptionHandler

View File

@@ -0,0 +1,5 @@
package $package.infrastructure.persistence;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ${entity}JpaRepository extends JpaRepository<${entity}Entity, $id_type> { }

View File

@@ -0,0 +1,32 @@
package $package.application.service;
$lombok_common_imports
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import $package.domain.model.$entity;
import $package.domain.service.${entity}Service;
import $package.application.mapper.${entity}Mapper;
import $package.application.exception.${entity}ExistException;
import org.springframework.dao.DataIntegrityViolationException;
import $package.presentation.dto.$EntityRequest;
import $package.presentation.dto.$EntityResponse;
@Service$service_annotations_block
@Transactional
public class Update${entity}Service {
private final ${entity}Service ${entity_lower}Service;
private final ${entity}Mapper mapper;
$update_constructor
public $EntityResponse update($id_type $id_name, $EntityRequest request) {
try {
$entity agg = mapper.toAggregate($id_name, request);
agg = ${entity_lower}Service.save(agg);
return mapper.toResponse(agg);
} catch (DataIntegrityViolationException ex) {
throw new ${entity}ExistException("Duplicate $entity");
}
}
}