The rule engine
The 3.0 generator has two parts:
| Decides | Where | |
|---|---|---|
| Rules | what is written - does this field get @Column(length=200) | Java classes (CodeRules) |
| Skeletons | where it goes - the outline of a file | .tmpl files |
Rules read the model and decide; how a string is written - semicolons, imports, property
declarations - is answered by a language seam (Syntax). That separation is why the same rules
generate both Java and Kotlin.
model (DSL) ──▶ Spec ──▶ rules ──▶ SourceCode ──▶ skeleton ──▶ file
│
Syntax (language)
Bundles
A bundle is a named list of rulesets. Two words in build.gradle choose one:
architecture | language = 'java' | language = 'kotlin' |
|---|---|---|
layered | jpa-springboot (2.x-compatible output); -Pv3: jpa-springboot-v3 | jpa-springboot-kt |
ddd | ddd-springboot | ddd-springboot-kt |
hexagonal | hexagonal-springboot | hexagonal-springboot-kt |
| Bundle | What it generates |
|---|---|
jpa-springboot | the same output as the 2.x pipeline (apart from the 2.x bug fixes the history lists) |
jpa-springboot-v3 | a cleaned-up output - only the imports in use, class annotations written by rules |
jpa-springboot-kt | Kotlin output |
ddd-springboot | v3 + aggregate boundaries - only roots get a repository, service and controller; immutable value objects; domain events |
hexagonal-springboot | ddd-springboot + an inbound port (<Entity>UseCase) per exposed root |
ddd-springboot-kt, hexagonal-springboot-kt | the same readings in Kotlin |
ts-model | TypeScript - DTOs, enums, search DTOs and a client that calls the controllers |
The two words are not a table in the plugin: the plugin passes them as written and the generator turns the pair into one bundle and one language switch, so they cannot disagree.
Opt-in, and the 2.x output kept
- Declare nothing and nothing changes. A build whose
elasticore { }block sets noarchitecture,languageorrulesruns the 2.x generator. A default written aslayeredwould look the same and would not be: the plugin sends nothing when nothing was declared. - Checked on every pull request: CI regenerates the reference project with 2.x's generator, which
must leave it exactly as committed, then compares the rule engine's output with 2.x's - per artifact
(
150 MATCH / 0 DIFF / 2 ACCEPTED) and over all 229 files 2.x generates for the reference project (0 differ / 0 missing / 0 extra: the four files a 2.x bug fix changes are accepted by name, one more differs only in import order). - New capabilities switch on only when you use them - a DSL keyword, a setting, a bundle.
Choosing rules in env.yml
Most projects never need this - the two words in build.gradle choose the bundle. A domain that
differs from the rest of the project names its rules:
elasticore:
config:
publisher:
rules: [hexagonal-springboot, ossie-export] # replaces the default for this domain
suppress: [jpa-comment] # turns one rule off; accumulates
| Key | Means |
|---|---|
rules | the rulesets to use - replaces the default. A name is a bundle, a ruleset, or a fully qualified CodeRules class |
suppress | rule ids to turn off - accumulates |
rules.kt | Kotlin's (wins over rules when present) |
rules.ts | TypeScript's - TypeScript never inherits the server's rules |
Order of precedence: the domain's env.yml, then the project (the system property elasticore.publisher.rules of the generation task),
then the built-in default. publisher.rules is read by the rule engine only; the 2.x annotation
processor does not read it.
Listing and adding rules
./gradlew elcore3 -PlistRules # per domain: the rules selected, per file kind, in order (writes nothing)
Team rules are added after the selected bundle, from build.gradle:
elasticore {
architecture = 'hexagonal'
rules = ['com.acme.rules.AcmeRules'] // added to the bundle
}
dependencies {
elasticore project(':acme-rules') // the generator's classpath, not the application's
}
Writing them: Team rules.
Two entry points, kept apart
The rule engine (elcore3) and the 2.x annotation processor (@ElastiCore) generate the same files
by different rules. A project that uses bundles uses the task only - do not put @ElastiCore on the
application class. The plugin registers the tasks; without the plugin they can be configured by hand
(see the rule guide in blueprint/elasticore-rule-guide.md).