FYP
2022-09-01 20:00:00

Simple Record IO Demo

This note serves as a tutorial for developing a dapp using Rust that provides a few basic functions to add and retrieve simple profile records that consist of a name, a 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 get_self 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 dapp can be regarded as a simple example illustrating how you can use the Rust CDK interfaces and macros to simplify writing dapps in Rust for the Internet Computer blockchain.

It 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.

Modify Cargo.toml

In the fyp project we created, modify src/fyp_backend/Cargo.toml:

[package]
name = "fyp_backend"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
candid = "0.7.14"
ic-cdk = "0.5.2"
ic-cdk-macros = "0.5.2"
serde = "1.0" # --- add this line

Update Interface Description File

Modify src/fyp_backend/fyp_backend.did, add the related types and interfaces descriptions:

type Profile = record {
    "name": text;
    "description": text;
    "keywords": vec text;
};

service : {
    "get_self": () -> (Profile) query;
    "get": (text) -> (Profile) query;
    "update": (Profile) -> ();
    "search": (text) -> (opt Profile) query;
}

Code the lib.rs

Delete the default content in src/fyp_backend/src/lib.rs, and add the following code:

// dependencies
use ic_cdk::{
  export::{
      candid::{CandidType, Deserialize},
      Principal,
  },
};
use ic_cdk_macros::*;
use std::cell::RefCell;
use std::collections::BTreeMap;

// data structures
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>,
}

// initialize memory pieces using RefCell
thread_local! {
static PROFILE_STORE: RefCell<ProfileStore> = RefCell::default();
static ID_STORE: RefCell<IdStore> = RefCell::default();
}

// update records
#[update]
fn update(profile: Profile) {
  let principal_id = ic_cdk::api::caller();
  ID_STORE.with(|id_store| {
      id_store
          .borrow_mut()
          .insert(profile.name.clone(), principal_id);
  });
  PROFILE_STORE.with(|profile_store| {
      profile_store.borrow_mut().insert(principal_id, profile);
  });
}

// get record related to current principal
#[query(name = "get_self")]
fn get_self() -> Profile {
  let id = ic_cdk::api::caller();
  PROFILE_STORE.with(|profile_store| {
      profile_store
          .borrow()
          .get(&id)
          .cloned()
          .unwrap_or_else(|| Profile::default())
  })
}

// get record by name
#[query]
fn get(name: String) -> Profile {
  ID_STORE.with(|id_store| {
      PROFILE_STORE.with(|profile_store| {
          id_store
              .borrow()
              .get(&name)
              .and_then(|id| profile_store.borrow().get(id).cloned())
              .unwrap_or_else(|| Profile::default())
      })
  })
}

// search record by string
#[query]
fn search(text: String) -> Profile {
  let text = text.to_lowercase();
  let mut result: Profile = Profile { name: "".to_string(), description: "".to_string(), keywords: vec![] };
  PROFILE_STORE.with(|profile_store| {
      for (_, p) in profile_store.borrow().iter() {
          if p.name.to_lowercase().contains(&text) || p.description.to_lowercase().contains(&text)
          {
              result = p.clone();
          }

          for x in p.keywords.iter() {
              if x.to_lowercase() == text {
                result = p.clone();
              }
          }
      }
  });
  result
}

Testing

Start the local execution environment and deploy the project using dfx, in the terminal:

To upload (update) an record:

dfx canister call fyp_backend update '(record {name = "Luxi"; description = "mountain dog"; keywords = vec {"scars"; "toast"}})'
# OUTPUT: ()

To get a record, in 3 ways:

dfx canister call fyp_backend get_self
# OUTPUT: 
# (
#   record {
#     name = "Luxi";
#     description = "mountain dog";
#     keywords = vec { "scars"; "toast" };
#   },
# )

dfx canister call fyp_backend get "Luxi"
# OUTPUT: 
# (
#   record {
#     name = "Luxi";
#     description = "mountain dog";
#     keywords = vec { "scars"; "toast" };
#   },
# )

dfx canister call fyp_backend search "scars"
# OUTPUT: 
# (
#   record {
#     name = "Luxi";
#     description = "mountain dog";
#     keywords = vec { "scars"; "toast" };
#   },
# )

References

Some information in this note come from the following external links: