Skip to main content
ElastiCORE 3.0 is a release candidate: 3.0.261022-rc1 on Maven Central. Projects on the 2.2 line: see the 2.2 documentation.
Version: 3.0

Rule API reference

Every type is in the io.elasticore.base.model.pub.rules package. The rules module takes elasticore-base as compileOnly (Your first ruleset). This page collects only what team rules use. The other public methods are used by ElastiCORE's own rules and may change between versions.

import io.elasticore.base.model.pub.rules.*;
import static io.elasticore.base.model.pub.rules.Ctx.ctx;

@Rule and CodeRules​

CodeRules is a marker interface with no methods to implement. Each method with @Rule is one rule. The method is void and needs no access modifier. The class must have a no-argument constructor.

AttributeDefaultMeans
value(required)The rule id. It appears in -PlistRules and is used in suppress. Add a team prefix (ps-…, acme-…)
target"entity"The kind of file it contributes to (Targets)
order0Lowest first within the same phase; ties in registration order
phaseHEADWhether a file rule runs before (HEAD) or after (TAIL) the field rules. Meaningless for field rules

The method's shape decides when it is called​

ParametersCalledtarget examples
(EntitySpec e)once each time a file of that entity is madeentity, dto, srchdto, repository, control …
(EntitySpec e, FieldSpec f)once per field of that fileentity, dto, srchdto
(DomainSpec d)once per domain filemapper, apiErrors, a domain file of your own
(EnumSpec e)once per enumenum
(PortSpec p)once per external integration portport
(ProtoServiceSpec p)once per proto portproto

Within one file the order is HEAD file rules → field rules (per field) → TAIL file rules. Team rules run in each phase after all of ElastiCORE's rules have finished, so they can remove or change what a default rule added; use order only when team rules need an order among themselves.

Reading the model​

EntitySpec - an entity, a DTO from dto.yml, a search DTO​

MethodIn the DSLExample (pet store)
getName()the entity name"PurchaseOrder"
domainName(), domainClassName()the domain"petstore", "Petstore"
hasMeta("x"), metaValue("x")@x in meta:, the value of @x(value)metaValue("expose") → "20"
metaValues("x")all of them, when the same word appears several times
metaText("access")the text as written of a paired value such as @access(read=public, update=owner)
aggregateRoot()whether it is a root (false for a member or value object)PurchaseOrder → true
containedIn()for a member, the name of its rootOrderLine → "PurchaseOrder"
isSplitForBehavior(e.getName())whether it is a root split into two files (XxxBase + the developer class)
fields(), allFields(), dtoFields(), pkFields()its own fields / including inherited / the DTO side / the identifiers
events(), eventClassName(ev), eventPackage()the @event(...) names, the generated event class name and package[Placed, Shipped, Cancelled]
commands() → name(), params()the business commands in commands:
queries() → QuerySpecthe methods in repository.yml (target repository)
pkg("dto")the package of an env.yml namespace key
config("ps.maxPageSize")elasticore.config.ps.maxPageSize in that domain's env.yml, null if absent
tablePrefix(), apiPath()config.table.prefix, config.apiPath"ps_", "/api"

FieldSpec - one field line​

MethodIn the DSLExample
name(), capitalName()the field name, for accessors"email", "Email"
typeName(), coreItemType()the declared type / the X of List<X>
required()!
hasDsl("x"), hasDslAny("x", "y")whether @x is there (case-insensitive)hasDsl("pii")
dslValue("x"), dslValueAny(...)the value of @x(value)dslValue("search") → "like"
dslProperty("onetomany", "mappedBy")a named attribute such as @OneToMany(mappedBy="parent")
maxLength()string(100) or @length (0 if absent)
label(), description()the -- description, @label
dbColumnName()@column or the snake_case name
isList(), isEntityRef(), isBaseType(), isStringType()a collection / a reference to another entity / a base type / a string
isMoney(), isDate(), isDateTime()money, date, datetime

DomainSpec - one domain​

MethodMeans
getName(), className()"petstore", "Petstore"
entities()every entity of the domain
namespace("event")the package of a namespace key (keys you added included), null if absent
config("a.b"), isOn("a.b")an env.yml setting / whether it is "true"

EnumSpec - enum.yml​

MethodMeans
getName()"OrderStatus"
constants() → name(), values()the values and each value's columns (["DRF", "Draft"])
fields()the column names in fields:

PortSpec - port.yml​

MethodMeans
getName(), protocol(), url()CreditCheckPort, "http"/"dbms", @url(...)
operations() → name(), httpMethod(), url(), returnType(), params()the calls in methods:

QuerySpec - methods in repository.yml​

MethodMeans
name()id:
sql()the query: string. null for a query derived from the method name
isSelect(), isNative()whether it reads / whether it is nativeQuery: true
returnType(), params()return:, params:

Writing code​

SourceCode - the file being made, returned by ctx(spec)​

Pass ctx(...) what the rule received, as it is: ctx(e), ctx(d), ctx(enumeration), ctx(port).

MethodDoes
ensureClassAnnotation(anno)a class annotation. Never added twice if already there. Import automatic
removeClassAnnotation(fqcn)removes a class annotation (including one a default rule added)
field(f) → FieldCodethat field's declaration
removeField(f)leaves that field out of this file
method("name") → ensure(anno)an annotation on every method with that name (repository, port …). A name that does not exist is reported in the build log
addMethod(source)adds a hand-written method at the end of the class (Annotations and methods)
excludeMethod("deleteById", …)leaves out generated endpoint methods
addImport(fqcn)registers an import (only those in use are rendered)
dontMake()does not make this file (SKIP(dontMake) in the log)
var(k, v), var(k, fmt, args…)fills ${k} in the skeleton. null is ignored - an empty value is ""
varLine(k, fmt, args…), varLines(k, list)adds lines where ${k} is
syntax()this file's language (Syntax.JAVA, Syntax.KOTLIN, Syntax.TYPESCRIPT)
requireSyntaxAnyOf(ruleId, Syntax…)used for another language's generation, fails that file instead of quietly making a wrong one
artifactName()which of the domain files this is (the class name of the ContributedArtifact)
render(), unfilledSlots()in tests: the whole file as a string / the names of unfilled ${…}

FieldCode - one field​

MethodDoes
ensure(anno)adds it only when the same annotation is not there (usually this one)
add(anno)always adds it (repeatable annotations)
merge(anno)adds only the attributes to the same annotation already there; adds it if absent
replace(anno)replaces the same annotation with this one
removeAnnotation(fqcn)removes it
type("String"), rename("x"), initial("0")the declared type / the name / the initial value
comment(fmt, …), before(fmt, …), after(fmt, …)the comment / a line above the declaration / a member after the declaration

AnnoCode - annotations​

Never build an annotation by concatenating strings. AnnoCode takes care of Java ({}) and Kotlin ([]) syntax, imports and quotes.

CodeResult
AnnoCode.of("jakarta.persistence.Column")@Column + import
AnnoCode.of("org.jmolecules.ddd.annotation.%s", "Identity")the name built with a format
.val("name", "user_id")name = "user_id" (a string - gets quotes)
.val("hidden", true), .val("min", 2)hidden = true, min = 2
.valRaw("access", "JsonProperty.Access.WRITE_ONLY")as written - access = JsonProperty.Access.WRITE_ONLY
.valRaw("length", "%d", 200)formatted
.value("x"), .valueRaw("A.B")the unnamed attribute - @X("x"), @X(A.B)
.qualified()with the full name (when the simple name clashes with jakarta.persistence.Entity)
AnnoCode.AnnoList.of(...)a list of annotations, such as indexes = {...}

ModelCheck - checks​

MethodResult
ModelCheck.warn(msg)a warning only. The same message once
ModelCheck.fail(msg)runs generation to the end, shows all failures together and fails the build (elcore3 exit code 1)
ModelCheck.failures(), ModelCheck.reset()checking results in tests / resetting between tests

Do not throw exceptions from a rule. Only the one file that rule was writing is left failed, and the cause is hard to find.

New files​

TypeDoes
ArtifactContributor → artifactsFor(EntitySpec e)declares a file per entity
DomainArtifactContributor → artifactsFor(DomainSpec d)declares a file per domain
ContributedArtifact.of(target, namespaceKey, className)the new target name (= the skeleton name), the package key, the class name
.orNamespace("entity")the key to use in a project that does not have that package key

Skeletons go in the rules module's src/main/resources/elasticore-template/v3/<target>.tmpl (Java) and elasticore-template/kt/<target>.tmpl (Kotlin). More in Making new files.

Syntax - the language seam​

The same rules make both Java and Kotlin. The generator writes annotations and imports in the right language, so team rules usually need no language branch. Only when you write code fragments yourself (lines of a new file, addMethod) do you branch on ctx(e).syntax() or ask Syntax:

CallJavaKotlin
s.statement("return x")return x;return x
s.getter("dto", "name")dto.getName()dto.name
s.setter("to", "name", "v")to.setName(v)to.name = v
s.localVar("T", "x", "init")T x = init;val x = init
s.newInstance("X", "")new X()X()
s.nullableType("String")StringString?
s.classLiteral("X")X.classX::class
s.escapeName("in")in`in`

TypeScript does not answer JVM concepts (classLiteral, sam, overriding …) and throws an exception. Team rules are added only to server (Java, Kotlin) generation, not to the TypeScript client.