Skip to content

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)