Adding and searching simple records

In this tutorial, you are going to write a program 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 programs 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 starting the tutorial, verify the following:

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

  • You have downloaded and installed the DFINITY Canister SDK package as described in Download and install.

  • You have downloaded the latest version of the DFINITY Canister Development Kit (CDK) for Rust from the Rust community’s crate registry or from the Rust CDK repository.

  • You have stopped any Internet Computer network processes running on the local computer.

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. Change to the folder you are using for your Internet Computer sample projects.

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

    dfx new rust_profile

    The command creates a new rust_profile project and Git repository for your project.

  4. Change to your project directory by running the following command:

    cd rust_profile

Modify the default configuration

In the Rust Canister Developer Quick Start, you saw that creating a new project adds a default dfx.json configuration file to your project directory.

Edit canister settings

To modify the default dfx.json configuration file for a Rust project:

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

  2. 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/profile.did",
    "wasm": "target/wasm32-unknown-unknown/release/rust_profile.wasm",
    "type": "custom"
    }
  3. Remove all of the rust_profile_assets configuration settings from the file.

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

  4. 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": {
        "ic": {
          "providers": [
            "https://gw.dfinity.network"
          ],
          "type": "persistent"
        },
        "local": {
          "bind": "127.0.0.1:8000",
          "type": "ephemeral"
        }
      },
      "version": 1
    }
  5. Save your change and close the dfx.json file to continue.

Add Cargo.toml settings to the project

To add Cargo.toml settings for the project:

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

  2. Create a new file named Cargo.toml if you created the project using dfx or open the default Cargo.toml file if you created the project using Cargo.

  3. Use the [workspace] key to specify the source file directories for your program.

    For example:

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

Modify the default program

The next step is to replace the default source code in the src/rust_counter/main.mo file with a Rust program that implements the getSelf, update, get, and search functions.

To modify the default template source code:

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

  2. Rename the default src/rust_profile/main.mo file to use the Rust file extension by running the following command:

    mv src/rust_profile/main.mo src/rust_profile/profile.rs
  3. Open the src/rust_profile/profile.rs file in a text editor and delete the existing content.

  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 required files to the source directory

Before you can build the Rust program, you must provide some additional settings in the source code directory.

To complete the environment for building Rust projects to be deployed on the Internet Computer, you need to do the following:

  • Add a second Cargo.toml file for the program.

  • Add a Candid interface description file to describe the type signatures for the program.

To add the required files:

  1. Change to the source code directory for your program.

    For example:

    cd src/rust_profile
  2. Create a second file named Cargo.toml and open it in a text editor.

  3. Configure settings for your project.

    For example, you should have a Cargo.toml file with settings similar to the following for this tutorial:

    [package]
    name = "rust_profile"
    version = "0.1.0"
    authors = ["DFINITY <[email protected]>"]
    edition = "2018"
    
    [lib]
    path = "profile.rs"
    crate-type = ["cdylib"]
    
    [dependencies]
    ic-cdk = { path = "../../../cdk-rs/src/ic-cdk", version = "0.1.1" }
    ic-cdk-macros = { path = "../../../cdk-rs/src/ic-cdk-macros", version = "0.1.1" }
    ic-types = "0.1.1"
    serde = "1.0.111"
    Replace the path to the ic-cdk and ic-cdk-macros packages with the appropriate path for your local computer.
  4. Save your changes and close the Cargo.toml file to continue.

  5. Create a new file named profile.did.

  6. Open the file in a text editor, then 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;
    }
  7. Save your changes and close the profile.did file to continue.

Start the network and deploy locally

Before you can build the rust_profile project, you need to connect to the Internet Computer network either running locally in your development environment or running remotely on a sub-network that you can access.

To start the network locally:

  1. Navigate back to the root directory of your project.

  2. Open a new terminal window or tab on your local computer.

    For example, you can do either of the following if running Terminal on macOS:

    • Click Shell, then select New Tab to open a new terminal in your current working directory.

    • Click Shell and select New Window, then run cd ~/ic-projects/rust_counter in the new terminal if your rust_counter project is in the ic-projects working folder.

    You should now have two terminals open with your project directory as your current working directory.

  3. Start the Internet Computer network on your local computer by running the following command:

    dfx start

    After you start the local network, the terminal displays messages about network operations.

  4. Leave the terminal that displays network operations open.

  5. Switch to your second terminal window or tab.

  6. Register, build, and deploy the canister for the project by running the following command:

    dfx deploy

Invoke functions on the deployed canister

After successfully deploying the canister, you can test the canister by invoking the functions it provides. For this tutorial:

  • Invoke the update function to add a profile.

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

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

To test invoking methods on the deployed canister:

  1. Run the following command to invoke the update function to create a profile record:

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

    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 program 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 invoke 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 program 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 program

This sample program only stores one profile for each unique user identity. If you were to extend this application by adding a second program 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 network

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

To stop the local network:

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

  2. Stop the Internet Computer network by running the following command:

    dfx stop