All Chassis DSL and Code Generator docs as a single page
(generated by)
cd docs && fd . -e md -e markdown | awk '/^(singlePageChassisDocs|index|_posts|blog|about|notDocumented)/ { next; } { print; }' | tac && cd ..
layout: page title: Introducing Chassis Code Generator subtitle: a gentle introduction menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../assets/Chassis.png —
Chassis Code Generator gentle introduction
currently to intro story is directly on the Chassis Homepage)
layout: page title: Chassis DSL subtitle: Specifying what to generate menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../assets/Chassis.png —
The Chassis DSL for generating Code
One of Kotlin’s nice language features that makes it ideal for creating pure Kotlin DSLs (Domain Specific Languages) are trailing lambda parameters, which allow to pass the lambda implementation outside the brackets when calling a function whose last parameter is a lambda. For a better hands-on explanation see e.g. Baeldung Kotlin DSL.
To be crystal clear here: the Chassis DSL is 100% pure Kotlin source-code
Chassis DSL elaborates on this and adds a few nice features to make it more usable and flexible.
- using Kotlin’s’
context()
feature for dsl related objects and functions (have a look at e.g. youtube) - especially we use the context to “pass” the
class DslCtx
through all of our DSL parsing. (DslCtx
is gathering all information the Chassis DSL contains) - Multiple parsing
PASS
es of our DSL (see class DslCtx)- multiple parses are necessary as we want to “use” parsed information of other models for the current model, but the other model might not have been parsed yet at all.
- via
class DslRef
(see class DslRef) we’re able to “reference” any other defined (sub)level element- for mor in detail on
DslRef
see (TODO TBD) - this enables us to do a lot of things
- specifying other models as to be extended super-classes/interface
- gather properties of some other model
- constrain Fillers or CRUD operations on other models or model-properties
- etc.
- for mor in detail on
One drawback of the DSL being pure kotlin sourcecode is security, as the DSL may contain arbitrary harmful code also.
But as Chassis and its DSL solely is used at development time (and CICD) this disadvantage is no issue for our use-case: generating code.
At the moment there is implemented only one toplevel DSL method:
continue with Chassis top-level modelgroup …
DslCtx PASS’es
Here’s an example of a DSL function implementation, defining the DSL showcase("someName" { ... showcase Sub(DSL) }
sub-DSL (see class DslShowcase)youtube)
As you can see any DSL function implementation should decide what to do in which DSL PASS (be careful though to still decent the DSL tree as inner sub-DSL might also want to do something in that PASS)
context(DslCtxWrapper)
class DslShowcaseDelegateImpl(simpleNameOfDelegator: String, delegatorRef: IDslRef)
: ADslDelegateClass(simpleNameOfDelegator, delegatorRef), IDslImplShowcaseDelegate
{
...
override fun showcase(simpleName: String, block: IDslApiShowcaseBlock.() -> Unit) {
log.info("fun {}(\"{}\") { ... } in PASS {}", object{}.javaClass.enclosingMethod.name, simpleName, dslCtx.currentPASS)
when (dslCtx.currentPASS) {
dslCtx.PASS_ERROR -> TODO()
dslCtx.PASS_FINISH -> { /* TODO implement me! */ }
dslCtx.PASS_1_BASEMODELS -> {
val dslImpl = theShowcaseBlocks.getOrPut(simpleName) { DslShowcaseBlockImpl(simpleName, selfDslRef) }
dslImpl.apply(block)
}
else -> {}
}
}
layout: page
title: node modelgroup { … }
subtitle: Things that you define for all models
and model elements (DTO, DCO, TABLEFOR)
menubar: data_menu_chassis
toc: false
show_sidebar: false
hero_image: ../assets/Chassis.png
—
modelgroup: Things that you define for all models
and model elements (DTO, DCO, TABLEFOR)
Tip: with alt/opt-RETURN you can import inner class/enum names statically. This can make your Chassis DSL much shorter and more readable!
For a more complete nameAndWhereto { ... }
see DSL block delegation showcase
modelgroup { … }
modelgroup("groupName") { /** @see com.hoffi.chassis.dsl.modelgroup.DslModelgroup */
nameAndWhereto { /** @see com.hoffi.chassis.dsl.whereto.IDslApiNameAndWheretoOnSubElements */
classPrefix("Simple")
packageName("entity")
dtoNameAndWhereto {
classPostfix("Dto")
packageName("dto")
}
tableNameAndWhereto {
classPostfix("Table")
packageName("table")
}
}
/** @see com.hoffi.chassis.dsl.modelgroupIDslApiKindClassObjectOrInterface */
constructorVisibility = IDslApiConstructorVisibility.VISIBILITY.PROTECTED
/** @see com.hoffi.chassis.dsl.modelgroup.IDslApiGatherPropertiesModelAndModelSubelementsCommon */
propertiesOfSuperclasses /* do not extend superclasses/superinterfaces, but gather their properties */
propertiesOf(MODELREFENUM.MODEL "modelgroupName" withModelName "modelName", GatherPropertiesEnum.PROPERTIES_AND_SUPERCLASS_PROPERTIES)
Things set on a modelgroup { ... }
(eventually depending on the strategy) are considered to be defined for all containing models and submodels
and therefore can be omitted in them respectively.
model
On defining property’s also see model properties)
modelgroup {
model("modelName") {
nameAndWhereto { /* see above */ }
extends {
replaceSuperclass = false
replaceSuperInterfaces = false
+ (MODELREFENUM.DTO inModelgroup "modelgroupName" withModelName "persistentDTOname")
- "modelName" // unary minus ==> interface ; as no modelgroup is given, it is assumed that "this" modelgroup contains a model with this name
- "otherModel"
// minusAssign("ref:name|other:string") // not implemented yet
// minusAssign() // not implemented yet
// not() // not implemented yet
// rem("...") // not implemented yet
}
/** @see com.hoffi.chassis.dsl.modelgroup.IDslApiGatherPropertiesModelAndModelSubelementsCommon */
propertiesOfSuperclasses /* do not extend superclasses/superinterfaces, but gather their properties */
propertiesOf(MODELREFENUM.MODEL "modelgroupName" withModelName "modelName", GatherPropertiesEnum.PROPERTIES_AND_SUPERCLASS_PROPERTIES)
property("thePropName1", TYP.STRING, ..., Tag.CONSTRUCTOR, Tag.DEFAULT_INITIALIZER, Tag.HASH_MEMBER, Tag.TO_STRING_MEMBER, Tag.PRIMARY)
property("thePropName2", Dummy::class, ..., Tag.CONSTRUCTOR, Tag.DEFAULT_INITIALIZER, Tag.HASH_MEMBER, Tag.TO_STRING_MEMBER)
property("thePropName3", ClassName("packageName", "ClassName"))
property("someModelObject", DTO of "otherModelgroup", mutable)
}
}
A model
is the named “outer” definition of a thing that can have multiple “incarnations”.
E.g. a model “Entity” can be
- a kotlin class in form of a DTO (Data Transfer Object)
- a kotlin class in form of a DCO (Data Compute Object)
- the DB representation of a DTO (TableFor(DTO))
- the DB representation of a DCO (TableFor(DC0))
- … (if more model subelements are defined)
Things set on a model { ... }
(eventually depending on the strategy) are considered to be defined for all containing models and submodels
and therefore can be omitted in them respectively.
This is particularly useful for defining properties that all subelements have.
dto
model subelements are things that “really” get code generated for.
modelgroup {
model("name") {
dto {
/** everything that a model has */
// plus e.g. props that only a dto has
property("dtoSpecificProp", TYP.STRING, mutable, Tag.CONSTRUCTOR, Tag.DEFAULT_INITIALIZER)
//propertiesOf( (MODEL inModelgroup PERSISTENTGROUP withModelName PERSISTENT__PERSISTENT) )
See(IDslApiInitializer::class)
// REPLACE, APPEND, MODIFY the initializer of a defined property
initializer("dtoSpecificProp", APPEND, "/* some dto specific comment */")
initializer("prio", APPEND, "/* some dto prio comment */")
// add/remove a property from `toString()` method
addToStringMembers("dtoSpecificProp", "createdAt")
removeToStringMembers("prio")
}
}
}
dco
modelgroup {
model("name") {
dco {
/* same as dto { ... } */
}
}
}
tableFor
modelgroup {
model("name") {
tableFor(MODELREFENUM.DTO) { // generate a table for the DTO of this model("name")
/* also all a dto/dco { ... } has, but you mainly won't need those */
initializer("name", APPEND, ".uniqueIndex()") // setting an index on the table column
// PRIMARY KEY already had been set on property via Tag.PRIMARY
}
}
}
crud
the Chassis DSL node crud { ... }
specifies which persistence/DB operations should be generated for which model subelements.
modelgroup {
model("name") {
tableFor(MODELREFENUM.DTO) {
crud {
// generate a crud operations for the DTO of this model("name")
See(IDslApiOuterCrudBlock::class)
STANDARD FOR DTO // just a helper
+DTO // unary plus means "mutual with itself
CRUDALL FOR DTO // just a helper
//
READ.viaAllVariants FOR DTO // only select methods but all variants of them (byJoin/bySelect)
CREATE FOR DTO // only insert
prefixed("somePrefix") {
// create the same methods that would be generated directly in `crud { ... }` node
// but prefix them with the given prefix
// and apply the depp or shallow Restrictions to them
See(IDslApiPrefixedCrudScopeBlock::class)
(READ.viaAllVariants FOR DTO) deepRestrictions {
IGNORE propName "someModelObject"
IGNORE("someModelObject", "prefix1")
}
(CREATE FOR DTO) deepRestrictions {
IGNORE propName "subentitys"
IGNORE("subentitys", "someModelObject", "prefix2")
}
}
}
}
}
}
filler
Fillers are static methods that take to submodel objects and fill the one from the other.
That means all the props that have the same name and same type will (recursively) copied over to the other.
As target as well as source you also can specify modelsubelements that are outside the current model node or even outside the current modelgroup.
The Logic for CopyBoundrys are the same as for crud { ... }
above.
As crud { ... }
operations use filler { ... }
defined stuff under the hood,
if a crud needs a filler, it will synthetically create it (meaning you do NOT have to define it here redundantly)
modelgroup {
model("name") {}
filler {
See(IDslApiOuterFillerBlock::class)
+DTO // DTO filled by a DTO
DTO mutual TABLE
DTO mutual TABLE
DTO from TABLE
TABLE from DTO
DTO from (DTO of ENTITY__SUBENTITY) // TODO check if this corresponding virtual Filler is also created because of (next line)
(DTO of ENTITY__SUBENTITY) from TABLE
//(DTO inModelgroup PERSISTENTGROUP withModelName PERSISTENT__PERSISTENT) from DTO
prefixed("withoutModels") {
See(IDslApiPrefixedCrudScopeBlock::class)
(DTO mutual TABLE) shallowRestrictions {
IGNORE propName "someModelObject∆" // TODO XXX ∆ check if prop exists!!!
IGNORE("dtoSpecificProp", "someObject", "aLocalDateTime")
IGNORE("someModelObject") // vararg
copyBoundry(IGNORE, "someModelObject") // vararg extended form
}
(DTO mutual TABLE) deepRestrictions {
IGNORE propName "subentitys"
IGNORE("subentitys") // vararg
copyBoundry(IGNORE, "subentitys") // vararg extended form
IGNORE model (DTO of ENTITY__SUBENTITY) onlyIf COLLECTIONTYP.COLLECTION
}
FOR((TABLE from DTO), (DTO from TABLE)) deepRestrictions {
IGNORE propName "subentitys"
IGNORE("subentitys") // vararg
}
+DTO deepRestrictions {
//FOR DTO {
copyBoundry(IGNORE, "subentitys", "someModelObject")
}
FOR(+DTO, +DTO) deepRestrictions {
copyBoundry(IGNORE, "subentitys", "someModelObject")
}
}
}
}
there are a number of generated functions for each filler:
e.g.: object FillerEntityDto
for model(“Entity”) dto classPostfix “Dto” mutual filler from/to itself:
fun cloneDeep(EntityDto): EntityDto
fun cloneShallowlgnoreModels(EntityDto): EntityDto
fun cloneShallowTakeSameModels(EntityDto): EntityDto
fun cloneWithNewModels(EntityDto): EntityDto
fun copyDeepinto(EntityDto, EntityDto): EntityDto
fun copyShallowAndTakeSameModelslnto(EntityDto, EntityDto): EntityDto
fun copyShallowIgnoreModelsInto(EntityDto, EntityDto): EntityDto
fun copyShallowWithNewModelslnto(EntityDto, EntityDto): EntityDto
for all prefixed("some") restrictions { ... }
above’s set of filler functions will be generated.
layout: page title: class properties of models and model elements subtitle: class props menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../assets/Chassis.png —
model and model element’s class properties
There are four types of properties:
TYP
properties, of a Chassis predefined type (see below)KClass<*>
propertiespoetType: TypeName
properties- model properties (a reference to another model or modelsubelement of the Chassis DSL)
currently predefined TYP
s are:
INT | TYP(Integer::class) |
LONG | TYP(Long::class) |
STRING | TYP(String::class) |
BOOL | TYP(Boolean::class) |
UUID | TYP(java.util) |
INSTANT | TYP(kotlinx.datetime.Instant::class) |
LOCALDATETIME | TYP(kotlinx.datetime.LocalDateTime::class) |
There are a plethora of function overloads defined to enable you with fewest possible typing to specify what you want the property characteristics to be.
- mutable: Mutable
- collectionType: COLLECTIONTYP
- NONE
- LIST
- SET
- COLLECTION
- ITERABLE
- initializer: Initializer
Initializer.of(format: String, vararg args: Any)
wrapper around kotlinPoet initializer, see KotlinPoet
- modifiers: MutableSet<com.squareup.kotlinpoet.KModifier>
- length: Int
- used for Database mapping if the persistent type has to have fixed length (e.g varchar(2048))
- tags: Tags (see code for a complete list)
- DEFAULT_INITIALIZER
- NO_DEFAULT_INITIALIZER
- CONSTRUCTOR
- CONSTRUCTOR_INSUPER
- COLLECTION_IMMUTABLE
- HASH_MEMBER
- PRIMARY
- TO_STRING_MEMBER
- TRANSIENT
- NULLABLE
- NULLABLE_GENERICTYPE
propertiesOf
the fun propertiesOf(...)
allows you to specify that the current (sub)model should (directly) have the properties of the referenced Chassis DSL (sub)model.
second parameter is one of (specifying which of the rerenced props to gather):
enum class GatherPropertiesEnum {
NONE,
PROPERTIES_ONLY_DIRECT_ONES,
PROPERTIES_AND_SUPERCLASS_PROPERTIES,
SUPERCLASS_PROPERTIES_ONLY
}
layout: page
title: nameAndWhereto
subtitle: deciding how things are named
and where generated files go to
menubar: data_menu_chassis
toc: false
show_sidebar: false
hero_image: ../assets/Chassis.png
—
nameAndWhereto: deciding how things are named
and where generated files go to
One of the most complex “things” dealing with the nitty gritties of code generation is:
- what is the name of it?
- which package it goes to?
- which subpackage it goes to?
- which location it goes to in the filesystem (basepath)?
- which sublocation in that basepath it goes to?
- does it have prefix and/or postfix?
- do I take a “common for all” prefix for any model in a modelgroup?
- does a specific prefix/postfix overwrite a defined prefix/postfix defined in the modelgroup? or concat it?
- does concat do concat before? or after? a prefix/postfix defined on a higher level?
- can I have an exception for a special single model subelement concerning all of this? (e.g. a common superclass/interface)?
To be best of all able to achieve what we have in mind, Chassis DSL tries to give you the most “adjustable” sub node nameAndWhereto
.
In combination with (TODO link) naming strategy resolution
(which is a bit tricky) you can adjust any naming knob for the generated classes, objects and interfaces that can wish for.
a complete nameAndWhereto { ... }
looks like the following
and can be place inside:
dslRun("runName").configure { ... }
modelgroup("mgName") { ... }
model("modelName") { ... }
dto|dco|tableFor|... { ... }
(at this place without the ability to specify for other model subelements)
nameAndWhereto {
baseDirAbsolute(absolute: String)
baseDirAbsolute(absolute: Path)
baseDir(concat: String)
baseDir(concat: Path)
pathAbsolute(absolute: String)
pathAbsolute(absolute: Path)
path(concat: String)
path(concat: Path)
classPrefixAbsolute(absolute: String)
classPrefix(concat: String)
classPrefixBefore(concat: String)
classPrefixAfter(concat: String)
classPostfixAbsolute(absolute: String)
classPostfix(concat: String)
classPostfixBefore(concat: String)
classPostfixAfter(concat: String)
basePackageAbsolute(absolute: String)
basePackage(concat: String)
packageNameAbsolute(absolute: String)
packageName(concat: String)
dtoNameAndWhereto {
// all the same of above
}
tableNameAndWhereto {
// all the same of above
}
}
again: to find out what interfaces the DSL node “enables” I recommend using intellij’s actions -> Navigate -> Goto by Reference Action -> Type Hierarchy
mode of operation
nameAndWhereto { ... }
deals with three things:
- the filesystem dir/path to where the class/object/interface is written to (without the package folder structure)
- the pre/postfixes of the class/object/interface (=model) name
- the
package
of the class/object/interface
For each of these three you set/alter two different variable values:
- global
- addendum
Also for each of the three things you have two variants of funcs:
- absolute ones
the underlying variable value is replaced by the given value - concat ones
the underlying variable value is appended/prepended with the given value
so. e.g. modelgroup can concat a prefix, model can concat another prefix and eventually the submodel another one
The global value might e.g. set in dslRun("runName").configure { ... }
) and the parsed modelgroups just operate on the addendum
This way you have fine granular power on specifying where things should go,
but still can deal with the eventual target on a higher level (e.g. modelgroup oder dslRun).
The real eventual values for dir, classname and package name depend on the implementations of the corresponding (TODO link Strategies) Strategies, which ultimately decide which values will win or overwritten or concatenated.
Naming strategies are evaluated in DslCtx.PASS_FINISH
in the respective finish()
functions of submodel DslImpl’s (dto, dco, tableFor, …)
(but also depend on preparation in finish() methods of modelgroup and model()
layout: page title: super classes and interfaces subtitle: class extends menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../assets/Chassis.png —
model and model elements super classes and interfaces
Either a model("modelName") { ... }
as well as its submoldels can specify that they extend a class or interface.
an extends { ... }
node can use the interface IDslApiExtendsProp
funcs:
var replaceSuperclass: Boolean
var replaceSuperInterfaces: Boolean
operator fun KClass<*>.unaryPlus() // + for super class
operator fun IDslRef.unaryPlus() // + for super class
/** inherit from same SubElement Type (e.g. dto/tableFor/...) with simpleName C.DEFAULT, of an element (e.g. model) in the same group which has this name */
operator fun String.unaryPlus() // + for super class (referencing this@modelgroup's name of ModelSubElement of this@modelsubelement(thisSimpleName)
operator fun KClass<*>.unaryMinus() // - for super interfaces
operator fun IDslRef.unaryMinus() // - for super interfaces
operator fun String.unaryMinus() // - for super interfaces
operator fun IDslApiExtendsProps.minusAssign(kClass: KClass<*>) // exclude from super class/interfaces
operator fun IDslApiExtendsProps.minusAssign(dslRef: DslRef) // exclude from super class/interfaces
operator fun String.not()
operator fun IDslApiExtendsProps.rem(docs: CodeBlock)
replaceSuperclass
and replaceSuperInterfaces
specify if any previous (in higher node levels) set superclasses/superinterfaces
should be replaced by the ones of the current extends { ... }
node.
If replaceSuperclass is false (the default) and the extends block sets a super class,
but a superclass already has been set on parsing a “higher up” node in the model hierarchy (e.g. model { extends { ... } }
)
a DslException will be thrown. (TODO: maybe better to let the Strategy decide??)
layout: page title: Chassis DSL crosscutting nodes subtitle: nodes that might appear under multiple nodes menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../assets/Chassis.png —
Crosscutting nodes that might appear under multiple nodes
For information how crosscutting nodes are implemented with interface delegation, see DSL block delegation showcase
Crosscutting nodes are nodes that can appear under multiple different other nodes.
Crosscutting nodes are implemented via Kotlin interface delegation to be able to stay “DRY”
(DRY = Don’t Repeat Yourself)
- DSL block delegation showcase
- name and whereto
- showcase (DSL Block Delegation)
- class mods: noop by now
- props and properties
- gather propertiesOf other (sub)model
- extends super classes and interfaces
layout: page title: Chassis DSL and codegen RUN subtitle: actually generating something from your Chassis DSL menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../assets/Chassis.png —
Actually generating something from your Chassis DSL
This is how you “execute” code generation from your Chassis DSL function(s):
object MainExamples {
@JvmStatic
fun main(args: Array<String>) {
val examplesRunName = "ExamplesRun"
val examplesDslRun = DslRun(examplesRunName)
examplesDslRun.configure {
nameAndWhereto {
baseDirAbsolute("../generated/examples/src/main/kotlin")
basePackageAbsolute("com.hoffi.generated.examples")
//dtoNameAndWhereto { ... }
}
}
// actually parsing the Chassis DSL here:
examplesDslRun.start("someDisc") {
baseModelsPersistent() // contains Chassis DSL top-level func
entitiesFunc() // contains Chassis DSL top-level func
dcosFunc() // contains Chassis DSL top-level func
}
// GenRun operates on the GenCtx, which was populated inside a DslRun's DslCtx
val examplesCodegenRun = GenRun(examplesDslRun.dslCtx.genCtx, examplesRunName)
examplesCodegenRun.start {
println("examples/${examplesCodegenRun.runIdentifier}".boxed(postfix = "\n"))
KotlinCodeGen(examplesCodegenRun).codeGen(MODELREFENUM.MODEL) // MODEL = ALL gen Subelements (DTO, DCO, TABLEFOR)
}
}
}
layout: page title: DslRef subtitle: Referencing Chassis DSL Elements menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../assets/Chassis.png —
DslRef and Referencing Chassis DSL Elements
tbd
layout: page title: Dsl Reffing subtitle: how to reference thing from other nodes menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../assets/Chassis.png —
DslRef and Referencing Chassis DSL Elements
There are several ways to reference another node in Chassis DSL..
Reffing: using a string representation of the DslRef
object DslRefString
(inside DslRef.kt
) offers some conversion of absolute full DslRef Strings (with or without DslDiscriminator)
to valid typed DslRef’s which you then could stuff into the DSL funcs as you like.
(but DslRefString is very basic right now, needs overhaul to use generic inline funcs to do so in the future)
reffing inside the same modelgroup node
tbd
Reffing: using infix methods of IDslApiModelReffing
DslImpls that implement IDslApiModelReffing
have some infix convenience function for (more or less) typesafe model reffing:
interface IDslApiModelReffing {
infix fun MODELREFENUM.of(thisModelgroupSubElementRef: IDslRef): DslRef.IModelOrModelSubelement
infix fun MODELREFENUM.of(thisModelgroupsModelSimpleName: String): DslRef.IModelOrModelSubelement // + for super class (referencing this@modelgroup's name of ModelSubElement MODELELEMENT.(DTO|TABLE)
infix fun MODELREFENUM.inModelgroup(otherModelgroupSimpleName: String): OtherModelgroupSubelementWithSimpleNameDefault // + for super class
infix fun OtherModelgroupSubelementWithSimpleNameDefault.withModelName(modelName: String): IDslRef
}
There is also a class DslImplModelReffing(val dslClass: ADslClass) : IDslApiModelReffing
that implements these so that the DslImpl doesn’t have to implement the interface by itself over and over again.
Inside Chassis DSL nodes that implement IDslApiModelReffing
you can get a DslRef by doing:
val aModelDslRef = (MODEL inModelgroup "someOtherModelgroupName" withModelName "someModelName")
val aDtoDslRef = (DTO inModelgroup "someOtherModelgroupName" withModelName "someModelName")
val aTableDslRef = (TABLE inModelgroup "someOtherModelgroupName" withModelName "someModelName")
// referencing with the same Chassis DSL modelgroup:
val aModelDslRef = (MODEL of "sameModelgroupModelName")
val aDtoDslRef = (DTO of "sameModelgroupModelName")
val aTableDslRef = (TABLE of "sameModelgroupModelName")
as non-parenthesis’ed infix functions are evaluated from left to right, you have to take care to set parenthesises the right way.
E.g. for a prefixed
crud
READ
with deepRestrictions
:
tableFor(DTO) {
crud {
prefixed("woModels") {
// left to right, so you have to use parenthesis'es around the DslRef
CRUDALL FOR (DTO inModelgroup ENTITYGROUP withModelName ENTITY__SOMEMODEL) deepRestrictions {
// left to right, so you have to use parenthesis'es around the DslRef
NEW model (DTO of ENTITY__SUBENTITY)
}
}
}
}
layout: page title: Chassis codegen subtitle: Generating Code menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../assets/Chassis.png —
using DSL outcome to generate Code
A GenRun operates on the GenCtx, which was populated inside a DslRun’s DslCtx
val examplesCodegenRun = GenRun(examplesDslRun.dslCtx.genCtx, examplesRunName)
examplesCodegenRun.start {
KotlinCodeGen(examplesCodegenRun).codeGen(MODELREFENUM.MODEL) // MODEL = ALL gen Subelements (DTO, DCO, TABLEFOR)
}
The GenCtx
contains the immutable result of a DslRun
in a “bite-sized” representation most suitable for code-generation.
Whilst performing code-generation a GenRun
reads information from the GenCtx
and might store/add information in its KotlinGenCtx
class GenCtxWrapper
wraps both to be neatly available to context(GenCtxWrapper)
classes and methods:
class GenCtxWrapper(val genCtx: GenCtx) {
val kotlinGenCtx = KotlinGenCtx._create()
}
implemented code generators
abstract class KotlinGenClassNonPersristent
- base class for e.g.
DTO
andDCO
, unrelated to persistent or other stuff (kind ofpojo
’s)
- base class for e.g.
abstract class KotlinGenExposedTable
- persistent table mapping generation (for Jetbrains Exposed db-framework)
abstract class AKotlinFiller
- base class for fillers (AND for persistent CRUD operations of
KotlinCrudExposed
)- KotlinFillerDto (nonPersistent stuff)
- KotlinFillerTable (persistent stuff) (for Jetbrains Exposed db-framework)
- base class for fillers (AND for persistent CRUD operations of
class KotlinCrudExposed
- CRUD (insert/select/update/delete) via Jetbrains Exposed db-framework
TODO codegen docs
maybe also some refactorings necessary…
how to get names from the strategies
sealed classes of codegen
etc. etc…
layout: page title: Code Architecture Overview subtitle: Arch Landing Page menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../../assets/Chassis.png —
The Chassis DSL for generating Code
Deployment Diagram (Chassis subprojects):
path: https://hoffimuc.com/assets/imagebinary/arch/drawio/Arch_Overview.drawio.svg
layout: page title: Dsl Conventions subtitle: DslApi…, DslImpl, Dsl…DelegateImpl, Dsl…BlockImpl menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../../assets/Chassis.png —
Chassis Dsl Conventions
implementing a DSL node
A Class implementing someNode { ... }
always is prefixed with DslImpl
and it’s first to constructor args are
- val simpleName: String
- val someNodeRef: IDslRef
It extens the abstract base class (of all DslImpl’s) ADslClass
.
And implements its IDslApiXxx
interfaces (which are used in the trailing lambda functions) e.g.:
override fun dslNodeName(simpleName: String, dslBlock: IDslApiXxx.() -> Unit) {
By letting the trailing lambda operate on an interface
IDslApi
we ensure that nothing else than defined in the IDslApi
is callable in the Chassis DSL (e.g. val log
might be visible in the Dsl if fun dslNodeName(..., dslBlock: DslImplSomeNode.() -> Unit) {
)
context(DslCtxWrapper)
class DslImplSomeNode(
val simpleName: String,
val someNodeRef: IDslRef,
...
) : ADslClass(),
IDslApiModel,
IdslApiSomeOther,
{
val log = LoggerFactory.getLogger(javaClass)
override val selfDslRef = modelRef
override fun dto(simpleName: String, dslBlock: IDslApiSomeNode.() -> Unit) {
val dslSomeNodeImpl: DslImplSomeNode = dslCtx.ctxObjCreate... { DslImplSomeNode(simpleName, DslRef.someNode(simpleName, selfDslRef)) }
}
Any DslApi...
must inherit from the top-level IDslApi
as this has the @DslMarker
/** DSL Contributing funcs and props (to get "DSL scoped"/@DslMarker marked) */
@ChassisDslMarker
interface IDslApi
/** all classes participating in the chassis DSL language must have this annotation for scope control</br>
* see https://kotlinlang.org/docs/type-safe-builders.html#scope-control-dslmarker */
@DslMarker annotation class ChassisDslMarker(vararg val impls: KClass<out IDslParticipator>)
DslImplXxx
hierarchies and their IDslApiXxx
Interface hierarchies can be a bit “overwhelming”,
I highly recommend using the Intellij Type Hierarchy
action.
layout: page title: DSL Block Delegation showcase subtitle: IDslApi by dslSomethingDelegateImpl menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../../assets/Chassis.png —
Dsl Block Delegation
Chassis DSL tries to use the DRY principle (Don’t Repeat Yourself) also for implementation of sub-DSL Structures, that may appear below multiple different DSL nodes.
Let’s take a showcase { ... }
substructure that may appear inside a model
as well as under a dto
,
model("someModel") {
showcase {
...
}
dto {
showcase {
...
}
}
}
Chassis solves this, by giving (creating via dslCtx) a delegate Instance (DslDelegateImpl) dslShowcaseDelegateImpl
which implements its corresponding IDslApiShowcase
and then delegates all sub-DSL Structure calls to it with IDslApiShowcase by dslShowcaseDelegateImpl
.
If you don’t know about kotlin interface delegation see the Kotlin Docs
context(DslCtxWrapper)
class DslModel(
val simpleName: String,
val modelRef: DslRef.model,
val dslShowcaseDelegateImpl: DslShowcaseDelegateImpl = dslCtx.ctxObjOrCreate(DslRef.showcase(simpleName, modelRef)),
) : ADslClass(),
IDslApiModel,
IDslApiShowcase by dslShowcaseDelegateImpl
{
val log = LoggerFactory.getLogger(javaClass)
override val selfDslRef = modelRef
The DslShocaseDelegateImpl
then implements the delegated node itself (NOT what is INSIDE that node!!!):
/** outer scope */
context(DslCtxWrapper)
class DslShowcaseDelegateImpl(
simpleNameOfDelegator: String,
delegatorRef: IDslRef
) : ADslDelegateClass(simpleNameOfDelegator, delegatorRef), IDslImplShowcaseDelegate {
override fun toString() = "${this::class.simpleName}(${theShowcaseBlocks.size})"
val log = LoggerFactory.getLogger(javaClass)
override val selfDslRef = DslRef.showcase(simpleNameOfDelegator, delegatorRef)
/** different gathered dsl data holder for different simpleName's inside the BlockImpl's */
override var theShowcaseBlocks: MutableMap<String, DslShowcaseBlockImpl> = mutableMapOf()
/** DslBlock funcs always operate on IDslApi interfaces */
override fun showcase(simpleName: String, block: IDslApiShowcaseBlock.() -> Unit) {
val dslImpl = theShowcaseBlocks.getOrPut(simpleName) { DslShowcaseBlockImpl(simpleName, selfDslRef) }
dslImpl.apply(block)
}
}
the DslShowcaseDelegateImpl
also holds one or more properties which the inner logic of the delegated node:
/** inner scope */
context(DslCtxWrapper)
class DslShowcaseBlockImpl(
val simpleName: String,
override val selfDslRef: IDslRef // that should be the Delegate of this and NOT the parentRef in the Dsl
) : ADslClass(), IDslImplShowcaseBlock
{
override fun toString() = "${this::class.simpleName}(${dslShowcasePropsData})"
val log = LoggerFactory.getLogger(javaClass)
// back reference to own DelegateImpl (from context or as function parameter)
val showcaseDelegate: DslShowcaseDelegateImpl = dslCtx.ctxObj(selfDslRef)
This way any DSL node that wants to have the showcase
node, just needs to add it to its constructor (creating it via context in its initializer)
and delegate the DslApiShowcase
to that constructor argument.
layout: page title: DSL Referencing subtitle: DslRef IDslRef menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../../assets/Chassis.png —
Dsl Referencing with DslRef and IDslRef
The ability to reference any DSL node from any other DSL node is one of the key features of Chassis DSL.
To achieve it any DslImpl
(no matter if a Delegate or a plain DslImpl) has to have a simpleName: String
and a selfDslRef: IDslRef
.
At its core a DslRef is a class in sealed class DslRef(...) : ADslRef(...) {
and
abstract class ADslRef(
override val simpleName: String,
override val parentDslRef: IDslRef,
override val refList: MutableList<DslRef.DslRefAtom> = mutableListOf()
) : IDslRef {
data class DslRefAtom(val dslRefName: String, val simpleName: String = C.DEFAULT)
As a DslRef ist a listOf DslRefAtom’s you easily see that the list represents uniquely the complete hierarchy in the Chassis Dsl.
The toString()
representation of a DslRef
clarifies the undlerlying principle (‘|’ separates DslRefAtom’s and ‘:’ separates the nodeName
and its simpleName
)
(you can ignore the leading DslDiscriminator … the use of that one has been refactored out somewhere in the past)
"disc:commonBasePersistentDisc|modelgroup:Persistentgroup|model:entity|dto:default|showcase:default"
Level | DslRefAtom nodeName | DslRefAtom simpleName |
---|---|---|
- | DslDiscriminator | commonBasePersistentDisc |
1 | modelgroup | Persistengroup |
2 | model | entity |
3 | dto | default |
4 | showcase | default |
to be a bit shorter, if the DslRefAtom simplename is default
then the :simpleName
parts of each is ommited and shortens to:
"disc:commonBasePersistentDisc|modelgroup:Persistentgroup|model:entity|dto|showcase"
As any DslImpl has to have as first two constructor Arguments simpleName: String, parentDslRef: IDslRef
(as these have to be passed to abstract super class ADslClass
)
any class DslImpl
can have a property val selfDslRef
as
class DslImplXxx(...) : ADslClass(simpleName: String, parentDslRef: IDslRef) {
override val selfDslRef = DslRef.nodeName(simpleName, parentDslRef)
}
the companion object { ... }
of class DslRef
also has some handy and neede convenience functions to extract lower level DslRefs from deeper nested DslRefs.
object DslRefString {
(defined in the same file as DslRef
) gives you some neat functions to convert the DslRef.toString()
representation into an actual
DslRef
of the right sealed DslRef class with the correct List<DslRef.DslRefAtom>
.
layout: page title: DSL Context subtitle: DslCtx menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../../assets/Chassis.png —
DslContext DslCtx passed through all of Chassis DSL parsing
DslContext holds all ADslClass
‘es (and more)
Beside taking care of the parsing PASS
‘es, the most central part is the inline function to create and manage all(!) ADslImpl classes via introspection:
context(DslCtxWrapper) // for calling constructor of ADslClass 1st parameter
inline fun <reified T : ADslClass> ctxObjOrCreate(dslRef: IDslRef): T {
...
}
context(DslCtxWrapper)
inline fun <reified T : ADslClass> ctxObjCreate(dslRef: IDslRef): T {
...
}
inline fun <reified T : ADslClass> ctxObj(key: IDslRef): T {
...
}
inline fun <reified T : ADslClass> addToCtx(aDslClass: T): T {
...
}
inline fun <reified T : ADslClass> ctxObjCreateNonDelegate(aDslClassCreateBlock: () -> T): T {
...
}
Using inline reified functions ensures that we can get a typesafe actual ADslClass
class instance by its IDslRef
from the DslCtx
and not just a generic ADslClass
// getting a specific `DslImplModel` and not just a base `ADslClass`
val dslModel: DslImplModel = dslCtx.ctxObj(parentDslRef)
layout: page
title: GenModel, ModelClassDataFromDsl,
ModelClassName and
EitherTypOrModelOrPoetTyp
subtitle: basic things to work with for code generation
menubar: data_menu_chassis
toc: true
show_sidebar: false
hero_image: ../assets/Chassis.png
—
bite-sized immutable Chassis DSL parsing result
Any “thing” you might want to generate code for eventual has a gazillion of variants, characteristics and subfeatures.
This usually leads to having a gazillion of when, if, then decisions in code generation code.
The following “two” things are, how Chassis tries to tackle this challenge.
sealed class GenModel
sealed class GenModel
is the (immutable!) bite-sized gathered and populated information about a Chassis DSL model { ... }
The “challenge” here is, that models can be all so different (Typ, model, class, poetType, collection, mutable, nullable, interface, …)
For each model subelement (DTO, DCO, TABLE, …) there is a sealed implementation of GenModel
.
sealed class GenModel extends ModelClassDataFromDsl
. Chassis tries to achieve a “uniform” programming experience this way.
As you can see you’ll almost never will work with the GenModel
itself, but just “passing it around” as GenModel …
but 99,9% of the time just using its ModelClassDataFromDsl
You can consider ModelClassDataFromDsl
as being the always alike “flesh” of any GenModel.
Any naming, path package (that is nameAndWhereto { ... }
related information + naming Strategies stuff)
is delegated to a GenModel’s ModelClassName
sealed class GenModel(modelSubElRef: DslRef.IModelSubelement, modelClassName: ModelClassName)
: ModelClassDataFromDsl(modelSubElRef, modelClassName) {
class DtoModelFromDsl(dtoRef: DslRef.dto, modelClassName: ModelClassName) : GenModel(dtoRef, modelClassName) { init { modelClassName.modelClassDataFromDsl = this } }
class TableModelFromDsl(tableRef: DslRef.table, modelClassName: ModelClassName) : GenModel(tableRef, modelClassName) { init { modelClassName.modelClassDataFromDsl = this } }
class DcoModelFromDsl(dcoRef: DslRef.dco, modelClassName: ModelClassName) : GenModel(dcoRef, modelClassName) { init { modelClassName.modelClassDataFromDsl = this } }
}
/** all props and sub-props are set on chassis DSL PASS_FINISH */
abstract class ModelClassDataFromDsl(
var modelSubElRef: DslRef.IModelSubelement,
val modelClassName: ModelClassName
) : Comparable<ModelClassDataFromDsl>,
IModelClassName by modelClassName
{
...
}
sealed class EitherTypOrModelOrPoetType
sealed class EitherTypOrModelOrPoetType
represents the possible Data Type of a model’s class or property.
Again the “challenge” here is, that a type can be many many things in any programming language.
Chassis tries to tackle this with uniform sealed classes also:
(note, that all these type representations also contain
a “shortcut” to its lateinit var modelClassName: ModelClassName
for convenience)
Both of Chassis’ DSL variants of properties KClass<*>
ClassName
(KotlinPoet) are represented by class EitherPoetType
sealed class EitherTypOrModelOrPoetType(override val initializer: Initializer) {
lateinit var modelClassName: ModelClassName
class EitherTyp(val typ: TYP, initializer: Initializer) : EitherTypOrModelOrPoetType(initializer)
class EitherModel(val modelSubElementRefOriginal: DslRef.IModelOrModelSubelement, initializer: Initializer) : EitherTypOrModelOrPoetType(initializer)
class EitherPoetType(val poetType: ClassName, override var isInterface: Boolean, initializer: Initializer) : EitherTypOrModelOrPoetType(initializer)
class NOTHING : EitherTypOrModelOrPoetType(Initializer.EMPTY)
}
layout: page
title: Dev Conventions
subtitle: Naming is one of the most important thing
in SW Development
menubar: data_menu_chassis
toc: true
show_sidebar: false
hero_image: ../assets/Chassis.png
—
Dev Conventions
Any time you have to decide (when(...) { }
) on something that might get further options if somewhere in the future is extended, e.g.:
atm a model { ... }
can only have dto { ... }
, dco { ... }
and tableFor { ... }
as sub nodes, but this might change in the future.
So if at any place in the sourcecode we have to do “something else” depending on the model sub-node,
there should be a whens lambda
, e.g. like in WhensDslRef
fun <R> whenModelSubelement(dslRef: IDslRef,
isDtoRef: () -> R,
isTableRef: () -> R,
isDcoRef: () -> R,
catching: (DslException) -> Throwable = { Throwable("when on '$dslRef' not exhaustive") }
): R {
when (MODELREFENUM.sentinel) { MODELREFENUM.MODEL, MODELREFENUM.DTO, MODELREFENUM.TABLE, MODELREFENUM.DCO -> {} } // sentinel to check if new MODELREFENUM was added
return when (dslRef) {
is DslRef.dto -> isDtoRef()
is DslRef.table -> isTableRef()
is DslRef.dco -> isDcoRef()
else -> throw catching(DslException("no (known) modelSubelement"))
}
}
and its usage example:
intersectPropsData.sourceVarName = WhensDslRef.whenModelSubelement(sourceGenModelFromDsl.modelSubElRef,
isDtoRef = { "source${intersectPropsData.sourceVarNamePostfix}" },
isDcoRef = { "source${intersectPropsData.sourceVarNamePostfix}" },
isTableRef = { "resultRow${intersectPropsData.sourceVarNamePostfix}" },
)
As you also can see there is a no-op sentinel line of code in fun whenModelSubelement(...)
:
when (MODELREFENUM.sentinel) { MODELREFENUM.MODEL, MODELREFENUM.DTO, MODELREFENUM.TABLE, MODELREFENUM.DCO -> {} }
As there is also an enum class MODELREFENUM
which is “relevant also for any decision on model-sub-node elements”
it is also added here, so that if you change it, you immediately get a compile error also at this code location
(which under other circumstances you might have forgotten to check and realise much more later whilst regression testing)
Using this convention, if a new model-sub-node is introduced and you add it to the fun <R> whenModelSubelement(...)
automagically all code locations show up in intellij with a compile error, that you have to “adjust”.
layout: page title: Universe subtitle: Things that are generated for you upfront menubar: data_menu_chassis toc: false show_sidebar: false hero_image: ../assets/Chassis.png —
Universe: Things that are generated for you upfront
We also need some “fix” classes that we use in generated code, like
- Annotations
object Defaults
with constant (like) definitions, e.g.DEFAULT_STRING
,NULL_STRING
,DEFAULT_UUID
,NULL_UUID
,DEFAULT_INSTANT
,NULL_INSTANT
- common base interface
WasGenerated
- a
class Dummy
- common base implementations, e.g.
IUuidDto
,UuidTable
- Helper Functions to be “DRY’
- PoetHelpers
Chassis will use its internal kotlinPoet to generate these for you
in a fixed package name and structure.
See object com.hoffi.chassis.shared.fix.Universe
layout: page title: Chassis Regression Testing subtitle: implementing new features and refactoring menubar: data_menu_chassis toc: true show_sidebar: false hero_image: ../assets/Chassis.png —
Regression Testing
Chassis chose Kotest as TestFramework, but ordinary JUnit5 Tests also work.
(Chassis is not really using DI (Dependency Injection) by now, but evaluated it using Koin.
Despite the Code atm not using DI extensively (as passing a Context with kotlin context(CtxWrapper)
proved kind of enough by now)
Chassis is prepared to use Koin DI in the future.
Especially the Testsuite is prepared to use Kotest with Koin in the Behaviour Driven Style.
Shared gradle TestFixure
s
For BDD Kotests to be more convenient the chassis /shared/build.gradle.kts
provides TestFixtures
in shared/src/testFixtures/kotlin/com/hoffi/chassis/shared/test
.
Namely:
package com.hoffi.chassis.shared.test
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.koin.KoinExtension
import io.kotest.koin.KoinLifecycleMode
import org.koin.test.KoinTest
@Suppress("UNCHECKED_CAST")
abstract class KoinBddSpec(val koinModules: List<org.koin.core.module.Module>, behaviorSpec: KoinBddSpec.() -> Unit): KoinTest, BehaviorSpec(behaviorSpec as BehaviorSpec.() -> Unit) {
constructor(vararg koinModules: org.koin.core.module.Module, behaviorSpec: KoinBddSpec.() -> Unit) : this(koinModules.asList(), behaviorSpec)
override fun extensions() = koinModules.map { KoinExtension(module = it, mockProvider = null, mode = KoinLifecycleMode.Root) }
}
And the KotestProjectConfig
see Kotest Project Level Config
package com.hoffi.chassis.shared.test
import io.kotest.common.ExperimentalKotest
import io.kotest.core.config.AbstractProjectConfig
import io.kotest.core.config.LogLevel
import io.kotest.core.extensions.Extension
import io.kotest.core.test.TestCase
import io.kotest.engine.test.logging.LogEntry
import io.kotest.engine.test.logging.LogExtension
class KotestProjectConfig : AbstractProjectConfig() {
override val globalAssertSoftly = true
override val logLevel = LogLevel.Info
override fun extensions(): List<Extension> = listOf(
object : LogExtension {
override suspend fun handleLogs(testCase: TestCase, logs: List<LogEntry>) {
logs.forEach { println(it.level.name + " - " + it.message) }
}
}
)
override suspend fun beforeProject() {
println("kotests: (beforeProject() of ${this::class.simpleName})")
}
override suspend fun afterProject() {
println("kotests finished. (afterProject() of ${this::class.simpleName})")
}
}
Gradle subProjects use these by declaring a special dependencies { ... }
dependency:
dependencies {
testFixturesImplementation(libs.bundles.testJunitKotestKoin)
}
buildLogic/libs.versions.toml
:
[libraries]
kointest = { module = "io.insert-koin:koin-test", version.ref = "kointest" }
kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotest-extensions-koin = { module = "io.kotest.extensions:kotest-extensions-koin", version.ref = "kotest-extensions-koin" }
kotest-framework-dataset = { module = "io.kotest:kotest-framework-datatest", version.ref = "kotest" }
kotest-framework-engine = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" }
kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
[bundles]
testJunitKotestKoin = [
"kointest",
"kotest-assertions-core",
"kotest-extensions-koin",
"kotest-framework-dataset",
"kotest-framework-engine",
]
Unfortunately I did not (yet) find a way to use src/test/resources/kotest.properties
from the shared project to the other subprojects)
ergo the resources/kotest.properties
for testing is unix softlinked in all subprojects!!!
Kotest with Koin DI (usaging of shared TestFixture’s KoinBddSpec)
The abstract class KoinBddSpec
above enables you to write Kotest Koin BDD Specs
with minimal boilerplate code on using Koin DI modules:
import io.kotest.common.ExperimentalKotest
import io.kotest.engine.test.logging.info
import io.kotest.matchers.string.shouldEndWith
import io.kotest.matchers.string.shouldNotEndWith
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.module.dsl.bind
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import org.koin.test.inject
// Kotest Koin BDD Test
class SmokeKoinBddSpec : KoinBddSpec(dummyModule, behaviorSpec = {
Given("an injected Dummy") {
val dummy: Dummy by inject()
When("calling f() on injected Dummy") {
info { "${SmokeKoinBddSpec::class.simpleName}: When(1)"}
val result = dummy.f()
Then("result should be 1") {
result shouldEndWith "with DummyDep(dp='depDummy', depDummy(1))"
}
Then("result should not be 2") {
result shouldNotEndWith "with DummyDep(dp='depDummy', depDummy(2))"
}
}
When("calling f() again on injected Dummy") {
info { "${SmokeKoinBddSpec::class.simpleName}: When(2)"}
val result = dummy.f()
Then("result should be 2") {
result shouldEndWith "with DummyDep(dp='depDummy', depDummy(2))"
}
}
}
})
// ===============================================================
// Fake stuff to demonstrate Kotest with Koin DI in BDD Spec Style
// ===============================================================
/** Koin dummy DI module */
val dummyModule = module {
factory { params -> DoIt(get(), params.get()) }
singleOf(::Dummy) { bind<IDummy>() }
singleOf(::DummyDep)
}
interface IDummy {
val p: String
fun f(): String
}
class Dummy (val dummyDep: DummyDep): IDummy {
//actual val dummyDep = dummyDep
override val p: String = "JVM"
override fun f(): String {
return "$p with $dummyDep"
}
}
class DummyDep {
override fun toString() = "DummyDep(dp='$dp', ${f()})"
val dp: String = "depDummy"
fun f() = "$dp(${count++})"
companion object {
var count = 1L
}
}
data class Par(val par: String)
class DoIt(val dummy: Dummy, val par: Par) : KoinComponent {
private val otherDummy: Dummy by inject()
fun doIt() {
println("par='${par}' ${dummy.f()}")
println("par='${par}' ${otherDummy.f()}")
}
}
Caveat Kotest does not find Kotests
You have to install the intellij Kotest plugin
for gradle to find Kotests you have to explicitly useJunitPlatform()
in your build.gradle.kts
tests configuration,
otherwise it will only find JUnit
Tests. (> No tests found
)
kotlin {
jvmToolchain(BuildLogicGlobal.jdkVersion)
tasks.withType<Test>().configureEach {
// since gradle 8.x JunitPlatform is the default and must not be configured explicitly anymore
useJUnitPlatform() // but if missing this line, kotlin kotests won't be found and run TODO
failFast = false
}
}