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:
update
function enables you to add a profile that consists of a name
, a description
, and keywords
.get_self
function returns the profile for the principal associated with the function caller.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.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:
record
and an array
of keywords—using the Candid interface description language.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
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;
}
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
}
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" };
# },
# )
Some information in this note come from the following external links: