Examples
Below several examples of migrations are offered.
Introduce a new protocol property
Consider the following protocol.
protocol[a] Before(var field1: Text) {};
Property field2 of type Number must be added. First, the original definition is replaced by the following
definition.
protocol[a] After(var field1: Text, var field2: Number) {};
This new field is populated with the value in the following migration.
migration("add property")
.transformProtocol(
currentTypeId = "/app-1.0.0?/example/Before",
targetTypeId = "/app-2.0.0?/example/After",
) {
put("field2") {
NumberValue(2)
}
},
Introduce a new protocol party
Consider the following protocol.
protocol[p1] Foo() { };
Party p2 must be added. First, the original definition is replaced by the following definition.
protocol[p1, p2] Foo() { };
This new party is added in the following migration.
migration("introduce a party")
.transformProtocol(
currentTypeId = "/app-1.0.0?/example/Foo",
targetTypeId = "/app-2.0.0?/example/Foo",
) {
parties(parties.single(), createParty(p2))
},
Replace an existing protocol property value
Consider the following protocol.
protocol[a] Foo(var n: Text) {};
Property n must be changed to some other value. This is accomplished in the following migration:
migration("replace value")
.transformProtocol(
currentTypeId = "/app-1.0.0?/example/Foo",
targetTypeId = "/app-2.0.0?/example/Foo",
) {
replace<TextValue>("n") {
createText("two")
}
},
Replace an existing protocol symbol value
Consider the following symbol and protocol definition.
symbol chf;
protocol[p] Foo(var u: chf) {};
If the current value of u is 0, then the value of u must be changed to 20. That is accomplished in the following
migration:
migration("replace symbol value")
.transformProtocol(
currentTypeId = "/app-1.0.0?/example/Foo",
targetTypeId = "/app-2.0.0?/example/Foo",
) {
if (get<SymbolValue>("u") ==
SymbolValue(NumberValue(0), NameType("chf"))
) {
replace<SymbolValue>("u") {
createSymbolValue(20, "chf")
}
}
},
Introduce an identifier property
Consider the following struct and protocol definition.
struct FavoriteStructId { id: Number };
protocol[owner] Before(firstItem: Text) {
var favorites: Map<FavoriteStructId, Text> = mapOf<FavoriteStructId, Text>()
.with(FavoriteStructId(1), firstItem);
permission[owner] addTitle(title: Text) { /* ... */ }
};
You would like to replace the user-defined struct definition with an identifier.
identifier FavoriteId;
protocol[owner] After() {
var favorites: Map<FavoriteId, Text> = mapOf<FavoriteId, Text>();
permission[owner] addTitle(title: Text) { /* ... */ }
};
This is accomplished in the following migration:
migration("transform user-defined value to a identifier value")
.transformProtocol(
currentTypeId = "/app-1.0.0?/example/Before",
targetTypeId = "/app-2.0.0?/example/After",
) {
replace<MapValue>("favorites") { oldMap -> // map of Pair<Number, Text>
val mapWithTransformedKeys = oldMap.value.mapKeys {
createIdentifier("/app-2.0.0?/example/FavoriteId")
}
createMap(LinkedHashMap(mapWithTransformedKeys))
}
},
Delete an existing protocol property
Consider the following protocol.
protocol[a] Before(var x: Number) {};
Property x must be removed. First, the existing definition is replaced by the following definition.
protocol[a] After() {};
This field is removed in the following migration.
migration("remove property")
.transformProtocol(
currentTypeId = "/app-1.0.0?/example/Before",
targetTypeId = "/app-2.0.0?/example/After",
) {
delete("x")
},
Collecting data using read
Reading values
Consider the following code.
protocol[a] Tiny(var n: Number) {};
We can collect the values of n from all instances of the protocol in the following migration.
val collectedNumbers = mutableListOf<NumberValue>()
migration("read and write")
.read("/app-1.0.0?/example/Tiny") {
collectedNumbers += get<NumberValue>("n")
}
collectedNumbers can then be used in transformations within the same migration.
Reading unions
Consider the following code.
union U { Text, Number };
protocol[p] Foo(var u: U) {};
The values u need to be collected in a step prior to an actual migration. The following migration accomplishes this.
val texts = mutableListOf<TextValue>()
val numbers = mutableListOf<NumberValue>()
migration("read union")
.read("/app-1.0.0?/example/Foo") {
when (val v = get<Value>("u")) {
is TextValue -> texts.add(v)
is NumberValue -> numbers.add(v)
else -> throw RuntimeException("Expected union value ${v::class}")
}
},
Note that if these do not need to be organized by type, they could also be stored in a List<Value> without the use of
a match-statement.
Creating new protocol instances using existing protocol data
Consider the following protocol.
protocol[p] Baz(var x: Text) { };
A new protocol Qux must be created that uses a value from Baz.
protocol[p] Qux(var x: Text) { };
The following migration emits a new protocol Qux, while Baz continues to exist.
migration("create protocol")
.transformProtocol(
currentTypeId = "/app-1.0.0?/example/Baz",
targetTypeId = "/app-2.0.0?/example/Baz",
) {
val x = get<TextValue>("x")
createProtocol("/app-2.0.0?/example/Qux", parties, listOf(x))
},
Replacing protocol instances using existing protocol data
Consider the following protocol.
protocol[p] Bar(var field1: Text) { };
It is to be replaced by a new protocol Foo that has the same value(s) as Bar.
protocol[p] Foo(var field1: Text) { };
The following migration emits a new protocol Foo, while Bar ceases to exist. Foo therefore gets Bar's
identifier.
migration("migrate protocol")
.transformProtocol(
currentTypeId = "/app-1.0.0?/example/Bar",
targetTypeId = "/app-2.0.0?/example/Foo",
),
The operation of transformProtocol and its various modes of operation are discussed in more detail here.
Split a protocol
Consider the following protocol.
protocol[p] Foo(var field1: Text, var field2: Number) { };
Two new protocols need to replace this protocol: Bar, which gets Foo's identity, and Baz, which is a new protocol
altogether.
protocol[p] Bar(var field1: Text) { };
protocol[p] Baz(var field2: Number) { };
The following migration replaces Foo with Bar, and also creates a new protocol Baz.
migration("split protocol")
.transformProtocol(
currentTypeId = "/app-1.0.0?/example/Foo",
targetTypeId = "/app-2.0.0?/example/Bar",
) {
val field2 = get<NumberValue>("field2")
createProtocol("/app-2.0.0?/example/Baz", parties, listOf(field2))
delete("field2")
},
A migration with multiple changes
Consider the following protocol.
identifier KeyId;
protocol[p] Foo(var id: Number, var beta: Number) {
initial state red;
state green;
var gamma: Text = "gamma";
var keyId: KeyId = KeyId();
};
The quite different protocol below is intended to replace it.
identifier Id;
protocol[p] Foo(var beta: Text) {
initial state wind;
state fire;
var zeta: Number = -1;
var id: Id = Id();
};
Note the use of replace, delete, put, get and state within one migration.
migration("multiple transformations")
// rename identifier type
.transformIdentifier(
currentTypeId = "/app-1.0.0?/example/KeyId",
targetTypeId = "/app-2.0.0?/example/Id",
)
.transformProtocol(
currentTypeId = "/app-1.0.0?/example/Foo",
targetTypeId = "/app-2.0.0?/example/Foo",
) {
// `beta` is changed into a string.
replace<NumberValue>("beta") { oldBeta ->
createText(oldBeta.value.toString())
}
// `gamma` is deleted and `zeta` is added (keep the old `id` Number value)
delete("gamma")
put("zeta") { get<NumberValue>("id") }
// rename identifier field from `keyId` to `id` (keep the old `keyId` identifier value)
put("id") {
withTransformations(get<IdentifierValue>("keyId")) { it }
}
delete("keyId")
// map the states
state("red" to "wind", "green" to "fire")
},
Using withTransformations
Consider the following definitions.
struct Bar1 { n: Number };
protocol[p] Foo1() {
var contents: List<Bar1> = listOf<Bar1>(Bar1(n = 2));
};
The following definitions need to replace the existing definitions. Note how not only does Bar1 change to Bar2, but
also turns from a List into a Set.
struct Bar2 { n: Number };
protocol[p] Foo2() {
var contents: Set<Bar2> = setOf<Bar2>(); // this is now a Set
permission[p] c() returns Number {
return 42;
};
};
The following migration accomplishes this:
migration("nested structs")
.transformStruct(
currentTypeId = "/app-1.0.0?/example/Bar1",
targetTypeId = "/app-2.0.0?/example/Bar2",
)
.transformProtocol(
currentTypeId = "/app-1.0.0?/example/Foo1",
targetTypeId = "/app-2.0.0?/example/Foo2",
) {
replace<ListValue>("contents") { old ->
// Change List to Set, and invoke the
// registered transformation for the struct
withTransformations(old) { transformed ->
// Due to withTransformations, 'transformed' is now
// a List of /app-2.0.0?/example/Bar.
// It does not know how to change the List to a Set.
createSet(transformed.value, type("/app-2.0.0?/example/Bar2"))
}
}
},
Note how type("/app-2.0.0?/example/Bar2") is used to retrieve the type reference of the Bar2 struct, which is a
user-defined type.
Using type
Consider the following protocol:
protocol[p] Before(var fooBar: List<FooBar>) {};
The protocol makes use of a user-defined type FooBar. FooBar is defined as a struct containing a Boolean and a
Number. There are also a couple of other user-defined types.
struct FooBar { foo: Boolean, bar: Number };
union Version { Number, Text };
struct VersionedFooBar { fooBar: FooBar, version: Version };
We now want our protocol to use versioned FooBars (Version being either a version Number or Text in this
example). We also want it to take another parameter, which is a list of Versions:
protocol[p] After(var fooBar: List<VersionedFooBar>, var baz: List<Version>) {};
The following migration transforms Before into After so that it uses VersionedFooBar rather than FooBar. It also
adds our new parameter (and populates the new field with a couple of bonus Version values).
Notice especially how we use the type function here to resolve the user-defined types. In the case of baz, this is
used to explicitly state that the elements of the list have a union type. Notice also how we do not need to provide
createList with a type -- if we leave it out, it can be inferred. As a rule of thumb this can be done whenever we're
not dealing with elements that have union types -- but if we are, it is better to specify the type in order to avoid
unforeseen consequences.
migration("type example")
.transformStruct(
currentTypeId = "/app-1.0.0?/example/FooBar",
targetTypeId = "/app-2.0.0?/example/FooBar",
)
.transformProtocol(
currentTypeId = "/app-1.0.0?/example/Before",
targetTypeId = "/app-2.0.0?/example/After",
) {
replace<ListValue>("fooBar") { oldList ->
createList(
oldList.value.map { oldFooBar ->
withTransformations(oldFooBar) { transformedFooBar ->
createStruct(
structTypeId = "/app-2.0.0?/example/VersionedFooBar",
values = mapOf(
"fooBar" to transformedFooBar,
"version" to createText("NONE"),
),
)
}
},
)
}
put("baz") {
createList(
listOf(createText("RELEASE"), createNumber(1)),
type("/app-2.0.0?/example/Version"),
)
}
},
Practical transformation patterns
The following examples demonstrate common patterns encountered in real-world migrations.
transformEnum: Simple version migration
When migrating an enum to a new version without changing variants, use a simple two-argument form:
val fromVersion = "1.0.0"
val toVersion = "2.0.0"
migration("$fromVersion to $toVersion")
.transformEnum(
"/app-$fromVersion?/product/Status",
"/app-$toVersion?/product/Status"
)
transformEnum: Renaming variants
When enum variants need to be renamed, provide explicit mappings:
migration("$fromVersion to $toVersion")
.transformEnum(
currentTypeId = "/app-$fromVersion?/product/Feature",
targetTypeId = "/app-$toVersion?/product/Feature",
mappings = mapOf(
"OLD_FEATURE_NAME" to "NEW_FEATURE_NAME",
"DEPRECATED_OPTION" to "CURRENT_OPTION"
)
)
Variants with identical names in both enums are mapped automatically and do not require explicit entries.
transformEnum: Protocol state consolidation
Use StatesEnumTypeId to transform protocol lifecycle states. This is useful when simplifying state machines or
removing deprecated states:
migration("$fromVersion to $toVersion")
.transformEnum(
currentDerivedTypeId = StatesEnumTypeId(protocolTypeId = "/app-$fromVersion?/workflow/Order"),
targetDerivedTypeId = StatesEnumTypeId(protocolTypeId = "/app-$toVersion?/workflow/Order"),
mappings = mapOf(
"DRAFT" to "ACTIVE",
"PENDING_REVIEW" to "ACTIVE"
)
)
.transformProtocol(
"/app-$fromVersion?/workflow/Order",
"/app-$toVersion?/workflow/Order"
) {
// Additional protocol transformations if needed
}
The transformEnum with StatesEnumTypeId automatically issues a state call on the corresponding
transformProtocol, so you don't need to specify state mappings twice.
transformStruct: Deleting fields
Remove deprecated fields from a struct:
migration("$fromVersion to $toVersion")
.transformStruct(
"/app-$fromVersion?/shared/CustomerInfo",
"/app-$toVersion?/shared/CustomerInfo"
) {
delete("legacyId")
delete("deprecatedField")
}
transformStruct: Restructuring fields
Rename or reorganize fields while preserving data:
migration("$fromVersion to $toVersion")
.transformStruct(
"/app-$fromVersion?/shared/ContactPerson",
"/app-$toVersion?/shared/ContactPerson"
) {
// Create a new nested struct from an existing field
put("contactDetails") {
withTransformations(get<StructValue>("contact")) { it }
}
delete("contact")
}
transformStruct: Creating union fields
Convert a simple reference to a union type:
migration("$fromVersion to $toVersion")
.transformStruct(
"/app-$fromVersion?/task/TaskItem",
"/app-$toVersion?/task/TaskItem"
) {
val entityRef = get<ProtocolReferenceValue>("entity")
val taskType = get<EnumValue>("type")
put("scope") {
createUnion(
"/app-$toVersion?/task/TaskScope",
if (taskType.variant == "GLOBAL") {
withTransformations(entityRef) { it }
} else {
// Create a different union variant based on type
createStruct(
"/app-$toVersion?/task/LocalScope",
mapOf("reference" to entityRef)
)
}
)
}
delete("entity")
}
transformProtocol: Conditional text replacement
Transform text values with conditional logic:
migration("$fromVersion to $toVersion")
.transformProtocol(
"/app-$fromVersion?/product/Component",
"/app-$toVersion?/product/Component"
) {
replace<TextValue>("name") {
if (it.value == "Old Product Name") {
TextValue("New Product Name")
} else {
it
}
}
}
transformProtocol: Converting mandatory to optional fields
Wrap existing values in Option types:
migration("$fromVersion to $toVersion")
.transformProtocol(
"/app-$fromVersion?/entity/Record",
"/app-$toVersion?/entity/Record"
) {
val existingValue = get<NumberValue>("legacyId")
delete("legacyId")
put("legacyId") {
createOptional(existingValue)
}
}
transformProtocol: Adding new fields with default values
Add new required fields with computed or default values:
migration("$fromVersion to $toVersion")
.transformProtocol(
"/app-$fromVersion?/entity/Item",
"/app-$toVersion?/entity/Item"
) {
put("category") {
createEnum("/app-$toVersion?/shared/Category", "DEFAULT")
}
put("createdAt") {
createDateTime(ZonedDateTime.now())
}
}
transformProtocol: State transformation with custom logic
Transform protocol states using conditional logic:
migration("$fromVersion to $toVersion")
.transformProtocol(
"/app-$fromVersion?/workflow/Process",
"/app-$toVersion?/workflow/Process"
) {
state {
when (it) {
"DRAFT" -> "ACTIVE"
"PENDING" -> "ACTIVE"
"CANCELLED" -> "TERMINATED"
else -> it
}
}
}
transformProtocol: Creating new protocols during transformation
Create additional protocol instances as part of a transformation:
migration("$fromVersion to $toVersion")
.transformProtocol(
"/app-$fromVersion?/container/Container",
"/app-$toVersion?/container/Container"
) {
val validators = get<ProtocolReferenceValue>("validators")
// Create a new protocol instance
createProtocol(
"/app-$toVersion?/product/NewComponent",
parties,
listOf(
createText("Auto-generated Component"),
createEnum("/app-$toVersion?/product/ComponentType", "SYSTEM"),
withTransformations(validators) { it }
)
)
}
transformProtocol: Grouping fields into new structs
Reorganize protocol fields by grouping related fields into struct types:
migration("$fromVersion to $toVersion")
.transformProtocol(
"/app-$fromVersion?/contract/Agreement",
"/app-$toVersion?/contract/Agreement"
) {
// Create a new struct grouping contact-related fields
val contactInfo = createStruct(
"/app-$toVersion?/contract/ContactInfo",
mapOf(
"address" to withTransformations(get<StructValue>("address")) { it },
"email" to get<TextValue>("email"),
"phone" to get<TextValue>("phone")
)
)
// Create another struct for billing settings
val billingSettings = createStruct(
"/app-$toVersion?/contract/BillingSettings",
mapOf(
"frequency" to withTransformations(get<EnumValue>("billingFrequency")) { it },
"method" to withTransformations(get<EnumValue>("paymentMethod")) { it }
)
)
// Add the new grouped fields
put("contactInfo") { withTransformations(contactInfo) { it } }
put("billingSettings") { withTransformations(billingSettings) { it } }
// Remove the original ungrouped fields
delete("address", "email", "phone", "billingFrequency", "paymentMethod")
}
Read-then-transform pattern
Collect data from multiple protocols before applying transformations. This pattern is essential when transformations depend on data from other protocol instances:
// Maps to collect data during read phase
val entityToConfig = mutableMapOf<ProtocolReferenceValue, EnumValue>()
val parentToChildren = mutableMapOf<ProtocolReferenceValue, SetValue>()
migration("$fromVersion to $toVersion")
// Read phase: collect data from related protocols
.read("/app-$fromVersion?/entity/Config") {
val entityRef = get<ProtocolReferenceValue>("entity")
val configType = get<EnumValue>("type")
entityToConfig[entityRef] = configType
}
.read("/app-$fromVersion?/entity/Parent") {
parentToChildren[reference] = get<SetValue>("children")
}
// Transform phase: use collected data
.transformProtocol(
"/app-$fromVersion?/entity/Item",
"/app-$toVersion?/entity/Item"
) {
// Use data collected in read phase
val config = entityToConfig[reference]
if (config != null) {
put("configType") {
withTransformations(config) { it }
}
} else {
put("configType") {
createEnum("/app-$toVersion?/entity/ConfigType", "UNKNOWN")
}
}
}
Modifying set fields
Add or modify elements in set fields:
migration("$fromVersion to $toVersion")
.transformProtocol(
"/app-$fromVersion?/container/Registry",
"/app-$toVersion?/container/Registry"
) {
replace<SetValue>("components") { existingSet ->
withTransformations(existingSet) { transformedSet ->
// Add a new element to the set
transformedSet.value.add(
createStruct(
"/app-$toVersion?/product/Component",
mapOf(
"name" to createText("New Required Component"),
"enabled" to createBoolean(true)
)
)
)
transformedSet
}
}
}
Complete migration with prototype mapping
A comprehensive migration using mapPrototypesInMigration for cleaner code:
val prototypes = mapPrototypesInMigration(
overrideEnums = listOf(
IdPair("/app-$fromVersion?/entity/OldType", "/app-$toVersion?/shared/NewType")
),
overrideStructs = listOf(
IdPair("/app-$fromVersion?/shared/LegacyInfo", "/app-$toVersion?/shared/ModernInfo")
),
overrideProtocols = listOf(
// New protocol in target (no source)
IdPair("", "/app-$toVersion?/workflow/NewProcess")
)
)
val order = prototypes.match("Order")
val orderStatus = prototypes.match("OrderStatus")
migration("${prototypes.current} to ${prototypes.target}")
.transformEnum(
orderStatus.current,
orderStatus.target,
mapOf("PENDING" to "PROCESSING")
)
.transformProtocol(order.current, order.target) {
put("version") {
createNumber(2)
}
}
// Re-tag all other unchanged prototypes
.retag(prototypes)