All Chassis docs in a single page

Chasiss DSL and Code Generator

All Chassis DSL and Code Generator docs as a single page

back to root

(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 PASSes 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.

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 -> {}
        }
    }

back to root


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.


back to root


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:

  1. TYP properties, of a Chassis predefined type (see below)
  2. KClass<*> properties
  3. poetType: TypeName properties
  4. model properties (a reference to another model or modelsubelement of the Chassis DSL)

currently predefined TYPs 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
}

back to root


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:

  1. the filesystem dir/path to where the class/object/interface is written to (without the package folder structure)
  2. the pre/postfixes of the class/object/interface (=model) name
  3. the package of the class/object/interface

For each of these three you set/alter two different variable values:

  1. global
  2. addendum

Also for each of the three things you have two variants of funcs:

  1. absolute ones
    the underlying variable value is replaced by the given value
  2. 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()


back to root


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??)


back to root


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)


back to root


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)
        }
    }
}

back to root


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 and DCO, unrelated to persistent or other stuff (kind of pojo’s)
  • 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)
  • 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…


back to root


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

link to DSL docs

Deployment Diagram (Chassis subprojects):

Chassis Deployment

path: https://hoffimuc.com/assets/imagebinary/arch/drawio/Arch_Overview.drawio.svg

Arch_Overview

ShowcaseClassUML

hero


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

link to DSL docs

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

link to DSL docs

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>.

link to DSL docs


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)

link to DSL docs


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

Back


back to root


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 TestFixures

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
    }
}


back to root