Optional Record Types, Templates

Hello everyone,

In the sample code below, you will find Source, Target, and Intermediate templates.

In this dummy use case, the intermediate template receives some optional record types and/or templates and extracts the fields of interest from them to create and fill the Target template accordingly.

Regardless of the time I spent on this, I could not be sure about the most efficient and proper way to do realize this simple idea. Moreover, things get easily uglier when a record type or template specified as input parameters contain some other templates or record types within them and maybe with some additional optional types.

I believe once you check the code & comments, the idea will become clearer to you. I’m open to any suggestions on the issue. Thank you very much for your interest and time in advance.

module Sample where

type Source_Key = (Party, Text, Int)
type Target_Key = (Party, Int)
type Intermediate_Key = (Party, Text)

data Passport = Passport with
  name : Optional Text
  age : Int
    deriving(Eq, Show)
    
template Source
  with
    sign : Party
    name: Text
    age: Int
  where
    signatory sign

    key (sign, name, age) : Source_Key
    maintainer key._1

template Target
  with
    sign: Party
    name: Text
    age: Int
    country: Text
    gender: Text
  where
    signatory sign

    key (sign, age) : Target_Key
    maintainer key._1

template Intermediate
  with
    sign : Party
    attr : Text
  where
    signatory sign

    key (sign, attr) : Intermediate_Key
    maintainer key._1

    controller sign can
      nonconsuming  Create : ()
        with
          passport :  Optional Passport
          template_key : Optional Source_Key
        do
          case passport of -- check if passport is set
            None -> -- pass.
                return()
            Some x -> 
                -- get fields of interest from the record type and assign them into corresponding set of variables.
                return ()
          
          case template_key of -- check if template key is set
            None -> -- do pass 
              return ()
            Some cid -> do 
              (_, fetched_template) <- fetchByKey @Source cid -- fetch the contract
              -- get fields of interest from the fetched template type and assign them into corresponding set of variables.
              return ()
          
          -- finally create the Target template and fill it with the previously taken data.
          create Target with sign = sign, name = fetched_template.name, age = passport.age.. 
          return ()

Hi @Chenav :wave:

The DA team is on a Christmas break so it might take a bit longer to get a reply.

Hope you understand :pray:

Hi @nemanja, of course. Merry Christmas everyone! :slight_smile:

Hi @Chenav, how do you want to handle the case where passport and template key are None? Your comments suggest that you want to continue. However, you won’t be able to create the Target contract afterwards if those do not exist.

I see a few options:

  1. Abort if they are None. That works, however in that case I’m not sure why you would be accepting an Optional at all.
  2. Continue but make some fields on Target optional to account for them being missing.
  3. Continue if at least one of them is set. In that case, what do you want to do if both are missing or if both are set but have different values for some fields?
  4. Continue but don’t create Target. In that case, I’d probably lean towards not making the fields optional in the first place.

Does one of those options cover what you had in mind?

As an alternative to having a single method with two optional arguments, you could have N methods (1-4) that match the expected valid combinations. Naming may get a bit verbose if there aren’t good, domain-specific terms for the operations, but that may help reduce confusion.

So, for example, if it is valid to specify either both or just a passport, you could do:

template Intermediate
  with
    sign : Party
    attr : Text
  where
    signatory sign

    key (sign, attr) : Intermediate_Key
    maintainer key._1

    controller sign can
      nonconsuming  Create : ()
        with
          passport :  Passport
          template_key : Source_Key
        do
          -- Both args are mandatory so you can count on them being here
          create Target with ...
          return ()
      nonconsuming  CreateWithPassport : ()
        with
          passport :  Passport
        do
          -- get fields of interest from the record type and assign them into corresponding set of variables.
          -- we know we don't have a template_key, so no need to check for it
          create Target with ...
          return ()

In effect, you’re moving the case statement on optional arguments ‘up’ one level in the code, while moving the responsibility for what to do if arguments are missing to the caller. This may in some cases provide a cleaner, easier-to-understand API, even though it will end up having more methods.