The sic
boilerplate generator¶
sic
is a tool for generating the boilerplate code that makes it possible to seamlessly interact with a CSL contract from another language.
Currently it supports two target languages: Kotlin and TypeScript.
Because Kotlin is a JVM-based language sic
indirectly supports other such languages like Java.
Likewise, TypeScript is a language that is compiled to JavaScript and is designed to be interoperable with JavaScript code, so sic
indirectly supports JavaScript as well.
Overview¶
When you have written a CSL contract and want to integrate it in a larger application you need some way of communicating with the system responsible for running the CSL contract. This can be done using the public API and one of the API clients, however, this means that you would have to take care of sending the right JSON-encoded data yourself, with no possibility of help from your language’s type checker.
sic
generates mappings from the CSL data types to native data types in the target language, meaning that you can get help from the target language’s type checker and IDE support when building values of these types.
Moreover, reports, events and entry points (templates) in the contract are mapped to suitable constructs in the target language.
Hence, instead of communicating directly using the Deon API, you can instead use specialized functions that use the generated native data types as input and output types, and which takes care of (de)serializing to the JSON format expected by Deon’s API.
This makes it easier to work with the contract as you get the support that you would otherwise get when working in the target language.
On top of that, it makes it dramatically less time-consuming to make changes to the CSL contract, as you will get the updated mappings for free by re-running sic
, and any inconsistencies in the way you use the generated functions will be immediately caught by the target language’s typechecker.
Given a CSL contract, sic
will generate the following components in the target language:
- Data type definitions
- Each data type in the contract will be mapped to a data type in the target language.
- Report functions
- For each report in the contract a corresponding function for invoking that report is created.
- Event application functions
- For each
Event
type in the contract a function for applying that event to a contract is created. - Contract instantiation functions
- For each top-level template in the contract a function for instantiating a contract from that template is created.
Usage¶
sic
is a command-line tool that works on Windows, MacOS, and Linux:
$ sic --help
sic <VERSION>
Usage: sic [-V|--version] [-n|--namespace NAMESPACE] [-t|--target ARG]
[--stdlib PATH] [-w|--write] [-d|--destination PATH] FILES
Available options:
-V,--version Print version information
-h,--help Show this help text
-n,--namespace NAMESPACE Namespace to put generated code in
-t,--target ARG Target language (default: Kotlin)
--stdlib PATH Use alternative CSL standard library
-w,--write Write generated source files to disk instead of just
printing them to stdout
-d,--destination PATH Root directory for generated source
files (default: "generated")
We shall use the contract “sic1.csl
” for demonstration of how to use sic
and for illustrating key points about the structure of the generated code:
type CustomerType
| Regular Int
| OneTime
type Address {
street: String,
number: Int,
floor: Int
}
type Customer {
name: String,
age: Int,
address: Address,
customerType: CustomerType
}
type AddCustomer : Event {
id : Int,
customer : Customer
}
// sum : List Int -> Int
val sum = \ints -> foldl (\(x : Int) -> \y -> x + y) 0 ints
// sumCustomerAge : List Customer -> Int
val sumCustomerAge =
\(customers : List Customer) ->
sum (List::map (\(c : Customer) -> c.age) customers)
template rec Shop(total) = <*> a: AddCustomer
where a.id = total then Shop(total + 1)
Using Kotlin as the target language¶
The default target language of sic
is Kotlin, and the default behaviour is to write the generated code to standard output.
Thus, when we run the command
$ sic sic1.csl
it will print a bunch of Kotlin code to the terminal.
If we pass the flag --write
to sic
it will write the code to disk:
$ sic --write contract.csl
Generating interface for sic1.csl
Wrote file generated/com/deondigital/api/contract/sic1/Sic1.kt
Wrote file generated/com/deondigital/api/contract/sic1/Sic1.csl.kt
We see here that one CSL contract is represented as two Kotlin source files in the package com.deondigital.api.contract.sic1
.
The generated code will be put into the root directory generated/
.
Both the package name and the root directory can be changed with the flags --namespace
(-n
) and --destination
(-d
), respectively.
The first file, Sic1.kt
, contains all the interface and data definitions that enable us to interact with the CSL contract from Kotlin in a convenient manner.
The second file, Sic1.csl.kt
, is an embedding of the contract source in Kotlin.
Data types¶
The file Sic1.kt
will contain, amongst many other things, the definitions of the following data types:
sealed class CustomerType : Convertible {
/* ... */
data class Regular(val field0: Int) : CustomerType() {
/* .. */
}
object OneTime : CustomerType() {
/* .. */
}
}
data class Address(
val street: String,
val number: Int,
val floor: Int) : Convertible {
/* ... */
}
data class Customer(
val name: String,
val age: Int,
val address: Address,
val customerType: CustomerType) : Convertible {
/* ... */
}
We have left out a lot of details here, but the snippet demonstrates how a sum type in CSL is converted to a sealed class
in CSL with a subclass for each constructor while a CSL record type is converted to a data class
.
The names of parameters of a data class
match the names in the CSL record.
Base types such as Int
and String
are represented by their native counterparts in Kotlin: kotlin.String
and kotlin.Int
.
Reports¶
The CSL reports are converted to functions in the target language with appropriate types. That is, the input and output types are mappings from the CSL type to the target language type as described in the above section.
In our generated Kotlin code, we can find the following interface:
interface ReportService {
fun sum(ints: List<Int>) : Int
fun sumCustomerAge(customers: List<Customer>) : Int
}
That is, the interface com.deondigital.api.sic1.ReportService
declares two functions, one for each of the CSL reports in sic1.csl
.
The input and output types are mapped from the corresponding CSL types; note that the CSL List a
type is mapped to Kotlin/Java’s List<T>
type.
To use the interface we can instantiate the class ReportServiceImpl
which takes two parameters in its constructor:
- A
reportCaller : (String, String, List<Value>) -> Value
which is the function used to issue the report call to the Deon API. The parameters of this function match the ones taken by theDeonApiClient::postReport
function, so in the usual case you can just supply thepostReport
method from aDeonApiClient
that is set up with the right URL. - A
contractId
– this is the contract on which the reports will be run.
The following snippet illustrates how one would typically run a report with this interface:
val apiClient = DeonAPIClient(API_URL) // Connect to the ledger
val contractId = "1234"
val r = ReportServiceImpl(apiClient::postReport, contractId)
val s = r.sumCustomerAge(listOf(
Customer("bob", 42, Address("Main st.", 1, 5), CustomerType.OneTime),
Customer("alice", 30, Address("Main st.", 10, 2), CustomerType.Regular(1)))
) // == 72
Contract instantiation¶
Every top-level template declaration in a CSL contract represents a possible instantiation point of a contract in the system.
The generated Kotlin code for sic1.csl
contains the following interface:
interface Instantiate<T> {
fun Shop(total: Int, peers : List<String> = listOf()) : T
}
Like the code for reports, instantiation is encapsulated in an interface with an accompanying implementation, InstantiateImpl
, with a function for each top-level template in the CSL contract.
In order to make it possible to use both the asynchronous and the synchronous interface from the DeonAPIClient
, the interface is parametric in the return type of the instantiation functions.
Moreover, all instantiation functions take the same parameters as the CSL templates (mapped to the Kotlin type), plus an additional parameter peers
that can be used to specify on which peers a contract should be instantiated on, e.g., the Corda backend.
This class takes three parameters in its constructor:
- A function for adding the contract declaration to the ledger:
addDecl : (String, String) -> String
. The parameters taken by this function match those taken byDeonAPIClient::addDeclaration
. - A function that sends the instantiation request of a given template in the declaration to the ledger:
instantiate : (String, String, List<InstantiationArgument>, QualifiedName, List<String>) -> T
. The parameters for this function match those of the synchronousDeonAPIClient::addContract
and the asynchronousDeonAPIClient::submitContract
. The return typeT
represents a contract id – aString
or aFuture<String>
, respectively. - An optional name to give the declaration – if left out the name of the CSL contract is used:
declarationName : String = "sic1"
.
The first time a contract is instantiated, the CSL source is added to the ledger as a declaration using the supplied function, and all subsequent instantiation requests will be instantiations of the resulting declaration id.
val apiClient = DeonAPIClient(API_URL)
// Let's create an instantiator with the synchronous API calls:
val inst = InstantiateImpl(apiClient::addDeclaration, apiClient::addContract)
// Adds the CSL declaration to the ledger and instantiates 'Shop'
val contractId1 = inst.Shop(42)
// Reuses the declaration and instantiates a new contract from 'Shop'
val contractId2 = inst.Shop(11)
Event application¶
Every subtype of Event
in the CSL contract is mapped to a function in the target language that applies an event of that type to a running contract.
Any fields that the event record might have is represented as a parameter to the event application function.
There is an interface that contains the necessary functions for applying events:
interface ContractService<T> {
fun event() : T
fun addCustomer(id: Int, customer: Customer) : T
}
It contains a function for applying a generic Event
and a function for applying our custom AddCustomer
event.
Because the AddCustomer
event record has two additional fields, id : Int
and customer : Customer
, the resulting Kotlin function accepts the corresponding parameters.
The return type is parameterized to allow it to be used with both the synchronous and the asynchronous interface, like it was the case for contract instantiation from Kotlin.
To add an event to a running contract, create an instance of the ContractServiceImpl
and supply the following parameters:
- The agent from which the event originates.
- A function for sending the actual event application request to the API,
addEvent : (com.deondigital.api.Event) -> T
. Unlike withInstantiate
andReportService
, the type of this function does not directly match the functionsDeonAPIClient::addEvent
andDeonApiClient::submitEvent
. These two functions both accept a contact id and the actual event, so you will need to supply a closure that binds the contract id. - A “time provider”,
timeProvider : () -> Instant
. This is a function that, when called, returns a timestamp that is used as the timestamp for the event.
The example snippet below creates a ContractServiceImpl
that uses the synchronous interface of DeonAPIClient
to apply events, and which uses the current time as the event time stamp ({ Instant.now() }
):
// 'agent', 'contractId', and 'apiClient' defined elsewhere
val c = ContractServiceImpl(agent,
{ event -> apiClient.addEvent(contractId, event) },
{ Instant.now() });
// Now we can apply two 'AddCustomer' events on the contract:
c.addCustomer(0, Customer("bob",
42,
Address("Main st.", 1, 5),
CustomerType.OneTime));
c.addCustomer(1, Customer("alice",
30,
Address("Main st.", 10, 2),
CustomerType.Regular(1)));
The sic-maven-plugin
¶
There exists a Maven plugin which is intended to make it simple to integrate the boilerplate generation into a build process.
Is is located in Deon Digital’s Nexus repository and you need to let Maven know that it can look after plugins there by adding the following to the <profile>
section in settings.xml
:
<pluginRepositories>
<pluginRepository>
<id>deon</id>
<url>https://nexus.deondigital.com/repository/deon/</url>
</pluginRepository>
</pluginRepositories>
Then, to use it in a Maven build you must add the following to the <plugins>
section of your pom.xml
:
<plugin>
<groupId>com.deondigital</groupId>
<artifactId>sic-maven-plugin</artifactId>
<!-- This is the version of the plugin itself,
not necessarily of the 'sic' executable -->
<version>v0.23.0-SNAPSHOT</version>
<executions>
<execution>
<goals><goal>sic</goal></goals>
<configuration>
<!-- This is the arguments that'll get passed to 'sic' -->
<argumentString>--write ../*.csl</argumentString>
<!-- This is the version of 'sic' that will be
fetched from Deon Digital's file server. -->
<version>v0.22.0</version>
<!-- If you have a 'sic' installed on your system
and in the PATH and want to use that, set
this to true -->
<!-- <useSystemSic>true</useSystemSic> -->
</configuration>
</execution>
</executions>
</plugin>
The plugin will take care of downloading the appropriate version of sic
for the platform you’re running on.
Using TypeScript as the target language¶
Had we run sic
with the flag --target TypeScript
instead we would have gotten the equivalent data definitions in TypeScript:
$ sic --write --target TypeScript sic1.csl
Generating interface for sic1.csl
Wrote file generated/./Sic1.ts
Wrote file generated/./sic1.csl.ts
Wrote file generated/./preamble.ts
We get three files: Sic1.ts
contains the data definitions and interfaces for our CSL contract, sic1.csl.ts
contains the CSL source, and preamble.ts
contains some shared internal functionality.
Data types¶
Amongst the definitions in the file Sic1.ts
are the mappings of the CSL types:
export type CustomerType
= { discr : "Regular", field0 : number }
| { discr : "OneTime" }
export type Address = {
street : string
_number : number
floor : number
}
export type Customer = {
name : string
age : number
address : Address
customerType : CustomerType
}
These definitions allow us to work in TypeScript directly with, e.g., a Customer
class with the field name
as a native string
.
Note that, because “number
” is a reserved word in TypeScript, the field number
in the CSL contract has been renamed to _number
in the TypeScript embedding.
Reports¶
If we inspect the generated TypeScript code, we will find the interface:
export interface Reports {
sum(ints : number[]) : Promise<number>
sumCustomerAge(customers : Customer[]) : Promise<number>
}
Like in Kotlin, this is an interface with a function for each CSL report. Also like in Kotlin, there is an implementation of this interface that takes two parameters:
- An implementation of the
ContractsAPI
(from@deondigital/api-client
), used for issuing the actual call to the API. - A contract id.
We can call a report with native TypeScript types analogously to the way we did it for Kotlin:
// 'deonApiClient' and 'contractId' defined elsewhere
const r = new Reports(deonApiClient.contracts, contractId)
const s = await r.sumCustomerAge([new Customer (
"bob",
42,
new Address (
"Main st.",
1,
5
),
{ discr: "OneTime" }
), new Customer (
"alice",
30,
new Address (
"Main st.",
10,
5
),
{ discr: "Regular", field0: 1 }
)])
Contract instantiation¶
The generated TypeScript code for sic1.csl
contains the following interface for contract instantiation:
export interface Instantiate {
Shop(total : number, peers? : string[]) :
Promise<{contractId: string,
commands: (agent: p.Agent) => Commands, reports: Reports}>
}
Like for calling reports, the structure in the TypeScript is similar to the structure in the Kotlin code: there is an interface with one function per template and each function takes as parameters the CSL template expression parameters plus an additional peers
parameter.
The return type is an asynchronous Promise
with a structure containing the id of the newly instantiated contract, and pre-instantiated objects for applying events and calling reports on it.
The implementation of this interface takes three parameters in its constructor:
- An instance of a
ContractsAPI
(from@deondigital/api-client
) to do the instantiation call. - An instance of a
DeclarationsAPI
(from@deondigital/api-client
) to put the CSL declaration on the ledger. - An optional name to give the declaration.
If it is left unspecified, the declaration will be named after the CSL file –
"sic1"
in our example.
As in the Kotlin implementation the declaration will be put onto the ledger the first time a contract is instantiated.
// 'deonApiClient' defined elsewhere
// Create an instantiator
const instantiate = new Instantiate(deonApiClient.contracts,
deonApiClient.declarations);
// Adds the CSL declaration to the ledger and instantiates 'Shop'
const c1 = await instantiate.Shop(42);
// c1.contractId contains the new contract's id
// Reuses the declaration and instantiates a new contract from 'Shop'
const c2 = await instantiate.Shop(11);
Event application¶
sic
has generated the following TypeScript interface for us:
export interface Commands {
Event($tag? : p.Tag) : Promise<p.Tag | void>
AddCustomer(id : number, customer : Customer, $tag? : p.Tag) : Promise<p.Tag | void>
}
Again, the pattern is quite like it was for Kotlin: one function per event type and the functions take as parameters the fields of the event.
To instantiate Commands
, supply the following parameters to the constructor:
- A
ContractsApi
instance from@deondigital/api-client
to communicate with the API. - The id of the contract on which to apply the events.
- The originating agent for the event.
- An optional “time provider” that returns the timestamp to be used in the event. It can be left out, in which case the current time is used.
In this snippet we create a Commands
instance and apply two AddCustomer
events on a contract:
// 'deonApiClient', 'contractId', and 'agent' defined elsewhere
const c = new Commands(deonApiClient.contracts,
contractId,
agent);
// Now we can apply two 'AddCustomer' events on the contract:
await c.AddCustomer(0, new Customer (
"bob",
42,
new Address ("Main st.", 1, 5),
{ discr: "OneTime" }
));
await c.AddCustomer(1, new Customer (
"alice",
30,
new Address ("Main st.", 10, 5),
{ discr: "Regular", field0: 1 }
));
The @deondigital/sic
NPM package¶
The sic
tool is distributed in the NPM package @deondigital/sic
.
It provides a handy way to install sic
:
$ npx @deondigital/sic
This will download the latest version of sic
and run it.
If you want sic
code generation as part of your build process, add @deondigital/sic
as a project dependency and add a "generate"
entry to the "scripts"
section of package.json
:
/* ... */
"scripts": {
/* ... */
"generate": "sic --target TypeScript --write *.csl"
},
"dependencies": {
/* ... */
"@deondigital/sic": "0.37.0"
}
Now you can use the generate
script in your project:
$ npm run generate
Note
The version number of the @deondigital/sic
package follows that of the rest of Deon Digital CSL.
Version 0.37.0
of @deondigital/sic
will download a version of sic
that is compatible with version 0.37.0
of the Deon Digital CSL runtime.