Targets
One entity in the YAML (PurchaseOrder) makes several files. Each kind of file has a name, and that
name is its target. A rule chooses the file it contributes to with @Rule(target = "..."). Without
it, the target is entity.
Which file is which target is the first word of each line of the generation log:
dto src/main/java/io/elasticore/sample/petstore/application/dto/CustomerDTO.java WROTE
srchdto src/main/java/io/elasticore/sample/petstore/application/dto/CustomerSrchDTO.java WROTE
YAML files, targets and method shapes
| YAML | File made | target | Rule method | What the rule reads |
|---|---|---|---|---|
entity.yml | Customer.java | entity (default) | (EntitySpec e), (EntitySpec e, FieldSpec f) | the entity, its fields |
entity.yml, dto.yml | CustomerDTO.java, ItemDTO.java | dto | (EntitySpec dto, FieldSpec f) | the DTO name, its fields |
a field's @search(...) | CustomerSrchDTO.java | srchdto | (EntitySpec s, FieldSpec f) | the search fields |
repository.yml | PurchaseOrderRepository.java | repository | (EntitySpec e) | e.queries() |
port.yml | CreditCheckPort.java | port | (PortSpec p) | p.operations() |
enum.yml | OrderStatus.java | enum | (EnumSpec en) | en.constants() |
A DTO from dto.yml also comes in as an EntitySpec. Read its name and fields the same way as an
entity's.
Who manages the file
Rules apply every time to the files the generator writes. A file the developer fills in is generated only once and belongs to the developer from then on, so rules apply only when it is first created.
| File | Whose | If a rule is added later |
|---|---|---|
entity, dto, srchdto, enum, repository, port, mapper, XxxCoreService, ports and use cases | the generator's | applied on the next generation |
The developer class of a split root (PurchaseOrder.java, target behavior) | the developer's | the generation log points out missing class annotations - add them by hand |
control, XxxService (service_extend), factory, domain services | the developer's | no notice. A file never changed is rewritten |
If you edit a generator's file by hand, the next generation skips it and tells you (-Pforce
overwrites it). Do what you want changed with a rule.
Extra files in DDD and hexagonal
| target | Example (pet store) | Whose |
|---|---|---|
entity | Customer.java; for a split root, PurchaseOrderBase.java | the generator's |
behavior | PurchaseOrder.java | the developer's |
event | PurchaseOrderPlacedEvent.java | the generator's |
repositoryPort, queryPort, usecase | CustomerRepositoryPort.java, CustomerUseCase.java | the generator's |
invariants, specifications, repositoryAdapter | CustomerRepositoryAdapter.java … | the generator's |
service / service_extend | CustomerCoreService.java / CustomerService.java | the generator's / the developer's |
control | CustomerController.java | the developer's |
mapper, apiErrors | PetstoreMapper.java, PetstoreApiErrors.java | the generator's / the developer's (per domain) |
A team rule with target = "entity" applies to both files of a split root. You do not have to care
whether a root is split. ./gradlew elcore3 -PlistRules shows the full list for your project.
Rules for domain files, made once per file (
once), take(DomainSpec d). Written as(EntitySpec e), they do not run, and a warning is printed.
dto.yml - hiding internal fields from the API docs
ItemDTO:
meta: dto @template(Item)
fields:
productName: string @ref(product.name) @internal # our marker
/** dto.yml: a field marked @internal is left out of the API docs. */
@Rule(value = "ps-internal", target = "dto")
void internal(EntitySpec dto, FieldSpec f) {
if (f.hasDsl("internal")) {
ctx(dto).field(f).merge(AnnoCode.of("io.swagger.v3.oas.annotations.media.Schema")
.val("hidden", true));
}
}
ElastiCORE already puts @Schema(description = ...) on DTO fields. ensure does nothing when the same
annotation is there (checked by a test), so adding an attribute takes merge. ItemDTO.java:
@Schema(description = "productName", hidden = true) // the rule adds hidden
private String productName;
repository.yml - no SELECT *, read-only queries
repository:
PurchaseOrder:
methods:
- id: summarizeByStatus
return: List<Object[]>
query: "SELECT o.status, COUNT(o) FROM PurchaseOrder o GROUP BY o.status"
/** repository.yml: a query that only reads runs read-only; "SELECT *" is refused. */
@Rule(value = "ps-queries", target = "repository")
void queries(EntitySpec e) {
for (QuerySpec query : e.queries()) {
if (query.sql() == null) continue; // Spring Data derives it from the method name
if (query.sql().toLowerCase().replaceAll("\\s+", " ").contains("select *")) {
ModelCheck.fail(e.getName() + "." + query.name() + ": name the columns instead of SELECT *");
}
if (query.isSelect()) {
ctx(e).method(query.name()).ensure(
AnnoCode.of("org.springframework.transaction.annotation.Transactional").val("readOnly", true));
}
}
}
@Query(nativeQuery=false, value=" SELECT o.status, COUNT(o) FROM PurchaseOrder o GROUP BY o.status")
@Transactional(readOnly = true) // added by the rule
List<Object[]> summarizeByStatus();
ctx(e).method("name") annotates every method with that name. For a name that does not exist, the
file is left as it is and the generation log says so:
[rule] method 'summarizeByState' is not declared in PurchaseOrderRepository (repository file); its annotations were not written
port.yml - marking external calls
port:
CreditCheckPort:
meta: http @url("https://credit.example.com/api")
methods:
checkCredit:
meta: method @get("/partners/{bizNo}/credit")
params:
bizNo: string
return: CreditResultDTO
/** port.yml: every HTTP call is marked with the system and the endpoint it reaches. */
@Rule(value = "acme-external-call", target = "port")
void externalCall(PortSpec port) {
ctx(port).ensureClassAnnotation(AnnoCode.of("com.acme.support.ExternalSystem").value(port.getName()));
for (PortSpec.Operation op : port.operations()) {
if (op.url() != null) {
ctx(port).method(op.name()).ensure(AnnoCode.of("com.acme.support.ExternalCall")
.value(op.httpMethod() + " " + op.url()));
}
}
}
@ExternalService(protocol="http", id="erp.CreditCheckPort", url="https://credit.example.com/api", ...)
@ExternalSystem("CreditCheckPort") // the rule
public interface CreditCheckPort {
@HttpEndpoint(url="/partners/{bizNo}/credit", method="GET", ...)
@ExternalCall("GET /partners/{bizNo}/credit") // the rule
CreditResultDTO checkCredit(String bizNo);
}
@ExternalSystem and @ExternalCall are annotations written in the application itself. The rule only
needs the annotation's name; the class does not have to be on the generator's classpath.
enum.yml - each enum once in the API docs
/** enum.yml: every enum is described once in the API docs and referenced from there. */
@Rule(value = "ps-enum-ref", target = "enum")
void enumRef(EnumSpec enumeration) {
ctx(enumeration).ensureClassAnnotation(AnnoCode.of("io.swagger.v3.oas.annotations.media.Schema")
.val("enumAsRef", true));
}
The pet store's OrderStatus.java:
@Schema(enumAsRef = true) // ps-enum-ref
@Getter
public enum OrderStatus {
An enum rule is called once per enum (once); there is no field shape (EnumSpec, FieldSpec). Read
the values with enumeration.constants() (c.name() → "PLACED", c.values() → that value's columns).