Adding and searching simple records

In this tutorial, you are going to write a dapp that provides a few basic functions to add and retrieve simple profile records that consist of a name, description, and an array of keywords.

This program supports the following functions:

  • The update function enables you to add a profile that consists of a name, a description, and keywords.

  • The getSelf function returns the profile for the principal associated with the function caller.

  • The get function performs a simple query to return the profile matching the name value passed to it. For this function, the name specified must match the name field exactly to return the record.

  • The search function performs a more complex query to return the profile matching all or part of the text specified in any profile field. For example, the search function can return a profile containing a specific keyword or that matches only part of a name or description.

This tutorial provides a simple example of how you can use the Rust CDK interfaces and macros to simplify writing dapps in Rust for the Internet Computer.

This tutorial demonstrates: * How to represent slightly more complex data—in the form of a profile as a record and an array of keywords—using the Candid interface description language. * How to write a simple search function with partial string matching. * How profiles are associated with a specific principal.

Before you begin

Before you start your project, verify the following:

  • You have an internet connection and access to a shell terminal on your local macOS or Linux computer.

  • You have downloaded and installed the Rust programming language and Cargo as described in the Rust installation instructions for your operating system.

    curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

    The Rust tool chain must be at version 1.46.0, or later.

  • You have downloaded and installed the DFINITY Canister Software Development Kit (SDK) package as described in Download and install.

  • You have cmake installed. For example, use Homebrew with the following command:

    brew install cmake

    For instructions on how to install Homebrew, see the Homebrew Documentation.

  • You have stopped any local execution environment processes running on your computer.

If you aren’t sure how to open a new terminal shell on your local computer, run commands in a terminal, or how to check for and install packages, see Preliminary steps for newcomers. If you are comfortable meeting the prerequisites without instructions, continue to Create a new project.

This tutorial takes approximately 20 minutes to complete.

Create a new project

To create a new project directory for this tutorial:

  1. Open a terminal shell on your local computer, if you don’t already have one open.

  2. Create a new project by running the following command:

    dfx new rust_profile
  3. Change to your project directory by running the following command:

    cd rust_profile

Modify the default configuration

In the Hello, World! Rust CDK Quick Start, you saw that creating a new project adds several template files to your project directory much like when you create a new Rust package using the cargo new command. You need to modify these default files and add some Rust-specific files to your project before you can build canister smart contracts that run on the Internet Computer.

To modify the default configuration for a Motoko project, you’ll need to complete the following steps:

Edit the default canister settings

One of the template files included in your project directory is a default dfx.json configuration file. This file contains settings required to build a project for the Internet Computer much like the Cargo.toml file provides build and package management configuration details for Rust programs. You need to modify the default settings in the dfx.json configuration file to build Rust programs that run on the Internet Computer as canister smart contracts.

To modify the default dfx.json configuration file:

  1. Check that you are still in the root directory for your project, if needed.

  2. Open the dfx.json configuration file in a text editor.

  3. Replace the canisters.rust_profile settings with settings for building a canister using the cargo build command.

    For example, under the rust_profile key, replace the main and type settings with settings like these:

    "build": "cargo build --target wasm32-unknown-unknown --package  rust_profile --release",
    "candid": "src/rust_profile/src/profile.did",
    "wasm": "target/wasm32-unknown-unknown/release/rust_profile.wasm",
    "type": "custom"
    }
  4. Remove all of the rust_profile_assets configuration settings from the file.

    The sample dapp for this tutorial doesn’t use any front-end assets, so you can remove those settings from the configuration file.

  5. Remove the defaults and dfx version settings.

    For example, the configuration file looks like this after you modify the settings:

    {
      "canisters": {
        "rust_profile": {
          "build": "cargo build --target wasm32-unknown-unknown --package  rust_profile --release",
          "candid": "src/rust_profile/profile.did",
          "wasm": "target/wasm32-unknown-unknown/release/rust_profile.wasm",
          "type": "custom"
          }
        },
      "networks": {
        "local": {
          "bind": "127.0.0.1:8000",
          "type": "ephemeral"
        }
      },
      "version": 1
    }
  6. Save your change and close the dfx.json file to continue.

Add a Cargo.toml file to the project

Because we are creating this new Rust project for the Internet Computer using the DFINITY Canister SDK, you need to manually create some Rust-specific files in specific locations within the project directory. Let’s start by creating the Cargo.toml configuration file that describes the packages in our project.

To add a Cargo.toml configuration file for the project:

  1. Check that you are still in the root directory for your project, if needed.

  2. Create a new file in the current directory named Cargo.toml.

  3. Open the Cargo.toml in a text editor.

  4. Use the [workspace] key to specify the source file directory for your program.

    For example:

    [workspace]
    members = [
      "src/rust_profile",
    ]
  5. Save your changes and close the file to continue.

Add Rust files to the source directory

Creating a new project creates a default src/rust_profile directory with a template main.mo. We’ll replace this default dapp with a Rust library lib.rs package and Cargo.toml files.

To prepare the source directory with Rust files:

  1. Check that you are in the root directory for your project, if needed.

  2. Create a new cargo package using a library template by running the following command:

    cargo init --lib src/rust_profile

    This command creates a src/rust_profile/src directory with a library (lib.rs) package and a Cargo.toml file in the src/rust_profile directory.

  3. Open the src/rust_profile/Cargo.toml file in a text editor.

    You use this file to configure the details used to build the Rust package. At a minimum, you need to configure the following sections with basic information about the package name, the crate type, and the version of the DFINITY Rust CDK libraries to use.

    • [lib]

    • [dependencies]

  4. Delete the existing [dependencies] section and replace it with the following:

    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    ic-cdk = "0.3"
    ic-cdk-macros = "0.3"
    serde = "1.0.111"
    When you deploy the dapp later in the tutorial, you might get an error message that the dependency version is wrong. If there is a newer version of the DFINITY Rust CDK, update the dependencies in the src/rust_profile/Cargo.toml file to match the latest version.
  5. Save your changes and close the file to continue.

Replace the default dapp

Now that you have the files in place for your Rust dapp, we can replace the template main.mo program with the Rust dapp we want to deploy on the Internet Computer.

To replace the default program:

  1. Check that you are still in the root directory for your project, if needed.

  2. Delete the template src/rust_profile/main.mo file by running the following command:

    rm src/rust_profile/main.mo
  3. Open the template src/rust_profile/src/lib.rs file in a text editor and delete the existing content.

    The next step is to add a Rust program that implements the getSelf, update, get, and search functions.

  4. Copy and paste the following sample code into the profile.rs file:

    use ic_cdk::export::{candid::{CandidType, Deserialize}, Principal};
    use ic_cdk::storage;
    use ic_cdk_macros::*;
    use std::collections::BTreeMap;
    
    type IdStore = BTreeMap<String, Principal>;
    type ProfileStore = BTreeMap<Principal, Profile>;
    
    #[derive(Clone, Debug, Default, CandidType, Deserialize)]
    struct Profile {
        pub name: String,
        pub description: String,
        pub keywords: Vec<String>,
    }
    
    #[query(name = "getSelf")]
    fn get_self() -> Profile {
        let id = ic_cdk::caller();
        let profile_store = storage::get::<ProfileStore>();
    
        profile_store
            .get(&id)
            .cloned()
            .unwrap_or_else(|| Profile::default())
    }
    
    #[query]
    fn get(name: String) -> Profile {
        let id_store = storage::get::<IdStore>();
        let profile_store = storage::get::<ProfileStore>();
    
        id_store
            .get(&name)
            .and_then(|id| profile_store.get(id).cloned())
            .unwrap_or_else(|| Profile::default())
    }
    
    #[update]
    fn update(profile: Profile) {
        let principal_id = ic_cdk::caller();
        let id_store = storage::get_mut::<IdStore>();
        let profile_store = storage::get_mut::<ProfileStore>();
    
        id_store.insert(profile.name.clone(), principal_id.clone());
        profile_store.insert(principal_id, profile);
    }
    
    #[query]
    fn search(text: String) -> Option<&'static Profile> {
        let text = text.to_lowercase();
        let profile_store = storage::get::<ProfileStore>();
    
        for (_, p) in profile_store.iter() {
            if p.name.to_lowercase().contains(&text) || p.description.to_lowercase().contains(&text) {
                return Some(p);
            }
    
            for x in p.keywords.iter() {
                if x.to_lowercase() == text {
                    return Some(p);
                }
            }
        }
    
        None
    }
  5. Save your changes and close the file to continue.

Add an interface description file

Candid is an interface description language (IDL) for interacting with canister smart contracts running on the Internet Computer. Candid files provide a language-independent description of a canister smart contract’s interfaces including the names, parameters, and result formats and data types for each function a canister defines.

By adding Candid files to your project, you can ensure that data is properly converted from its definition in Rust to run safely on the Internet Computer.

To see details about the Candid interface description language syntax, see the Candid Guide or the Candid crate documentation.

To add a Candid file for this tutorial:

  1. Check that you are still in the root directory for your project, if needed.

  2. Create a new file named profile.did in the src/rust_profile/src directory.

  3. Open the `src/rust_profile/src/profile.did`file in a text editor.

  4. Copy and paste the following type declaration and service definition for the getSelf, update, get, and search functions:

    type Profile_2 = record {
        "name": text;
        "description": text;
        "keywords": vec text;
    };
    type Profile = Profile_2;
    
    service : {
        "getSelf": () -> (Profile_2) query;
        "get": (text) -> (Profile_2) query;
        "update": (Profile_2) -> ();
        "search": (text) -> (opt Profile_2) query;
    }
  5. Save your changes and close the profile.did file to continue.

Start the local execution environment

Before you can build the rust_profile project, you need to connect to the local execution environment running in your development environment or the decentralized Internet Computer mainnet.

To start local execution environment:

  1. Check that you are still in the root directory for your project, if needed.

  2. Start the local execution environment on your computer in the background by running the following command:

    dfx start --background --clean

    Depending on your platform and local security settings, you might see a warning displayed. If you are prompted to allow or deny incoming network connections, click Allow.

Register, build, and deploy your project

After you connect to the local execution environment running in your development environment, you can register, build, and deploy your project locally.

To register, build, and deploy:

  1. Check that you are still in root directory for your project directory, if needed.

  2. Register, build, and deploy the canisters specified in the dfx.json file by running the following command:

    dfx deploy

    The dfx deploy command output displays information about each of the operations it performs similar to the following excerpt:

    Creating a wallet canister on the local network.
    The wallet canister on the "local" network for user "pubs_user_id" is "rwlgt-iiaaa-aaaaa-aaaaa-cai"
    Deploying all canisters.
    Creating canisters...
    Creating canister "rust_profile"...
    "rust_profile" canister created with canister id: "rrkah-fqaaa-aaaaa-aaaaq-cai"
    Building canisters...
    Executing 'cargo build --target wasm32-unknown-unknown --package  rust_profile --release'
       Compiling rust_profile v0.1.0 (/Users/pubs/rust_profile/src/rust_profile)
        Finished release [optimized] target(s) in 2.83s
    Installing canisters...
    Creating UI canister on the local network.
    The UI canister on the "local" network is "ryjl3-tyaaa-aaaaa-aaaba-cai"
    Installing code for canister rust_profile, with canister_id rrkah-fqaaa-aaaaa-aaaaq-cai
    Deployed canisters.

Call functions on the deployed canister

After successfully deploying the canister, you can test the canister by calling the functions it provides.

For this tutorial:

  • Call the update function to add a profile.

  • Call the getSelf function to display the profile for the principal identity.

  • Call the search function to look up the profile using a keyword.

To test the deployed canister:

  1. Call the update function to create a profile record by running the following command:

    dfx canister call rust_profile update '(record {name = "Luxi"; description = "mountain dog"; keywords = vec {"scars"; "toast"}})'
  2. Call the getSelf function to retrieve a profile record by running the following command:

    dfx canister call rust_profile getSelf

    The command returns the profile you used the update function to add. For example:

    (  record {
        name = "Luxi";
        description = "mountain dog";
        keywords = vec { "scars"; "toast" };
      },
    )

    In its current form, the dapp only stores and returns one profile. If you run the following command to add a second profile using the update function, the command replaces the Luxi profile with the Dupree profile:

    dfx canister call rust_profile update '(record {name = "Dupree"; description = "black dog"; keywords = vec {"funny tail"; "white nose"}})'

    You can use the get, getSelf, and search functions, but they will only return results for the Dupree profile.

  3. Run the following command to call the search function:

    dfx canister call rust_profile search '("black")';

    This command finds the matching profile using the description and returns the profile:

    (
      opt record {
        name = "Dupree";
        description = "black dog";
        keywords = vec { "funny tail"; "white nose" };
      },

Adding profiles for new identities

In its current form, the dapp only stores one profile—the one associated with the principal invoking the commands. To test that the get, getSelf, and search functions do what we want them to, we need to add some new identities that can have different profiles.

To add identities for testing:

  1. Create a new user identity by running the following command:

    dfx identity new Miles
    Creating identity: "Miles".
    Created identity: "Miles".
  2. Call the update function to add a profile for the new identity.

    dfx --identity Miles canister call rust_profile update '(record {name = "Miles"; description = "Great Dane"; keywords = vec {"Boston"; "mantle"; "three-legged"}})'
  3. Call the getSelf function to view the profile associated with the default user identity.

    dfx canister call rust_profile getSelf

    The command displays the profile currently associated with the default identity, in this example, the Dupree profile:

    (
      record {
        name = "Dupree";
        description = "black dog";
        keywords = vec { "funny tail"; "white nose" };
      },
    )
  4. Call the getSelf function using the Miles user identity by running the following command:

    dfx --identity Miles canister call rust_profile getSelf

    The command displays the profile currently associated with the Miles identity, in this example:

    (
      record {
        name = "Miles";
        description = "Great Dane";
        keywords = vec { "Boston"; "mantle"; "three-legged" };
      },
    )
  5. Call the search function using part of the description or a keyword to further test the whether the correct profile is returned.

    For example, to verify the Miles profile is returned, you might run the following command:

    dfx canister call rust_profile search '("Great")'

    The command returns the Miles profile:

    (
      opt record {
        name = "Miles";
        description = "Great Dane";
        keywords = vec { "Boston"; "mantle"; "three-legged" };
      },
    )
  6. Call the search function to further test the whether the correct profile is returned.

    For example, to verify the Dupree profile is returned, you might run the following command:

    dfx canister call rust_profile search '("black")'

    The command returns the Dupree profile:

    (
      opt record {
        name = "Dupree";
        description = "black dog";
        keywords = vec { "funny tail"; "white nose" };
      },
    )

Extending the sample dapp

This sample dapp only stores one profile for each unique user identity. If you were to extend this dapp by adding a second method for linking social connections to each users profile, you would be well on your way to recreating the LinkedUp sample application using Rust.

Stop the local execution environment

After you finish experimenting with your program, you can stop the local execution environment so that it doesn’t continue running in the background.

To stop the local execution environment:

  1. In the terminal that displays network operations, press Control-C to interrupt the local execution environment process.

  2. Stop the local execution environment by running the following command:

    dfx stop