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 FooBar
s (Version
being either a version Number
or Text
in this
example). We also want it to take another parameter, which is a list of Version
s:
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"),
)
}
},