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("replace").transformProtocol(beforeTypeId, afterTypeId) {
    put("field2") {
        NumberValue(2)
    }
},

Introduce a new protocol party

Consider the following protocol.

protocol[p1] Foo0() { };

Party p2 must be added. First, the original definition is replaced by the following definition.

protocol[p1, p2] Foo1() { };

This new party is added in the following migration.

migration("introduce_party").transformProtocol(
    currentTypeId = MigrationTester.typeId("Foo0"),
    targetTypeId = MigrationTester.typeId("Foo1"),
) { 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_prop_val").transformProtocol(
    currentTypeId = MigrationTester.typeId("Foo"),
    targetTypeId = MigrationTester.typeId("Foo"),
) {
    replace<TextValue>("n") {
        createText("two")
    }
},

Replace an existing protocol symbol value

Consider the following symbol and protocol definition.

symbol pas;
protocol[p] Foo(var u: pas) {};

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("modify symbol value")
    .transformProtocol(
        MigrationTester.typeId("Foo"),
        MigrationTester.typeId("Foo"),
    ) {
        if (get<SymbolValue>("u") ==
            SymbolValue(NumberValue(0), NameType("pas"))
        ) {
            replace<SymbolValue>("u") {
                createSymbolValue(20, "pas")
            }
        }
    },

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(
        MigrationTester.typeId("Before"),
        MigrationTester.typeId("After"),
    ) {
        replace<MapValue>("favorites") { oldMap -> // map of Pair<Number, Text>
            val mapWithTransformedKeys = oldMap.value.mapKeys {
                createIdentifier(MigrationTester.typeId("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")
    .transformProtocol(beforeTypeId, afterTypeId) {
        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-write")
    .read(MigrationTester.typeId("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("readUnion")
    .read(MigrationTester.typeId("Foo")) {
        when (val v = get<Value>("u")) {
            is NumberValue -> numbers.add(v)
            is TextValue -> texts.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("v1")
    .transformProtocol(
        MigrationTester.typeId("Baz"),
        MigrationTester.typeId("Baz"),
    ) {
        val x = get<TextValue>("x")
        createProtocol(MigrationTester.typeId("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("v1")
    .transformProtocol(
        currentTypeId = MigrationTester.typeId("Bar"),
        targetTypeId = MigrationTester.typeId("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("v1").transformProtocol(
    currentTypeId = MigrationTester.typeId("Foo"),
    targetTypeId = MigrationTester.typeId("Bar"),
) {
    val field2 = get<NumberValue>("field2")
    createProtocol(MigrationTester.typeId("Baz"), parties, listOf(field2))
    delete("field2")
},

A migration with multiple changes

Consider the following protocol.

identifier KeyId;

protocol[p] Foo1(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] Foo2(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")
    .transformIdentifier(
        MigrationTester.typeId("KeyId"),
        MigrationTester.typeId("Id"),
    ) // rename identifier type
    .transformProtocol(MigrationTester.typeId("Foo1"), MigrationTester.typeId("Foo2")) {
        // `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() {
    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() {
    // This is now a Set, rather than a List
    contents: Set<Bar2> = setOf<Bar2>();

    permission[p] c() returns Number {
        return 42;
    };
};

The following migration accomplishes this:

migration("migration")
    .transformStruct(MigrationTester.typeId("Bar1"), MigrationTester.typeId("Bar2"))
    .transformProtocol(MigrationTester.typeId("Foo1"), MigrationTester.typeId("Foo2")) {
        replace<ListValue>("contents") { old ->
            // Change List to Set, and invoke the
            // registered transformation for the struct
            withTransformations(old) {
                // Due to withTransformations, 'old' is now a List of Bar2.
                // It does not know how to change the List to a Set.
                createSet(it.value, type(MigrationTester.typeId("Bar2")))
            }
        }
    },

Note how type("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")
    .transformProtocol(beforeTypeId, afterTypeId) {
        replace<ListValue>("fooBar") { oldfooBar ->
            createList(
                oldfooBar.value.map { oldFooBar ->
                    createStruct(
                        "/testpkg/VersionedFooBar",
                        mapOf(
                            "fooBar" to oldFooBar,
                            "version" to createText("NONE"),
                        ),
                    )
                },
            )
        }
        put("baz") {
            createList(
                listOf(createText("RELEASE"), createNumber(1)),
                type("/testpkg/Version"),
            )
        }
    },