Project Chassis Kotlin Code-Generator
Over and over and over again…
TL;DR:
- have a look at the DSL chassis DSL examples
- find generated (non persistence related) DTO kotlin code in github generated DTO
- find generated (persistence related) exposed tables and CRUD operations in github generated TABLE
current example DSL (4 DTOs, 1 DCO with Fillers and DB CRUDs plus 4 abstract DTOs Base-Classes):
| | lines | #files |
|:---------:|-------:|--------:|
| DSL | 319 | 3 |
| generated | 2190 | 43 |
| % | ~700 % | ~1400 % |
LIST rows.file.link
WHERE draft = false OR draft = null
SORT file.name ASC GROUP BY file.folder
SORT file.name asc
hardcoded
- codegen:
- dev:
- dev/arch:
- dsl:
- intro:
The Story
Whilst my extensive career in SW consulting and development I realized … 80% of the work is marshalling and unmarshalling data structures and copying properties between hierarchies of related but independent (sub)datastructures consistently.
From the Internet (json, gRPC, XML, you name it) to your deserialized object, split up to x business objects, recursively copied to your persistent objects to be written to a database, and then back again towards the internet. Every little adding of a model or even a property to some object causes a gazillion of files to be changed consistently.
Consistently also means: in your backend, in your middle-services, in your frontend(s) in your API specs … (this also means in several independent repositories … (btw: openAPI sucks)
All so often I saw those changes being the cause for broken tests, CICD pipelines (or even production takeouts).
Also - over time - any project evolves several variants for each (un)marshalling and CRUD operation. In one case you may want to load an object “flat”, in another cases with “a little bit” of contained objects, sometimes all the object tree (loading half of the database redundantly) and sometimes some (sub)Objects need some nasty inner transformations (because business (sub)domains often differ in just “nasty tiny details”).
Code Generation
While code generation with text templates ever was a pain in the arse (and ever will be), since the last century frameworks evolved that are really “usable” for the purpose of code-generation.
KotlinPoet is one of these really nice frameworks.
And as Kotlin also has the nice feature of trailing lambda parameters (the function-implementation
can be placed outside of the function call parentheses), we can use pure kotlin code as DSL (no json, no yaml, no toml, just code).
(good thing: security is no issue in code generation … as we’re in our code-project anyways)
(BTW: Chassis DSL can reference any DSL Subelement from any other DSL Subelement,
no more xml/yml/json referencing hell…
and as the DSL is pure Kotlin code, we get syntax highlighting, code formatting and coloring
for free in the IDE of your choice)
Chassis CodeGenerator to the rescue
So the DSL “definition” of an entity dto and an entity dco that share some properties and have the same super-classes can easily look like:
modelgroup(ENTITYGROUP) {
constructorVisibility = PROTECTED
model(ENTITY__BASEINTERFACE) {
kind = INTERFACE
dto {}
}
model(ENTITY__ENTITY) {
extends {
+ (MODEL inModelgroup PERSISTENTGROUP withModelName COMMON__PERSISTENT_OPTIMISTIC)
- ENTITY__BASEINTERFACE
}
property("name", TYP.STRING, mutable, Tag.CONSTRUCTOR, Tag.HASH_MEMBER, Tag.TO_STRING_MEMBER)
property("value", TYP.STRING, mutable, length = 4096, Tag.CONSTRUCTOR, Tag.DEFAULT_INITIALIZER, Tag.HASH_MEMBER, Tag.TO_STRING_MEMBER)
property("aLocalDateTime", TYP.LOCALDATETIME, mutable)
property("someObject", Dummy::class, mutable, Initializer.of("%T.%L", Dummy::class, "NULL"), length = C.DEFAULT_INT, Tag.TRANSIENT)
property("someModelObject", DTO of ENTITY__SOMEMODEL, mutable)
property("subentitys", "modelgroup:$ENTITYGROUP|model:$ENTITY__SUBENTITY", DTO, COLLECTIONTYP.SET, Tag.CONSTRUCTOR, Tag.DEFAULT_INITIALIZER, Tag.NULLABLE)
property("listOfStrings", TYP.STRING, COLLECTIONTYP.LIST, Tag.COLLECTION_IMMUTABLE, Tag.CONSTRUCTOR)
dto {
property("dtoSpecificProp", TYP.STRING, mutable, Tag.CONSTRUCTOR, Tag.DEFAULT_INITIALIZER)
initializer("prio", APPEND, "/* some dto prio comment */")
addToStringMembers("dtoSpecificProp", "createdAt")
}
tableFor(DTO) {
propertiesOf(DTO, GatherPropertiesEnum.PROPERTIES_AND_SUPERCLASS_PROPERTIES)
initializer("name", APPEND, ".uniqueIndex()")
initializer("prio", APPEND, "/* some table prio comment */")
crud {
STANDARD FOR DTO
}
}
}
filler {
+DTO // DTO filled by a DTO
DTO mutual TABLE
DTO from (DTO of ENTITY__SUBENTITY)
}
}
On generating Code from above’s Chassis DSL
- you get static “Fillers” to recursively copy any object to any(!) other object (as long as the prop names and their types are compatible)
- database CRUD (insert, select, update, delete) static methods to operate towards and from the RDBMS (currently only via JetBrains exposed)
- read all contained objects in one SQL (via JOIN)
- read via separate selects (for contained models)
- via DSL specification of CopyBoundrys you can generate variants for each of these operations
- omiting subtrees or types or prop-names, or replacing them or transforming them …
Your business code can (recursively ;) ) concentrate on what there is to solve for business:
transaction {
CrudSimpleEntityTableCREATE.batchInsertDb(simpleEntityDtoList)
}
...
var selectedEntityDtos = transaction {
CrudSimpleEntityTableREAD.readByJoin {
(SimpleEntityTable.prio lessEq 3)
}
}
This is what you’re (currently) able to generate (all kotlin)
- Dto classes/interfaces/objects (Data-Transfer-Objects)
- properties with “known” types
- mutable and immutable (currently
Integer
,Long
,String
,Boolean
,java.util.uuid
,kotlinx.datetime.Instant
,kotlinx.datetime.LocalDateTime
(see TYP.kt) - mutable and immutable collection types (currently
List
,Set
,Collection
,Iterable
) - nullable, default initializers or initializers specified in DSL
- mutable and immutable (currently
- properties by referencing their KClass<…>, mutable and immutable (also for collection generic-type)
- nullable or initializers specified in DSL
- properties by referencing other
model
types defined somewhere else in (other) chassis DSLs - by Tagging properties in the Dsl, they can “contribute” to generated things e.g.
toString()
equals()
hashCode()
- primary (public/protected)
constructor
(via Tag on props) companion object
create()
createWithUuid()
val NULL = ...
(therefore, no need to have nullable model variables anymore!)
- no generic types yet (TBD)
- extending super classes and interfaces
- also by referencing other DSL model instances as super type or interfaces
- ability to specify “common” super classes/interfaces for all models in a modelgroup or all model instances in a model
- gather (add) all properties (or/and of that ones super classes) by just saying e.g.:
propertiesOf(DTO inModelgroup "PERSISTENTGROUP" withModelName "SomeModelName")
- use this to define an abstract model (code generation might be suppressed for that one) and reference its props from models in your DSL which should also have exactly THAT set of props (DRY).
- properties with “known” types
- Fillers
- generate static copy methods to fill any DSL model from any other DSL model
(as long as the prop names and their types are compatible) - specifying CopyBoundrys to specify in DSL how “deep” to copy model objects (or e.g. instead do not recurse but give fresh initialized model instances to a prop)
- Dto <—> Dto
- Dto <—> Dco
- Table (SQL resultRow) <—> Dto
- generate static copy methods to fill any DSL model from any other DSL model
- Table RDBMS Objects (currently for https://github.com/JetBrains/Exposed)
- by just specifying
tableFor(DTO)
in the DSL model- no many2many mappings yet
- one2one to another DSL Model
- one2many, many2one to another DSL Model
- if the model has a property named
uuid
with TYPjava.util.Uuid
- generate PRIMARY KEY to Table and enable FOREIGN KEY integrity on generated jetbrain exposed Tables
- DSL enable to set DB related property stuff, like
PK
,nullable
,index
, etc.
- by just specifying
- CRUD DB access methods (CREATE/insert, READ/select, UPDATE/update, DELETE/delete)
- either via DB
joins
of all (recursive) contained DSL model objects
(inside the DSL model object you want to “CRUD”)
model instance has to have aPK named 'uuid' for join's to work
- or via distinct
select
s to the db for each (recursive) contained DSL model object - specifying CopyBoundrys to specify in DSL how deep to “CRUD” from DB
- this will generate separate
filler
andCRUD
Methods for each “set/prefix/businessCase” that needs its own CopyBoundrys
- this will generate separate
- either via DB
using:
- ability to use/references other defined models and modelinstances to construct your code class
- reference another DSL model/instance as super-class, super interface, prop type, prop collection type
- specify abstract base classes and use their properties and super-class/interfaces to “include” directly (without extending class inheritance)
(propertiesOf(DTO inModelgroup "PERSISTENTGROUP" withModelName "SomeModelName")
)
- flexible naming strategies for
- naming props, classes/interfaces/objects, methods, table_names, column_names,
(CamelCase, snake_case, kebab-case, capitalized, decapitalized, prefixed, postfixed, replace pre/postfix, add to current pre/postfix, etc.)
- naming props, classes/interfaces/objects, methods, table_names, column_names,
- flexible destination spec in DSL
- absolute package, addendum pre/postfix to package, same for classe names and same for destination path of generated classes
- either in DSL run or
- modelgroup (for all models and model instances (dto/table/…))
- model (for all model instances of that model (dto/table))
- model instance
- implement your own strategy like
SPECIAL_WINS_ON_ABSOLUTE_STUFF_BUT_CONCAT_ADDENDUMS
- absolute package, addendum pre/postfix to package, same for classe names and same for destination path of generated classes
- take preferred defaults without having to specify too much in the DSL
- just switch the strategies and get “your corporate design governance” compliant code
- just switch the strategies and get “your corporate design governance” compliant code
and on top still very alpha and very work in progress!!!
Known Limitations
- no Model classes with generic types, no List/Set/Collections which hold a generic type (sure works for
List<MyModel>
) - no many2many RDBMS table mapping yet
- cannot have more than one FK Relation to the same Model (e.g. one2Many to ModelX and on2One also to ModelX)
- primary key is
val uuid : java.util.Uuid
(nothing else implemented yet … no, no Integer PKs, no autoincrements) - DTOs get their PK UUID on instantiation in code … so they have their “identity” given at time of “birth” (that is object instantiation)
(otherwise each model instance would have NO IDENTITY until they are written to the DB for the first time) - many loose ends which are not implemented yet
- many corner cases that will explode as by now I was concentrating on the “how-to”s and “architecture” instead of feature-completeness and robustness
TODO
current ToDos:
- implementing COLLECTION, ITERABLE prop types
- (additional/remove)ToString members on
DslModelgroup
- Primary Keys and FK Constraints on Database Tables (if not UuidTable)
- Cascading delete
coming up:
-
write the docs on arch, principles, modules and code generation to enable people to participate in this project
-
DB access: SQL UPDATE and DELETE Functions (honouring contained model instances and CopyBoundrys)
- custom jetbrain exposed lambda parameters to “tweak stuff” on DB CRUD operations from Table’s with CopyBoundries (eager/lazy loading via different generated functions)
- further CRUD DB operations with “own function names” for each CopyBoundry
-
Exposed Extensions
- upsert ?
- insert/delete if not exists stackoverflow: how-can-i-do-insert-if-not-exists-in-mysql
-
many to many relations
-
Properties with generic types
-
collections with generic generic types
-
properties with generic types that are models
-
collections with generic types that are models
-
DB mappings for above properties
Future TODOs
- primary key of more than one column
- more generics on models and functions
- generating other things than kotlin code (e.g. openAPI spec)
- generating API code (REST, gRPC, ProtoBuffers, …)
- generating PlantUML
- …