Annotations and methods
Most of what a rule does to generated code is "add or remove something somewhere". All of it is said to
the file being made (SourceCode), which ctx(spec) returns. Sorting imports, laying out lines and
Java/Kotlin syntax are the generator's job.
Where
| Where | Add | Remove |
|---|---|---|
| Class | ctx(e).ensureClassAnnotation(anno) | ctx(e).removeClassAnnotation("a.b.C") |
| Field | ctx(e).field(f).ensure(anno) | ctx(e).field(f).removeAnnotation("a.b.C") |
| Method (by name) | ctx(e).method("name").ensure(anno) | - |
| The field itself | - | ctx(e).removeField(f) |
| The file itself | - | ctx(e).dontMake() |
Removing also removes what the default rules added, because team rules always run after the default rules.
/** Takes the @DynamicUpdate the built-in rules put on every entity back off. */
@Rule("ps-no-dynamic-update")
void noDynamicUpdate(EntitySpec e) {
ctx(e).removeClassAnnotation("org.hibernate.annotations.DynamicUpdate");
}
In the pet store this rule removed @DynamicUpdate not only from the generator's files
(Customer.java, PurchaseOrderBase.java …) but also from the developer's classes of the split roots
(PurchaseOrder.java, OrderLine.java). The generator keeps a developer class's table annotations
(@Entity, @Table, @DynamicUpdate …) in line with the DSL, and leaves the body and the other
annotations the developer added alone.
Fields: ensure, add, merge, replace
| Method | If the same annotation is already there | If not | When |
|---|---|---|---|
ensure(anno) | does nothing | adds it | most of the time |
add(anno) | adds one more | adds it | repeatable annotations |
merge(anno) | adds only the attributes | adds it | one more attribute on the @Schema or @Column a default rule added |
replace(anno) | replaces it with this one | adds it | the team's value instead of the default rule's |
ElastiCORE already puts @Schema(description = ...) on DTO fields, so adding hidden = true takes
merge. With ensure nothing happens - see the example in
Targets.
/** A field marked @pii in the DSL is accepted in requests but never sent back in responses. */
@Rule(value = "ps-pii", target = "dto")
void pii(EntitySpec dto, FieldSpec f) {
if (f.hasDsl("pii")) {
ctx(dto).field(f).ensure(AnnoCode.of("com.fasterxml.jackson.annotation.JsonProperty")
.valRaw("access", "JsonProperty.Access.WRITE_ONLY"));
}
}
@Schema(description = "email address" )
@Size(max=100)
@Email(message = "Email format invalid")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) // ps-pii
private String email;
Checked by running it: send email to sign-up (POST /api/petstore/customer) and it is stored, while
the response JSON has no email.
{"customerId":3,"firstName":"Rule","homeAddress":{…},"lastName":"Demo","login":"rulesdemo3","role":"U"}
AnnoCode - building an annotation
Never build an annotation by concatenating strings ("@Column(name = \"" + n + "\")"). AnnoCode
takes care of quotes, imports, and Java's {} versus Kotlin's []. That is why a rule needs no
if (kotlin).
| Code | Result |
|---|---|
AnnoCode.of("jakarta.validation.constraints.Size").val("min", 2) | @Size(min = 2) |
.val("name", "user_id") | name = "user_id" - strings get quotes |
.val("hidden", true) | hidden = true |
.valRaw("access", "JsonProperty.Access.WRITE_ONLY") | as written - constants, class literals |
.valRaw("length", "%d", 200) | formatted |
.value("CreditCheckPort") | the unnamed attribute - @ExternalSystem("CreditCheckPort") |
AnnoCode.of("org.jmolecules.ddd.annotation.%s", "Entity").qualified() | @org.jmolecules.ddd.annotation.Entity - the full name, when the simple name clashes with jakarta.persistence.Entity |
The annotation class does not have to be on the generator's classpath. The rule only writes its name;
the class is needed when the application compiles (implementation 'org.jmolecules:jmolecules-ddd:…').
Annotating a method - method("name")
Annotates a method of the generated file, by name. Use it for methods whose name is in the DSL, such as
the query methods in repository.yml and the external call methods in port.yml. Example in
Targets.
Adding a method - addMethod
Adds a method the rule wrote by hand at the end of the class (before the closing brace). For when every root needs the same helper method:
/** Every root entity class says who it is in an audit log: "Customer#42". */
@Rule("ps-audit-label")
void auditLabel(EntitySpec e) {
if (!e.aggregateRoot() || e.hasMeta("abstract") || e.pkFields().size() != 1) return;
FieldSpec id = e.pkFields().get(0);
ctx(e).addMethod(String.format(
"\t/** Who this is in an audit log: %s#<id>. */%n"
+ "\tpublic String auditLabel() {%n"
+ "\t\treturn \"%s#\" + get%s();%n"
+ "\t}", e.getName(), e.getName(), id.capitalName()));
}
/** Who this is in an audit log: Customer#<id>. */
public String auditLabel() {
return "Customer#" + getCustomerId();
}
}
- The method body goes in exactly as the rule wrote it, so it is language-specific. To use it in a
Kotlin project too, branch on
ctx(e).syntax()or askSyntax. - Use it on files the generator rewrites every time, such as entities. On a developer's file
(controllers,
XxxService) rules apply only when the file is first created. - In a root split into two files (
PurchaseOrderBase+ the developer'sPurchaseOrder) the method goes only into the generator'sPurchaseOrderBase, and the developer class inherits it. To add a method to the developer class only, write the rule with@Rule(target = "behavior")- it applies when that file is first created. - Enums (
target = "enum") take added methods too; they go after the constants and the constructor. - 3.0.261022-rc1 has a bug:
addMethodis dropped on a class with no methods at all (an entity with only fields), and it also goes once more into the developer class of a split root. It is fixed in the next release. On rc1, write it as members after the identifier field instead:ctx(e).field(id).after("public String auditLabel() {").after("\treturn …;").after("}")(in a field rule).
Leaving out endpoints - excludeMethod
The DSL already has @restExclude(deleteById) and @readonly. When you want the same with a word of
your team's own, leave out endpoints of the generated controller by name.
| Name | Endpoint |
|---|---|
create | POST / |
read | GET /{id} |
search | POST /search |
update, batchUpdate | PUT /, PUT /batch |
deleteById, deleteBatch | DELETE /{id}, POST /delete |
schemaInfo | GET /schema/info |
Leaving out deleteById also leaves out deleteBatch, which calls it; leaving out update also
leaves out batchUpdate.
/** meta @nodelete: the REST API has no delete endpoints. */
@Rule(value = "ps-no-delete", target = "control")
void noDelete(EntitySpec e) {
if (e.hasMeta("nodelete")) ctx(e).excludeMethod("deleteById");
}
Category:
meta: entity @root @editable @expose(20) @access(read=public) @nodelete
The generated CategoryController has no @DeleteMapping and no POST /delete; at run time
DELETE /api/petstore/category/1000 and POST /api/petstore/category/delete return 405.
Use excludeMethod in a team rule on controllers (target = "control"). The hexagonal use case
port (CategoryUseCase) is made before the team rules run, so leaving deleteById out of the service
(service) makes CategoryService implements CategoryUseCase fail to compile. To leave it out of the
service and the port too, use @restExclude(deleteById) in the DSL - the default rules read it
directly and leave it out of every file together.
A controller is a developer's file. The rule does not apply to a controller you have already changed; a controller never changed is brought in line on the next generation.
Changing a field's declaration
Besides annotations, FieldCode changes the declaration. Most of this is better done in the DSL; use
a rule only when it is a convention for the whole team.
| Method | Does |
|---|---|
type("String") | the declared type |
rename("x") | the name |
initial("0") | the initial value |
comment("…") | the field comment |
before("…") | a line above the declaration |
after("…") | a member after the declaration (calling it several times continues the lines) |