Your first ruleset
Attach a team rules module, ps-rules, to the pet store project (hexagonal,
Java). By the end of this page every aggregate root is generated with jMolecules' @AggregateRoot, and
every identifier field with @Identity.
1. Rules live in a separate module
./gradlew build generates code before it compiles the application, so the generator cannot see
rules inside the application's sources. Rules go in a separate module that is compiled first.
elasticore-petstore/
├── settings.gradle ← include 'ps-rules'
├── build.gradle ← connects the rules to the generator
├── ps-rules/ ← the new module (any name)
│ ├── build.gradle
│ └── src/
│ ├── main/java/com/example/rules/PetstoreRules.java
│ ├── main/resources/elasticore-template/v3/ ← skeletons for new files (chapter 5)
│ └── test/java/com/example/rules/PetstoreRulesTest.java
└── src/ … ← the application
include 'ps-rules'
plugins {
id 'java-library'
}
java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
repositories {
mavenCentral()
}
dependencies {
// The generator runs these rules; it is not part of the application.
// The same version as the io.elasticore plugin.
compileOnly 'io.elasticore:elasticore-base:3.0.261022-rc1'
testImplementation 'io.elasticore:elasticore-base:3.0.261022-rc1'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2'
}
tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
tasks.named('test') { useJUnitPlatform() }
Write the same elasticore-base version as the plugin (id 'io.elasticore' version '…'). If the
API the rules were compiled against differs from the generator's, generation fails with
NoSuchMethodError. Why compileOnly: at run time the generator brings its own elasticore-base.
2. Connecting it to the generator
elasticore {
architecture = 'hexagonal'
language = 'java'
springBoot = '4'
rules = ['ossie-export', 'com.example.rules.PetstoreRules'] // added after the bundle
}
dependencies {
// The team rules: on the generator's classpath only, not in the application.
elasticore project(':ps-rules')
// What the team rules put on the classes (@AggregateRoot, @ValueObject, @Identity).
implementation 'org.jmolecules:jmolecules-ddd:1.10.0'
// ... the existing dependencies as they are
}
| Line | Means |
|---|---|
rules = [...] | The full names of the ruleset classes. They are added after the bundle that architecture and language chose - do not write the bundle again. The pet store already uses ossie-export, so the class goes after it |
elasticore project(':ps-rules') | elasticore is the generator's classpath. It does not go into the application jar, and this module is compiled before generation |
jmolecules-ddd | The annotations the rules add are needed when the application compiles |
Always set architecture as well. Without it the 2.x-compatible generator runs and the rule engine
does not.
3. The first rules
package com.example.rules;
import io.elasticore.base.model.pub.rules.AnnoCode;
import io.elasticore.base.model.pub.rules.CodeRules;
import io.elasticore.base.model.pub.rules.EntitySpec;
import io.elasticore.base.model.pub.rules.FieldSpec;
import io.elasticore.base.model.pub.rules.Rule;
import static io.elasticore.base.model.pub.rules.Ctx.ctx;
/** The pet store team's rules for the code ElastiCORE generates. */
public class PetstoreRules implements CodeRules {
private static final String JMOLECULES = "org.jmolecules.ddd.annotation.%s";
/** A root gets @AggregateRoot, an @embeddable gets @ValueObject; an @abstract base class gets neither. */
@Rule("ps-aggregate-root")
void aggregateRoot(EntitySpec e) {
if (e.hasMeta("abstract")) return;
if (e.hasMeta("embeddable")) {
ctx(e).ensureClassAnnotation(AnnoCode.of(JMOLECULES, "ValueObject"));
} else if (e.aggregateRoot()) {
ctx(e).ensureClassAnnotation(AnnoCode.of(JMOLECULES, "AggregateRoot"));
}
}
/** Every @id field gets @Identity. */
@Rule("ps-identity")
void identity(EntitySpec e, FieldSpec f) {
if (f.hasDsl("id")) {
ctx(e).field(f).ensure(AnnoCode.of(JMOLECULES, "Identity"));
}
}
}
| Code | Means |
|---|---|
implements CodeRules | A marker interface with no methods to implement. The class must be public and have a no-argument constructor |
@Rule("ps-aggregate-root") | This method is a rule. The value is the rule id - use a team prefix (ps-) so it never clashes with a default rule |
(EntitySpec e) | Called once per entity file |
(EntitySpec e, FieldSpec f) | Called once per field |
e.aggregateRoot(), e.hasMeta("embeddable") | Read @root and @embeddable from the YAML |
ctx(e) | The file being made |
ensureClassAnnotation(...) | A class annotation. Never added twice if already there; the import is automatic |
ctx(e).field(f).ensure(...) | An annotation on that field |
AnnoCode.of(JMOLECULES, "Identity") | The annotation to add - its full name (%s formatting allowed) |
No target is given, so both rules contribute to the entity file (entity).
4. Checking that it is connected
./gradlew elcore3 -PlistRules
This makes no files; it shows, per domain, the rules that run now, in the order they run. What the pet store actually printed (in part):
[rules] petstore: hexagonal-springboot + ossie-export + com.example.rules.PetstoreRules
entity
0 head access-signin-check ServiceAccessRules
3 head ddd-reference-context DddReferenceRules
5 head v3-class-annotations JpaEntityAnnotationRules
6 head v3-lombok JpaEntityFileRules
7 head v3-auditing-listener JpaEntityAnnotationRules
8 head ddd-behavior-base DddBehaviorRules
0 head ps-aggregate-root PetstoreRules
5 field jpa-field-desc JpaEntityBaseRules
10 field jpa-id JpaEntityBaseRules
…
71 field ddd-state-initial DddStateRules
0 field ps-identity PetstoreRules
80 tail ddd-value-object DddValueObjectRules
82 tail ddd-reference DddReferenceRules
- If the first line shows
com.example.rules.PetstoreRules, it is connected. - Even with
order0, our rules run in each phase (head,field) after all of ElastiCORE's rules. So they can remove what a default rule added, or add on top of it. - The columns: target (
entity) ·order· phase (head/field/tail,oncefor domain and enum rules) · rule id · class.
5. The generated code
./gradlew build
Customer.java (a generator's file, rewritten every time):
@EntityListeners(AuditingEntityListener.class)
@AggregateRoot // ps-aggregate-root
public class Customer implements java.io.Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "customer_id")
@Identity // ps-identity
private Long customerId;
The generator also adds import org.jmolecules.ddd.annotation.AggregateRoot;. Address
(@embeddable) gets @ValueObject.
A developer's file gets a notice
PurchaseOrder is a root split into two files. PurchaseOrderBase.java, which holds the fields, is the
generator's, so the rules apply to it as usual; but PurchaseOrder.java, where the business logic goes,
is the developer's, and the generator does not rewrite it. Instead, the generation log points out
the missing class annotations:
behavior src/main/java/io/elasticore/sample/petstore/domain/model/PurchaseOrder.java SKIP(user-owned)
[elcore3] src/main/java/io/elasticore/sample/petstore/domain/model/PurchaseOrder.java: the rules also put these on the class, which this file does not have - it is yours, so add them by hand if they apply:
@AggregateRoot
Add it by hand once, as the notice says. Roots created after the rule is in place get it from the start.
import org.jmolecules.ddd.annotation.AggregateRoot;
@AggregateRoot
@Entity
@Table(name = "ps_purchase_order")
public class PurchaseOrder extends PurchaseOrderBase {
Next
- Removing annotations, adding attributes, annotating methods, adding methods: Annotations and methods
- DSL words of your team's own, like
@pii, and build checks: DSL words and model checks - Testing only the rules, without building every time: Testing