5 releases

0.2.2 Mar 8, 2022
0.2.1 Mar 7, 2022
0.2.0 Mar 4, 2022
0.1.1 Mar 3, 2022
0.1.0 Mar 1, 2022

#10 in #daml


Used in daml

Apache-2.0

3MB
11K SLoC

Documentation Crate maintenance-status

Daml Derive

This crate provides procedural macros for generating Rust types from daml.

This crate should not be used directly, instead you should depend on the daml crate and enable the derive feature:

[dependencies]
daml = { version = "0.2.2", features = [ "derive" ] }

License

daml-derive is distributed under the terms of the Apache License (Version 2.0).

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in time by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

See LICENSE for details.

Copyright 2022


lib.rs:

Procedural macros for generating Rust types and conversions from Daml types and Archives.

Overview

Two mechanisms are provided for representing Daml types in Rust:

  • Custom attributes which can be applied to Rust structures which generate Daml type converters.
  • A procedural macro code generator which takes a Daml dar file as input and generates Rust types annotated with the custom attributes.

Custom Attributes

This section explains how to use the provided custom attributes to annotate Rust types to generate the Daml ledger API data conversion code required to be able to use them with a Daml ledger.

Mapping Daml Structures to Rust

Daml structures are modelled using various Rust language constructs in conjunction with custom attributes procedural macros as shown in the following table:

Daml Concept Rust Construct Custom Attribute
Daml Template struct [macro@DamlTemplate]
Daml Template Choices impl block [macro@DamlChoices]
Daml Data (Record) struct [macro@DamlData]
Daml Data (Variant) enum [macro@DamlVariant]
Daml Enum enum [macro@DamlEnum]

Mapping Daml Data Types to Rust

The following table lists the mappings between Daml build-in primitive types and Rust type aliases:

Daml Type Rust Type Alias Concrete Rust Type Notes
Int DamlInt64 i64
Numeric DamlNumeric bigdecimal::BigDecimal Note: BigDecimal crate to be replaced
Text DamlText String
Bool DamlBool bool
Party DamlParty String
Date DamlDate chrono::Date
Time DamlTime chrono::DateTime
() DamlUnit ()
ContractId a DamlContractId String Note: this mapping is likely to change
List a or [a] DamlList<T> Vec<T> type T must be another Rust type alias
TextMap a DamlTextMap<T> HashMap<String, T> type T must be another Rust type alias
Optional a DamlOptional<T> Option<T> type T must be another Rust type alias

Note that the concrete Rust types are shown here as a convenience only, in all cases the Rust type alias must be used when representing Daml constructs so that the Daml types can be determined.

Parameterized Types

The parameterized types (List<T>, TextMap<T> and Optional<T>) may be freely nested to an arbitrary depth and may be used in all context a type is expected such as templates & data fields as well as choice parameters.

For example these are examples of valid types:

let int: DamlInt64;
let party: DamlParty;
let opt_numeric: DamlOptional<DamlNumeric10>;
let list_of_int: DamlList<DamlInt64>;
let list_of_opt_int: DamlList<DamlOptional<DamlInt64>>;
let list_of_opt_map_party: DamlList<DamlOptional<DamlTextMap<DamlParty>>>;
let opt_list_data: DamlOptional<DamlList<MyData>>;

Recursive Data Types

Daml Data (both Records and Variants) may be recursive. For example:

data Foo = Foo
  with
    bar : Optional Text
    foo : Foo

Both [macro@DamlData] and [macro@DamlVariant] types may therefore be defined recursively. However modelling such structures in Rust requires that any recursively defined items be held via an indirection, typically via a heap allocation smart pointer such as Box<T>, to ensure a non-infinite size for the struct or enum (see here for details).

The above example can therefore be represented as follows:

#[DamlData]
pub struct Foo {
    bar: DamlOptional<DamlText>,
    foo: Box<Foo>,
}

Note that Box<T> is the only form of indirection currently supported and it may be used anywhere T is used.

Prelude

All of the above Rust type aliases are defined in the prelude module of the daml crate and can included by using daml::prelude::*.

Modules

Rust struct and enum types annotated with the custom attributes provided by this crate are not required to be nested in Rust modules that mirror the Daml module hierarchy. All of the standard Rust name resolution and visibility rules apply and therefore it is recommend to mirror the Daml hierarchy where possible to avoid namespace collisions.

For example, the MyData data type defined in the Fuji.MyModule.MySubModule Daml module would likely be declared as follows:

mod fuji {
    mod my_module {
        mod my_sub_module {
            use daml::prelude::*;
            #[DamlData]
            pub struct MyData {}
        }
    }
}

Example

Given the following Daml template declared in the Fuji.PingPong module of a given package:

template Ping
  with
    sender: Party
    receiver: Party
    count: Int
  where
    signatory sender
    observer receiver

    controller receiver can
      ResetCount : ()
        with
          new_count: Int
        do
          create Pong with sender; receiver; count = new_count
          return ()

This can be represented in Rust by using the [macro@DamlTemplate] and [macro@DamlChoices] custom attributes:

use daml::prelude::*;

#[DamlTemplate(package_id = r"...package id hash omitted...", module_name = "Fuji.PingPong")]
pub struct Ping {
    pub sender: DamlParty,
    pub receiver: DamlParty,
    pub count: DamlInt64,
}

#[DamlChoices]
impl Ping {
    #[ResetCount]
    fn reset_count(&self, new_count: DamlInt64) {}
}

A new Ping can then be created as follows:

let ping = Ping::new("Alice", "Bob", 0);

To create an instance of the Ping template on a Daml ledger a DamlCreateCommand specific to our ping data needs to be constructed. This can be done as follows:

let create_ping_command = ping.create_command();

The generated DamlCreateCommand can then be submitted to the Daml ledger via the DamlCommandService or DamlCommandSubmissionService as usual.

Once the contract instance has been created on the Daml ledger and the corresponding DamlCreatedEvent has been received then it can be converted into a Rust type as follows:

let ping_contract: PingContract = created_event.try_into()?;
}

Note that the DamlCreatedEvent returned by the Daml ledger is converted into a PingContract rather than a plain Ping. The PingContract type is a struct and provides methods data() -> Ping and id() -> &PingContractId to access the Ping data and contract id respectively:

assert_eq!("Alice", ping_contract.data().sender);
assert_eq!("Bob", ping_contract.data().receiver);
assert_eq!(0, ping_contract.data().count);
assert_eq!("#0:0", ping_contract.id().contract_id);

NOTE: The contract id may be refactored to use a separate type in future.

The PingContract types provides a method for each choice defined by the Daml template along with any parameters that choice may have. To exercise a choice on a Daml ledger a DamlExerciseCommand specific to our contract is needed. The can be constructed as follows:

let exercise_command = ping_contract.id().reset_count_command(5);
}

The generated DamlExerciseCommand can then be submitted to the Daml ledger via the DamlCommandService or DamlCommandSubmissionService as usual.

Note that the name of the choice method must match the name of the Daml choice (in snake_case) with a _command suffix and the choice parameters must match between the Daml and Rust representations.

See the documentation for [macro@DamlTemplate], [macro@DamlChoices] & [macro@DamlData] for full details and examples.

Errors

Returns the underlying DamlError (runtime-only) if the try_into() conversion from a DamlValue to an annotated type fails.

Panics

Panics (compile-time only) if errors are detected in the annotated struct, enum or impl blocks.

Dependencies

~6.5–9.5MB
~168K SLoC