Migration DSL
NPL Migrations are used to modify existing prototypes or introduce new ones. The Kotlin migration DSL (Domain Specific Language) is used to write such is the tool to write migrations.
Migration integration
In a big project migrations can be very big and in many cases contain a lot of hard-coded strings, repetitive blocks, and boilerplate. This invites a lot of copy/paste practices that can lead to errors.
To help with this both the migration runner and DSL provide some convenience functions to make migrations more concise and less error-prone.
In a typical migration only a small portion of the protocol instances need a custom transformation that alters their
states/data. The others merely need to have the instance ID (prototypeId) which they point to, changed. This should not
be thought of as a no-op. The new NPL code can contain substantial logic changes for example. The platform dsl provides
the retag()
(see: retag in noumena docs) operation that will simplify the re-tagging of many prototypes.
The migration runner provides a function called mapPrototypesInMigration
to discover all the prototype IDs in a
migration. The function reduces the amount of hard-coded values in migration scripts and allows connecting any script
with the migration context. mapPrototypesInMigration
is built on top of a simpler convenience function in the DSL:
com.noumenadigital.platform.migration.dsl.MigrationHelperKt.mapPrototypes()
.
The simplest way to use mapPrototypesInMigration
function in a migration script is as the following:
val protos = mapPrototypesInMigration()
val contract = protos.match("Contract")
val tac = protos.match { endsWith("TermsAndConditions") }
migration("${protos.current} to ${protos.target}")
.transformProtocol(contract.current, contract.target) {
// do some transformation
}
.transformProtocol(tac.current, tac.target) {
// do some transformation
}
.retag(protos) // re-tag all other protocols, structs, identifiers, and enums
mapPrototypesInMigration()
will read the migration.yml
file loaded at runtime and create a PrototypeIdPairs
instance that maps the last two changesets (second to last as 'current' and last as 'target'). This should be all that
is needed in many situations.
Using match
Finding an IdPair
-matching 'current' prototype can be done in two ways:
With the default matcher:
val contract = protos.match("Contract")
The above will try to perform a simple match on prototypes containing "Contract". As a consequence it will not work for example if there are prototypes having "Contract" in the name with suffixes (e.g. "Contract", "ContractA").
With custom matcher:
val tac = protos.match { endsWith("TermsAndConditions") }
Using custom matcher gives a possibility to apply any matcher. In the provided example endsWith
is used to match,
therefore if there are prototypes with suffixes (like "TermsAndConditionsB"), the matcher will still work correctly and
match only the prototype ending with "TermsAndConditions".
Special cases
mapPrototypesInMigration()
supports some simple special cases. However, if a migration is too complicated it might
make sense to build the com.noumenadigital.platform.migration.dsl.PrototypeIdPairs
by hand, or use the lower-level
com.noumenadigital.platform.migration.dsl.MigrationHelperKt.mapPrototypes()
function.
mapPrototypesInMigration()
arguments allow to:
- override protocols
- override structs
- override identifiers
- override enums
- override the current and/or target tag for all prototypes
Adding a prototype in target
When a new protocol appears in the target npl there will be no re-mapping to do. So an override should be added to indicate that it should be ignored.
val protos = mapPrototypesInMigration(overrideProtocols = listOf(IdPair("", "/sys-2.0?/SomeNewThing")))
note: similar overrides can also be specified for structs, identifiers, and enums
Discarding a prototype
When a prototype in the current system should not be carried forward then mark it as such with an override. Then
retag()
will not try to re-tag it when no transforms have been defined.
val protos = mapPrototypesInMigration(overrideProtocols = listOf(IdPair("/sys-1.0?/SomeOldThing", "")))
note: similar overrides can also be specified for structs, identifiers, and enums
Renaming a prototype
When a prototype has been renamed then the prototype mapper cannot derive these automagically. In this case an override
should be added. The target value in the IdPair
must match the name in the npl for this override to be applied.
val protos = mapPrototypesInMigration(
overrideProtocols = listOf(
IdPair(
"/sys-1.0?/SomeBadlyNamedThing",
"/sys-1.0?/SomeBetterNamedThing"
)
)
)
note: similar overrides can also be specified for structs, identifiers, and enums
Custom current
and/or target
tags
If a system already containing npl and protocol instances is being bootstrapped during a migration for the first time then we need a migration that will take whatever the state of the current system is to a known migrated state.
The migration descriptor file could look as follows:
systemUnderAudit: sys
- name: 1.0
changes:
- migrate:
dir-list: npl/app
script: migration.kts
In migration.kts
we should initialize the prototype pairs with the appropriate current tag which can not be derived
from the migration.yml
file.
// if no tag was used
val protos = mapPrototypesInMigration(current = "")
// if some arbitrary tag was used
val protos = mapPrototypesInMigration(current = "arbitrary-name-alpha")