メインコンテンツまでスキップ

lesson-2_インタフェースを定義しよう

🤝 インタフェースを定義しよう

実装した機能をフロントエンドから呼び出す前に、インタフェースを定義する必要があります。ICPでは、キャニスターのインタフェースはCandidという言語で記述します。

Candid とは

Candidは、サービスのパブリック・インタフェースを記述することを主な目的としたインタフェース記述言語です。Candidの主な利点のひとつは、言語にとらわれず、Motoko、Rust、JavaScriptなどの異なるプログラミング言語で書かれたサービスとフロントエンド間の相互運用を可能にすることです。詳細はこちらをご覧ください。

Motokoでキャニスターを記述した場合、プログラムをコンパイルする際にコンパイラが自動的にCandidで記述されたファイル.didを生成してくれます。しかし、Rustでは2023年9月時点でそのような機能は組み込まれておらず、Candidインタフェースを自動生成するためには設定が必要です。

まずは、src/encrypted_notes/backend/lib.rsを更新します。export_candidマクロをインポートして、ファイルの一番下に追加してください。

use ic_cdk_macros::{export_candid, query, update};

...

// ===== ファイルの一番下に定義してください。 =====
// .didファイルを生成します。
export_candid!();

次に、candid-extractorをインストールします。これは、Rustファイルをコンパイルした際に生成されるWASMファイルから、Candidの定義を抽出するCLIツールです。

cargo install candid-extractor

最後に、Candidファイルを生成するためのスクリプトを追加します。プロジェクトのルートディレクトにscriptsディレクトリを作成し、その中にdid.shというファイルを作成してください。

 ICP-Encrypted-Notes/
+├── scripts/
+│ └── did.sh
├── src/
├── dfx.json
├── LICENSE
├── README.md
└── dfx.json

did.shに、下記を記述してください。先ほどインストールしたcandid-extractorを使用して、WASMファイルからCandidインタフェースを生成するスクリプトとなります。

#!/bin/bash

function generate_did() {
local canister=$1
canister_root="src/$canister"

cargo build --manifest-path="$canister_root/Cargo.toml" \
--target wasm32-unknown-unknown \
--release --package "$canister"

candid-extractor "target/wasm32-unknown-unknown/release/$canister.wasm" > "$canister_root/$canister.did"
}

# The list of canisters of your project
CANISTERS=encrypted_notes_backend

for canister in $(echo $CANISTERS | sed "s/,/ /g")
do
generate_did "$canister"
done

これで、Candidインタフェースを自動生成する準備ができました! それでは、実際に生成してみましょう。下記のコマンドをプロジェクトのルートディレクトリで実行してください。

npm run generate:did

src/encrypted_notes_backend/encrypted_notes_backend.didを確認してみましょう。

type EncryptedNote = record { id : nat; data : text };
service : {
addNote : (text) -> ();
deleteNote : (nat) -> ();
getNotes : () -> (vec EncryptedNote) query;
updateNote : (EncryptedNote) -> ();
};

typeから始まる部分は、バックエンドキャニスターで定義したが記述されています。serviceから始まる部分は、バックエンドキャニスターの関数が記述されます。関数は"関数名": (引数の型) -> (戻り値の型)という形式となり、引数や戻り値がない場合は、()を指定します。型は、Candidがサポートする型となります(例:RustのString型はtextを指定)。型に関する詳細は、こちらをご覧ください。

📝 テストスクリプトの作成

インタフェースが定義されたので、前回のレッスンで実装したバックエンドキャニスターの関数が正しく動作するかを確認するためのテストスクリプトを作成します。

scripts/の中にtest.shというファイルを作成してください。

 ICP-Encrypted-Notes/
├── scripts/
│ ├── did.sh
+│ └── test.sh
├── src/
├── dfx.json
├── LICENSE
├── README.md
└── dfx.json

test.shに、下記を記述してください。

#!/bin/bash

compare_result() {
local label=$1
local expect=$2
local result=$3

if [ "$expect" = "$result" ]; then
echo "$label: OK"
return 0
else
echo "$label: ERR"
diff <(echo $expect) <(echo $result)
return 1
fi
}

TEST_STATUS=0

# ===== 準備 =====
dfx stop
rm -rf .dfx
dfx start --clean --background

# テストで使用するユーザーを作成する
dfx identity new test-user --storage-mode=plaintext || true
dfx identity use test-user

# キャニスターのデプロイ
dfx deploy encrypted_notes_backend

# ===== テスト =====
FUNCTION='addNote'
echo -e "\n===== $FUNCTION ====="
EXPECT='()'
RESULT=`dfx canister call encrypted_notes_backend $FUNCTION '("First text!")'`
compare_result "Return none" "$EXPECT" "$RESULT" || TEST_STATUS=1

FUNCTION='getNotes'
echo -e "\n===== $FUNCTION ====="
EXPECT='(vec { record { id = 0 : nat; data = "First text!" } })'
RESULT=`dfx canister call encrypted_notes_backend $FUNCTION`
compare_result "Return note list" "$EXPECT" "$RESULT" || TEST_STATUS=1

FUNCTION='updateNote'
echo -e "\n===== $FUNCTION ====="
EXPECT='()'
RESULT=`dfx canister call encrypted_notes_backend $FUNCTION '(
record {
id = 0;
data = "Updated first text!"
}
)'`
compare_result "Return none" "$EXPECT" "$RESULT" || TEST_STATUS=1
# 確認
FUNCTION='getNotes'
EXPECT='(vec { record { id = 0 : nat; data = "Updated first text!" } })'
RESULT=`dfx canister call encrypted_notes_backend $FUNCTION`
compare_result "Check with $FUNCTION" "$EXPECT" "$RESULT" || TEST_STATUS=1

FUNCTION='deleteNote'
echo -e "\n===== $FUNCTION ====="
EXPECT='()'
RESULT=`dfx canister call encrypted_notes_backend $FUNCTION '(0)'`
compare_result "Return none" "$EXPECT" "$RESULT" || TEST_STATUS=1
# 確認
FUNCTION='getNotes'
EXPECT='(vec {})'
RESULT=`dfx canister call encrypted_notes_backend $FUNCTION`
compare_result "Check with $FUNCTION" "$EXPECT" "$RESULT" || TEST_STATUS=1

# ===== 後始末 =====
dfx identity use default
dfx identity remove test-user
dfx stop

# ===== テスト結果の確認 =====
echo '===== Result ====='
if [ $TEST_STATUS -eq 0 ]; then
echo '"PASS"'
exit 0
else
echo '"FAIL"'
exit 1
fi

テストスクリプトの内容を簡単に確認していきましょう。

キャニスター実行環境を立ち上げて、encrypted_notes_backendをデプロイします。テストで使用するユーザーをdfx identity newで作成することができます。

# ===== 準備 =====
dfx stop
rm -rf .dfx
dfx start --clean --background

# テストで使用するユーザーを作成する
dfx identity new test-user --storage-mode=plaintext || true
dfx identity use test-user

# キャニスターのデプロイ
dfx deploy encrypted_notes_backend

dfx canister callでencrypted_notes_backendキャニスターの関数を呼び出します。関数の実行結果(RESULT)と期待する値(EXPECT)をcompare_resultに渡します。

# ===== テスト =====
FUNCTION='addNote'
echo -e "\n===== $FUNCTION ====="
EXPECT='()'
RESULT=`dfx canister call encrypted_notes_backend $FUNCTION '("First text!")'`
compare_result "Return none" "$EXPECT" "$RESULT" || TEST_STATUS=1

...

テストスクリプトの上部に定義されているcompare_result()は、関数の戻り値と期待する値を比較して一致しているかどうかを確認します。一致している場合はOKを出力して0を返します。一致していない場合はERRと差分を表示して1を返します。

# scripts/test.sh
compare_result() {
local label=$1
local expect=$2
local result=$3

if [ "$expect" = "$result" ]; then
echo "$label: OK"
return 0
else
echo "$label: ERR"
diff <(echo $expect) <(echo $result)
return 1
fi
}

compare_resultを呼び出す部分で、compare_result "Return ..." "$EXPECT" "$RESULT" || TEST_STATUS=1と定義していました。これは、compare_resultの戻り値が0でない場合(つまり、実行結果と期待値が一致していない場合)にはTEST_STATUS1を代入するという意味です。TEST_STATUSは、テストの結果を格納する変数です。初期値は0で、テストがすべて成功した場合は0、失敗した場合は1が格納されます。

最後に、テスト用に作成したidentityを削除してテスト結果を出力します。TEST_STATUS0の場合はPASS1の場合はFAILが出力されます。

# ===== 後始末 =====
dfx identity use default
dfx identity remove test-user
dfx stop

# ===== テスト結果の確認 =====
echo '===== Result ====='
if [ $TEST_STATUS -eq 0 ]; then
echo '"PASS"'
exit 0
else
echo '"FAIL"'
exit 1
fi

それではテストスクリプトを実行してみましょう。

npm run test

全てのテストにパスし、最後に"PASS"が出力されたら成功です。

# デプロイに関する出力...

===== addNote =====
Return none: OK

===== getNotes =====
Return note list: OK

===== updateNote =====
Return none: OK
Check with getNotes: OK

===== deleteNote =====
Return none: OK
Check with getNotes: OK

# 後始末に関する出力...

===== Result =====
"PASS"

バックエンドキャニスターに定義した関数が問題なく機能することを確認できました!

🙋‍♂️ 質問する

ここまでの作業で何かわからないことがある場合は、Discordの#icpで質問をしてください。

ヘルプをするときのフローが円滑になるので、エラーレポートには下記の4点を記載してください ✨

1. 質問が関連しているセクション番号とレッスン番号
2. 何をしようとしていたか
3. エラー文をコピー&ペースト
4. エラー画面のスクリーンショット

次のレッスンに進み、フロントエンドキャニスターに認証機能を実装しましょう!