Team rules (rulesets)
"Our annotation on every root entity", "personal data never in a response", "a command without an event fails the build" - team rules as code, plugged into generation. Write the rule once instead of hand-editing generated files, and every entity gets it on every regeneration. ElastiCORE's own Java, Kotlin, TypeScript, DDD and hexagonal generation are rules of exactly this kind.
public class AcmeRules implements CodeRules {
/** A field marked @secret in the DSL is accepted in requests but never sent back. */
@Rule(value = "acme-secret", target = "dto")
void secret(EntitySpec entity, FieldSpec field) {
if (field.hasDsl("secret")) {
ctx(entity).field(field).ensure(AnnoCode.of("com.fasterxml.jackson.annotation.JsonProperty")
.valRaw("access", "JsonProperty.Access.WRITE_ONLY"));
}
}
/** A root that takes commands must declare its events - otherwise the build fails. */
@Rule("acme-commands-need-events")
void commandsNeedEvents(EntitySpec entity) {
if (!entity.commands().isEmpty() && entity.events().isEmpty()) {
ModelCheck.fail(entity.getName() + " has commands but no @event.");
}
}
}
elasticore { rules = ['com.acme.rules.AcmeRules'] }
dependencies { elasticore project(':acme-rules') }
bizNo: string(12)! @secret # a marker of your own in the DSL
The elasticore configuration is the generator's classpath, not the application's; the module is
compiled before generation. Rules named in rules are added after the bundle the build selected.
The shape of a rule
CodeRules is a marker interface: every method with @Rule is a rule.
@Rule attribute | Default | Means |
|---|---|---|
value | (required) | the rule id - for tracing and suppress |
target | "entity" | the kind of file it contributes to: entity, dto, srchdto, enum, repository, service, control, mapper, port, Q ... |
order | 0 | lower runs first |
phase | HEAD | HEAD before the field rules, TAIL after |
The signature decides when it runs: (EntitySpec e) once per entity, (EntitySpec e, FieldSpec f)
once per field, (DomainSpec d) once per domain, (EnumSpec e) per enum.
| Reading the model | Writing code |
|---|---|
f.hasDsl("secret"), f.dslValue("length"), f.required(), f.isEntityRef() | ctx(e).field(f).ensure(anno), .add, .merge, .replace, .removeAnnotation(fqcn) |
e.getName(), e.pkg("dto"), e.fields(), e.hasMeta("expose") | AnnoCode.of("...Column").val("name", "user_id").valRaw("length", "%d", 200) |
e.aggregateRoot(), e.commands(), e.events() | ctx(e).removeField(f), ctx(e).dontMake(), ModelCheck.fail(...) |
Never build an annotation by concatenating strings - AnnoCode writes it, and the language seam
(Syntax) decides whether it is Java's {} or Kotlin's []. A rule has no if (kotlin): the same
rule serves both languages.
Turning a rule off, seeing what runs
publisher:
suppress: [jpa-comment] # no more @Comment annotations
./gradlew elcore3 -PlistRules # per domain, per file kind, the rules in order - with their ids
Going further
Two guides are copied into every project's blueprint/ folder (in Korean):
elasticore-rule-dev-guide.md- writing a first rule step by step: adding and removing annotations, a YAML marker of your own, build checks, tests.elasticore-ddd-customize-guide.md- turning generated files off, marking every class with its DDD role (jMolecules), a DSL word of your own that makes a new file, model checks with team settings, testing rules with and without an application, Kotlin.
The rule API in full is in elasticore-rule-guide.md, chapters 3 to 6.