├── .gitignore ├── newbie ├── assets │ ├── 1_Debug.png │ ├── 2_Debug.png │ ├── 3_Debug.png │ ├── 4_Debug.png │ ├── 5_Debug.png │ ├── 6_Debug.png │ ├── 7_Mutex_Arc.png │ ├── 8_Mutex_Arc.png │ ├── 9_Mutex_Arc.png │ └── 10_Thread_Process.png ├── Iterator.md ├── Cargo.md ├── Ownership_and_Borrowing.md ├── From_Into.md ├── NewType_Wrapper.md ├── Debug.md └── Mutex_Arc.md ├── rust-challenges ├── 08-front-end-app │ ├── img │ │ ├── Untitled.png │ │ ├── Untitled 1.png │ │ ├── Untitled 2.png │ │ ├── Untitled 3.png │ │ ├── Untitled 4.png │ │ ├── Untitled 5.png │ │ ├── Untitled 6.png │ │ ├── Untitled 7.png │ │ ├── Untitled 8.png │ │ ├── Untitled 9.png │ │ ├── Untitled 10.png │ │ ├── Untitled 11.png │ │ ├── Untitled 12.png │ │ ├── Untitled 13.png │ │ ├── Untitled 14.png │ │ ├── Untitled 15.png │ │ └── Untitled 16.png │ └── README.md ├── 05-realtime-chatapp │ ├── part1 │ │ ├── img │ │ │ ├── postman.png │ │ │ └── tree_folder.png │ │ └── README.md │ ├── part2 │ │ ├── img │ │ │ ├── Untitled.png │ │ │ ├── Untitled 1.png │ │ │ ├── Untitled 10.png │ │ │ ├── Untitled 11.png │ │ │ ├── Untitled 12.png │ │ │ ├── Untitled 13.png │ │ │ ├── Untitled 14.png │ │ │ ├── Untitled 2.png │ │ │ ├── Untitled 3.png │ │ │ ├── Untitled 4.png │ │ │ ├── Untitled 5.png │ │ │ ├── Untitled 6.png │ │ │ ├── Untitled 7.png │ │ │ ├── Untitled 8.png │ │ │ ├── Untitled 9.png │ │ │ └── Untitled15.png │ │ └── README.md │ └── part3 │ │ ├── img │ │ ├── Untitled.png │ │ ├── Untitled 1.png │ │ ├── Untitled 2.png │ │ ├── Untitled 3.png │ │ └── Untitled 4.png │ │ └── README.md ├── 07-basic-web-service │ └── part 1 │ │ └── image │ │ ├── JWT.png │ │ ├── GetAll.png │ │ ├── NotJWT.png │ │ ├── CheckJWT.png │ │ ├── DeleteUser.png │ │ ├── LoginUser.png │ │ ├── run entity.png │ │ ├── run migrate.png │ │ └── Put JWT in Header.png ├── 06-connect-blockchain │ ├── sui │ │ ├── part1 │ │ │ ├── img │ │ │ │ └── hello_wolrd.jpg │ │ │ └── README.md │ │ └── part2 │ │ │ ├── img │ │ │ ├── Objects_In_Wallet.png │ │ │ └── Shared_Object_Input.png │ │ │ └── README.md │ └── README.md ├── 03-faucet-bot │ └── README.md ├── 01-json-parser │ └── README.md ├── 02-chatbot-discord │ └── README.md └── 04-weather-cli │ └── README.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /newbie/assets/1_Debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/newbie/assets/1_Debug.png -------------------------------------------------------------------------------- /newbie/assets/2_Debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/newbie/assets/2_Debug.png -------------------------------------------------------------------------------- /newbie/assets/3_Debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/newbie/assets/3_Debug.png -------------------------------------------------------------------------------- /newbie/assets/4_Debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/newbie/assets/4_Debug.png -------------------------------------------------------------------------------- /newbie/assets/5_Debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/newbie/assets/5_Debug.png -------------------------------------------------------------------------------- /newbie/assets/6_Debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/newbie/assets/6_Debug.png -------------------------------------------------------------------------------- /newbie/assets/7_Mutex_Arc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/newbie/assets/7_Mutex_Arc.png -------------------------------------------------------------------------------- /newbie/assets/8_Mutex_Arc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/newbie/assets/8_Mutex_Arc.png -------------------------------------------------------------------------------- /newbie/assets/9_Mutex_Arc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/newbie/assets/9_Mutex_Arc.png -------------------------------------------------------------------------------- /newbie/assets/10_Thread_Process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/newbie/assets/10_Thread_Process.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 1.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 2.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 3.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 4.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 5.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 6.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 7.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 8.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 9.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 10.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 11.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 12.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 13.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 14.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 15.png -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/img/Untitled 16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/08-front-end-app/img/Untitled 16.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part1/img/postman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part1/img/postman.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part3/img/Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part3/img/Untitled.png -------------------------------------------------------------------------------- /rust-challenges/07-basic-web-service/part 1/image/JWT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/07-basic-web-service/part 1/image/JWT.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part1/img/tree_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part1/img/tree_folder.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 1.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 10.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 11.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 12.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 13.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 14.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 2.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 3.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 4.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 5.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 6.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 7.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 8.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled 9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled 9.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/img/Untitled15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part2/img/Untitled15.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part3/img/Untitled 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part3/img/Untitled 1.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part3/img/Untitled 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part3/img/Untitled 2.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part3/img/Untitled 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part3/img/Untitled 3.png -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part3/img/Untitled 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/05-realtime-chatapp/part3/img/Untitled 4.png -------------------------------------------------------------------------------- /rust-challenges/07-basic-web-service/part 1/image/GetAll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/07-basic-web-service/part 1/image/GetAll.png -------------------------------------------------------------------------------- /rust-challenges/07-basic-web-service/part 1/image/NotJWT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/07-basic-web-service/part 1/image/NotJWT.png -------------------------------------------------------------------------------- /rust-challenges/07-basic-web-service/part 1/image/CheckJWT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/07-basic-web-service/part 1/image/CheckJWT.png -------------------------------------------------------------------------------- /rust-challenges/07-basic-web-service/part 1/image/DeleteUser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/07-basic-web-service/part 1/image/DeleteUser.png -------------------------------------------------------------------------------- /rust-challenges/07-basic-web-service/part 1/image/LoginUser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/07-basic-web-service/part 1/image/LoginUser.png -------------------------------------------------------------------------------- /rust-challenges/07-basic-web-service/part 1/image/run entity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/07-basic-web-service/part 1/image/run entity.png -------------------------------------------------------------------------------- /rust-challenges/07-basic-web-service/part 1/image/run migrate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/07-basic-web-service/part 1/image/run migrate.png -------------------------------------------------------------------------------- /rust-challenges/06-connect-blockchain/sui/part1/img/hello_wolrd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/06-connect-blockchain/sui/part1/img/hello_wolrd.jpg -------------------------------------------------------------------------------- /rust-challenges/07-basic-web-service/part 1/image/Put JWT in Header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/07-basic-web-service/part 1/image/Put JWT in Header.png -------------------------------------------------------------------------------- /rust-challenges/06-connect-blockchain/sui/part2/img/Objects_In_Wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/06-connect-blockchain/sui/part2/img/Objects_In_Wallet.png -------------------------------------------------------------------------------- /rust-challenges/06-connect-blockchain/sui/part2/img/Shared_Object_Input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbi-academy/rust-developer-vietnam/HEAD/rust-challenges/06-connect-blockchain/sui/part2/img/Shared_Object_Input.png -------------------------------------------------------------------------------- /rust-challenges/06-connect-blockchain/README.md: -------------------------------------------------------------------------------- 1 | # Interact with blockchain using Rust SDK 2 | 3 | + Connect blockchain 4 | + Call blockchain (Transaction and Query) 5 | + This is a sample folder showing how to interact with functionality on the specific Blockchain using the Rust SDK 6 | 7 | # Blockchain 8 | 9 | + EVM 10 | + Polkadot 11 | + Sui 12 | + Aptos 13 | + Near 14 | + Cosmos 15 | + ... 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust Developer Vietnam - OpenEdu101 - VBI Academy 2 | 3 | Đây là series để mọi người chia sẻ kiến thức từ newbie đến nâng cao dành cho các bạn quan tâm tới Rust 4 | 5 | ## Mục đích: 6 | + **Làm quen open-source** 7 | + Chia sẻ và kết nối 8 | + Thảo luận và phản biện 9 | 10 | ## Cách đóng góp 11 | 12 | + Fork repo 13 | + Chia sẻ kiến thức liên quan tới Rust theo cách hiểu của bạn hoặc bổ sung thông tin kiến thức (Hiện tại có 1 folder là `newbie`, tương lai sẽ có nhiều category hơn) 14 | + Tạo PR 15 | 16 | 17 | ## Ngôn ngữ 18 | Tiếng Việt | Tiếng Anh 19 | 20 | 21 | ## Join Discord: 22 | https://discord.gg/xMwmgQRJJr 23 | 24 | ## Join Facebook: Rust Developer VietNam 25 | https://www.facebook.com/groups/rustdevelopersvietnam 26 | 27 | ## Join Facebook: VBI Vietnam Dev and Tech Forum 28 | https://www.facebook.com/groups/vbivietnamdevtech 29 | -------------------------------------------------------------------------------- /newbie/Iterator.md: -------------------------------------------------------------------------------- 1 | ## Cách sử dụng Iterator Trait 2 | 3 | ### Nó là gì ? 4 | 5 | Nó là 1 interface (trait ) của thư viện chuẩn Rust cung cấp dùng để chạy vòng lặp cho collections như arrays, slices , vector, ... 6 | 7 | ### Cách sử dụng : 8 | 9 | - impl trait `Iterator` 10 | - có thể thay thế cho việc dùng vòng lặp (for, while,… ) tuỳ chỉnh được nhiều hơn 11 | 12 | ### Một số ưu điểm: 13 | 14 | - Tuỳ chỉnh dễ dàng 15 | - hỗ trợ thêm các method như map , filter, … 16 | - Tường minh 17 | 18 | ### Ví dụ 1: 19 | 20 | Normal way : 21 | 22 | ```rust 23 | for number in 0..10 { 24 | println!("{}", number); 25 | } 26 | ``` 27 | 28 | Iterator way: 29 | 30 | ```rust 31 | for number in (0..10).iter() { 32 | println!("{}", number); 33 | } 34 | ``` 35 | 36 | ### Ví dụ 2: Tuỳ chỉnh với Iterator (Sum of Squares) 37 | 38 | Normal way: 39 | ```rust 40 | pub fn sum_of_squares(vals: &[u32]) -> u32 { 41 | let mut total = 0; 42 | for val in vals { 43 | total += val*val; 44 | } 45 | total 46 | } 47 | ``` 48 | Iterator way: 49 | 50 | ```rust 51 | pub fn sum_of_squares(vals: impl Iterator) -> u32 { 52 | vals.map(|value| value * value).sum() 53 | } 54 | ``` 55 | Ưu điểm khi sử dụng Iterator trong trường hợp này: 56 | 57 | + input đầu vào `vals` có thể là `Vec`, hoặc `&[u32]`, hoặc kiểu dữ liệu dc trait `Iterator` implement -> dynamic 58 | + sử dụng method `map` và `sum` -> tường minh 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /rust-challenges/03-faucet-bot/README.md: -------------------------------------------------------------------------------- 1 | # Faucet discord 2 | 3 | ## Hash Function : Hàm băm 4 | 5 | Là một hàm toán học chuyển đổi dữ liệu đầu vào thành 1 chuỗi số cố định 6 | 7 | f(x) = y 8 | 9 | x: input 10 | 11 | y: hash value 12 | 13 | f(): hàm băm 14 | 15 | Một số hàm băm phổ biến: SHA256, MD5 16 | 17 | 18 | Đặc tính: 19 | 20 | - Xác định được (Deterministic) : Đối với một đầu vào cụ thể, hàm băm sẽ tạo ra 1 đầu ra cố định 21 | - Kích thước đầu ra cố định: ví dụ SHA256 → đầu ra (hash value) là 256 bit 22 | - Không thể đảo ngược quá trình 23 | - Kháng xung đột ( Collision Resistance): 2 giá trị đầu vào không thể tạo ra cùng 1 giá trị băm 24 | 25 | https://emn178.github.io/online-tools/sha256.html 26 | 27 | ## Public Key: Khoá công khai 28 | 29 | - Chia sẻ công khai 30 | - Sử dụng để mã hoá dữ liệu 31 | - được tạo ra từ private key 32 | 33 | Ví dụ trong ETH:0xb9b355abf795769d9137e11377f08b82a5542fa49efde43a65c4496ca3ab866c 34 | 35 | ## Private Key : Khoá riêng tư 36 | 37 | - Phần bí mật không chia sẻ cho bất kì ai 38 | - Nó được sử dụng để giải mã dữ liệu đã được mã hoá bằng khoá công khai 39 | - 32 bytes 40 | 41 | Ví dụ trong ETH: 5ab1e5271387ec6f75b74fa4c7d1d26c924b5d79f6e8e99f0775a37abf2774b8 42 | 43 | ## Address 44 | 45 | - Rút gọn từ public key bằng cách sử dụng hàm băm keccak-256 46 | - Lấy 20 bytes cuối cùng sau khi dùng hàm băm 47 | 48 | Ví dụ trong ETH: `0xb9b355abf795769d9137e11377f08b82a5542fa4` 49 | 50 | Làm thế nào để generate private key và public key ( cặp khoá ) 51 | 52 | - Có thể sử dụng ví không lưu ký như Metamask, Trust Wallet để generate ra private key và public key 53 | - Sử dụng code để có thể tự generate cho mình 54 | 55 | https://secretscan.org/generator 56 | 57 | ## Cấu trúc cơ bản của blockchain 58 | 59 | 60 | ## Yêu cầu trước khi vào challenge 61 | 62 | - Hiểu cơ bản liên quan tới Rust 63 | - Hiểu cách gọi http, sử dụng serenity cơ bản trong phần Challenge trước 64 | - Hiểu cơ bản các khái niệm liên quan tới blockchain 65 | 66 | ## Bài toán 67 | 68 | Faucet testnet coin cho 1 user nào đó yêu cầu 69 | 70 | ## Thư viện hỗ trợ tương tác với blockchain 71 | 72 | - Ethers 73 | - Web3 74 | 75 | ## Các bước thực hiện 76 | 77 | - Sử dụng template tạo command từ serenity-rs 78 | - Check balance của 1 address nào đó trên Sepolia (Ethereum Testnet) 79 | - Lấy gía hiện tại của ETH 80 | - Viết command faucet testnet coin 81 | 82 | Link code: 83 | https://github.com/CocDap/Rust-Challenges/tree/main/faucet-bot 84 | -------------------------------------------------------------------------------- /newbie/Cargo.md: -------------------------------------------------------------------------------- 1 | ## Cargo trong Rust 2 | 3 | ### Nó là gì ? 4 | Cargo là tool quản lý thư viện của Rust. Nó giúp cho bạn cài đặt thư viện 1 cách nhanh chóng, biên dịch code, public code trên crates.io 5 | 6 | ### cargo init 7 | Khởi tạo dự án Rust ban đầu 8 | 9 | Cú pháp cơ bản tạo `binary` package: 10 | 11 | ```bash 12 | cargo init 13 | ``` 14 | 15 | Kết quả: tạo ra file `main.rs` và `Cargo.toml` -> `binary` package, nghĩa là tạo ra project chạy ứng dụng 16 | 17 | ``` 18 | . 19 | ├── Cargo.toml 20 | └── src 21 | └── main.rs 22 | 23 | 2 directories, 2 files 24 | ``` 25 | 26 | 27 | Cú pháp cơ bản tạo `library` package: 28 | 29 | ```bash 30 | cargo init --lib 31 | ``` 32 | 33 | Kết quả: tạo ra file `lib.rs` và `Cargo.toml` -> `library` package, nghĩa là tạo ra project dùng làm thư viện , người khác có thể sử dụng 34 | 35 | ``` 36 | . 37 | ├── Cargo.toml 38 | └── src 39 | └── lib.rs 40 | 41 | 2 directories, 2 files 42 | ``` 43 | 44 | ### cargo build 45 | Biên dịch code rust sang mã máy 46 | 47 | + Biên dịch code với chế độ debug 48 | 49 | ```bash 50 | cargo build 51 | ``` 52 | 53 | + Biên dịch code với chế độ optimized (chế độ chạy ứng dụng thực tế) 54 | 55 | ```bash 56 | cargo build --release 57 | ``` 58 | 59 | ### cargo check 60 | 61 | Kiểm tra project có thể biên dịch hay không. 62 | 63 | ```bash 64 | cargo check 65 | ``` 66 | 67 | ### cargo test 68 | 69 | Chạy các tests của dự án. 70 | Lưu ý muốn viết test case thì cần có `#[cfg(test)]` 71 | 72 | Ví dụ : 73 | 74 | File `main.rs`: 75 | 76 | ```rust 77 | pub fn add(a: i32, b: i32) -> i32 { 78 | a + b 79 | } 80 | ``` 81 | 82 | Viết test trong file `main.rs`: 83 | 84 | ```rust 85 | // định nghĩa viết group tests 86 | #[cfg(test)] 87 | // định nghĩa module test 88 | mod tests { 89 | // import tất cả các hàm public, struct, enum, biến toàn cục ,.. của file main.rs 90 | use super::*; 91 | 92 | // Viết unit test 93 | #[test] 94 | fn test_add() { 95 | // assertion 96 | assert_eq!(add(2, 2), 4); 97 | } 98 | } 99 | ``` 100 | 101 | 102 | ```bash 103 | cargo test 104 | ``` 105 | 106 | ### cargo run 107 | 108 | Chạy dự án 109 | 110 | ```bash 111 | cargo run 112 | ``` 113 | 114 | ### cargo add 115 | Thêm thư viện bên ngoài vào dự án hiện tại. Các bạn có thể check các thư viện ở trên `crates.io` 116 | 117 | Ví dụ : https://crates.io/crates/serde 118 | 119 | 120 | ```bash 121 | cargo add 122 | ``` 123 | 124 | ### cargo tree 125 | Hiển thị đồ thị phụ thuộc của thư viện (sub-dependencies) 126 | 127 | Cú pháp: 128 | 129 | ```bash 130 | cargo tree 131 | ``` 132 | 133 | ``` 134 | ├── log v0.4.21 135 | ├── serde v1.0.200 136 | └── thiserror v1.0.59 137 | └── thiserror-impl v1.0.59 (proc-macro) 138 | ├── proc-macro2 v1.0.81 139 | │ └── unicode-ident v1.0.12 140 | ├── quote v1.0.36 141 | │ └── proc-macro2 v1.0.81 (*) 142 | └── syn v2.0.60 143 | ├── proc-macro2 v1.0.81 (*) 144 | ├── quote v1.0.36 (*) 145 | └── unicode-ident v1.0.12 146 | ``` 147 | 148 | ## Tham khảo 149 | https://doc.rust-lang.org/cargo/ 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /rust-challenges/01-json-parser/README.md: -------------------------------------------------------------------------------- 1 | # JSON PARSER 2 | 3 | ## Yêu cầu trước khi vào Challenge 4 | 5 | - Cài đặt Rust và môi trường 6 | - Biết cơ bản về Rust 7 | - Sẵn sàng thử thách đầu tiên 8 | 9 | ## Mục đích 10 | 11 | Làm quen với Rust để implement các ứng dụng cụ thể - JSON Parser ( phân tích cú pháp của dữ liệu đó có phải là loại JSON hay không ) 12 | 13 | ## JSON 14 | 15 | - Javascript Objection Notion là dạng dữ liệu nhẹ , linh hoạt , dễ sử dụng, tạo thành từ cặp khoá -giá trị (key-value) 16 | - Khoá là 1 chuỗi (string) 17 | - Giá trị có thể là 1 chuỗi (string) , số (number) , mảng(array) hoặc đối tượng (object) 18 | 19 | ### Ví dụ về json 20 | 21 | ```json 22 | { 23 | "title": "Rust Challenge", 24 | "year": 2023, 25 | "live": true, 26 | "organizers": ["vbi", "techfest"], 27 | "presenter": { 28 | "name": "Dung", 29 | "age": 27, 30 | "occupation": "Blockchain Engineer" 31 | } 32 | } 33 | ``` 34 | ### Ứng dụng thực tế của JSON 35 | 36 | - Data exchange: API 37 | 38 | `https://catfact.ninja/fact` 39 | 40 | `{"fact":"A healthy cat has a temperature between 38 and 39 degrees Celcius.","length":66}` 41 | 42 | - Configuration Files: https://cloud.google.com/appengine/docs/admin-api/creating-config-files 43 | 44 | - Data Storage: JSON database (MongoDB sử dụng phiên bản BSON là phiên bản binary JSON hoặc một số DB như CosmosDB, …) 45 | 46 | JSON: `{ "name": "John", "age": 30 }` 47 | 48 | ### Một số JSON parsers có sẵn ở một số ngôn ngữ lập trình 49 | 50 | Javascript: `JSON.parse()` function 51 | 52 | Python: `json` module 53 | 54 | Rust : `serde_json` crate 55 | 56 | … 57 | 58 | ## Bài toán 59 | 60 | Làm thế nào để parse đầu vào `string` sang JSON format 61 | 62 | ### Phân tích đầu vào/ đầu ra: 63 | 64 | - Đầu vào : Chuỗi ký tự 65 | - Đầu ra: JSON format 66 | 67 | ### Bạn sẽ học được những gì trong buổi học hôm nay: 68 | 69 | - Sử dụng if let Some 70 | - Error Handling 71 | - Trait Iterator 72 | - Viết unit test 73 | 74 | ### Cung cấp một số kiến thức cơ bản trước khi implement JSON Parser 75 | 76 | - Trait - Generic type 77 | - If let Some 78 | - Phân biệt cách sử dụng iter() và peek() 79 | 80 | ### Cách giải quyết 81 | 82 | Vì cấu trúc JSON có chuẩn , dựa vào chuẩn ta có các quan sát như sau : 83 | 84 | - Parse string : bắt đầu bằng kí tự `"` và kết thúc bằng kí tự `"` 85 | - Parse number : kiểm tra có phải là number hay không 86 | - Parse boolean: kiểm tra có phải true hay false 87 | - Parse array: bắt đầu bằng kí tự `[` 88 | - Parse object: bắt đầu bằng kí tự `}` 89 | 90 | 91 | ### Testing 92 | 93 | + Trường hợp 1: JSON rỗng 94 | ```json 95 | {} 96 | ``` 97 | 98 | + Trường hợp 2: JSON có 1 key và 1 value 99 | ```json 100 | {"key":"value"} 101 | ``` 102 | 103 | + Trường hợp 3: JSON có nhiều key và nhiều value (string, number, boolean) 104 | ```json 105 | { 106 | "key1": true, 107 | "key2": false, 108 | "key4": "value", 109 | "key5": 101 110 | } 111 | ``` 112 | 113 | + Trường hợp 4: Nested JSON 114 | ```json 115 | { 116 | "key": "value", 117 | "key1": 101, 118 | "key2": {}, 119 | "key3": [] 120 | } 121 | ``` 122 | + Trường hợp 5: 123 | 124 | ```json 125 | { 126 | "title": "Rust", 127 | "year": 2023, 128 | "live": true, 129 | "organizers": ["vbi", "techfest"], 130 | "presenter": { 131 | "name": "Dung", 132 | "age": 27, 133 | "occupation": "Engineer" 134 | } 135 | } 136 | ``` 137 | 138 | 139 | ## Link code 140 | https://github.com/CocDap/Rust-Challenges/tree/main/json-parser 141 | -------------------------------------------------------------------------------- /rust-challenges/02-chatbot-discord/README.md: -------------------------------------------------------------------------------- 1 | # Discord Chatbot 2 | 3 | ## Giới thiệu về HTTP Protocol 4 | 5 | ## HTTP là gì ? 6 | 7 | HTTP (HyperText Transfer Protocol) `giao thức truyền tải siêu văn bản` là một giao thức ứng dụng trong bộ giao thức TCP/IP, được sử dụng để truyền tải dữ liệu giữa máy khách ( client) và máy chủ (server) . HTTP là giao thức cơ bản của World Wide Web (WWW), cho phép người dùng truy cập và tải về các tài nguyên như văn bản, hình ảnh, video, âm thanh 8 | 9 | ## 10 | 11 | ### HTTP Version 12 | 13 | Version của HTTP protocol 14 | 15 | ### URI 16 | 17 | Xác định tài nguyên mà client muốn truy cập 18 | 19 | ### HTTP Method 20 | 21 | Xác định hành động mà client muốn máy chủ thực hiện : GET, POST, PUT, DELETE 22 | 23 | ### HTTP Request Headers 24 | 25 | Chứa thông tin bổ sung về yêu cầu, ví dụ như loại nội dung (image, json, video, ..), kích thước nội dung 26 | 27 | ### HTTP Body 28 | 29 | Chứa thông tin sẽ được gửi cho server ( ví dụ username, password khi login ) 30 | 31 | Ví dụ 32 | ``` 33 | GET /articles/latest HTTP/1.1 34 | Host: www.example.com 35 | Accept: application/json 36 | Connection: Keep-Alive 37 | ``` 38 | - Phương thức: GET 39 | - URI: /articles/latest 40 | - HTTP version: HTTP/1.1 41 | - Headers: Host ( máy chủ), Accept (loại dữ liệu có thể chấp thuận ) , Connection ( mong muốn request cho đến lúc timeout) 42 | 43 | 44 | Ví dụ khác: 45 | 46 | ``` 47 | POST /users HTTP/1.1 48 | Host: www.example.com 49 | Content-Type: application/json 50 | 51 | { 52 | "email": "johndoe@example.com", 53 | "password":"123456789" 54 | } 55 | ``` 56 | 57 | ## HTTP Response 58 | 59 | ### HTTP Status Code (Phần mã trạng thái ) 60 | 61 | XÁc định thành công hay thất bại của yêu cầu từ client ví dụ 62 | 63 | - 200 : Ok 64 | - 404: Không tìm thấy 65 | - 500: Lỗi máy chủ 66 | 67 | ### HTTP Response Headers 68 | 69 | Chứa các thông tin bổ sung về phản hồi, chẳng hạn như loại nội dung, kích thước nội dung,… 70 | 71 | ### HTTP Response Body 72 | 73 | Bao gồm dữ liệu của tài nguyên mà máy khách yêu cầu 74 | 75 | ``` 76 | HTTP/1.1 200 OK 77 | Content-Type: application/json 78 | 79 | { 80 | "message": "Success", 81 | "data": { 82 | "name": "Dung", 83 | "age": 30, 84 | "occupation": "Blockchain Engineer" 85 | } 86 | } 87 | ``` 88 | 89 | 90 | ## Giới thiệu cơ bản về Async , Await 91 | 92 | **`async`** và **`await`** được sử dụng để hỗ trợ lập trình không đồng bộ 93 | 94 | `async` Được sử dụng để định nghĩa một hàm hoặc code block làm việc không đồng bộ 95 | 96 | **`await`**: Được sử dụng trong một hàm **`async`** để chờ (await) kết quả của một thao tác không đồng bộ 97 | 98 | ## Các thư viện hỗ trợ HTTP trong Rust 99 | 100 | ### Reqwest: Higher level HTTP Client 101 | 102 | ### Hyper : low-level HTTP library 103 | 104 | ### Axum : web server 105 | 106 | ### Warp : web server 107 | 108 | ## Yêu cầu trước khi vào challenge 109 | 110 | - Hiểu cơ bản liên quan tới Rust 111 | - Hiểu cơ bản HTTP Request, Response 112 | - Sử dụng framework 113 | 114 | ## Phần 1: Call HTTP để tương tác với Discord server 115 | 116 | - Login 117 | - Get thông tin user 118 | - Logout 119 | 120 | Link code: https://github.com/CocDap/Rust-Challenges/tree/main/discord-chatbot/discord-api 121 | 122 | 123 | ## Phần 2: Discord chatbot GM sử dụng serenity-rs 124 | 125 | - Sử dụng framework Serenity-rust tạo chatbot GM 126 | 127 | Link code: https://github.com/CocDap/Rust-Challenges/tree/main/discord-chatbot/discord-bot 128 | ## Kiến thức học được sau khi xây dựng chatbot bằng Rust 129 | 130 | - Error Handling 131 | - Config file 132 | - Cách sử dụng serde, serde_json 133 | - Cách sử dụng reqwest 134 | - Run chatbot 135 | 136 | ## Tài liệu tham khảo 137 | - Serenity-rs 138 | - https://discord.com/developers/docs/reference 139 | 140 | 141 | -------------------------------------------------------------------------------- /rust-challenges/04-weather-cli/README.md: -------------------------------------------------------------------------------- 1 | # Weather API CLI 2 | 3 | ## Bài toán 4 | Viết 1 ứng dụng cli (command line interface) thông báo thông tin dự báo thời tiết tại một số địa điểm ở thời điểm quá khứ , hiện tại, tương lai 5 | 6 | 7 | ## Các kiến thức bạn sẽ học được trong phần challenges này 8 | ### Error Handling 9 | 10 | Như những challenge trước có hướng dẫn cách bạn custom lỗi cho ứng dụng, đối với thử thách này bạn có thể sử dụng `thiserror` crate. Crate này khá phổ biến trong việc error handling trong rust, vì sự tiện lợi và customize dễ dàng 11 | 12 | Ví dụ: 13 | 14 | Thay vì chúng ta định nghĩa enum rùi dùng trait `From` hoặc `Into` hoặc một số trait khác để map enum sang `string` và ngược lại. Ta có thể sử dụng `thiserror` thông qua `macro` #[error(...)] 15 | 16 | 17 | ```rust 18 | use thiserror::Error; 19 | 20 | #[derive(Debug, Error)] 21 | pub enum Error { 22 | #[error("Date provided is out of range. Valid range: {0} - {1}")] 23 | DateOutOfRange(String, String), 24 | #[error("Hour is out of range. Valid range: 0 - 24")] 25 | HourOutOfRange, 26 | #[error("Reqwest error: {0:#?}")] 27 | Reqwest(#[from] reqwest::Error), 28 | #[error("Invalid date format, must be in YYYY-MM-DD")] 29 | InvalidDate(#[from] chrono::ParseError), 30 | } 31 | ``` 32 | 33 | ### Cách call api 34 | 35 | Phần này có đề cập ở những thử thách trước , các bạn có thể xem lại nhé 36 | 37 | ### Cách sử dụng serde 38 | 39 | `Serde` crate cũng là 1 thư viện sử dụng rất phổ biến khi làm việc với cấu trúc dữ liệu JSON. Hiểu đơn giản là map giữa JSON type và kiểu dữ liệu trong Rust. 40 | 41 | Ví dụ: 42 | + Định nghĩa response khi call api. Deserialize nghĩa là mình convert sang kiểu dữ liệu của Rust ( nếu map), trong trường hợp này là struct 43 | 44 | ```rust 45 | #[derive(Debug, Deserialize)] 46 | 47 | pub struct FutureResponse { 48 | pub location: Location, 49 | pub forecast: Forecast, 50 | } 51 | ``` 52 | 53 | + Unwrap deserialize (trong trường hợp example) 54 | 55 | ```rust 56 | let table = json_to_table(&response).with(Style::rounded()).to_string(); 57 | ``` 58 | 59 | ### Cách sử dụng clap 60 | + Clap là 1 thư viện hỗ trợ viết ứng dụng cli, hỗ trợ custom nhanh chóng và tuỳ biến bằng cách sử dụng macro 61 | 62 | Ví dụ: 63 | 64 | ```rust 65 | #[derive(Parser)] 66 | #[command(name = "Rust Weather CLI tool")] 67 | #[command(version = "1.0")] 68 | #[command(about = "Handy dandy tool to check the weather forecast", long_about = None)] 69 | enum WeatherCli { 70 | /// Check the current weather 71 | Current { 72 | /// Pass US Zipcode, UK Postcode, Canada Postalcode, IP address, Latitude/Longitude (decimal degree) or city name. 73 | #[arg(short, long)] 74 | query: String, 75 | }, 76 | /// Check the forecast weather. Going from 1 - 14 days in the future from now 77 | Forecast { 78 | /// Pass US Zipcode, UK Postcode, Canada Postalcode, IP address, Latitude/Longitude (decimal degree) or city name. 79 | #[arg(short, long)] 80 | query: String, 81 | /// Number of days of weather forecast. Value ranges from 1 to 14 82 | #[arg(long)] 83 | days: u8, 84 | /// Date should be between today and next 14 day in yyyy-MM-dd format. e.g. '2015-01-01' 85 | #[arg(long)] 86 | date: Option, 87 | /// Must be in 24 hour. For example 5 pm should be hour=17, 6 am as hour=6 88 | #[arg(long)] 89 | hour: Option, 90 | /// Enable/Disable Air Quality data in forecast API output. Example, aqi=true or aqi=false 91 | #[arg(long)] 92 | aqi: Option, 93 | }, 94 | /// Check the forecast weather. Going from 14 - 300 days in the future from now 95 | Future { 96 | /// Pass US Zipcode, UK Postcode, Canada Postalcode, IP address, Latitude/Longitude (decimal degree) or city name. 97 | #[arg(short, long)] 98 | query: String, 99 | /// Date should be between 14 days and 300 days from today in the future in yyyy-MM-dd format (i.e. dt=2023-01-01) 100 | #[arg(long)] 101 | date: Option, 102 | }, 103 | /// Check the forecast history 104 | History { 105 | /// Pass US Zipcode, UK Postcode, Canada Postalcode, IP address, Latitude/Longitude (decimal degree) or city name. 106 | #[arg(short, long)] 107 | query: String, 108 | /// Date on or after 1st Jan, 2015 in yyyy-MM-dd format 109 | #[arg(long)] 110 | date: String, 111 | /// Date on or after 1st Jan, 2015 in yyyy-MM-dd format 112 | /// 'end_date' should be greater than 'date' parameter and difference should not be more than 30 days between the two dates. 113 | #[arg(long)] 114 | end_date: Option, 115 | /// Must be in 24 hour. For example 5 pm should be hour=17, 6 am as hour=6 116 | #[arg(long)] 117 | hour: Option, 118 | }, 119 | } 120 | ``` 121 | 122 | ## Link tham khảo 123 | + https://serde.rs/ 124 | + https://www.weatherapi.com/ 125 | + https://docs.rs/clap/latest/clap/ 126 | + https://docs.rs/thiserror/latest/thiserror/ 127 | 128 | 129 | ## Tác giả 130 | 131 | https://github.com/VintageWander/rust-weather-cli -------------------------------------------------------------------------------- /newbie/Ownership_and_Borrowing.md: -------------------------------------------------------------------------------- 1 | ## Hiểu khái niệm và cách thức hoạt động của ownership & borrowing trong Rust 2 | 3 | ### Ownership là gì ? 4 | 5 | "Ownership" trong Rust, một chủ đề rất hay và quan trọng! Hãy tưởng tượng bạn có một chiếc bánh pizza. Trong Rust, "Ownership" giống như quy tắc xác định ai có quyền ăn miếng pizza đó. Đây là một cách Rust giữ cho bộ nhớ của programs được sắp xếp gọn gàng và an toàn - giống như cách bạn không muốn ai khác cắn vào miếng pizza của mình 😁! 6 | 7 | ##### Các features: 8 | + **Mỗi mảnh bộ nhớ có một chủ nhân**: Trong Rust, mỗi mảnh bộ nhớ được cấp phát (chẳng hạn như một biến) đều có một "owner". Điều này giúp Rust biết khi nào cần giải phóng bộ nhớ đó. 9 | + **Chỉ có một chủ nhân tại một thời điểm**: Giống như việc chỉ có một người có thể cầm miếng pizza (không ai muốn một miếng pizza bị cắn chung đúng không 😔?), một mảnh bộ nhớ trong Rust chỉ có thể có một owner tại một thời điểm. 10 | + **Khi chủ nhân ra đi, bộ nhớ được dọn dẹp**: Khi một owner (chẳng hạn như một biến) ra khỏi phạm vi hoạt động (hãy tưởng tượng họ đi khỏi bàn ăn), Rust tự động giải phóng bộ nhớ mà owner đó quản lý. Đây là cách Rust giải phóng bộ nhớ mà không cần đến garbage collector (trình dọn rác). 11 | 12 | ### Một số ưu điểm: 13 | 14 | - **An toàn và tiết kiệm cho bộ nhớ 🛡️**: giúp ngăn chặn lỗi rò rỉ bộ nhớ và đảm bảo rằng bộ nhớ không bị truy cập trái phép. Bằng cách quản lý bộ nhớ hiệu quả, "Ownership" giúp giảm bớt việc sử dụng tài nguyên không cần thiết. Điều này rất quan trọng đối với các ứng dụng yêu cầu hiệu năng cao hoặc chạy trên phần cứng có hạn chế. Nó giống như việc bạn sử dụng mỗi miếng pizza một cách triệt để. 15 | 16 | - **Tránh concurrency errors 😷**: Khi nhiều tiến trình cố gắng truy cập vào cùng một dữ liệu, "Ownership" giống như một quản lý hàng đợi thông minh. Nó giúp ngăn chặn lỗi liên quan đến sự đồng thời, đảm bảo mỗi tiến trình lần lượt "ăn" mà không "đụng hàng". 17 | 18 | - **Tối ưu hiệu năng (Không cần garbage collector) 🚀**: giúp chạy nhanh hơn, giống như là bạn chạy marathon mà không cần mang theo một cái ba lô nặng nề. "Ownership" giúp quản lý bộ nhớ một cách hiệu quả, làm tăng hiệu năng tổng thể của những đoạn code. 19 | 20 | - **Dễ dàng handle predict behavior 🧐**: khi bạn biết rõ ai là owner của từng mảnh bộ nhớ, việc dự đoán và hiểu hành vi của "programs" trở nên dễ dàng hơn. Nó giống như việc biết rõ ai sẽ ăn miếng pizza cuối cùng trước khi hộp pizza mở. 21 | 22 | ### Ví dụ ownership: 23 | ###### Ví dụ đơn giản minh họa cách ownership giúp quản lý bộ nhớ: 24 | 25 | ```rust 26 | fn main() { 27 | let my_pizza = String::from("Bánh Pizza Pepperoni"); // Bạn có một chiếc pizza! 28 | eat_pizza(my_pizza); // Bạn đưa chiếc pizza của mình cho hàm `eat_pizza`. 29 | 30 | // Ôi không! Bạn không thể sử dụng `my_pizza` ở đây nữa. Nó đã được "ăn" (ownership đã được chuyển giao). 31 | // println!("Tôi vẫn còn {}", my_pizza); // Compiler rust sẽ báo lỗi. 32 | } 33 | 34 | fn eat_pizza(pizza: String) { 35 | println!("Tôi đang ăn {}!", pizza); // Chiếc pizza đang được "ăn" ở đây. 36 | // Sau khi hàm này kết thúc, biến `pizza` sẽ hết phạm vi và bộ nhớ của nó được giải phóng. 37 | } 38 | /* 39 | Output: "Tôi đang ăn Bánh Pizza Pepperoni!" 40 | */ 41 | /* 42 | Tạo một String có tên my_pizza. 43 | Gọi hàm eat_pizza và truyền my_pizza cho nó. Bây giờ, eat_pizza sở hữu chiếc pizza. 44 | Bên trong eat_pizza, chiếc pizza được sử dụng (trong trường hợp này, nó chỉ được in ra màn hình console). 45 | Một khi eat_pizza hoàn thành, biến pizza sẽ hết phạm vi, và bộ nhớ của nó được tự động giải phóng. Điều này giống như việc bạn đã ăn xong chiếc pizza – không còn gì để dùng nữa. 46 | Trở lại trong hàm main, nếu bạn cố gắng sử dụng my_pizza lần nữa, Rust sẽ ngăn lại. Tại sao? Bởi vì bạn không còn sở hữu chiếc pizza nữa vì đã đưa nó đi mất rồi 😡! 47 | */ 48 | ``` 49 | 50 | ### Borrowing là gì ? 51 | 52 | "Borrowing" trong Rust là một khái niệm quan trọng khác, liên quan chặt chẽ với "Ownership". Hãy tưởng tượng bạn có một cuốn sách hay và bạn muốn cho bạn bè mượn để đọc, nhưng vẫn đảm bảo cuốn sách đó cuối cùng sẽ quay trở lại của tay bạn. Đó chính là ý tưởng cơ bản của "Borrowing" trong Rust. 53 | 54 | #### Giải Thích "Borrowing: 55 | **Borrowing tạm thời**: Khi bạn "mượn" một giá trị trong Rust, bạn tạm thời truy cập vào nó mà không lấy đi quyền sở hữu. Điều này được thực hiện thông qua tham chiếu. 56 | 57 | **Tham chiếu không đổi và tham chiếu đổi**: 58 | 59 | ###### - Tham chiếu không thay đổi Immutable (&): Cho phép bạn đọc dữ liệu mà không thay đổi nó. Bạn có thể có nhiều tham chiếu không đổi tới cùng một dữ liệu cùng một lúc. 60 | 61 | ###### - Tham chiếu đổi Mutable (&mut): Cho phép bạn thay đổi dữ liệu. Chỉ có thể có một tham chiếu đổi tới một dữ liệu tại một thời điểm. 62 | **Quy tắc an toàn**: Rust đảm bảo rằng không bao giờ có tham chiếu đổi khi có tham chiếu không đổi khác đang tồn tại. Điều này ngăn chặn dữ liệu bị thay đổi khi đang được đọc, giúp tránh các lỗi liên quan đến sự đồng thời. 63 | 64 | #### Ví Dụ về "Borrowing": 65 | 66 | ###### Giả sử bạn có một hàm đọc nội dung của một cuốn sách mà không thay đổi nó: 67 | 68 | ```rust 69 | fn main() { 70 | let mut book = String::from("The Rust Programming Language"); 71 | 72 | read_book(&book); // Mượn `book` qua tham chiếu không đổi 73 | 74 | annotate_book(&mut book, " - Sách hay!"); // Chỉnh sửa `book` với tham chiếu có thể đổi 75 | 76 | read_book(&book); //Book đã được chỉnh sửa 77 | 78 | println!("Tôi vẫn sở hữu book: {}", book); 79 | } 80 | 81 | fn read_book(book: &String) { 82 | println!("Đang đọc: {}", book); 83 | // `book` không thể thay đổi ở đây vì nó là tham chiếu không đổi. 84 | } 85 | 86 | fn annotate_book(book: &mut String, note: &str) { 87 | book.push_str(note); 88 | println!("Đã add ghi chú vào book"); 89 | } 90 | 91 | 92 | /* 93 | Output: 94 | Đang đọc: The Rust Programming Language 95 | Đã add ghi chú vào book 96 | Đang đọc: The Rust Programming Language - Sách hay! 97 | Tôi vẫn sở hữu book: The Rust Programming Language - Sách hay! 98 | */ 99 | /* 100 | book.push_str(note); thay đổi nội dung của book. 101 | */ 102 | ``` -------------------------------------------------------------------------------- /newbie/From_Into.md: -------------------------------------------------------------------------------- 1 | ## From và Into Trait trong Rust 2 | 3 | ### Yêu cầu kiến thức 4 | + Hiểu cơ bản cách sử dụng và ứng dụng của `Trait` 5 | 6 | 7 | ### Trait `From` và `Into` là gì 8 | 9 | 1. `From` và `Into` là 2 trait cung cấp sẵn bởi Rust, nhằm với mục đích chuyển đổi kiểu dữ liệu A sang dữ liệu B và ngược lại một cách an toàn 10 | 11 | 2. Có vẻ như các bạn có thể hình dung được nó như cú pháp `as` (chuyển đổi dữ liệu) , nhưng From và Into khác biệt ở chỗ là flexible trong cách chuyển đổi, quản lý chặt chẽ khi chuyển đổi xảy ra lỗi, hỗ trợ complex custom data 12 | 13 | #### From 14 | 15 | + Code: 16 | 17 | ```rust 18 | pub trait From: Sized { 19 | // Required method 20 | fn from(value: T) -> Self; 21 | } 22 | ``` 23 | 24 | + Source: https://doc.rust-lang.org/std/convert/trait.From.html 25 | 26 | + `From for U` : Muốn chuyển đổi kiểu dữ liệu T sang U dùng method `from` 27 | 28 | + Dựa vào trait `From`, mô tả hành vi cụ thể cho 1 đối tượng cụ thể 29 | 30 | + Ví dụ : Chuyển độ F sang độ C và ngược lại 31 | 32 | ```rust 33 | struct Fahrenheit(f64); 34 | struct Celsius(f64); 35 | 36 | // Chuyển độ C sang độ F 37 | impl From for Fahrenheit { 38 | fn from(c: Celsius) -> Self { 39 | Fahrenheit(c.0 * 1.8 + 32.0) 40 | } 41 | } 42 | 43 | // Chuyển độ F sang độ C 44 | impl From for Celsius { 45 | fn from(f: Fahrenheit) -> Self { 46 | Celsius((f.0 - 32.0) / 1.8) 47 | } 48 | } 49 | 50 | fn main() { 51 | let boiling_f = Fahrenheit(212.0); 52 | let boiling_c = Celsius::from(boiling_f); 53 | println!("Nhiệt độ sôi của nước: {} độ C", boiling_c.0); 54 | 55 | let freezing_c = Celsius(0.0); 56 | let freezing_f = Fahrenheit::from(freezing_c); 57 | println!("Nhiệt độ đóng băng của nước: {} độ F", freezing_f.0); 58 | 59 | } 60 | ``` 61 | 62 | 4. Một số ví dụ thực tế 63 | + `rand` crate : https://github.com/rust-random/rand/blob/master/src/seq/index.rs#L127 64 | + `base64` crate: https://github.com/marshallpierce/rust-base64/blob/master/src/decode.rs#L78 -> phục vụ cho mục đích trả về lỗi 65 | + `syn` crate: https://github.com/dtolnay/syn/blob/master/src/lit.rs#L484 66 | 67 | + ... 68 | 69 | 70 | #### Into 71 | 72 | + Code: 73 | 74 | ```rust 75 | pub trait Into: Sized { 76 | // Required method 77 | fn into(self) -> T; 78 | } 79 | ``` 80 | 81 | + Source: https://doc.rust-lang.org/std/convert/trait.Into.html 82 | 83 | + `Into`: Nếu đã implement `From for U` thì Rust sẽ tự động implement trait `Into for T`
84 | Và để minh chứng cho việc Rust tự động implement trait `Into` cho `T` với `T` đã implement `From`, chúng ta có thể vào đường link này để xem trong source code của Rust 85 | [https://doc.rust-lang.org/src/core/convert/mod.rs.html#746-761](https://doc.rust-lang.org/src/core/convert/mod.rs.html#746-761)
86 | Đoạn code bên dưới đã được lược bỏ phần comment để tập trung vào phần code logic 87 | ```rust 88 | #[stable(feature = "rust1", since = "1.0.0")] 89 | impl Into for T 90 | where 91 | U: From, 92 | { 93 | #[inline] 94 | #[track_caller] 95 | fn into(self) -> U { 96 | U::from(self) 97 | } 98 | } 99 | ``` 100 | Phân tích code: 101 | ```rust 102 | impl Into for T 103 | where 104 | U: From 105 | ... 106 | ``` 107 | Đoạn code này là khai báo việc mình muốn implement `Into` cho `T`, với điều kiện U phải có `From`
108 | Tại vì `T` là generic, suy ra `Into` được đặt lên TẤT CẢ các dạng tồn tại trong rust, miễn `U: From` 109 | ```rust 110 | { 111 | #[inline] 112 | #[track_caller] 113 | fn into(self) -> U { 114 | U::from(self) 115 | } 116 | } 117 | ``` 118 | Đoạn code này chỉ đơn thuần là gọi hàm `from` từ `U`, tại vì điều kiện `U: From` dẫn đến `U` có hàm `from` để chạy và `U::from()` trả về `T` 119 | 120 | + Ví dụ : Dựa vào ví dụ trên, ta đã implement `trait From for U`, nghĩa là implement `impl From for Fahrenheit` và `impl From for Celsius` -> có thể sử dụng `.into()` để chuyển đổi 121 | 122 | 123 | 124 | ```rust 125 | fn main() { 126 | let boiling_f = Fahrenheit(212.0); 127 | // Khi dùng into thì cần kèm theo kiểu dữ liệu cần chuyển đổi 128 | // Chuyển độ F sang C 129 | let boiling_c: Celsius = boiling_f.into(); 130 | println!("Nhiệt độ sôi của nước: {} độ C", boiling_c.0); 131 | 132 | // Chuyển độ C sang F 133 | let freezing_c = Celsius(0.0); 134 | let freezing_f: Fahrenheit = freezing_c.into(); 135 | println!("Nhiệt độ đóng băng của nước: {} độ F", freezing_f.0); 136 | } 137 | ``` 138 | 139 | ### Kết luận 140 | + Mối quan hệ: `From for U` và `Into for T` là 2 trait có sẵn được sử dụng thông dụng trong Rust, thường sẽ implement `impl From for U` thì có thể sử dụng `Into for T` 141 | 142 | + Chuyển đổi ko có lỗi : Nghĩa là chuyển đổi kiểu dữ liệu A sang B và ngược lại luôn thành công. Nếu handle được lỗi khi convert , ta có thể sử dụng cặp trait tương tự 143 | là `TryFrom` và `TryInto` 144 | 145 | https://doc.rust-lang.org/std/convert/trait.TryFrom.html 146 | 147 | 148 | **Bài toán : Convert String sang IPV4Address** 149 | 150 | ```rust 151 | // Import TryFrom 152 | use std::convert::TryFrom; 153 | 154 | // Định nghĩa IpV4Address 155 | // Ví dụ : 127.0.0.1 -> [127, 0, 0, 1] 156 | #[derive(Debug)] 157 | struct IpV4Address { 158 | ip: [u8; 4], 159 | } 160 | 161 | // Định nghĩa lỗi khi convert string sang IpV4Address 162 | #[derive(Debug)] 163 | enum IpV4AddressError { 164 | InvalidFormat, 165 | OutOfRange, 166 | } 167 | 168 | // Implement trait TryFrom convert từ String sang IpV4Address 169 | // Đối với trait TryFrom , có thêm associated type Error -> custom lỗi trả về 170 | impl TryFrom for IpV4Address { 171 | // Lỗi trả về đang định dạng enum 172 | type Error = IpV4AddressError; 173 | 174 | fn try_from(value: String) -> Result { 175 | // split chuỗi dựa vào dấu . 176 | let ip_splited: Vec<&str> = value.split('.').collect(); 177 | 178 | // check valid length format 179 | if ip_splited.len() != 4 { 180 | return Err(IpV4AddressError::InvalidFormat); 181 | } 182 | 183 | let mut ip = [0u8; 4]; 184 | for (i, s) in ip_splited.iter().enumerate() { 185 | //parse string sang number u8 186 | match s.parse::() { 187 | Ok(number) => ip[i] = number, 188 | Err(_) => return Err(IpV4AddressError::OutOfRange), 189 | } 190 | } 191 | 192 | Ok(IpV4Address { ip }) 193 | } 194 | } 195 | 196 | fn main() -> Result<(), IpV4AddressError> { 197 | let valid_ip_str = "127.0.0.1".to_string(); 198 | let invalid_ip_str = "999.999.999.999".to_string(); 199 | 200 | let valid_ip = IpV4Address::try_from(valid_ip_str)?; 201 | println!("Valid Ip:{:?}", valid_ip); 202 | 203 | let invalid_ip = IpV4Address::try_from(invalid_ip_str)?; 204 | println!("InValid Ip:{:?}", invalid_ip); 205 | Ok(()) 206 | } 207 | ``` 208 | 209 | Kết quả: 210 | 211 | ``` 212 | Valid Ip:IpV4Address { ip: [127, 0, 0, 1] } 213 | Error: OutOfRange 214 | ``` 215 | 216 | -------------------------------------------------------------------------------- /newbie/NewType_Wrapper.md: -------------------------------------------------------------------------------- 1 | ## NewType Wrapper trong Rust 2 | 3 | ### Yêu cầu kiến thức 4 | + Hiểu cơ bản cách sử dụng và ứng dụng của `Trait` 5 | 6 | 7 | ### NewType Wrapper là gì 8 | `NewType Wrapper` là 1 pattern sử dụng phương pháp `wrap`(đóng gói) các kiểu dữ liệu có sẵn trong Rust, cho phép chúng ta có thể mở rộng nhiều chức năng hơn 9 | 10 | ### Ví dụ cơ bản 11 | 12 | 13 | #### Giả sử in ra Vector String 14 | + Ta thấy rằng Vector và String là 2 kiểu dữ liệu mà Rust cung cấp 15 | + Khi in ra thì `trait Debug` đã implement cho Vector (https://doc.rust-lang.org/std/vec/struct.Vec.html#impl-Debug-for-Vec%3CT,+A%3E) 16 | 17 | ```rust 18 | fn main(){ 19 | let vec = vec!["One".to_string(), "Two".to_string(), "Three".to_string()]; 20 | println!("Vec:{:?}", vec); 21 | } 22 | ``` 23 | 24 | Kết quả: 25 | ``` 26 | Vec:["One", "Two", "Three"] 27 | ``` 28 | 29 | #### Giờ mình muốn kết quả in ra là các phần tử phân tách nhau bằng dấu chấm phẩy 30 | Kết quả mình mong muốn: 31 | ``` 32 | Vec:["One"; "Two"; "Three"] 33 | ``` 34 | 35 | #### Làm sao nhỉ -> Implement trait Debug cho Vec 36 | 37 | ```rust 38 | use std::fmt; 39 | 40 | // Re-implement trait Debug cho Vec 41 | impl fmt::Debug for Vec { 42 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 43 | write!(f, "[{}]", self.join(", ")) } 44 | } 45 | 46 | fn main(){ 47 | let vec = vec!["One".to_string(), "Two".to_string(), "Three".to_string()]; 48 | println!("Vec:{:?}", vec); 49 | } 50 | ``` 51 | 52 | #### Rust không cho phép Re-implement Trait cho Vec 53 | 54 | ```| impl fmt::Debug for Vec { 55 | | ^^^^^^^^^^^^^^^^^^^^----------- 56 | | | | 57 | | | `Vec` is not defined in the current crate 58 | | impl doesn't use only types from inside the current crate 59 | | 60 | = note: define and implement a trait or new type instead 61 | ``` 62 | 63 | -> Vi phạm `Orphan Rule` (You can implement a trait for a type only if either the trait or the type is defined in your crate) 64 | -> Nghĩa là bạn có thể implement trait cho kiểu dữ liệu nếu trait hoặc kiểu dữ liệu đó đang định nghĩa ở module/crate của bạn 65 | -> Trong trường hợp trên 66 | + Debug : có import vào crate 67 | + Vec: đây là private struct , nên chúng ta không thể import 68 | 69 | -> Suy ra vi phạm quy tắc `Orphan Rule` 70 | 71 | 72 | #### Cách giải quyết -> Sử dụng New Type Wrapper 73 | 74 | ```rust 75 | use std::fmt; 76 | // sử dụng kiểu dữ liệu mới -> wrap kiểu dữ liệu có sẵn 77 | // Wrapper là kiểu dữ liệu customized , mình có quyền extend chức năng và re-implement trait 78 | struct Wrapper(Vec); 79 | // Re-implement Debug for Wrapper 80 | impl fmt::Debug for Wrapper { 81 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 82 | 83 | write!(f, "Wrapper([")?; 84 | // Iterate từng phần tử trong Vector 85 | for (i, elem) in self.0.iter().enumerate() { 86 | if i > 0 { 87 | write!(f, "; ")?; 88 | } 89 | write!(f, "\"{}\"", elem)?; 90 | } 91 | write!(f, "])") 92 | } 93 | } 94 | 95 | fn main() { 96 | let w = Wrapper(vec!["One".to_string(), "Two".to_string(), "Three".to_string()]); 97 | println!("{:?}", w); 98 | } 99 | ``` 100 | 101 | Kết quả: 102 | ``` 103 | Wrapper(["One"; "Two"; "Three"]) 104 | ``` 105 | 106 | ### Ví dụ thực tế 107 | 108 | #### Sử dụng newtype Wrapper với From/Into và TryFrom/TryInto 109 | 110 | Phần này mình đã giới thiệu trong phần `From_Into.md` 111 | [From_Into.md](From_Into.md) 112 | 113 | #### Một số references 114 | 1. https://paritytech.github.io/polkadot-sdk/master/sp_core/struct.H160.html 115 | 116 | + Chúng ta sẽ không hiểu ý nghĩa thực sự của `[u8;20]` để làm gì 117 | 118 | ```rust 119 | let array: [u8; 20] = [ 120 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 121 | 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 122 | ]; 123 | ``` 124 | 125 | + Sử dụng new type wrapper cho mục đích là biểu diễn thông tin Hash của 1 address ETH 126 | 127 | ```rust 128 | pub struct H160(pub [u8;20]); 129 | let array: [u8; 20] = [ 130 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 131 | 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 132 | ]; 133 | let hash = H160(array); 134 | ``` 135 | 136 | + Bài toán cơ bản: Convert String to H160 137 | 138 | Phần này mình có thể tự định nghĩa 1 newtype wrapper riêng hoặc có thể sử dụng một số crates như `sp-core`, ... 139 | 140 | Step 0: Tạo project mới 141 | 142 | ``` 143 | cargo new --lib newtype 144 | ``` 145 | 146 | Step 1: Import `sp-core` (using cargo add sp-core) 147 | 148 | Step 3: Implement và test convert String sang kiểu wrapper type 149 | 150 | ```rust 151 | use core::str::FromStr; 152 | use sp_core::H160; 153 | 154 | #[derive(Clone, Debug, PartialEq)] 155 | 156 | pub enum ConvertH160Error { 157 | InvalidAddress(String), 158 | } 159 | 160 | pub fn is_valid_address(address: &str) -> Result { 161 | H160::from_str(&address) 162 | .map(Into::into) 163 | .ok() 164 | .ok_or(ConvertH160Error::InvalidAddress(address.to_string())) 165 | } 166 | 167 | #[cfg(test)] 168 | mod tests { 169 | use super::*; 170 | 171 | #[test] 172 | fn test_is_valid_address() { 173 | let valid_address_str = "0x783FC27915754512E72b5811599504eCa458E4C5"; 174 | assert!(is_valid_address(valid_address_str).is_ok()); 175 | } 176 | 177 | #[test] 178 | fn test_is_not_valid_address() { 179 | let invalid_1 = "0x123"; 180 | assert!(is_valid_address(invalid_1).is_err()); 181 | let invalid_2 = "1223"; 182 | assert!(is_valid_address(invalid_2).is_err()); 183 | let invalid_3 = "i am a robot"; 184 | assert!(is_valid_address(invalid_3).is_err()); 185 | let invalid_4 = "0x783FC27915754512E72b5811599504eCa458E4C51012"; 186 | assert!(is_valid_address(invalid_4).is_err()); 187 | 188 | 189 | } 190 | } 191 | ``` 192 | 193 | Run test case 194 | ``` 195 | cargo test 196 | ``` 197 | 198 | Step 4: Mở rộng chức năng cho `H160` như initialize `0` , convert sang `bytes` ,... 199 | 200 | 201 | 2. https://github.com/solana-labs/solana/blob/master/sdk/src/signer/keypair.rs#L25 202 | 203 | 204 | 205 | 206 | ### Ưu/Nhược điểm khi sử dụng NewType Wrapper 207 | 208 | #### Ưu điểm 209 | 210 | ##### 1. Type safety : Dữ liệu an toàn 211 | 212 | 1. Vấn đề: Giả dụ mình tạo user với email và password. Đôi lúc mình sơ ý điền password trước , email sau. Khiến cho việc validate không an toàn. 213 | 214 | ```rust 215 | pub fn create_user(email: &str, password: &str) -> Result { 216 | validate_email(email)?; 217 | validate_password(password)?; 218 | let password_hash = hash_password(password)?; 219 | // Save user to database 220 | // Trigger welcome emails 221 | // ... 222 | Ok(User) 223 | } 224 | ``` 225 | 226 | 2. Giải quyết: Constraint biến đầu vaò bắt buộc là Wrapper Email và Wrapper Password -> `Thông tin đầu vào rõ ràng và đảm bảo dữ liệu an toàn` 227 | 228 | ```rust 229 | #[derive(Debug, Clone, PartialEq)] 230 | pub struct EmailAddress(String); 231 | 232 | #[derive(Debug, Clone, PartialEq)] 233 | pub struct Password(String); 234 | 235 | pub fn create_user(email: EmailAddress, password: Password) -> Result { 236 | validate_email(&email)?; 237 | validate_password(&password)?; 238 | let password_hash = hash_password(&password)?; 239 | // ... 240 | Ok(User) 241 | } 242 | ``` 243 | 244 | ##### 2. Implemetation tuỳ chỉnh: 245 | Khi tạo ra 1 new type wrapper thì việc implement trait để mở rộng chức năng của new type wrapper 246 | 247 | #### Nhược điểm 248 | 249 | ##### 1. Khó implement dành cho beginner 250 | ##### 2. Tính tương tác: 251 | Bạn sẽ khó khăn khi convert từ newtype wrapper sang kiểu dữ liệu mà được wrap hoặc ngược lại 252 | -------------------------------------------------------------------------------- /newbie/Debug.md: -------------------------------------------------------------------------------- 1 | ## Cách debug trong Rust 2 | 3 | Trong quá trình code một chương trình ứng dụng, ta có thể gặp phải bug( lỗi) . Điều này dẫn đến toàn bộ ứng dụng có thể lỗi. Vậy chúng ta cần biết cách debug sao cho fix được lỗi mà ta đang gặp phải 4 | 5 | 6 | ### Cách đơn giản nhất là kiểm tra terminal , sử dụng macro println!, panic! 7 | 8 | #### Kiểm tra terminal 9 | 10 | Ta có 1 ví dụ lỗi cơ bản sau: 11 | ```rust 12 | fn main() { 13 | let items = vec![10, 20, 30, 40, 50]; 14 | let mut result = 0; 15 | 16 | for i in 0..=items.len() { 17 | result += items[i]; 18 | 19 | } 20 | println!("Result : {}", result); 21 | } 22 | ``` 23 | 24 | Khi chạy chương trình , bug xảy ra 25 | 26 | ```thread 'main' panicked at src/main.rs:5:41: 27 | index out of bounds: the len is 5 but the index is 5 28 | note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 29 | ``` 30 | 31 | Dựa vào thông tin lỗi được in ra từ terminal, có thể phán đoán được lỗi sai ở đâu 32 | 33 | #### Sử dụng println! 34 | 35 | - Giả sử không phán đoán được lỗi ở đâu, có thể sử dụng println! để in ra kết quả . nếu như không xảy ra lỗi thì vẫn in ra bình thường 36 | 37 | Ví dụ: 38 | In ra `i` và `items[i]` để kiểm tra 39 | 40 | ```rust 41 | fn main() { 42 | let items = vec![10, 20, 30, 40, 50]; 43 | let mut result = 0; 44 | 45 | for i in 0..=items.len() { 46 | println!("Item {}: {}", i, items[i]); 47 | result += items[i]; 48 | 49 | } 50 | println!("Result : {}", result); 51 | } 52 | ``` 53 | 54 | - Kết quả sau khi chạy chương trình: 55 | 56 | ```Item 0: 10 57 | Item 1: 20 58 | Item 2: 30 59 | Item 3: 40 60 | Item 4: 50 61 | thread 'main' panicked at src/main.rs:6:41: 62 | index out of bounds: the len is 5 but the index is 5 63 | note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 64 | ``` 65 | 66 | **Từ kết quả trên, ta phán đoán được vòng lặp bị sai , lặp quá giới hạn `len` của `vector`** 67 | 68 | - Fix 69 | 70 | ```rust 71 | fn main() { 72 | let items = vec![10, 20, 30, 40, 50]; 73 | let mut result = 0; 74 | 75 | for i in 0..items.len() { 76 | println!("Item {}: {}", i, items[i]); 77 | result += items[i]; 78 | 79 | } 80 | println!("Result : {}", result); 81 | } 82 | ``` 83 | 84 | - Kết quả hiện ta từ terminal 85 | 86 | ```Item 0: 10 87 | Item 1: 20 88 | Item 2: 30 89 | Item 3: 40 90 | Item 4: 50 91 | Result : 150 92 | ``` 93 | 94 | 95 | 96 | ### Sử dụng debug tool trên VSCode 97 | Khi chương trình quá lớn, khả năng manually debug bằng cách println! hoặc panic! khó khả thi. Chúng ta có thể dùng công cụ debug tool trên VSCode 98 | 99 | Yêu cầu: Đã cài đặt `rust-analyzer` 100 | 101 | - Cài thêm debugging support trên extension 102 | + Window: https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools 103 | + MacOs, Linux: https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb 104 | 105 | Ví dụ: Ta sử dụng Iterator để tạo ra dãy Fibonaci như sau 106 | 107 | ```rust 108 | #[derive(Default, Debug)] 109 | pub struct Fibonacci { 110 | /// The most recent value this iterator has yielded 111 | prev: Option, 112 | /// The second most recent value that this iterator has yielded 113 | prev_prev: Option, 114 | } 115 | 116 | impl Iterator for Fibonacci { 117 | type Item = u32; 118 | 119 | fn next(&mut self) -> Option { 120 | let new_value = match (self.prev, self.prev_prev) { 121 | (Some(pre), Some(pre_pre)) => { 122 | let new_value = pre + pre_pre; 123 | self.prev_prev = self.prev; 124 | self.prev = Some(new_value); 125 | new_value 126 | } 127 | (Some(_pre), None) => { 128 | self.prev_prev = self.prev; 129 | self.prev = Some(1); 130 | 1 131 | } 132 | (None, None) => { 133 | self.prev = Some(0); 134 | 0 135 | } 136 | (_, _) => return None, 137 | }; 138 | 139 | Some(new_value) 140 | } 141 | } 142 | 143 | fn main() { 144 | 145 | let mut fibo = Fibonacci{ prev: Some(1), prev_prev: Some(0)}; 146 | fibo.next(); 147 | 148 | println!("Fibo sequence after next:{:?}", fibo); 149 | fibo.next(); 150 | println!("Fibo sequence after second next:{:?}", fibo); 151 | 152 | fibo.next(); 153 | println!("Fibo sequence after third next:{:?}", fibo); 154 | 155 | fibo.next(); 156 | println!("Fibo sequence after fourth next:{:?}", fibo); 157 | } 158 | ``` 159 | 160 | Có thể sử dụng debug tool để debug kết quả trong quá trình next(): 161 | + Đánh dấu Checkpoint bằng cách click dòng code bạn muốn debug (nơi bạn mong muốn debug -> hiển thị kết quả) 162 | ![Checkpoint](./assets/1_Debug.png) 163 | 164 | + Click vào nút `Run and Debug` (hình con bọ trên VSCode) 165 | ![Run and Debug](./assets/2_Debug.png) 166 | 167 | + Click `Debug Exec ở góc bên trái màn hình` để tiến hành debug 168 | ![Debug Exec](./assets/3_Debug.png) 169 | 170 | + Xuất hiện giá trị biến local, static, global 171 | ![Variables](./assets/4_Debug.png) 172 | 173 | + Nhấn nút `Step into` để vào trong hàm `next` 174 | ![Variables](./assets/5_Debug.png) 175 | 176 | + Tiếp tục `Step into` để chạy từng code trong hàm `next` 177 | ![Variables](./assets/6_Debug.png) 178 | 179 | ### Cách sử dụng Error Handling 180 | Cách này chúng ta chủ động định nghĩa lỗi cho chương trình, hạn chế quá trình bug xảy ra 181 | Các bạn có thể tham khảo ở đây: 182 | https://github.com/CocDap/Rust-Bootcamp-2024/tree/main/08-Rust-Error-Handling 183 | 184 | 185 | ### Cách sử dụng crate log 186 | 187 | Log hiểu đơn giản là in ra thông tin kết quả của 1 dòng lệnh nào đó. Log dùng để debug, monitor của 1 ứng dụng nhằm cho việc phát hiện lỗi trở nên dễ dàng hơn. Nó cơ bản là println!, nhưng sẽ gồm nhiều chức năng hơn: 188 | + Level-based : WARN, INFO, DEBUG, ERROR. Những level này nhằm thông báo rằng dòng lệnh in ra với mục đích gì 189 | + Custom log: đẹp hơn, dễ quan sát hơn 190 | 191 | Ví dụ: 192 | 193 | 1. Install `log` và `env_logger` 194 | 195 | ```bash 196 | cargo add log env_logger 197 | ``` 198 | 199 | 2. Đặt dòng lệnh `log` bạn muốn log giá trị ra 200 | Ví dụ: 201 | 202 | ```rust 203 | impl Iterator for Fibonacci { 204 | type Item = u32; 205 | 206 | fn next(&mut self) -> Option { 207 | let new_value = match (self.prev, self.prev_prev) { 208 | (Some(pre), Some(pre_pre)) => { 209 | let new_value = pre + pre_pre; 210 | self.prev_prev = self.prev; 211 | self.prev = Some(new_value); 212 | new_value 213 | } 214 | (Some(_pre), None) => { 215 | self.prev_prev = self.prev; 216 | self.prev = Some(1); 217 | 1 218 | } 219 | (None, None) => { 220 | self.prev = Some(0); 221 | 0 222 | } 223 | (_, _) => return None, 224 | }; 225 | log::debug!(target:"Fibonacci", "New Value:{}", new_value); 226 | Some(new_value) 227 | } 228 | } 229 | ``` 230 | 231 | Đối với ví dụ này, dùng debug để in ra giá trị `new_value` sau khi thực hiện `next`. Thêm phần `target` để hiểu diễn giải thông tin này của của hàm `fibonacci` 232 | 233 | 234 | 3. Custom log - Thêm thời gian cho log 235 | 236 | + Lưu ý cài đặt `chrono` crate để lấy thời gian 237 | + Import thư viện 238 | ```rust 239 | // custom log 240 | use env_logger::{Builder, Env}; 241 | // write thông tin thêm vào log 242 | use std::io::Write; 243 | ``` 244 | 245 | + Code chính 246 | ```rust 247 | fn main() { 248 | let mut builder = Builder::from_env(Env::default()); 249 | builder.format(|buf, record| { 250 | writeln!(buf, "{} [{}] -{}", chrono::Local::now().format("%d-%m-%Y %H:%M:%S"), record.level(),record.args()) 251 | }); 252 | builder.init(); 253 | let mut fibo = Fibonacci{ prev: Some(1), prev_prev: Some(0)}; 254 | fibo.next(); 255 | fibo.next(); 256 | fibo.next(); 257 | fibo.next(); 258 | 259 | } 260 | 261 | ``` 262 | + Chạy chương trình 263 | 264 | ```bash 265 | RUST_LOG=debug cargo run 266 | ``` 267 | 268 | + Kết qủa 269 | ``` 270 | Compiling test-rust-developer v0.1.0 (/Users/xxx/test/test-rust-developer) 271 | Finished dev [unoptimized + debuginfo] target(s) in 0.48s 272 | Running `target/debug/test-rust-developer` 273 | [2024-05-04T04:01:28Z DEBUG Fibonacci] New Value:1 274 | [2024-05-04T04:01:28Z DEBUG Fibonacci] New Value:2 275 | [2024-05-04T04:01:28Z DEBUG Fibonacci] New Value:3 276 | [2024-05-04T04:01:28Z DEBUG Fibonacci] New Value:5 277 | ``` 278 | 279 | 4. Xuất thông tin ra file .log 280 | + Import thư viện 281 | 282 | ```rust 283 | use std::fs::File; 284 | ``` 285 | 286 | + Tạo file log, nhúng file log vào `builder` 287 | 288 | ```rust 289 | fn main() { 290 | let log_file = File::create("fibonacci.log").unwrap(); 291 | 292 | let mut builder = Builder::from_env(Env::default()); 293 | builder.format(|buf, record| { 294 | writeln!( 295 | buf, 296 | "{} [{}] -{}", 297 | chrono::Local::now().format("%d-%m-%Y %H:%M:%S"), 298 | record.level(), 299 | record.args() 300 | ) 301 | }); 302 | // nhúng file log 303 | builder 304 | .target(env_logger::Target::Pipe(Box::new(log_file))) 305 | .init(); 306 | 307 | let mut fibo = Fibonacci { 308 | prev: Some(1), 309 | prev_prev: Some(0), 310 | }; 311 | fibo.next(); 312 | fibo.next(); 313 | fibo.next(); 314 | fibo.next(); 315 | } 316 | ``` 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | -------------------------------------------------------------------------------- /rust-challenges/06-connect-blockchain/sui/part1/README.md: -------------------------------------------------------------------------------- 1 | # Connect Sui Blockchain with Rust 2 | 3 | Xin chào mọi người mình là [SaitamaCoder](https://github.com/FucktheKingcode) đây! Dạo gần đây mình có 1 task làm về backend với đề là làm sao kết nối với Sui Blockchain. Mà cay thật là trong docs của Sui SDKs không có giới thiệu về hàm MoveCall của nó trong Rust. 4 | Thế là qua nhiều ngày nằm cay nếm mật, thẩm d... với cái lib của nó vậy nên mình mới có thể thành công được. Thôi không dài dòng triển ngay luôn nào! 5 | 6 | # Kiến thức yêu cầu 7 | 8 | - Biết cơ bản về Rust 9 | - Biết cơ bản về Sui Blockchain 10 | - Hiểu về thư viện Sui SDKs (đặc biệt là Rust SDK của nó) 11 | 12 | # Kết quả đạt được sau bài này 13 | 14 | - Hiểu được cách để gọi các hàm trong module của Sui Blockchain. 15 | - Hiểu thêm về cách tạo một Object với Rust 16 | 17 | # Hướng dẫn 18 | 19 | # Thêm thư các thư viện sau vào file Cargo.toml: 20 | 21 | ```toml 22 | sui-sdk = { git = "https://github.com/mystenlabs/sui", package = "sui-sdk"} 23 | sui-config = { git = "https://github.com/mystenlabs/sui", package = "sui-config"} 24 | sui-json-rpc-types = { git = "https://github.com/mystenlabs/sui", package = "sui-json-rpc-types"} 25 | sui-keys = { git = "https://github.com/mystenlabs/sui", package = "sui-keys"} 26 | shared-crypto = { git = "https://github.com/mystenlabs/sui", package = "shared-crypto"} 27 | tokio = { version = "1.2", features = ["full"] } 28 | anyhow = "1.0" 29 | futures = "0.3.30" 30 | tracing = "0.1.40" 31 | reqwest = "0.12.4" 32 | serde_json = "1.0.117" 33 | serde = "1.0.203" 34 | bcs = "0.1.6" 35 | ``` 36 | 37 | # Tạo file util.rs như sau: 38 | 39 | Đây là đoạn code dùng để tạo hàm setup_for_write() và retrieve_wallet() theo hướng dẫn của Mysten Labs cung cấp để có thể nhận diện được ví Sui trong máy tính và dùng nó để tương tác với Sui blockchain. 40 | 41 | ```rust 42 | // SPDX-License-Identifier: Apache-2.0 43 | 44 | use sui_config::{ 45 | sui_config_dir, Config, PersistedConfig, SUI_CLIENT_CONFIG, SUI_KEYSTORE_FILENAME, 46 | }; 47 | use sui_keys::keystore::{AccountKeystore, FileBasedKeystore}; 48 | use sui_sdk::{ 49 | sui_client_config::{SuiClientConfig, SuiEnv}, 50 | wallet_context::WalletContext, 51 | }; 52 | use tracing::info; 53 | 54 | use sui_sdk::types::{ 55 | base_types::SuiAddress, 56 | crypto::SignatureScheme::ED25519, 57 | }; 58 | use sui_sdk::{SuiClient, SuiClientBuilder}; 59 | 60 | pub async fn setup_for_write() -> Result<(SuiClient, SuiAddress, SuiAddress), anyhow::Error> { 61 | let (client, active_address) = setup_for_read().await?; 62 | let wallet = retrieve_wallet()?; 63 | let addresses = wallet.get_addresses(); 64 | let addresses = addresses 65 | .into_iter() 66 | .filter(|address| address != &active_address) 67 | .collect::>(); 68 | let recipient = addresses 69 | .first() 70 | .expect("Cannot get the recipient address needed for writing operations. Aborting"); 71 | 72 | Ok((client, active_address, *recipient)) 73 | } 74 | 75 | pub async fn setup_for_read() -> Result<(SuiClient, SuiAddress), anyhow::Error> { 76 | let client = SuiClientBuilder::default().build_testnet().await?; 77 | println!("Sui testnet version is: {}", client.api_version()); 78 | let mut wallet = retrieve_wallet()?; 79 | assert!(wallet.get_addresses().len() >= 2); 80 | let active_address = wallet.active_address()?; 81 | 82 | println!("Wallet active address is: {active_address}"); 83 | Ok((client, active_address)) 84 | } 85 | 86 | pub fn retrieve_wallet() -> Result { 87 | let wallet_conf = sui_config_dir()?.join(SUI_CLIENT_CONFIG); 88 | let keystore_path = sui_config_dir()?.join(SUI_KEYSTORE_FILENAME); 89 | 90 | if !keystore_path.exists() { 91 | let keystore = FileBasedKeystore::new(&keystore_path)?; 92 | keystore.save()?; 93 | } 94 | 95 | if !wallet_conf.exists() { 96 | let keystore = FileBasedKeystore::new(&keystore_path)?; 97 | let mut client_config = SuiClientConfig::new(keystore.into()); 98 | 99 | client_config.add_env(SuiEnv::testnet()); 100 | client_config.add_env(SuiEnv::devnet()); 101 | client_config.add_env(SuiEnv::localnet()); 102 | 103 | if client_config.active_env.is_none() { 104 | client_config.active_env = client_config.envs.first().map(|env| env.alias.clone()); 105 | } 106 | 107 | client_config.save(&wallet_conf)?; 108 | info!("Client config file is stored in {:?}.", &wallet_conf); 109 | } 110 | 111 | let mut keystore = FileBasedKeystore::new(&keystore_path)?; 112 | let mut client_config: SuiClientConfig = PersistedConfig::read(&wallet_conf)?; 113 | 114 | let default_active_address = if let Some(address) = keystore.addresses().first() { 115 | *address 116 | } else { 117 | keystore 118 | .generate_and_add_new_key(ED25519, None, None, None)? 119 | .0 120 | }; 121 | 122 | if keystore.addresses().len() < 2 { 123 | keystore.generate_and_add_new_key(ED25519, None, None, None)?; 124 | } 125 | 126 | client_config.active_address = Some(default_active_address); 127 | client_config.save(&wallet_conf)?; 128 | 129 | let wallet = WalletContext::new(&wallet_conf, Some(std::time::Duration::from_secs(60)), None)?; 130 | 131 | Ok(wallet) 132 | } 133 | ``` 134 | 135 | # Bây giờ chúng ta đến bước quan trong nhất là gọi hàm trên Sui Blockchain 136 | 137 | Ông bạn [Harry](https://github.com/hien-p) hiền lành của mình đã tạo một module trên Sui blokchain với cái tên đã được mã hóa dí dỏm là **"hello_wolrd"** và Package ID là "0x883393ee444fb828aa0e977670cf233b0078b41d144e6208719557cb3888244d". Trong module này có 2 hàm gồm một hàm "ping" không cần có tham số và một hàm là **"hello_world"** có 1 tham số kiểu U64. 138 | 139 | ## Đầu tiên chúng ta import các thư viện sau: 140 | 141 | ```rust 142 | mod utils; 143 | use shared_crypto::intent::Intent; 144 | use sui_config::{sui_config_dir, SUI_KEYSTORE_FILENAME}; 145 | use sui_keys::keystore::{AccountKeystore, FileBasedKeystore}; 146 | use sui_sdk::{ 147 | rpc_types::SuiTransactionBlockResponseOptions, 148 | types::{ 149 | base_types::ObjectID, programmable_transaction_builder::ProgrammableTransactionBuilder, quorum_driver_types::ExecuteTransactionRequestType, transaction::{Argument, CallArg, Command, Transaction, TransactionData}, Identifier, TypeTag 150 | }, 151 | }; 152 | use utils::setup_for_write; 153 | ``` 154 | 155 | ## Tương tác với Sui Blockchain bằng ví Sui Client 156 | 157 | Mình sẽ lấy ra từ sui client ví người gọi giao dịch và người nhận (nếu có): 158 | 159 | ```rust 160 | let (sui, sender, _ ) = setup_for_write().await?; 161 | ``` 162 | 163 | Chúng ta sẽ phải tìm coins của chúng ta trên ví để trả phí gas 164 | 165 | ```rust 166 | let coins = sui 167 | .coin_read_api() 168 | .get_coins(sender, None, None, None) 169 | .await?; 170 | let coin = coins.data.into_iter().next().unwrap(); 171 | ``` 172 | 173 | Tạo một ProgrammableTransactionBuilder 174 | 175 | ```rust 176 | let mut ptb = ProgrammableTransactionBuilder::new(); 177 | ``` 178 | 179 | Vì trong Sui Blockchain everything là Object 180 | 181 | ![Hello Wolrd](img/hello_wolrd.jpg) 182 | 183 | Nên bạn muốn đưa vào một tham số để gọi hàm nào đó trong module thì cũng phải đưa nó về Object của Sui 184 | 185 | ```rust 186 | let input_value = 10u64; 187 | let input_argument = CallArg::Pure(bcs::to_bytes(&input_value).unwrap()); 188 | ``` 189 | 190 | Chúng ta gán Object này vào mảng input trong Sui 191 | 192 | ```rust 193 | ptb.input(input_argument); 194 | ``` 195 | 196 | Chúng ta gọi hàm MoveCall bằng ProgrammableTransactionBuilder như sau. 197 | 198 | Lưu ý: Chúng ta truyền vào đúng index trong mảng input mà chúng ta đã truyền vào trước đó. 199 | Trong ví dụ này mình truyền vào 1 tham số nên index là 0, còn nếu các bạn không có tham số nào thì không cần truyền gì hết "arguments: vec![Argument::Input(0)]" là được. 200 | 201 | ```rust 202 | ptb.command(Command::MoveCall(Box::new( 203 | sui_sdk::types::transaction::ProgrammableMoveCall { 204 | package: ObjectID::from_hex_literal("0x883393ee444fb828aa0e977670cf233b0078b41d144e6208719557cb3888244d").unwrap(), 205 | module: Identifier::new("hello_wolrd").unwrap(), 206 | function: Identifier::new("hello_world").unwrap(), 207 | type_arguments: vec![], 208 | arguments: vec![Argument::Input(0)], 209 | } 210 | ))); 211 | ``` 212 | 213 | Cuối cùng chúng ta kết thúc transaction này bằng cách gọi 214 | 215 | ```rust 216 | let builder = ptb.finish(); 217 | ``` 218 | 219 | Và cuối cuối cùng các bạn nhớ chạy lệnh sau để đưa transaction trên vào chuỗi nhé! 220 | Lưu ý: Trong ví Sui Client của bạn phải có sui để trả phí gas nhé 221 | 222 | ```rust 223 | let gas_budget = 10_000_000; 224 | let gas_price = sui.read_api().get_reference_gas_price().await?; 225 | // create the transaction data that will be sent to the network 226 | let tx_data = TransactionData::new_programmable( 227 | sender, 228 | vec![coin.object_ref()], 229 | builder, 230 | gas_budget, 231 | gas_price, 232 | ); 233 | 234 | // 4) sign transaction 235 | let keystore = FileBasedKeystore::new(&sui_config_dir()?.join(SUI_KEYSTORE_FILENAME))?; 236 | let signature = keystore.sign_secure(&sender, &tx_data, Intent::sui_transaction())?; 237 | 238 | // 5) execute the transaction 239 | print!("Executing the transaction..."); 240 | let transaction_response = sui 241 | .quorum_driver_api() 242 | .execute_transaction_block( 243 | Transaction::from_data(tx_data, vec![signature]), 244 | SuiTransactionBlockResponseOptions::full_content(), 245 | Some(ExecuteTransactionRequestType::WaitForLocalExecution), 246 | ) 247 | .await?; 248 | print!("done\n Transaction information: "); 249 | println!("{:?}", transaction_response); 250 | Ok(()) 251 | ``` 252 | 253 | Cuối cùng cảm ơn các bạn đã xem và mình là [SaitamaCoder](https://github.com/FucktheKingcode), hy vọng gặp lại nhé! 254 | -------------------------------------------------------------------------------- /newbie/Mutex_Arc.md: -------------------------------------------------------------------------------- 1 | # Giải thích đơn giản Mutex, Arc 2 | Hiện tại đối với kiến thức của mình thì vẫn chưa hiểu tầng sâu nhất của memory, thread, process, ... đây là những khái niệm quan trọng trong lập trình `concurrency`. Hi vọng trong tương lai sẽ có bài deep dive hơn về các kiến thức cơ bản này. 3 | 4 | Bây giờ chúng ta sẽ đi tìm hiểu kiến thức cơ bản trước , đó là 5 | 6 | ## Thread và Process 7 | 8 | Để hiểu hơn `Process` và `Thread` , mình giải thích qua một ví dụ đơn giản như sau: 9 | 10 | Một công ty X hiện tại đang triển khai 1 ứng dụng bán hàng. Để có 1 sản phẩm như vậy thì quản lý công ty X sẽ giao việc cho các Leader như sau: 11 | + Leader A chịu trách nhiệm phần Backend 12 | + Leader B chịu trách nhiệm phần Frontend 13 | + Leader C chịu trách nhiệm phần Devops 14 | + Leader D chịu trách nhiệm phần UI/UX 15 | ... 16 | 17 | Như vậy, để có 1 sản phẩm nào đó thì cần sự kết hợp của rất nhiều người trong cùng 1 tổ chức 18 | 19 | Vậy ở đây chúng ta sẽ map thử `Process` và `Thread` thông qua ví dụ trên như sau: 20 | 21 | + `Process` : hiểu là muốn chạy 1 ứng dụng hoàn chỉnh 22 | + `Thread`: hiểu là mỗi leader sẽ chịu trách nhiệm thực thi từng chức năng sau đó tập hợp thành 1 ứng dụng 23 | 24 | 25 | Hiểu tổng quát hơn : 26 | + `Process` : 1 chương trình 27 | + `Thread` : 1 đơn vị thực thi chương trình trong process ( sử dụng sức mạnh của CPU để tính toán) 28 | 29 | ![Thread_Process](./assets/10_Thread_Process.png) 30 | 31 | Có thể thấy ví dụ trên ta thấy rất nhiều processs (chương trình) đang chạy trên máy của mình , Ví dụ process Google Chrome hiện tại đang chạy với 48 thread. Khi chúng ta start các chương trình , cơ bản sẽ start các process để chạy chương trình đó ví dụ Google Chrome, Code , Terminal , … 32 | 33 | 34 | ## Mutex 35 | ### Lý do có Mutex 36 | Ví dụ: 37 | 1. Ta có 1 chương trình như sau 38 | ```rust 39 | let x = 4; 40 | ``` 41 | 42 | Memory sẽ lưu giá trị 4 ở 1 vùng nhớ nào đó 43 | 44 | ![Memory_store_value](./assets/7_Mutex_Arc.png) 45 | 46 | 47 | 2. Nâng cấp chương trình code như sau: 48 | ```rust 49 | let mut x = 4; 50 | x +=1; 51 | ``` 52 | + Đối với trường hợp single-thread thì ko xảy ra vấn đề 53 | 54 | ![Single_thread](./assets/8_Mutex_Arc.png) 55 | 56 | 57 | CPU đọc giá trị từ Memory (x =4) -> Xử lý chương trình `+1` (ở đây thread 4 xử lý) -> Lưu giá trị mới vào memory (x=5) 58 | 59 | + Đối với trường hợp xử lý multi-thread thì lại có vấn đề như sau 60 | 61 | ![Multi_thread](./assets/9_Mutex_Arc.png) 62 | 63 | CPU đọc giá trị từ Memory (x=4) -> Có 2 thread cùng truy cập vùng nhớ của x -> Xử lý gần như đồng thời chương trình `+1` -> Kết quả thực tế có thể là `6` 64 | 65 | Khi xử lý multi-thread thì có thể xuất hiện lỗi `data race` , là một lỗi phổ biến khi mà 2 hoặc nhiều thread cùng truy cập một vùng nhớ và xử lý cùng 1 chương trình 66 | 67 | 68 | 69 | ### Mutex là gì 70 | Mutex (mutual exclusion) là một cơ chế đồng bộ chỉ cho phép 1 thread truy cập dữ liệu tại 1 thời điểm 71 | 72 | Có 2 quy tắc chính: 73 | 74 | 1. Muốn truy cập được dữ liệu trong `Mutex` , thread đó phải đưa ra tín hiệu cho Mutex bằng cách `lock()` dữ liệu đó 75 | 2. Vì Mutex có nhiệm vụ cho phép duy nhất 1 thread truy cập dữ liệu tại 1 thời điểm , khi 1 thread đã hoàn thành xong chương trình , cần `unlock` để các thread khác có thể truy cập 76 | 77 | 78 | ### Biểu diễn code : Single-thread 79 | 80 | ```rust 81 | use std::sync::Mutex; 82 | 83 | fn main() { 84 | // wrap giá trị 5 cho Mutex 85 | let m = Mutex::new(5); 86 | 87 | { 88 | // Lock 89 | // chỉ có main thread mới được access và thay đổi giá trị 90 | let mut num = m.lock().unwrap(); 91 | *num = 6; 92 | } 93 | 94 | println!("m = {m:?}"); 95 | } 96 | ``` 97 | 98 | ### Biểu diễn code : Multi-thread 99 | 100 | Bài toán : Đếm số từ 0 tới 9 sử dụng multi-thread 101 | 102 | ```rust 103 | use std::sync::Mutex; 104 | use std::thread; 105 | 106 | fn main() { 107 | let counter = Mutex::new(0); 108 | let mut handles = vec![]; 109 | 110 | for _ in 0..10 { 111 | // tạo ra 1 thread mới 112 | // giải thích cú pháp move 113 | // Khi dùng move trong closure -> tính ownership của biến counter sẽ chuyển cho owner khác trong scope của thread 114 | // Nếu không dùng move trong closure -> sử dụng reference của biến counter trong scope của thread -> Compile Error bởi vì thời gian sống (lifetime) của thread < lifetime của biến counter -> Nghĩa là biến counter vẫn còn tồn tại trong khi scope của thread đã kết thúc -> Vi phạm quy tắc ownership 115 | let handle = thread::spawn(move || { 116 | // lock 117 | // cho phép thread duy nhất hiện tại được access biến counter 118 | let mut num = counter.lock().unwrap(); 119 | 120 | *num += 1; 121 | }); 122 | handles.push(handle); 123 | } 124 | 125 | for handle in handles { 126 | // chờ tất cả các thread con thực hiện xong 127 | // thì khi đó chương trình kết thúc (main thread kết thúc) 128 | handle.join().unwrap(); 129 | } 130 | 131 | println!("Result: {}", *counter.lock().unwrap()); 132 | } 133 | ``` 134 | 135 | Khi chạy cargo run: 136 | ``` 137 | error[E0382]: borrow of moved value: `counter` 138 | --> src/main.rs:119:29 139 | | 140 | 103 | let counter = Mutex::new(0); 141 | | ------- move occurs because `counter` has type `Mutex`, which does not implement the `Copy` trait 142 | ... 143 | 107 | let handle = thread::spawn(move || { 144 | | ------- value moved into closure here, in previous iteration of loop 145 | ... 146 | 119 | println!("Result: {}", *counter.lock().unwrap()); 147 | | ^^^^^^^ value borrowed here after move 148 | 149 | For more information about this error, try `rustc --explain E0382`. 150 | ``` 151 | 152 | ### Vấn đề: 153 | + Vòng lặp đầu tiên -> biến `counter` đã chuyển ownership trong phạm vi scope của thread -> biến `counter` sẽ drop sau khi scope thread kết thúc -> Khi đến vòng lặp thứ 2 -> biến `counter` không còn tồn tại -> Lỗi ownership 154 | 155 | => Chúng ta không thể `move` tính ownership của biến `counter` cho nhiều thread được 156 | 157 | 158 | ### Giải pháp cho vấn đề sử dụng Mutex - Multi-Thread 159 | 160 | => Sử dụng Multiple Ownership với Multiple Threads 161 | 162 | => Sử dụng smart pointer `Rc` 163 | => Mọi người có thể tham khảo : https://www.youtube.com/watch?v=8O0Nt9qY_vo 164 | 165 | 166 | Cũng như code trên, nhưng chúng ta sẽ sử dụng thêm `clone` để tạo nhiều ownership với nhiều thread 167 | 168 | ```rust 169 | use std::rc::Rc; 170 | use std::sync::Mutex; 171 | use std::thread; 172 | 173 | fn main() { 174 | // Wrap Mutex và Rc 175 | // Chức năng: 176 | // Mutex: cho phép 1 thread truy cập cùng 1 thời điểm 177 | // Rc: Reference Counted -> Tạo mutile ownership 178 | 179 | let counter = Rc::new(Mutex::new(0)); 180 | let mut handles = vec![]; 181 | 182 | for _ in 0..10 { 183 | // Tạo 1 ownership tương ứng cho 1 thread 184 | let counter = Rc::clone(&counter); 185 | let handle = thread::spawn(move || { 186 | let mut num = counter.lock().unwrap(); 187 | 188 | *num += 1; 189 | }); 190 | handles.push(handle); 191 | } 192 | 193 | for handle in handles { 194 | handle.join().unwrap(); 195 | } 196 | 197 | println!("Result: {}", *counter.lock().unwrap()); 198 | } 199 | ``` 200 | 201 | ### Vấn đề khi sử dụng Rc 202 | 203 | ``` 204 | error[E0277]: `Rc>` cannot be sent between threads safely 205 | --> src/main.rs:142:36 206 | | 207 | 142 | let handle = thread::spawn(move || { 208 | | ------------- ^------ 209 | | | | 210 | | ______________________|_____________within this `{closure@src/main.rs:142:36: 142:43}` 211 | | | | 212 | | | required by a bound introduced by this call 213 | 143 | | let mut num = counter.lock().unwrap(); 214 | 144 | | 215 | 145 | | *num += 1; 216 | 146 | | }); 217 | | |_________^ `Rc>` cannot be sent between threads safely 218 | | 219 | = help: within `{closure@src/main.rs:142:36: 142:43}`, the trait `Send` is not implemented for `Rc>` 220 | note: required because it's used within this closure 221 | --> src/main.rs:142:36 222 | | 223 | 142 | let handle = thread::spawn(move || { 224 | | ^^^^^^^ 225 | note: required by a bound in `spawn` 226 | --> /Users/hodung/.rustup/toolchains/1.74-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/thread/mod.rs:683:8 227 | | 228 | 680 | pub fn spawn(f: F) -> JoinHandle 229 | | ----- required by a bound in this function 230 | ... 231 | 683 | F: Send + 'static, 232 | | ^^^^ required by this bound in `spawn` 233 | ``` 234 | 235 | => Khi sử dụng `Rc` thì compiler báo lỗi liên quan tới `thread-safe` 236 | => Nên nhớ rằng : Các thread sẽ cùng truy cập chung 1 memory (Shared-stated concurrency) 237 | 238 | 239 | 240 | ### Giải pháp khi sử dụng share-stated (các thread có thể access cùng 1 resource) 241 | 242 | => Sử dụng Atomic Reference Counting (`Arc`) 243 | => Tại vì sao có Atomic ( Mình xin phép chia sẻ bài khác). Mọi người có thể tham khảo video này: 244 | https://www.youtube.com/watch?v=rMGWeSjctlY&t=8529s 245 | 246 | 247 | => Rust đảm bảo khi sử dụng Arc thì thead-safe , có nghĩa là an toàn dữ liệu khi mà chạy đa luồng 248 | 249 | => Cách sử dụng khá giống với `Rc` 250 | 251 | ```rust 252 | use std::sync::{Arc, Mutex}; 253 | use std::thread; 254 | 255 | fn main() { 256 | 257 | // Wrap Mutex trong Arc 258 | // Mutex: cho phép 1 thread truy cập giá trị tại cùng 1 thời điểm 259 | // Arc: share-stated thread-safe 260 | let counter = Arc::new(Mutex::new(0)); 261 | let mut handles = vec![]; 262 | 263 | for _ in 0..10 { 264 | let counter = Arc::clone(&counter); 265 | let handle = thread::spawn(move || { 266 | let mut num = counter.lock().unwrap(); 267 | 268 | *num += 1; 269 | }); 270 | handles.push(handle); 271 | } 272 | 273 | for handle in handles { 274 | handle.join().unwrap(); 275 | } 276 | 277 | println!("Result: {}", *counter.lock().unwrap()); 278 | } 279 | ``` 280 | 281 | => Kết quả : `Result: 10` 282 | 283 | 284 | ## Kết luận 285 | 286 | Khi sử dụng Arc cùng với Mutex thì ta giải quyết được 2 vấn đề khi chạy concurrency 287 | 288 | 1. Mutex: Tránh hiện tượng `data race` , nghĩa là chỉ duy nhất 1 thread truy cập giá trị tại cùng 1 thời điểm 289 | 290 | 2. Arc : Thread-safe khi nhiều thread cùng sử dụng 1 memory 291 | 292 | -------------------------------------------------------------------------------- /rust-challenges/06-connect-blockchain/sui/part2/README.md: -------------------------------------------------------------------------------- 1 | # Connect Sui Blockchain with Rust 2 | 3 | Xin chào mọi người mình là [SaitamaCoder](https://github.com/FucktheKingcode) đây! Vẫn là task làm về backend đó với đề là làm sao kết nối với Sui Blockchain nhưng lại là Shared Object và Vector. Mà cay thật là trong docs của Sui SDKs không có giới thiệu về hàm Shared Object và MakeMoveVec của nó trong Rust. 4 | Thế là qua nhiều ngày nằm cay nếm mật, thẩm d... lần 2 với cái lib của nó vậy nên mình mới có thể thành công được. Thôi không dài dòng triển ngay luôn nào! 5 | 6 | # Kiến thức yêu cầu 7 | 8 | - Biết cơ bản về Rust 9 | - Biết cơ bản về Sui Blockchain 10 | - Hiểu về thư viện Sui SDKs (đặc biệt là Rust SDK của nó) 11 | - Đã đọc part 1 12 | 13 | # Kết quả đạt được sau bài này 14 | 15 | - Hiểu được cách để gọi các hàm trong module của Sui Blockchain. 16 | - Hiểu thêm về cách tạo một Object với Rust 17 | - Hiểu thêm về Shared Object, digest, version và hàm MakeMoveVec 18 | 19 | # Hướng dẫn 20 | 21 | # Thêm thư các thư viện sau vào file Cargo.toml: 22 | 23 | ```toml 24 | sui-sdk = { git = "https://github.com/mystenlabs/sui", package = "sui-sdk"} 25 | sui-config = { git = "https://github.com/mystenlabs/sui", package = "sui-config"} 26 | sui-json-rpc-types = { git = "https://github.com/mystenlabs/sui", package = "sui-json-rpc-types"} 27 | sui-keys = { git = "https://github.com/mystenlabs/sui", package = "sui-keys"} 28 | shared-crypto = { git = "https://github.com/mystenlabs/sui", package = "shared-crypto"} 29 | tokio = { version = "1.2", features = ["full"] } 30 | anyhow = "1.0" 31 | futures = "0.3.30" 32 | tracing = "0.1.40" 33 | reqwest = "0.12.4" 34 | serde_json = "1.0.117" 35 | serde = "1.0.203" 36 | bcs = "0.1.6" 37 | ``` 38 | 39 | # Tạo file util.rs như sau: 40 | 41 | Đây là đoạn code dùng để tạo hàm setup_for_write() và retrieve_wallet() theo hướng dẫn của Mysten Labs cung cấp để có thể nhận diện được ví Sui trong máy tính và dùng nó để tương tác với Sui blockchain. 42 | 43 | ```rust 44 | // SPDX-License-Identifier: Apache-2.0 45 | 46 | use sui_config::{ 47 | sui_config_dir, Config, PersistedConfig, SUI_CLIENT_CONFIG, SUI_KEYSTORE_FILENAME, 48 | }; 49 | use sui_keys::keystore::{AccountKeystore, FileBasedKeystore}; 50 | use sui_sdk::{ 51 | sui_client_config::{SuiClientConfig, SuiEnv}, 52 | wallet_context::WalletContext, 53 | }; 54 | use tracing::info; 55 | 56 | use sui_sdk::types::{ 57 | base_types::SuiAddress, 58 | crypto::SignatureScheme::ED25519, 59 | }; 60 | use sui_sdk::{SuiClient, SuiClientBuilder}; 61 | 62 | pub async fn setup_for_write() -> Result<(SuiClient, SuiAddress, SuiAddress), anyhow::Error> { 63 | let (client, active_address) = setup_for_read().await?; 64 | let wallet = retrieve_wallet()?; 65 | let addresses = wallet.get_addresses(); 66 | let addresses = addresses 67 | .into_iter() 68 | .filter(|address| address != &active_address) 69 | .collect::>(); 70 | let recipient = addresses 71 | .first() 72 | .expect("Cannot get the recipient address needed for writing operations. Aborting"); 73 | 74 | Ok((client, active_address, *recipient)) 75 | } 76 | 77 | pub async fn setup_for_read() -> Result<(SuiClient, SuiAddress), anyhow::Error> { 78 | let client = SuiClientBuilder::default().build_testnet().await?; 79 | println!("Sui testnet version is: {}", client.api_version()); 80 | let mut wallet = retrieve_wallet()?; 81 | assert!(wallet.get_addresses().len() >= 2); 82 | let active_address = wallet.active_address()?; 83 | 84 | println!("Wallet active address is: {active_address}"); 85 | Ok((client, active_address)) 86 | } 87 | 88 | pub fn retrieve_wallet() -> Result { 89 | let wallet_conf = sui_config_dir()?.join(SUI_CLIENT_CONFIG); 90 | let keystore_path = sui_config_dir()?.join(SUI_KEYSTORE_FILENAME); 91 | 92 | if !keystore_path.exists() { 93 | let keystore = FileBasedKeystore::new(&keystore_path)?; 94 | keystore.save()?; 95 | } 96 | 97 | if !wallet_conf.exists() { 98 | let keystore = FileBasedKeystore::new(&keystore_path)?; 99 | let mut client_config = SuiClientConfig::new(keystore.into()); 100 | 101 | client_config.add_env(SuiEnv::testnet()); 102 | client_config.add_env(SuiEnv::devnet()); 103 | client_config.add_env(SuiEnv::localnet()); 104 | 105 | if client_config.active_env.is_none() { 106 | client_config.active_env = client_config.envs.first().map(|env| env.alias.clone()); 107 | } 108 | 109 | client_config.save(&wallet_conf)?; 110 | info!("Client config file is stored in {:?}.", &wallet_conf); 111 | } 112 | 113 | let mut keystore = FileBasedKeystore::new(&keystore_path)?; 114 | let mut client_config: SuiClientConfig = PersistedConfig::read(&wallet_conf)?; 115 | 116 | let default_active_address = if let Some(address) = keystore.addresses().first() { 117 | *address 118 | } else { 119 | keystore 120 | .generate_and_add_new_key(ED25519, None, None, None)? 121 | .0 122 | }; 123 | 124 | if keystore.addresses().len() < 2 { 125 | keystore.generate_and_add_new_key(ED25519, None, None, None)?; 126 | } 127 | 128 | client_config.active_address = Some(default_active_address); 129 | client_config.save(&wallet_conf)?; 130 | 131 | let wallet = WalletContext::new(&wallet_conf, Some(std::time::Duration::from_secs(60)), None)?; 132 | 133 | Ok(wallet) 134 | } 135 | ``` 136 | 137 | # Bây giờ chúng ta đến bước quan trong nhất là gọi hàm trên Sui Blockchain 138 | 139 | Một module trên Sui blokchain với cái tên là **"gamecards"** và Package ID là "0xc74620c25579b75ac8f6d0d670a4663944ff7f29d6e856f6b33e0a35a34c5a06". Trong module này có hàm "create_room" cần truyền vào một Shared Object và 1 Vector. 140 | 141 | ## Đầu tiên chúng ta import các thư viện sau: 142 | 143 | ```rust 144 | // Import necessary modules and libraries 145 | mod utils; 146 | use std::str::FromStr; 147 | 148 | use shared_crypto::intent::Intent; 149 | use sui_config::{sui_config_dir, SUI_KEYSTORE_FILENAME}; 150 | use sui_json_rpc_types::{SuiObjectDataOptions, SuiObjectResponse}; 151 | use sui_keys::keystore::{AccountKeystore, FileBasedKeystore}; 152 | use sui_sdk::{ 153 | rpc_types::SuiTransactionBlockResponseOptions, 154 | types::{ 155 | base_types::{ObjectID, ObjectRef, SequenceNumber}, 156 | digests::{self, Digest, ObjectDigest}, 157 | object, 158 | programmable_transaction_builder::ProgrammableTransactionBuilder, 159 | quorum_driver_types::ExecuteTransactionRequestType, 160 | sui_serde::SuiStructTag, 161 | transaction::{Argument, CallArg, Command, ObjectArg, Transaction, TransactionData}, 162 | Identifier, 163 | TypeTag 164 | }, 165 | SuiClient, 166 | SuiClientBuilder, 167 | }; 168 | use utils::setup_for_write; 169 | ``` 170 | 171 | ## Tương tác với Sui Blockchain bằng ví Sui Client 172 | 173 | Mình sẽ lấy ra từ sui client ví người gọi giao dịch và người nhận (nếu có): 174 | 175 | ```rust 176 | let (sui, sender, _ ) = setup_for_write().await?; 177 | ``` 178 | 179 | Chúng ta sẽ phải tìm coins của chúng ta trên ví để trả phí gas 180 | 181 | ```rust 182 | let coins = sui 183 | .coin_read_api() 184 | .get_coins(sender, None, None, None) 185 | .await?; 186 | let coin = coins.data.into_iter().next().unwrap(); 187 | ``` 188 | 189 | Tạo một ProgrammableTransactionBuilder 190 | 191 | ```rust 192 | let mut ptb = ProgrammableTransactionBuilder::new(); 193 | ``` 194 | 195 | Bây giờ ta cần truyền vào một Shared Object có dạng như sau: 196 | 197 | ![Shared Object Input](img/Shared_Object_Input.png) 198 | 199 | Đây là đoạn code thực hiện điều đó 200 | 201 | ```rust 202 | // Define the game room object ID 203 | let game_room_id = ObjectID::from_hex_literal("0x52509952e7b80b08880238e9737e8f70e223418816e5a85bf82575ef84ecc545") 204 | .unwrap(); 205 | // Define the object ID 206 | let object_id = ObjectID::from_hex_literal("0xb28e2aa6a21db55873a1b81983cbd19544459971b67ba2ddbd7b8d6575d7c2d1").unwrap(); 207 | // Fetch the game room object details with specified options 208 | let object = sui.read_api().get_object_with_options(object_id, 209 | SuiObjectDataOptions { 210 | show_type: true, 211 | show_owner: true, 212 | show_previous_transaction: true, 213 | show_display: true, 214 | show_content: true, 215 | show_bcs: true, 216 | show_storage_rebate: true, 217 | }, 218 | ).await?; 219 | 220 | // Get the version of the game room object 221 | let object_version = object.clone().data.unwrap().version; 222 | // Specify if the object is mutable 223 | let is_mutable = true; 224 | // Create a CallArg for the game room object 225 | let game_room_input = CallArg::Object(ObjectArg::SharedObject{ 226 | id: game_room_id, 227 | initial_shared_version: object_version, 228 | mutable: is_mutable, 229 | }); 230 | ``` 231 | 232 | Chúng ta gọi hàm MoveMakeVec bằng ProgrammableTransactionBuilder như sau. 233 | 234 | Đầu tiên chúng ta sẽ phải truyền vào 1 object Vector 235 | 236 | ```rust 237 | let game_card_id = ObjectID::from_hex_literal("0x440b328ba3c90f203f439f6fc4c5aa40b7ca41d28317d5bb9b6c0207cfebc693") 238 | .unwrap(); 239 | // Fetch the game card object details with specified options 240 | let game_card_object = sui.read_api().get_object_with_options(game_card_id, 241 | SuiObjectDataOptions { 242 | show_type: true, 243 | show_owner: true, 244 | show_previous_transaction: true, 245 | show_display: true, 246 | show_content: true, 247 | show_bcs: true, 248 | show_storage_rebate: true, 249 | }, 250 | ).await?; 251 | 252 | // Get the version of the game card object 253 | let game_card_version = game_card_object.clone().data.unwrap().version; 254 | // Get the digest of the game card object 255 | let game_card_digests = game_card_object.data.unwrap().digest; 256 | // Create an ObjectRef for the game card object 257 | let game_card_object_ref: ObjectRef = (game_card_id, game_card_version, game_card_digests); 258 | // Create a CallArg for the game card object 259 | let game_card_input = CallArg::Object(ObjectArg::ImmOrOwnedObject(game_card_object_ref)); 260 | // Add the game card object as an input to the transaction 261 | ptb.input(game_card_input); 262 | ``` 263 | 264 | Sau đó gọi hàm MakeMoveVec để có thể biến Object đó thành 1 Vector on Sui 265 | 266 | ```rust 267 | ptb.command(Command::MakeMoveVec(None, vec![ 268 | Argument::Input(1), 269 | ])); 270 | ``` 271 | 272 | Cuối cùng cảm ơn các bạn đã xem và mình là [SaitamaCoder](https://github.com/FucktheKingcode), hy vọng gặp lại nhé! 273 | -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part1/README.md: -------------------------------------------------------------------------------- 1 | # Xây dựng real time chat app (P1) 2 | 3 | Chào mọi người, hiện tại mình đang bắt đầu học về ngôn ngữ lập trình Rust. Sau khi học các kiến thức cơ bản để áp dụng kiến thức đã học thì hôm nay mình sẽ cùng mọi người xây dựng real time chat app bằng Rust nhé. 4 | 5 | # Kiến thức yêu cầu 6 | 7 | - Biết cơ bản về ngôn ngữ Rust 8 | - Biết về cơ chế hoạt động của server (HTTP, request, response,…) 9 | 10 | # Kết quả đạt được sau bài viết này 11 | 12 | - Setup codebase 13 | - Hiểu cơ bản về async await 14 | - Áp dụng được các kiến thức cơ bản của Rust 15 | - Xây dựng API cơ bản 16 | - Basic authentication với JWT 17 | 18 | # Hướng dẫn 19 | 20 | ## Tạo Web Service 21 | 22 | Đầu tiên ta sẽ tạo 1 project mới 23 | 24 | ```rust 25 | cargo new chat-app 26 | cd chat-app 27 | ``` 28 | 29 | Trong phần này mình sẽ sử dụng framework [axum - Rust (docs.rs)](https://docs.rs/axum/latest/axum/#high-level-features). Đây là framework phát triển web được sử dụng khá phổ biến trong Rust. 30 | 31 | Mình sẽ thêm vào Cargo.toml các crate cần thiết cho project 32 | 33 | ```toml 34 | [dependencies] 35 | axum = "0.7.5" 36 | serde = { version = "1.0.198", features = ["derive"]} 37 | serde_json = "1.0.116" 38 | tokio = { version = "1.37.0", features = ["full"] } 39 | tracing = "0.1.40" 40 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"]} 41 | ``` 42 | 43 | Bên docs họ có cung cấp 1 đoạn code example để setup web server 44 | 45 | ```rust 46 | use axum::{ 47 | routing::{get, post}, 48 | http::StatusCode, 49 | Json, Router, 50 | }; 51 | use serde::{Deserialize, Serialize}; 52 | 53 | #[tokio::main] 54 | async fn main() { 55 | // initialize tracing 56 | tracing_subscriber::fmt::init(); 57 | 58 | // build our application with a route 59 | let app = Router::new() 60 | // `GET /` goes to `root` 61 | .route("/", get(root)) 62 | // `POST /users` goes to `create_user` 63 | .route("/users", post(create_user)); 64 | 65 | // run our app with hyper, listening globally on port 3000 66 | let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 67 | axum::serve(listener, app).await.unwrap(); 68 | } 69 | 70 | // basic handler that responds with a static string 71 | async fn root() -> &'static str { 72 | "Hello, World!" 73 | } 74 | 75 | async fn create_user( 76 | // this argument tells axum to parse the request body 77 | // as JSON into a `CreateUser` type 78 | Json(payload): Json, 79 | ) -> (StatusCode, Json) { 80 | // insert your application logic here 81 | let user = User { 82 | id: 1337, 83 | username: payload.username, 84 | }; 85 | 86 | // this will be converted into a JSON response 87 | // with a status code of `201 Created` 88 | (StatusCode::CREATED, Json(user)) 89 | } 90 | 91 | // the input to our `create_user` handler 92 | #[derive(Deserialize)] 93 | struct CreateUser { 94 | username: String, 95 | } 96 | 97 | // the output to our `create_user` handler 98 | #[derive(Serialize)] 99 | struct User { 100 | id: u64, 101 | username: String, 102 | } 103 | ``` 104 | 105 | Ta copy đoạn code trên vào file [`main.rs`](http://main.rs) sau đó run dự án để test thử xem chạy được không 106 | 107 | Mọi người tải [cargo-watch - crates.io: Rust Package Registry](https://crates.io/crates/cargo-watch) (giống như nodemon) hiểu đơn giản là khi bất kì sự thay đổi nào với mã nguồn thì nó sẽ build và run lại file binary 108 | 109 | ```powershell 110 | cargo watch -q -c -w src/ -x run 111 | ``` 112 | 113 | Ta dùng Postman để kiểm tra xem server hoạt động chưa nhé 114 | 115 | ![Postman](img/postman.png) 116 | 117 | ## Tìm hiểu các thành phần bên trong 118 | 119 | Tiếp đến ta đi vào chi tiết source code để hiểu rõ hơn về các thành phần bên trong 120 | 121 | ```rust 122 | #[tokio::main] 123 | async fn main() { 124 | // initialize tracing 125 | tracing_subscriber::fmt::init(); 126 | 127 | // build our application with a route 128 | let app = Router::new() 129 | // `GET /` goes to `root` 130 | .route("/", get(root)) 131 | // `POST /users` goes to `create_user` 132 | .route("/users", post(create_user)); 133 | 134 | // run our app with hyper, listening globally on port 3000 135 | let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 136 | axum::serve(listener, app).await.unwrap(); 137 | } 138 | ``` 139 | 140 | - async / await: Khi ta chạy chương trình, lúc này chỉ có main thread được chạy và những task nào cần tốn nhiều thời gian để xử lý (await) sẽ được xử lý ở background trong khi đó main thread sẽ tiến hành xử lý những task khác, sau khi task kia xử lý xong ở background thì main thread sẽ quay lại task đó và tiến hành xử lý tiếp. 141 | - Ví dụ: khi client request đến server một resource nào đó mà server cần đọc từ file mà quá trình đọc từ file có thể sẽ rất lâu nếu mà ta chạy đồng bộ thì phải đợi main thread xử lý xong việc đọc file rồi mới xử lý được các task khác. Lúc này khi 1 client khác request đến thì sẽ phải đợi main thread xử lý xong việc đọc file rồi mới đến lượt mình 142 | - Một vài điều cần lưu ý khi làm việc với async / await 143 | - await chỉ được gọi trong async function 144 | - #[tokio::main]: attribute → chạy hàm main trong context bất đồng bộ 145 | - tracing: setup log system để tiện debug, monitor,… 146 | - Router: định nghĩa các path để truy cập các resource 147 | - HTTP method: GET, POST, PUT, DELETE 148 | - listener: set ip address và port cho web server 149 | - serve: chạy server trên listener đã tạo ở trên 150 | 151 | ```rust 152 | // basic handler that responds with a static string 153 | async fn root() -> &'static str { 154 | "Hello, World!" 155 | } 156 | 157 | async fn create_user( 158 | // this argument tells axum to parse the request body 159 | // as JSON into a `CreateUser` type 160 | Json(payload): Json, 161 | ) -> (StatusCode, Json) { 162 | // insert your application logic here 163 | let user = User { 164 | id: 1337, 165 | username: payload.username, 166 | }; 167 | 168 | // this will be converted into a JSON response 169 | // with a status code of `201 Created` 170 | (StatusCode::CREATED, Json(user)) 171 | } 172 | ``` 173 | 174 | - Đây là 2 handler function ứng với mỗi route được định nghĩa 175 | - Tất cả các **handler** function đều là **async** function 176 | 177 | ```rust 178 | Json(payload): Json 179 | ``` 180 | 181 | Trong trường hợp này, Json là 1 extractor và deserialize JSON về Rust type có attribute là Deserialize (struct CreateUser) 182 | 183 | ⇒ Lúc này payload sẽ là 1 instance của struct CreateUser 184 | 185 | Extractor trong Axum là Axum sẽ tự động trích xuất các phần cần thiết từ HTTP Request mà ta cần sử dụng như 186 | 187 | - Header 188 | - Body 189 | - Query params 190 | - … 191 | 192 | ```rust 193 | -> (StatusCode, Json) 194 | ``` 195 | 196 | Trong trường hợp này Json serialize từ Rust type có attribute là Serialize (struct User) về JSON 197 | 198 | ```rust 199 | // the input to our `create_user` handler 200 | #[derive(Deserialize)] 201 | struct CreateUser { 202 | username: String, 203 | } 204 | 205 | // the output to our `create_user` handler 206 | #[derive(Serialize)] 207 | struct User { 208 | id: u64, 209 | username: String, 210 | } 211 | ``` 212 | 213 | - Phần định nghĩa struct thì có lẽ mọi người đã quá quen thuộc trong đây có 1 phần đặc biệt mình muốn nói đó là **attribute** trong Rust 214 | - **Serialize**: convert từ Rust type sang JSON 215 | - **Deserialize**: convert từ JSON sang Rust Type 216 | 217 | ## Setup codebase 218 | 219 | Đoạn code setup server từ example của docs tất cả đang nằm trong 1 file. Bây giờ ta tiến hành chia cấu trúc thư mục cho dễ quản lý. Mình sẽ tổ chức folder theo hướng feature based 220 | 221 | Trong bài viết này, mình chỉ tập trung vào 2 feature 222 | 223 | - auth: login, verify_token 224 | - users: create 225 | 226 | ![Tree Folder](img/tree_folder.png) 227 | 228 | - module `enums` quản lý các enum type như routes,… 229 | - module `features` gồm các sub module tương ứng với chức năng của từng đối tượng 230 | - `handler.rs` định nghĩa các hàm xử lý các request 231 | - `model.rs` định nghĩa các struct để request, response,… 232 | - `routes.rs` định nghĩa các route cho feature 233 | - `main.rs` khởi chạy server 234 | - `router.rs` định nghĩa router cho server bằng cách kết hợp các router con đã định nghĩa ở mỗi feature 235 | 236 | ### Quản lý các route path cho mỗi feature bằng Enum 237 | 238 | - Áp dụng kiến thức **Convert Enum sang 1 kiểu dữ liệu khác** 239 | `enums/routes.rs` 240 | 241 | ```rust 242 | const AUTH_PATH: &str = "/auth"; 243 | const USERS_PATH: &str = "/users"; 244 | 245 | pub enum RoutePath { 246 | AUTH, 247 | USERS, 248 | } 249 | 250 | impl RoutePath { 251 | pub fn get_path(&self) -> &'static str { 252 | match self { 253 | RoutePath::AUTH => AUTH_PATH, 254 | RoutePath::USERS => USERS_PATH, 255 | } 256 | } 257 | } 258 | ``` 259 | 260 | `router.rs` 261 | 262 | Mình show trước cho mọi người code trong file này để mọi người biết được server có những router chính nào và từ đây ta cũng dễ dàng đi đến từng router của mỗi feature. Mình sẽ đi từ tổng quan đến chi tiết thì mọi người sẽ để nắm được flow hoạt động hơn 263 | 264 | ```rust 265 | use axum::{Router}; 266 | 267 | use crate::{ 268 | enums::routes::RoutePath, 269 | features::{ 270 | auth::routes::get_routes as get_auth_routes, 271 | users::routes::get_routes as get_user_routes, 272 | }, 273 | }; 274 | 275 | pub fn create_router() -> Router { 276 | let auth_routes = get_auth_routes(); 277 | let user_routes = get_user_routes(); 278 | 279 | let api_routes = Router::new() 280 | .nest(RoutePath::AUTH.get_path(), auth_routes) 281 | .nest(RoutePath::USERS.get_path(), user_routes); 282 | 283 | Router::new().nest("/api", api_routes) 284 | } 285 | ``` 286 | 287 | ## Basic authentication với JWT (feature auth) 288 | 289 | `features/auth/router.rs` 290 | 291 | Đây là các route của feature auth 292 | 293 | ```rust 294 | use axum::{routing::post, Router}; 295 | 296 | use super::handler::{login, verify}; 297 | 298 | pub fn get_routes() -> Router { 299 | Router::new() 300 | .route("/login", post(login)) 301 | .route("/verify", post(verify)) 302 | } 303 | ``` 304 | 305 | `features/auth/handler.rs` 306 | 307 | Bây giờ mình sẽ đi vào chi tiết từng handler function 308 | 309 | ```rust 310 | use axum::{http::{HeaderMap, StatusCode}, Json}; 311 | 312 | use super::model::{Claims, LoginRequest, LoginResponse}; 313 | 314 | use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; 315 | 316 | pub async fn login( 317 | Json(payload): Json, 318 | ) -> (StatusCode, Json) { 319 | let LoginRequest { email, password } = payload; 320 | 321 | let claims = Claims { 322 | sub: email, 323 | exp: (chrono::Utc::now() + chrono::Duration::days(1)).timestamp() as usize, 324 | }; 325 | 326 | let secret = "my_secret"; 327 | 328 | let token = encode( 329 | &Header::default(), 330 | &claims, 331 | &EncodingKey::from_secret(secret.as_ref()), 332 | ) 333 | .unwrap(); 334 | 335 | let resp = LoginResponse { 336 | msg: String::from("Login Success"), 337 | token: token, 338 | }; 339 | 340 | (StatusCode::CREATED, Json(resp)) 341 | } 342 | 343 | pub async fn verify(header_map: HeaderMap) -> Result, StatusCode> { 344 | if let Some(token) = header_map.get("Authorization") { 345 | let token = token.to_str().unwrap().replace("Bearer ", ""); 346 | match decode::( 347 | &token, 348 | &DecodingKey::from_secret("my_secret".as_ref()), 349 | &Validation::default(), 350 | ) { 351 | Ok(token_data) => { 352 | return Ok(Json(token_data.claims.sub)); 353 | } 354 | Err(_) => { 355 | return Err(StatusCode::UNAUTHORIZED); 356 | } 357 | } 358 | } 359 | Err(StatusCode::UNAUTHORIZED) 360 | } 361 | ``` 362 | 363 | - login 364 | ```rust 365 | let LoginRequest { email, password } = payload; 366 | ``` 367 | Đây là kĩ thuật destructuring struct để lấy ra các field cần thiết 368 | JWT gồm 3 thành phần chính 369 | - Header: chứa thông tin về thuật toán dùng để mã hóa 370 | - Payload (Claims): chứa các metadata như thời gian hết hạn token, subject,… 371 | - Signature: được tạo ra bằng cách mã hóa header và payload từ secret key 372 | [chrono - crates.io: Rust Package Registry](https://crates.io/crates/chrono): crate hỗ trợ xử lý về thời gian 373 | [jsonwebtoken - crates.io: Rust Package Registry](https://crates.io/crates/jsonwebtoken): crate hỗ trợ tạo JWT 374 | Mình thêm các dependencies sau vào Cargo.toml 375 | ```rust 376 | jsonwebtoken = "9.3.0" 377 | chrono = "0.4.38" 378 | ``` 379 | - verify: hàm này chỉ để kiểm tra việc mình decode JWT ra 380 | HeaderMap là extractor của axum để lấy các thông tin Header của Request như authorization,… 381 | decode nhận vào 3 tham số 382 | token: truyền lên thông qua Header của Request 383 | secret key: để decode JWT 384 | Validation: kiểm tra xem token còn hạn không,… 385 | 386 | `features/auth/model.rs` 387 | 388 | Đây là các struct hỗ trợ cho các handler function trên 389 | 390 | ```rust 391 | use serde::{Deserialize, Serialize}; 392 | 393 | #[derive(Serialize)] 394 | pub struct LoginResponse { 395 | pub msg: String, 396 | pub token: String, 397 | } 398 | 399 | #[derive(Deserialize)] 400 | pub struct LoginRequest { 401 | pub email: String, 402 | pub password: String, 403 | } 404 | 405 | #[derive(Serialize, Deserialize)] 406 | pub struct Claims { 407 | pub sub: String, // the subject of the token 408 | pub exp: usize, // the expiry time 409 | } 410 | ``` 411 | 412 | ## Xây dựng API (feature user) 413 | 414 | Ở bài viết này do chưa kết nối với database nên mình chỉ làm hàm tạo user. 415 | 416 | Có sự thay đổi xíu là mình sẽ sử dụng thư viện [uuid - crates.io: Rust Package Registry](https://crates.io/crates/uuid) để tạo id cho user 417 | 418 | Mình thêm dependency sau vào Cargo.toml 419 | 420 | ```rust 421 | uuid = { version = "1.8.0", features = ["v4", "serde"] } 422 | ``` 423 | 424 | `features/users/routes.rs` 425 | 426 | Đây là route của feature user 427 | 428 | ```rust 429 | use axum::{routing::post, Router}; 430 | 431 | use super::handler::create_user; 432 | 433 | pub fn get_routes() -> Router { 434 | Router::new() 435 | .route("/", post(create_user)) 436 | } 437 | ``` 438 | 439 | `features/users/handler.rs` 440 | 441 | Đây là handler function cho route 442 | 443 | ```rust 444 | use axum::{ 445 | http::StatusCode, 446 | Json, 447 | }; 448 | use uuid::Uuid; 449 | 450 | use super::model::{CreateUser, User}; 451 | 452 | pub async fn create_user( 453 | // this argument tells axum to parse the request body 454 | // as JSON into a `CreateUser` type 455 | Json(payload): Json, 456 | ) -> (StatusCode, Json) { 457 | // insert your application logic here 458 | let user: User = User { 459 | id: Uuid::new_v4(), 460 | username: payload.username, 461 | }; 462 | 463 | // this will be converted into a JSON response 464 | // with a status code of `201 Created` 465 | (StatusCode::CREATED, Json(user)) 466 | } 467 | ``` 468 | 469 | Đoạn code chỉ thay đổi chỗ id của user sẽ được tạo ngẫu nhiên 470 | 471 | `features/users/model.rs` 472 | 473 | Đây là model sử dụng cho handler function trên 474 | 475 | ```rust 476 | use serde::{Deserialize, Serialize}; 477 | use uuid::Uuid; 478 | 479 | // the input to our `create_user` handler 480 | #[derive(Deserialize)] 481 | pub struct CreateUser { 482 | pub username: String, 483 | } 484 | 485 | // the output to our `create_user` handler 486 | #[derive(Serialize)] 487 | pub struct User { 488 | pub id: Uuid, 489 | pub username: String, 490 | } 491 | ``` 492 | 493 | ## Tổng kết 494 | 495 | Trong bài viết này ta đã: 496 | 497 | - Biết cách setup web server sử dụng axum framework 498 | - Tổ chức source code theo hướng feature 499 | - Cài đặt thành công chức năng đăng nhập với jwt, tạo mới user 500 | 501 | ## Bài viết tiếp theo 502 | 503 | - Kết nối, thiết kế database 504 | - Tạo middleware 505 | - Tiếp tục xây dựng API 506 | - Sử dụng socket để làm real time 507 | 508 | Mình chưa có quá nhiều kinh nghiệm với Rust trong việc xây dựng Backend. Trong bài viết có sai sót gì mọi người cùng thảo luận góp ý nhé. 509 | 510 | Cảm ơn mọi người đã đọc. 511 | 512 | ## Github 513 | 514 | Mọi người có thể xem source code hoàn chỉnh ở đây nhé. 515 | 516 | [https://github.com/Learning-Tech-Workspace/learn-rust-backend](https://github.com/Learning-Tech-Workspace/learn-rust-backend) 517 | 518 | ## Postman 519 | 520 | [https://www.postman.com/navigation-astronaut-22006281/workspace/rust](https://www.postman.com/navigation-astronaut-22006281/workspace/rust) 521 | 522 | # Reference 523 | 524 | [Rust Axum Full Course - Web Development (GitHub repo updated to Axum 0.7)](https://www.youtube.com/watch?v=XZtlD_m59sM) 525 | 526 | https://github.com/tokio-rs/axum/blob/main/ECOSYSTEM.md#tutorials 527 | 528 | https://github.com/joelparkerhenderson/demo-rust-axum 529 | 530 | [JWT Authentication in Rust | A Step-by-Step Guide](https://www.youtube.com/watch?v=p2ljQrRl0Mg) 531 | -------------------------------------------------------------------------------- /rust-challenges/08-front-end-app/README.md: -------------------------------------------------------------------------------- 1 | # Xây dựng Front End với Rust 2 | 3 | Hi mọi người lại là mình tung-lee đây. Ta đã quá quen thuộc với việc code front end bằng JavaScript nhưng mọi người có bao giờ tự hỏi liệu mình có thể code front end với Rust không? Mình vừa không cần phải học thêm ngôn ngữ mới mà vừa có thể trao dồi kỹ năng lập trình với ngôn ngữ Rust. Một mũi tên trúng 2 đích luôn :))) Ok mở màn nhiêu đó đủ rồi mình bắt đầu vào việc luôn đây. 4 | 5 | # Kiến thức yêu cầu 6 | 7 | - Biết cơ bản về JavaScript, React 8 | - Biết cơ bản về Rust 9 | 10 | # WebAssembly 11 | 12 | Có một khái niệm mới mà mọi người cần phải hiểu làm sao browser có thể thực thi được code viết bằng ngôn ngữ Rust vì mọi người có thể đã biết browser có V8 Engine để thông dịch (interpreter) và thực thi code JavaScript cho nên chỉ có JavaScript chạy được trên browser 13 | 14 | Còn Rust là ngôn ngữ cần phải biên dịch sang mã máy (bytecode) để có thể thực thi được và bytecode được build ra thực thi được ở platform build ra nó (cố định) 15 | 16 | Ví dụ khi ta biên dịch mã nguồn Rust ở hệ điều hành Windows và dựa vào kiến trúc CPU thì ta chỉ có thể thực thi được bytecode ở hệ điều hành Windows nếu đem folder build đó qua MacOS chạy thì sẽ bị lỗi 17 | 18 | Ngoài ra, V8 Engine có hỗ trợ thực thi web assembly - là khái niệm mới mà mình cần tìm hiểu. 19 | 20 | ![Untitled](img/Untitled.png) 21 | 22 | WebAssembly (viết tắt wasm) là low-level bytecode cho web 23 | 24 | Các lợi thế của WebAssembly 25 | 26 | - Native level performance (code Rust chạy trên native hiệu năng ra sao thì chạy trên web y chang vậy) 27 | - Đưa nhiều ngôn ngữ lập trình vào web platform (các ngôn ngữ hiện tại có thể biên dich ra wasm thì có C, C++, **Rust**,…) 28 | - Nhanh, an toàn,… hơn JavaScript 29 | 30 | `.wat` giống như assembly language `.asm` nhưng mà mình không cần viết bằng ngôn ngữ này mà viết bằng Rust, C++ rồi chỉnh compilation target để build ra .wasm luôn 31 | 32 | `.wasm` như mã máy có thể chạy trên tất cả các trình duyệt 33 | 34 | WebAssembly không thể thay thế JavaScript mà tương tác qua lại với JavaScript 35 | 36 | Ví dụ: Figma 37 | 38 | - UI: sử dụng React 39 | - Chức năng: sử dụng C++ cho hiệu năng cao 40 | 41 | Khi thiết lập build setting cho game trong Unity thì nó build ra web assembly 42 | 43 | ## Minh họa 44 | 45 | Có một trang web hỗ trợ compile code sang WebAssembly nhưng chỉ hỗ trợ ngôn ngữ C++. Mình chỉ minh họa cho mọi người xem khi compile sang wasm thì nó sẽ như thế nào nhưng có vẻ như trang web không còn hoạt động nữa, mình chờ compile khá lâu rồi nhưng chưa có kết quả :))) Mọi người có thể thử ở web này xem được không nhé [mbebenita.github.io/WasmExplorer/](https://mbebenita.github.io/WasmExplorer/) 46 | 47 | ![Untitled](img/Untitled%201.png) 48 | 49 | Còn phần kế tiếp mình sẽ minh họa trên ngôn ngữ Rust 50 | 51 | ## Thiết lập build target cho Rust 52 | 53 | Để có thể build ra wasm mọi người cần phải tải 54 | 55 | ```bash 56 | rustup target add wasm32-unknown-unknown 57 | ``` 58 | 59 | Sau đó mọi người có thể kiểm tra xem đã tải được chưa thông qua lệnh sau 60 | 61 | ```bash 62 | rustup show 63 | ``` 64 | 65 | ![Untitled](img/Untitled%202.png) 66 | 67 | Bây giờ mình sẽ thử build ra 2 target và so sánh xem chúng có khác gì nhau không? 68 | 69 | Target mặc định trên máy mình là `x86_64-unknown-linux-gnu` nên mình chỉ cần chạy lệnh 70 | 71 | ```bash 72 | cargo build 73 | ``` 74 | 75 | File binary của mình là file `rust_test` 76 | 77 | ![Untitled](img/Untitled%203.png) 78 | 79 | Để có thể xem file binary này mọi người có thể truy cập [Online binary file viewer (iamkate.com)](https://iamkate.com/code/binary-file-viewer/) 80 | 81 | ![Untitled](img/Untitled%204.png) 82 | 83 | Xong rồi mình sẽ build với target là wasm, mọi người nhập lệnh sau 84 | 85 | ```bash 86 | cargo build --target wasm32-unknown-unknown 87 | ``` 88 | 89 | ![Untitled](img/Untitled%205.png) 90 | 91 | Sau khi build xong mọi người có thể truy cập trang web trên để đọc xem file .wasm có những gì (nó cũng chỉ là bytecode) 92 | 93 | ![Untitled](img/Untitled%206.png) 94 | 95 | Mọi người có thể thấy kích thước của file `wasm` nhẹ hơn kích thước bytecode khi build với target native có thể là do target wasm đã tối ưu để cho file wasm nhẹ hơn để tải nhanh hơn trên web. 96 | 97 | Bây giờ mình sẽ tiến hành tạo các function đơn giản rồi build ra wasm 98 | 99 | ```rust 100 | fn main() {} 101 | 102 | #[no_mangle] 103 | pub extern "C" fn add(num1: u64, num2: u64) -> u64 { 104 | num1 + num2 105 | } 106 | 107 | #[no_mangle] 108 | pub extern "C" fn mul(num1: u64, num2: u64) -> u64 { 109 | num1 * num2 110 | } 111 | 112 | ``` 113 | 114 | Để khi build ra wasm mà ta có thể truy cập đến những function này thì mọi người cần phải thêm 115 | 116 | - #[no_mangle] 117 | - pub extern "C" 118 | 119 | Tạm thời mọi người cứ làm theo như vậy nhé mình cũng chưa hiểu rõ về 2 thằng này lắm 120 | 121 | Sau đó mọi người buid với target wasm 122 | 123 | ```rust 124 | cargo build --target wasm32-unknown-unknown 125 | ``` 126 | 127 | Sau đó mọi người thêm crate `wasmprinter` 128 | 129 | ```rust 130 | cargo add wasmprinter 131 | ``` 132 | 133 | Thư viện này sẽ parse từ `wasm` sang `wat` là mã assembly mà con người có thể đọc được 134 | 135 | ```rust 136 | let wat = wasmprinter::print_file( 137 | "/home/asus/Workspace/rust/rust-test/target/wasm32-unknown-unknown/debug/rust-test.wasm", 138 | ) 139 | .unwrap(); 140 | File::create("rust-test.wat") 141 | .unwrap() 142 | .write_all(wat.as_bytes()) 143 | .unwrap(); 144 | ``` 145 | 146 | Sau đó mọi người chạy chương trình 147 | 148 | ```rust 149 | cargo run 150 | ``` 151 | 152 | Mọi người sẽ thấy có 1 file `.wat` sẽ được tạo ra và file đó có nội dung như sau 153 | 154 | ![Untitled](img/Untitled%207.png) 155 | 156 | Mọi người nhấn tổ hợp phím `Ctrl + F` và nhập từ muốn tìm kiếm là `func $` + tên function để tìm kiếm các function mình đã định nghĩa 157 | 158 | Sau đó mình sẽ tạo 1 project front end để thử chạy wasm. Mình sẽ tạo dự án React Typescript bằng vite 159 | 160 | Sau khi tạo xong mọi người copy file wasm đã build vào thư mục public 161 | 162 | ![Untitled](img/Untitled%208.png) 163 | 164 | Sau đó mọi người mở file `App.tsx` và thêm đoạn code sau 165 | 166 | ```jsx 167 | async function loadWebAssembly(fileName: string) { 168 | const resp: Response = await fetch(fileName); 169 | const buffer: ArrayBuffer = await resp.arrayBuffer(); 170 | const webAssembly = await WebAssembly.instantiate(buffer); 171 | console.log(webAssembly); 172 | return webAssembly.instance; 173 | } 174 | 175 | useEffect(() => { 176 | async function run() { 177 | const instance = await loadWebAssembly("/rust-test.wasm"); 178 | console.log(instance); 179 | console.log(instance.exports.add); 180 | const add = instance.exports.add as (a: bigint, b: bigint) => bigint; 181 | const mul = instance.exports.mul as (a: bigint, b: bigint) => bigint; 182 | const sum = add(BigInt(40), BigInt(2)); 183 | const diff = mul(BigInt(40), BigInt(2)); 184 | console.log(sum); 185 | console.log(diff); 186 | } 187 | run(); 188 | }, []); 189 | ``` 190 | 191 | Mình có thể `console.log(webAssembly)` để xem có những thông tin nào 192 | 193 | ![Untitled](img/Untitled%209.png) 194 | 195 | field module là array lưu các thông tin memory, các function, global liên quan. Theo như mình nghĩ nó sẽ lưu các thông tin liên quan đến memory được sử dụng ở trên browser vì lúc này wasm sẽ sử dụng memory của browser (cái này mình không chắc lắm) 196 | 197 | ![Untitled](img/Untitled%2010.png) 198 | 199 | Để có thể sử dụng được các hàm trong wasm thì ta phải truy cập field instance 200 | 201 | ![Untitled](img/Untitled%2011.png) 202 | 203 | Như mọi người có thể thấy được các function mình đã định nghĩa và các thông tin bộ nhớ, global liên quan, bây giờ mình thử in object function ra để xem nó có khác gì function mình đã định nghĩa không nhé 204 | 205 | ![Untitled](img/Untitled%2012.png) 206 | 207 | Ok sau khi mình in ra thì nó là native code (chứ không phải là code JavaScript) 208 | 209 | ![Untitled](img/Untitled%2013.png) 210 | 211 | Nhấn vào thì nó sẽ điều hướng sang phần source và ta đang xem nội dung của .wat 212 | 213 | Một điều mình thấy không hay đó là mình không biết được rõ type để có thể truyền vào. 214 | 215 | ## Blockchain 216 | 217 | Một số hệ blockchain ví dụ như cosmos thì khi build cũng target đến wasm và gọi là wasm smart contract. Này mình giới thiệu qua thôi chứ mình cũng chưa tìm hiểu kỹ. 218 | 219 | # Rust FE 220 | 221 | Ok thế là xong về một khái niệm quan trọng để ta có thể giải thích được vì sao Rust có thể code được Front end. Giờ thì triển đến phần chính luôn nhé bên Rust có một framework hỗ trợ code Front End với lượt Star hiện tại là `30.1k` , cộng đồng cũng khá to lớn 222 | 223 | ## Giới thiệu về Yew 224 | 225 | - Component Based giống React (hook, props, function component,…) 226 | - Tương tác được với JavaScript 227 | 228 | ## Thiết lập vscode 229 | 230 | ### User Snippet 231 | 232 | Mọi người vào link sau [https://yew.rs/docs/getting-started/editor-setup#vs-code](https://yew.rs/docs/getting-started/editor-setup#vs-code) lấy đoạn user snippet để có template tạo component. Sau đó vào Manage > User Snippet > rust.json > paste nội dung đã lấy vào > save 233 | 234 | 235 | 236 | ![Untitled](img/Untitled%2014.png) 237 | 238 | ### Extension 239 | 240 | Extension [https://yew.rs/docs/getting-started/editor-setup#vs-code-1](https://yew.rs/docs/getting-started/editor-setup#vs-code-1) hỗ trợ syntax highlight,… 241 | 242 | ## Trunk 243 | 244 | Trunk is a WASM web application bundler for Rust. (giống vite, webpack) 245 | 246 | Trunk sẽ tự động build lại app mỗi khi source code thay đổi 247 | 248 | Ngoài ra mọi người có thể config trunk thông qua file sau 249 | 250 | `Trunk.toml` 251 | 252 | ```toml 253 | [serve] 254 | # The address to serve on LAN. 255 | address = "127.0.0.1" 256 | # The address to serve on WAN. 257 | # address = "0.0.0.0" 258 | # The port to serve on. 259 | port = 8000 260 | ``` 261 | 262 | ## Hướng dẫn 263 | 264 | Mọi người tạo dự án mới 265 | 266 | ```jsx 267 | cargo new fe-app 268 | cd fe-app 269 | ``` 270 | 271 | Sau đó mọi người thêm crate `yew` vào `Cargo.toml` 272 | 273 | ```jsx 274 | [dependencies] 275 | yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] } 276 | ``` 277 | 278 | feature `csr` là client side rendering 279 | 280 | Sau đó mọi người mở file `main.rs` và thay đổi như sau 281 | 282 | ```rust 283 | use yew::prelude::*; 284 | 285 | #[function_component(App)] 286 | fn app() -> Html { 287 | html! { 288 | <> 289 |

{"Hello World"}

290 | 291 | } 292 | } 293 | 294 | fn main() { 295 | yew::Renderer::::new().render(); 296 | } 297 | ``` 298 | 299 | Khai báo macro `function_component` bất cứ khi nào mọi người muốn tạo component và function_component phải trả về `Html` 300 | 301 | macro `html!` giống như định nghĩa JSX bên React (fragment, cách truyền dữ liệu để render,…) 302 | 303 | macro khai báo tên component 304 | 305 | Sau đó mọi người tạo file `index.html` 306 | 307 | ```html 308 | 309 | 310 | 311 | Chat App 312 | 313 | 314 | 315 | ``` 316 | 317 | Thay vì ở React ta phải tìm element nào là root thì yew mặc định root sẽ là body tag khi function `yew::Renderer::::new().render()` được gọi trong hàm `main`  318 | 319 | Sau đó sử dụng trunk để build ra wasm và chạy app 320 | 321 | ```rust 322 | trunk serve 323 | ``` 324 | 325 | ### Quản lý code 326 | 327 | Mình muốn có 1 folder riêng để quản lý các component (giống như cách tổ chức thường sử dụng cho React). 328 | 329 | Mọi người tạo folder `components` ở thư mục gốc. Sau đó mọi người tạo file `app.rs`. 330 | 331 | ![Untitled](img/Untitled%2015.png) 332 | 333 | Mọi người copy các đoạn code liên quan đến App component ở file `main.rs` sang file `app.rs` nhé 334 | 335 | ### UI + Logic 336 | 337 | Mình định làm giao diện để có thể sử dụng cái realtime chat-app server mình đã làm ở series trước nhưng mình có tìm hiểu qua thì Rust chưa có nhiều crate hỗ trợ socket client nên mình không hứa chắc sẽ làm được đâu nhé :)))) 338 | 339 | Đầu tiên mình sẽ tạo `Title` component và props cho nó 340 | 341 | `src/components/title.rs` 342 | 343 | ```rust 344 | use yew::prelude::*; 345 | 346 | #[derive(Properties, PartialEq)] 347 | pub struct TitleProps { 348 | pub content: Option, 349 | } 350 | 351 | #[function_component(Title)] 352 | pub fn title(props: &TitleProps) -> Html { 353 | let TitleProps { content } = props; 354 | html! { 355 | <> 356 | if let Some(content) = content { 357 |

{content}

358 | } else { 359 |

{"Front End App"}

360 | } 361 | 362 | } 363 | } 364 | 365 | ``` 366 | 367 | Để định nghĩa struct props cho component cần thêm macro `Properties` và `PartialEq` 368 | 369 | Ở đây mình có sử dụng kĩ thuật `conditional rendering` 370 | 371 | Sau đó mình tạo component `Message` 372 | 373 | Mọi người tạo thêm file [types.rs](http://types.rs) ở thư mục gốc để lưu các type dữ liệu cần hiển thị 374 | 375 | `src/components/message.rs` 376 | 377 | ```rust 378 | #[derive(Debug, PartialEq, Clone)] 379 | pub struct Message { 380 | pub content: String, 381 | pub author: String, 382 | } 383 | ``` 384 | 385 | 2 macro cần phải có là `PartialEq` và `Clone`. 386 | 387 | ```rust 388 | use yew::{function_component, html, Html, Properties}; 389 | 390 | use crate::types::Message as MessageType; 391 | 392 | #[derive(PartialEq, Properties)] 393 | pub struct MessageProps { 394 | pub msg: MessageType, 395 | } 396 | 397 | #[function_component(Message)] 398 | pub fn message(props: &MessageProps) -> Html { 399 | let MessageProps { msg } = props; 400 | let MessageType { content, author } = msg; 401 | html! { 402 |
403 |

{content}

404 |

{author}

405 |
406 | } 407 | } 408 | ``` 409 | 410 | Sau đó mọi người tạo component `BoxChat` 411 | 412 | `src/components/box_chat.rs` 413 | 414 | ```rust 415 | use yew::{function_component, html, Html, Properties}; 416 | 417 | use crate::components::message::Message; 418 | use crate::types::Message as MessageType; 419 | 420 | #[derive(PartialEq, Properties)] 421 | pub struct BoxChatProps { 422 | pub messages: Vec, 423 | } 424 | 425 | #[function_component] 426 | pub fn BoxChat(props: &BoxChatProps) -> Html { 427 | let BoxChatProps { messages } = props; 428 | html! { 429 |
430 | {messages.iter().map(|msg| { 431 | html! { 432 | 433 | } 434 | }).collect::()} 435 |
436 | } 437 | } 438 | ``` 439 | 440 | Ở đây mình sử dụng kĩ thuật để render 1 list (array, vec,…) 441 | 442 | Sau đó mình thêm các component vào `app.rs` 443 | 444 | ```rust 445 | use yew::prelude::*; 446 | 447 | use crate::components::box_chat::BoxChat; 448 | use crate::components::title::Title; 449 | use crate::types::Message; 450 | 451 | #[function_component(App)] 452 | pub fn app() -> Html { 453 | let messages = vec![ 454 | Message { 455 | content: "Hello Ben!".to_string(), 456 | author: "Alex".to_string(), 457 | }, 458 | Message { 459 | content: "Hi Alex!".to_string(), 460 | author: "Ben".to_string(), 461 | }, 462 | ]; 463 | html! { 464 | <> 465 | 466 | <BoxChat messages={messages} /> 467 | </> 468 | } 469 | } 470 | ``` 471 | 472 | **Lưu ý:** 473 | 474 | Component `Title` không hiểu sao mặc dù mình để field trong props là option nhưng không truyền vào thì nó sẽ báo lỗi. Mọi người ai biết thì giải đáp giúp mình nhé. 475 | 476 | Sau đó thì mình sẽ cần thêm input element để lấy được input chat của người dùng và mình cần quan tâm đến các thứ sau đây 477 | 478 | - event 479 | 480 | Mọi người có thể tìm kiếm các event ứng với Event type trong Rust ở link sau [https://yew.rs/docs/concepts/html/events#available-events](https://yew.rs/docs/concepts/html/events#available-events) 481 | 482 | Và lưu ý event nào trong html phải ứng với Event type trong Rust nếu không sẽ báo lỗi và lỗi nó báo không rõ ràng lắm 483 | 484 | - callback có thể sử dụng để child component đưa data lên parent component 485 | 486 | Đầu tiên mọi người cứ tạo component trước đi 487 | 488 | `src/components/input.rs` 489 | 490 | ```rust 491 | use std::ops::Deref; 492 | 493 | use gloo::console::log; 494 | use log::info; 495 | use wasm_bindgen::JsCast; 496 | use web_sys::HtmlInputElement; 497 | use yew::prelude::*; 498 | 499 | #[derive(PartialEq, Properties)] 500 | pub struct InputProps { 501 | pub r#type: String, 502 | } 503 | 504 | #[function_component(Input)] 505 | pub fn input(props: &InputProps) -> Html { 506 | let on_change = Callback::from(|evt: Event| { 507 | let target = evt.target().unwrap(); // We can safely unwrap here because we know that the target is an input element 508 | info!("{:?}", target.clone()); 509 | log!(target.clone()); 510 | let input = target.unchecked_into::<HtmlInputElement>(); 511 | info!("{:?}", input.value()); 512 | log!(input.value()); 513 | }); 514 | 515 | let InputProps { r#type } = props; 516 | let r#type = r#type.deref().to_string(); 517 | 518 | html! { 519 | <input type={r#type} onchange={on_change} /> 520 | } 521 | } 522 | ``` 523 | 524 | Không như JavaScript khi ta lấy được `target` của event là lấy được `html element` và từ đó lấy được value thì với Rust phải có type cụ thể để biết element đó là gì. 525 | 526 | Mình cần có thêm crate `wasm-bindgen` để có `JsCast` và crate `web-sys` để có thể tương tác với web api trên browser 527 | 528 | ### Debug 529 | 530 | Sử dụng println sẽ không in ra terminal đâu vì wasm chạy trên browser cho nên phải sử dụng các crate sau để debug 531 | 532 | `gloo` , `wasm-logger` , `log` 533 | 534 | - gloo: giống console log 535 | - wasm-logger + log: giống như debug trên terminal nhưng thay vì in trên terminal thì sẽ in trên console của browser 536 | 537 | Theo mình thấy nên kết hợp cả 2 để debug cho thuận tiện 538 | 539 | ![Untitled](img/Untitled%2016.png) 540 | 541 | Phía trên là sử dụng `log` với `wasm-logger` 542 | 543 | Phía dưới là sử dụng `gloo` 544 | 545 | Sau đó tạo component button luôn 546 | 547 | `src/components/button.rs` 548 | 549 | ```rust 550 | use yew::prelude::*; 551 | 552 | #[derive(PartialEq, Properties)] 553 | pub struct ButtonProps { 554 | pub label: String, 555 | } 556 | 557 | #[function_component] 558 | pub fn Button(props: &ButtonProps) -> Html { 559 | let ButtonProps { label } = props; 560 | html! { 561 | <button>{label}</button> 562 | } 563 | } 564 | ``` 565 | 566 | ### State 567 | 568 | Có 2 cách xử lý: 569 | 570 | 1. pass callback dưới dạng props 571 | 2. pass use state dưới dạng props (giống React) 572 | 573 | Trong trường hợp này mình sẽ tiếp cận theo cách thứ 2 nhé 574 | 575 | ```rust 576 | use std::ops::Deref; 577 | 578 | use wasm_bindgen::JsCast; 579 | use web_sys::HtmlInputElement; 580 | use yew::prelude::*; 581 | 582 | #[derive(PartialEq, Properties)] 583 | pub struct InputProps { 584 | pub r#type: String, 585 | pub state: UseStateHandle<String>, 586 | } 587 | 588 | #[function_component(Input)] 589 | pub fn input(props: &InputProps) -> Html { 590 | let state = props.state.clone(); 591 | 592 | let on_change = Callback::from(move |evt: Event| { 593 | let target = evt.target().unwrap(); // We can safely unwrap here because we know that the target is an input element 594 | let input = target.unchecked_into::<HtmlInputElement>(); 595 | state.set(input.value()); 596 | }); 597 | 598 | let r#type = props.r#type.deref().to_string(); 599 | 600 | html! { 601 | <input type={r#type} onchange={on_change} /> 602 | } 603 | } 604 | ``` 605 | 606 | Ở đây có vài chỗ mình vẫn đang tìm hiểu này là bên ngôn ngữ Rust thôi 607 | 608 | - keyword move 609 | - Khi mình sử dụng props trong callback thì bị borrowed value và cần phải clone ra 610 | 611 | Mọi người ai biết thì mình cùng thảo luận nhé 612 | 613 | `app.rs` 614 | 615 | ```rust 616 | use std::ops::Deref; 617 | 618 | use gloo::console::log; 619 | use yew::prelude::*; 620 | 621 | use crate::components::box_chat::BoxChat; 622 | use crate::components::button::Button; 623 | use crate::components::input::Input; 624 | use crate::components::title::Title; 625 | use crate::types::Message; 626 | 627 | #[function_component(App)] 628 | pub fn app() -> Html { 629 | let message = use_state(|| "".to_string()); 630 | 631 | let messages = vec![ 632 | Message { 633 | content: "Hello Ben!".to_string(), 634 | author: "Alex".to_string(), 635 | }, 636 | Message { 637 | content: "Hi Alex!".to_string(), 638 | author: "Ben".to_string(), 639 | }, 640 | ]; 641 | html! { 642 | <> 643 | <Title content={Some("FE App")}/> 644 | <BoxChat messages={messages} /> 645 | <Input r#type="text" state={message.clone()} /> 646 | <Button label="Send"/> 647 | <p>{message.deref().to_string()}</p> 648 | </> 649 | } 650 | } 651 | ``` 652 | 653 | Cơ chế hoạt động state của yew mình cảm thấy hơi đặc biệt mặc dù clone ra 1 state khác nhưng nó vẫn có mối liên hệ với nhau 654 | 655 | ## Tổng kết 656 | 657 | Trong bài viết này ta đã: 658 | 659 | - Hiểu về WebAssembly 660 | - Biết cách sử dụng event, callback, state bên Yew 661 | 662 | ## Github 663 | 664 | [https://github.com/Learning-Tech-Workspace/learn-rust-fe](https://github.com/Learning-Tech-Workspace/learn-rust-fe) 665 | 666 | Trong bài viết có sai sót gì mọi người cùng thảo luận góp ý nhé. 667 | 668 | Cảm ơn mọi người đã đọc. -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part3/README.md: -------------------------------------------------------------------------------- 1 | # Xây dựng real time chat app (P3) 2 | 3 | # Kiến thức đạt được sau bài viết này 4 | 5 | - Biết cơ bản về Error Handling 6 | - Upload file - Serve static file 7 | - Xây dựng middleware 8 | - WebSocket 9 | 10 | # Hướng dẫn 11 | 12 | ## Cài đặt các crate cần thiết 13 | 14 | `Cargo.toml` 15 | 16 | ```toml 17 | axum = {version = "0.7.5", features = ["multipart"]} 18 | socketioxide = {version = "0.13.1", features = ["tracing"]} 19 | tower = "0.4.13" 20 | tower-http = {version = "0.5.2", features=["cors", "fs", "trace"]} 21 | thiserror = "1.0.61" 22 | ``` 23 | 24 | - axum 25 | - multipart → upload file 26 | - thiserror → hỗ trợ Error Handling 27 | - tower → tạo các service (middleware) 28 | - tower-http: cung cấp các middleware thường sử dụng cho HTTP 29 | - cors 30 | - fs → serve static file 31 | - trace → log thông tin request đến 32 | - socketioxie: hỗ trợ socket bên phía server 33 | - tracing: in ra log 34 | 35 | ## Error handling 36 | 37 | Đây là cheatsheet tổng hợp về các function xử lý cho `Result` enum [rust-error-cheatsheet.md (github.com)](https://gist.github.com/e-t-u/70f25d4566468adc43a4df43667cedb6) 38 | 39 | Hoặc mọi người có thể xem tại 40 | 41 | - [Result in std::result - Rust (rust-lang.org)](https://doc.rust-lang.org/std/result/enum.Result.html) 42 | - [Option in std::option - Rust (rust-lang.org)](https://doc.rust-lang.org/std/option/enum.Option.html) 43 | 44 | Trong bài viết trước, như mình đã đề cập có những function mình `unwrap` tận 2 lần mà nếu giá trị là `None` đối với `Option` hoặc `Err` đối với `Result` thì sẽ bị `panic` (crash chương trình) 45 | 46 | Thông thường đối với unrecoverable error (lỗi không thể phục hồi) như thế này thì ta phải chuyển nó thành recoverable error (lỗi có thể phục hồi) hoặc trả lỗi về nhưng không làm crash chương trình 47 | 48 | Có nhiều cách để xử lý lỗi như `match`,… `Nhược điểm` khi sử dụng match để xử lý lỗi là sẽ có trường hợp nhiều match lồng nhau (match hell) 49 | 50 | ⇒ Có thể sử dụng các chain method được implement cho `Option` hoặc `Result` để xử lý lỗi 51 | 52 | - map_err: map Error type hiện tại trong Result sang một Error type khác 53 | - ok_or_else: map từ Option sang Result với Err type sẽ được xác định với giá trị trả về trong closure 54 | 55 | Mình thường chuyển Option về Result để xử lý lỗi. 56 | 57 | Mọi người tạo file `error.rs` ở thư mục gốc 58 | 59 | ```rust 60 | use axum::{ 61 | http::StatusCode, 62 | response::{IntoResponse, Response}, 63 | Json, 64 | }; 65 | use serde::Serialize; 66 | use thiserror::Error; 67 | 68 | pub type Result<T> = std::result::Result<T, Error>; 69 | 70 | #[derive(Debug, Error)] 71 | pub enum Error { 72 | // Environment variable errors 73 | #[error("Environment variable {0} not found")] 74 | EnvVarNotFound(String), 75 | 76 | // Database errors 77 | #[error("Database connection failed")] 78 | DatabaseConnectionFailed, 79 | #[error("Insert failed: {0}")] 80 | InsertFailed(#[source] sea_orm::error::DbErr), 81 | #[error("Query failed {0}")] 82 | QueryFailed(#[source] sea_orm::error::DbErr), 83 | #[error("Update failed: {0}")] 84 | UpdateFailed(#[source] sea_orm::error::DbErr), 85 | #[error("Record not found")] 86 | RecordNotFound, 87 | #[error("Delete failed: {0}")] 88 | DeleteFailed(#[source] sea_orm::error::DbErr), 89 | 90 | // File errors 91 | #[error("Create file failed")] 92 | CreateFileFailed, 93 | 94 | #[error("File type invalid")] 95 | FileTypeInvalid, 96 | 97 | // JWT errors 98 | #[error("JWT decode failed: {0}")] 99 | DecodeJwtFailed(String), 100 | 101 | // Auth errors 102 | #[error("Please login first")] 103 | TokenNotFound, 104 | } 105 | 106 | impl IntoResponse for Error { 107 | fn into_response(self) -> Response { 108 | #[derive(Serialize)] 109 | struct ErrorResp { 110 | status: String, 111 | message: String, 112 | } 113 | 114 | ( 115 | StatusCode::INTERNAL_SERVER_ERROR, 116 | Json(ErrorResp { 117 | status: StatusCode::INTERNAL_SERVER_ERROR.to_string(), 118 | message: self.to_string(), 119 | }), 120 | ) 121 | .into_response() 122 | } 123 | } 124 | 125 | ``` 126 | 127 | - `#[error()]` : implement trait Display ⇒ khi gọi to_string() thì sẽ in ra nội dung như khai báo trong macro 128 | - `#[source]` : mình định nghĩa high level error trong Error type của mình để end-user có thể hiểu và macro này sẽ xác định low level error khiến chương trình gặp lỗi 129 | 130 | ```rust 131 | pub type Result<T> = std::result::Result<T, Error>; 132 | ``` 133 | 134 | Đây là 1 best practice mà mình thấy mọi người thường sử dụng. Mình sẽ alias cho type `Result` của mình với generic type E của `Err` là `Error` do mình định nghĩa 135 | 136 | Sau đó mọi người sẽ implement trait IntoResponse cho Error do mình định nghĩa 137 | 138 | ```rust 139 | impl IntoResponse for Error { 140 | fn into_response(self) -> Response { 141 | #[derive(Serialize)] 142 | struct ErrorResp { 143 | status: String, 144 | message: String, 145 | } 146 | 147 | ( 148 | StatusCode::INTERNAL_SERVER_ERROR, 149 | Json(ErrorResp { 150 | status: StatusCode::INTERNAL_SERVER_ERROR.to_string(), 151 | message: self.to_string(), 152 | }), 153 | ) 154 | .into_response() 155 | } 156 | } 157 | ``` 158 | 159 | Sau đó mình sẽ chỉnh sửa lại `return type` cho các `handler function` sẽ return về `Result` của mình thì khi handler function trả về `Err(Error)` thì hàm `into_response` của Error sẽ được gọi và trả về client 160 | 161 | Nhắc lại về `?` operator 162 | 163 | - Giống unwrap nhưng bắt buộc function phải trả về `Result` 164 | - Nếu gặp lỗi thì sẽ return về `Err(Error)`(thoát khỏi function) cho caller 165 | 166 | Bây giờ mình sẽ xử lý lỗi ở các file trước đó mà mình chỉ sử dụng mỗi unwrap 167 | 168 | **Lưu ý**: nếu mọi người muốn xem file code hoàn chỉnh thì mọi người có thể vào repo để xem chi tiết nhé ở đây mình chỉ show những phần thay đổi liên quan đến error handling cho mọi người dễ theo dõi 169 | 170 | `main.rs` 171 | 172 | ```rust 173 | // ... 174 | 175 | mod error; 176 | 177 | use error::{Error, Result}; 178 | 179 | #[tokio::main] 180 | async fn main() -> Result<()> { 181 | // ... 182 | 183 | let opt = ConnectOptions::new( 184 | env::var("DATABASE_URL").map_err(|_| Error::EnvVarNotFound("DATABASE_URL".to_string()))?, 185 | ); 186 | 187 | let db_connection = Database::connect(opt) 188 | .await 189 | .map_err(|_| Error::DatabaseConnectionFailed)?; 190 | 191 | // ... 192 | 193 | Ok(()) 194 | } 195 | ``` 196 | 197 | `src/features/users/handler.rs` 198 | 199 | ```rust 200 | use crate::error::{Error, Result}; 201 | 202 | pub async fn create_user( 203 | // ... 204 | ) -> Result<impl IntoResponse> { 205 | // ... 206 | 207 | user_model 208 | .insert(&db_connection) 209 | .await 210 | .map_err(|e| Error::InsertFailed(e))?; 211 | 212 | Ok(( 213 | StatusCode::CREATED, 214 | Json(json!( 215 | { 216 | "message": "User created successfully" 217 | } 218 | )), 219 | )) 220 | } 221 | 222 | pub async fn get_user_by_id( 223 | // ... 224 | ) -> Result<impl IntoResponse> { 225 | let user = user::Entity::find() 226 | .filter(Condition::all().add(user::Column::Id.eq(id))) 227 | .one(&db_connection) 228 | .await 229 | .map_err(|e| Error::QueryFailed(e))? 230 | .ok_or_else(|| Error::RecordNotFound)?; 231 | 232 | // ... 233 | 234 | Ok((StatusCode::OK, Json(result))) 235 | } 236 | 237 | pub async fn update_user( 238 | // ... 239 | ) -> Result<impl IntoResponse> { 240 | let mut user: user::ActiveModel = user::Entity::find() 241 | .filter(Condition::all().add(user::Column::Id.eq(id))) 242 | .one(&db_connection) 243 | .await 244 | .map_err(|e| Error::QueryFailed(e))? 245 | .ok_or_else(|| Error::RecordNotFound)? 246 | .into(); 247 | 248 | // ... 249 | 250 | user.update(&db_connection) 251 | .await 252 | .map_err(|e| Error::UpdateFailed(e))?; 253 | 254 | Ok(( 255 | StatusCode::ACCEPTED, 256 | Json(json!( 257 | { 258 | "message": "User updated successfully" 259 | } 260 | )), 261 | )) 262 | } 263 | 264 | pub async fn delete_user( 265 | // ... 266 | ) -> Result<impl IntoResponse> { 267 | let user = user::Entity::find() 268 | .filter(Condition::all().add(user::Column::Id.eq(id))) 269 | .one(&db_connection) 270 | .await 271 | .map_err(|e| Error::QueryFailed(e))? 272 | .ok_or_else(|| Error::RecordNotFound)?; 273 | 274 | user::Entity::delete_by_id(user.id) 275 | .exec(&db_connection) 276 | .await 277 | .map_err(|e| Error::DeleteFailed(e))?; 278 | 279 | Ok(( 280 | StatusCode::NO_CONTENT, 281 | Json(json!( 282 | { 283 | "message": "User deleted successfully" 284 | } 285 | )), 286 | )) 287 | } 288 | 289 | pub async fn get_all_users( 290 | // . 291 | ) -> Result<impl IntoResponse> { 292 | let users: Vec<UserDTO> = user::Entity::find() 293 | .all(&db_connection) 294 | .await 295 | .map_err(|e| Error::QueryFailed(e))? 296 | .into_iter() 297 | .map(|user| UserDTO { 298 | id: user.id, 299 | name: user.name, 300 | email: user.email, 301 | avatar: user.avatar, 302 | is_online: user.is_online, 303 | }) 304 | .collect(); 305 | 306 | Ok((StatusCode::OK, Json(users))) 307 | } 308 | ``` 309 | 310 | `src/features/group/handler.rs` 311 | 312 | ```rust 313 | use crate::error::{Error, Result}; 314 | 315 | pub async fn create_group( 316 | // ... 317 | ) -> Result<impl IntoResponse> { 318 | // ... 319 | let new_group = group_model 320 | .insert(&db_connection) 321 | .await 322 | .map_err(|e| Error::InsertFailed(e))?; 323 | 324 | // ... 325 | 326 | user_group::Entity::insert_many(records) 327 | .exec(&db_connection) 328 | .await 329 | .map_err(|e| Error::InsertFailed(e))?; 330 | 331 | Ok(( 332 | StatusCode::CREATED, 333 | Json(json!( 334 | { 335 | "message": "Group created successfully" 336 | } 337 | )), 338 | )) 339 | } 340 | 341 | pub async fn get_group_by_id( 342 | // ... 343 | ) -> Result<impl IntoResponse> { 344 | let group = group::Entity::find() 345 | .filter(Condition::all().add(group::Column::Id.eq(id))) 346 | .one(&db_connection) 347 | .await 348 | .map_err(|e| Error::QueryFailed(e))? 349 | .ok_or_else(|| Error::RecordNotFound)?; 350 | 351 | let user_ids: Vec<Uuid> = user_group::Entity::find() 352 | .filter(Condition::all().add(user_group::Column::GroupId.eq(group.id))) 353 | .all(&db_connection) 354 | .await 355 | .map_err(|e| Error::QueryFailed(e))? 356 | .into_iter() 357 | .map(|user_group_model| user_group_model.user_id) 358 | .collect(); 359 | 360 | let mut users: Vec<UserDTO> = vec![]; 361 | for user_id in user_ids.into_iter() { 362 | let user = user::Entity::find() 363 | .filter(Condition::all().add(user::Column::Id.eq(user_id))) 364 | .one(&db_connection) 365 | .await 366 | .map_err(|e| Error::QueryFailed(e))? 367 | .ok_or_else(|| Error::RecordNotFound)?; 368 | // ... 369 | } 370 | 371 | // ... 372 | 373 | Ok((StatusCode::OK, Json(result))) 374 | } 375 | ``` 376 | 377 | `src/features/chat/handler.rs` 378 | 379 | ```rust 380 | use crate::error::{Error, Result} 381 | 382 | pub async fn chat( 383 | // ... 384 | ) -> Result<impl IntoResponse> { 385 | // ... 386 | 387 | let message = message_model 388 | .insert(&db_connection) 389 | .await 390 | .map_err(|e| Error::InsertFailed(e))?; 391 | 392 | // ... 393 | 394 | conversation_model 395 | .insert(&db_connection) 396 | .await 397 | .map_err(|e| Error::InsertFailed(e))?; 398 | 399 | Ok(( 400 | StatusCode::CREATED, 401 | Json(json!( 402 | { 403 | "message": "Chat created successfully" 404 | } 405 | )), 406 | )) 407 | } 408 | ``` 409 | 410 | Mình chưa xử lý lỗi cho feature auth vì mình cẩn tổ chức lại code 1 tí ở phần sau `Middleware` 411 | 412 | ## Middleware 413 | 414 | Trước khi xây dựng middleware để check trong request có token gửi lên cùng hay không để xác minh danh tính của người gửi request thì mình muốn tách cái phần logic liên quan đến JWT ra 1 file riêng được đặt trong thư mục `utils` 415 | 416 | Mọi người tạo folder `utils` ở thư mục gốc và tạo file `jwt.rs`. Sau đó mọi người chuyển các phần code liên quan đến JWT trước đó qua file này 417 | 418 | **Mục đích:** tách biệt phần xử lý logic cho feature auth và cho JWT riêng 419 | 420 | Mình sẽ lưu secret key ở `.env` 421 | 422 | ```bash 423 | JWT_SECRET=my_secret 424 | ``` 425 | 426 | `src/utils/jwt.rs` 427 | 428 | ```rust 429 | use std::env; 430 | 431 | use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; 432 | use serde::{Deserialize, Serialize}; 433 | use uuid::Uuid; 434 | 435 | use crate::error::{Error, Result}; 436 | 437 | pub fn encode_jwt(user_id: Uuid) -> Result<String> { 438 | let claims = Claims { 439 | sub: String::from("Login Token"), 440 | exp: (chrono::Utc::now() + chrono::Duration::days(1)).timestamp() as usize, 441 | user_id, 442 | }; 443 | 444 | let secret = 445 | env::var("JWT_SECRET").map_err(|_| Error::EnvVarNotFound("JWT_SECRET".to_string()))?; 446 | 447 | let token = encode( 448 | &Header::default(), 449 | &claims, 450 | &EncodingKey::from_secret(secret.as_ref()), 451 | ) 452 | .unwrap(); 453 | 454 | Ok(token) 455 | } 456 | 457 | pub fn decode_jwt(token: String) -> Result<Uuid> { 458 | let secret = 459 | env::var("JWT_SECRET").map_err(|_| Error::EnvVarNotFound("JWT_SECRET".to_string()))?; 460 | 461 | let Claims { 462 | user_id, 463 | sub: _, 464 | exp: _, 465 | } = decode::<Claims>( 466 | &token, 467 | &DecodingKey::from_secret(secret.as_ref()), 468 | &Validation::default(), 469 | ) 470 | .map_err(|e| Error::DecodeJwtFailed(e.to_string()))? 471 | .claims; 472 | 473 | Ok(user_id) 474 | } 475 | 476 | #[derive(Serialize, Deserialize)] 477 | pub struct Claims { 478 | pub sub: String, // the subject of the token 479 | pub exp: usize, // the expiry time 480 | pub user_id: Uuid, 481 | } 482 | ``` 483 | 484 | Ở function `decode_jwt` mình chuyển từ `match` sang `map_err` để xử lý lỗi vì `match` làm code dài dòng 485 | 486 | Sau đó mình sửa lại function `login` và xóa đi function `verify` (trước sử dụng để test decode token) 487 | 488 | `src/features/auth/handler.rs` 489 | 490 | ```rust 491 | use axum::{http::StatusCode, response::IntoResponse, Extension, Json}; 492 | 493 | use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; 494 | 495 | use entity::user; 496 | 497 | use crate::{ 498 | error::{Error, Result}, 499 | utils::jwt, 500 | }; 501 | 502 | use super::model::{LoginRequest, LoginResponse}; 503 | 504 | pub async fn login( 505 | Extension(db_connection): Extension<DatabaseConnection>, 506 | Json(payload): Json<LoginRequest>, 507 | ) -> Result<impl IntoResponse> { 508 | let LoginRequest { email, password } = payload; 509 | 510 | let user = user::Entity::find() 511 | .filter(user::Column::Email.eq(email)) 512 | .filter(user::Column::Password.eq(password)) 513 | .one(&db_connection) 514 | .await 515 | .map_err(|e| Error::QueryFailed(e))? 516 | .ok_or(Error::RecordNotFound)?; 517 | 518 | let token = jwt::encode_jwt(user.id)?; 519 | 520 | let resp = LoginResponse { 521 | msg: String::from("Login Successfully!"), 522 | token: token, 523 | }; 524 | 525 | Ok((StatusCode::CREATED, Json(resp))) 526 | } 527 | ``` 528 | 529 | Trước khi chat thì mình phải đăng nhập để server định danh được đây là ai và để kiểm tra xem người dùng đã đăng nhập chưa thì mình sẽ định nghĩa 1 middleware kiểm tra request này có `header authentication` gửi lên cùng token không? 530 | 531 | `src/features/auth/middleware.rs` 532 | 533 | ```rust 534 | use axum::{ 535 | extract::Request, 536 | http::{header::AUTHORIZATION, HeaderMap}, 537 | middleware::Next, 538 | response::IntoResponse, 539 | }; 540 | use sea_orm::{DatabaseConnection, EntityTrait}; 541 | 542 | use crate::{ 543 | error::{Error, Result}, 544 | utils::jwt::decode_jwt, 545 | }; 546 | 547 | use entity::user; 548 | 549 | pub async fn check_login( 550 | headers: HeaderMap, 551 | mut req: Request, 552 | next: Next, 553 | ) -> Result<impl IntoResponse> { 554 | let token = headers 555 | .get(AUTHORIZATION) 556 | .ok_or_else(|| Error::TokenNotFound)? 557 | .to_str() 558 | .or_else(|e| Err(Error::Unknown(e.to_string())))?; 559 | 560 | let token = token.replace("Bearer ", ""); 561 | 562 | let user_id = decode_jwt(token)?; 563 | 564 | let db_connection = req 565 | .extensions() 566 | .get::<DatabaseConnection>() 567 | .ok_or_else(|| Error::DatabaseConnectionFailed)?; 568 | 569 | let user = user::Entity::find_by_id(user_id) 570 | .one(db_connection) 571 | .await 572 | .map_err(|e| Error::QueryFailed(e))? 573 | .ok_or_else(|| Error::RecordNotFound)?; 574 | 575 | req.extensions_mut().insert(user.id); 576 | 577 | let res = next.run(req).await; 578 | 579 | Ok(res) 580 | } 581 | ``` 582 | 583 | **Lưu ý**: 584 | 585 | - function sử dụng làm middleware phải là async function 586 | - Thứ tự các tham số của middleware function 587 | - Các extractor (HeaderMap, Json,…) là các tham số đứng đầu 588 | - `Request` luôn là tham số đứng sau Next 589 | - `Next` là tham số cuối cùng 590 | 591 | ```rust 592 | let db_connection = req 593 | .extensions() 594 | .get::<DatabaseConnection>() 595 | .ok_or_else(|| Error::DatabaseConnectionFailed)?; 596 | ``` 597 | 598 | Hoặc có thể dùng `Extension` extractor để truy cập dữ liệu được chia sẻ giữa các handler, middleware 599 | 600 | ```rust 601 | req.extensions_mut().insert(user_id); 602 | ``` 603 | 604 | **Lưu ý**: `user_id` chỉ tồn tại trong 1 chu kì request/response, sau khi request được xử lý xong và response được gửi về client thì `req` object (bao gồm cả extension) sẽ bị xóa đi 605 | 606 | ```rust 607 | let res = next.run(req).await; 608 | ``` 609 | 610 | `Request` object tiếp tục đi đến các middleware tiếp theo trong middleware stack và handler function 611 | 612 | Bây giờ những router hoặc route nào cần middleware này thì thêm vào 613 | 614 | `src/features/chat/routes.rs` 615 | 616 | ```rust 617 | use axum::{middleware, routing::post, Router}; 618 | 619 | use crate::features::auth::middleware::check_login; 620 | 621 | use super::handler::chat; 622 | 623 | pub fn get_routes() -> Router { 624 | Router::new() 625 | .route("/", post(chat)) 626 | .layer(middleware::from_fn(check_login)) 627 | } 628 | ``` 629 | 630 | `src/features/group/routes.rs` 631 | 632 | ```rust 633 | use axum::{ 634 | middleware, 635 | routing::{get, post}, 636 | Router, 637 | }; 638 | 639 | use crate::features::auth::middleware::check_login; 640 | 641 | use super::handler::{create_group, get_group_by_id}; 642 | 643 | pub fn get_routes() -> Router { 644 | let protected_routes = Router::new() 645 | .route("/", post(create_group)) 646 | .layer(middleware::from_fn(check_login)); 647 | let public_routes = Router::new().route("/:id", get(get_group_by_id)); 648 | 649 | Router::new().merge(public_routes).merge(protected_routes) 650 | } 651 | ``` 652 | 653 | Mình sẽ tách làm 2 router 654 | 655 | - protected: cần phải đăng nhập thì mới truy cập được 656 | - public: truy cập thoải mái 657 | 658 | ## Upload file - Serve static file 659 | 660 | Mọi người tạo folder `public` ở thư mục gốc nhé. Đây là folder chứa các file upload và static file mà client có thể truy cập đến. Mọi người tạo folder con của `public` là `uploads` để lưu các file upload ở đây 661 | 662 | ### Upload file 663 | 664 | Mọi người thêm các crate sau vào `Cargo.toml` 665 | 666 | - `mime` chứa các constant về content type 667 | - `regex` để hỗ trợ lấy các chuỗi dựa trên pattern 668 | 669 | ```jsx 670 | mime = "0.3.17" 671 | regex = "1.10.4" 672 | ``` 673 | 674 | `src/features/users/handler.rs` 675 | 676 | ```rust 677 | pub async fn update_avatar( 678 | Extension(db_connection): Extension<DatabaseConnection>, 679 | Path(id): Path<Uuid>, 680 | mut multipart: Multipart, 681 | ) -> Result<impl IntoResponse> { 682 | while let Some(field) = multipart.next_field().await.unwrap() { 683 | let field_name = field.name().unwrap().to_string(); 684 | 685 | if field_name == "avatar" { 686 | let file_name = field.file_name().unwrap(); 687 | let content_type = field.content_type().unwrap(); 688 | 689 | let regex = Regex::new(mime::IMAGE_STAR.as_ref()).unwrap(); 690 | 691 | if regex.is_match(&content_type) { 692 | let mut user: user::ActiveModel = user::Entity::find() 693 | .filter(Condition::all().add(user::Column::Id.eq(id))) 694 | .one(&db_connection) 695 | .await 696 | .map_err(|e| Error::QueryFailed(e))? 697 | .ok_or_else(|| Error::RecordNotFound)? 698 | .into(); 699 | 700 | user.avatar = Set(Some(file_name.to_string())); 701 | 702 | let mut file = File::create(format!("./public/uploads/{file_name}")) 703 | .await 704 | .map_err(|_| Error::CreateFileFailed)?; 705 | let data = field.bytes().await.unwrap(); 706 | file.write(&data).await.unwrap(); 707 | 708 | user.update(&db_connection) 709 | .await 710 | .map_err(|e| Error::UpdateFailed(e))?; 711 | } else { 712 | return Err(Error::FileTypeInvalid); 713 | } 714 | } 715 | } 716 | 717 | Ok(( 718 | StatusCode::OK, 719 | Json(json!({ "message": "Avatar updated successfully" })), 720 | )) 721 | } 722 | ``` 723 | 724 | Multipart extractor có được do thêm feature `multipart` ở crate `axum` 725 | 726 | ```rust 727 | mut multipart: Multipart, 728 | ``` 729 | 730 | Tại sao multipart lại có `mut` như mọi người xem qua code thì đâu có chỗ nào mình thay đổi cho multipart? 731 | 732 | **Giải thích**: Trong struct Multipart có 1 field `inner` (có thể hiểu như con trỏ) để truy cập từng đoạn byte (block data) ứng với các field được gửi lên trong HTTP Request Body 733 | 734 | Ví dụ: Mình có form-data như sau 735 | 736 | - id: 123 737 | - name: Alex 738 | - avatar: img.png 739 | 740 | Tất cả các field đều có giá trị ở dạng stream (byte) ⇒ multipart sẽ chia thành từng đoạn stream ứng với từng field và có “con trỏ” để truy cập đến từng đoạn stream đó ⇒ multipart bị thay đổi (do “con trỏ” thay đổi) 741 | 742 | `while let` nếu gặp giá trị `None` thì sẽ break khỏi vòng lặp 743 | 744 | Mình sẽ sử dụng module `file` của crate `tokio` để chạy async còn module `file` của `std` là chạy sync nhé 745 | 746 | ### Serve static file 747 | 748 | `fallback_service` khi request không vào được bất kỳ route nào thì nó sẽ vào cái service (middleware) này 749 | 750 | `tower-http` cung cấp sẵn middleware để serve static file và mình chỉ cần truyền tên folder mà mình muốn public 751 | 752 | `main.rs` 753 | 754 | ```rust 755 | let app = create_router().layer(Extension(db_connection)) 756 | .fallback_service(ServeDir::new("public")); 757 | ``` 758 | 759 | # WebSocket 760 | 761 | - WebSocket cho phép giao tiếp song song hai chiều giữa client và server (giao tiếp hai chiều nghĩa là ở mô hình client-server thì client sẽ gửi request lên server rồi server response về dữ liệu tuy nhiên với WebSocket client hay server đều có thể gửi dữ liệu đến và response dữ liệu cho bên còn lại bằng cách phát ra sự kiện) 762 | - Kết nối giữa client và server sẽ tồn tại cho đến khi một trong hai bên chấm dứt kết nối. Sau khi đóng kết nối bởi một trong hai máy, kết nối sẽ bị chấm dứt cả hai đầu. 763 | 764 | ![Untitled](img/Untitled.png) 765 | 766 | Luồng hoạt động 767 | 768 | ![Untitled](img/Untitled%201.png) 769 | 770 | Mô tả luồng hoạt động chung cho các chức năng có sử dụng WebSocket: 771 | 772 | - Bước 1: Client phát ra sự kiện 773 | - Bước 2: Server lắng nghe được sự kiện này và gọi hàm xử lý sự kiện tương ứng 774 | - Bước 3: Nếu hàm xử lý cần tương tác với cơ sở dữ liệu thì tiến hành lấy, thêm, sửa, xóa dữ liệu 775 | - Bước 4: Database Server trả response về server (app) 776 | - Bước 5: Sau đó Server sẽ phát ra sự kiện để cho client có thể update lại 777 | - Bước 6: Client lắng nghe sự kiện được phát ra từ server và gọi hàm xử lý tương ứng 778 | 779 | Mình chỉ tập trung xử lý socket bên phía server ngoài ra crate `socketioxide` tương thích với socket.io client 780 | 781 | ```rust 782 | use std::env; 783 | 784 | use axum::{http::HeaderValue, Extension}; 785 | use dotenv::dotenv; 786 | 787 | pub mod enums; 788 | pub mod features; 789 | 790 | mod error; 791 | mod router; 792 | mod socket; 793 | mod utils; 794 | 795 | use router::create_router; 796 | use sea_orm::{ConnectOptions, Database}; 797 | use socketioxide::SocketIo; 798 | use tower::ServiceBuilder; 799 | use tower_http::trace::TraceLayer; 800 | use tower_http::{cors::CorsLayer, services::ServeDir}; 801 | 802 | use socket::on_connect; 803 | 804 | use error::{Error, Result}; 805 | 806 | #[tokio::main] 807 | async fn main() -> Result<()> { 808 | // ... 809 | 810 | let (layer, io) = SocketIo::new_layer(); 811 | 812 | io.ns("/", on_connect.with(check_login)); 813 | 814 | // build our application with a route 815 | let app = create_router() 816 | .fallback_service(ServeDir::new("public")) 817 | .layer( 818 | ServiceBuilder::new() 819 | .layer(TraceLayer::new_for_http()) 820 | .layer(CorsLayer::new().allow_origin("*".parse::<HeaderValue>().unwrap())) 821 | .layer(Extension(db_connection)) 822 | .layer(layer), 823 | ); 824 | 825 | // ... 826 | 827 | Ok(()) 828 | } 829 | 830 | ``` 831 | 832 | **Lưu ý**: phải đặt `fallback_service` lên trên `socket` layer nếu không thì request sẽ không thể truy cập vào socket server 833 | 834 | Thiết lập socket server 835 | 836 | - `SocketIoLayer`: middleware kiểm tra request gửi lên server có liên quan tới socket không, xong sẽ điều hướng các request này đến handler socket tương ứng 837 | - `SocketIo` xử lý toàn bộ socket.io context (phát sự kiện và gửi data về client, nhận sự kiện và data gửi lên từ client,….) 838 | 839 | Các middleware mới mà mình sử dụng 840 | 841 | - `TraceLayer`: log các thông tin liên quan đến request (`tower-http` cung cấp) 842 | - `CorsLayer`: cho phép client khác domain có thể truy cập vào resource của server thay vì lỗi cors do cơ chế của browser (`tower-http` cung cấp) 843 | 844 | Ở đây mình sử dụng `ServiceBuilder` để kết hợp các middleware layer thành 1 layer. Mọi người nên đặt các middleware trong đây thay vì đặt riêng lẻ bên ngoài 845 | 846 | Vì nếu không đặt các middleware trong `ServiceBuilder` thì thứ tự request đi qua các middleware sẽ là từ dưới lên trên như trong hình 847 | 848 | ```rust 849 | use axum::{routing::get, Router}; 850 | 851 | async fn handler() {} 852 | 853 | let app = Router::new() 854 | .route("/", get(handler)) 855 | .layer(layer_one) 856 | .layer(layer_two) 857 | .layer(layer_three); 858 | ``` 859 | 860 | ![Untitled](img/Untitled%202.png) 861 | 862 | Còn nếu sử dụng `ServiceBuilder` thì request sẽ đi qua middleware từ trên xuống dưới 863 | 864 | Nếu gặp lỗi thì nó sẽ return lỗi về luôn tại middleware đó mà không đi đến middleware layer kế tiếp 865 | 866 | Còn next thì sẽ đi đến middleware tiếp theo trong stack nếu còn, còn không sẽ đi đến handler function rồi handler function xử lý trả về response sau đó response sẽ đi qua các middleware function trước đó 867 | 868 | Mình tạo folder `socket` ở thư mục gốc. Sau đó định nghĩa function `on_connect` đây là function xử lý một socket client kết nối thành công đến server và quản lý việc phát sự kiện đến client, nhận sự kiện từ client,… 869 | 870 | `mod.rs` 871 | 872 | ```rust 873 | use socketioxide::extract::SocketRef; 874 | use tracing::info; 875 | 876 | use crate::socket::handler::{handle_join, handle_message}; 877 | 878 | pub mod handler; 879 | pub mod model; 880 | 881 | pub async fn on_connect(socket: SocketRef) { 882 | info!("socket connected {}", socket.id); 883 | 884 | socket.on("message", handle_message); 885 | socket.on("join", handle_join); 886 | socket.on_disconnect(handler_disconnect) 887 | } 888 | ``` 889 | 890 | Mình có thêm middleware check người dùng đã đăng nhập chưa trước khi socket connect đến server `io.ns("/", on_connect.with(check_login));` 891 | 892 | - Nếu kết quả trả về là `Ok(())`, middleware tiếp theo sẽ được gọi hoặc nếu không còn middleware nào nữa, socket sẽ được kết nối. 893 | - Nếu kết quả trả về là lỗi, kết nối namespace sẽ bị từ chối và lỗi sẽ được trả về. 894 | 895 | `src/socket/mod.rs` 896 | 897 | ```rust 898 | // middleware 899 | pub async fn check_login(socket: SocketRef) -> Result<()> { 900 | let token = socket 901 | .req_parts() 902 | .headers 903 | .get(AUTHORIZATION) 904 | .ok_or_else(|| Error::TokenNotFound)? 905 | .to_str() 906 | .or_else(|e| Err(Error::Unknown(e.to_string())))?; 907 | 908 | Ok(()) 909 | } 910 | ``` 911 | 912 | `src/socket/handler.rs` 913 | 914 | ```rust 915 | use sea_orm::DatabaseConnection; 916 | use socketioxide::{ 917 | extract::{Data, SocketRef}, 918 | socket::DisconnectReason, 919 | }; 920 | use tracing::info; 921 | 922 | use crate::{ 923 | error::Error, 924 | features::chat::{model::Chat, service::insert_chat}, 925 | socket::model::MessageOut, 926 | }; 927 | 928 | use super::model::JoinRoom; 929 | 930 | pub async fn handle_message(socket: SocketRef, Data(data): Data<Chat>) { 931 | let db_connection = socket 932 | .req_parts() 933 | .extensions 934 | .get::<DatabaseConnection>() 935 | .ok_or_else(|| Error::DatabaseConnectionFailed) 936 | .unwrap(); 937 | 938 | let _ = insert_chat(db_connection.to_owned(), data.clone()).await; 939 | 940 | let resp = MessageOut { 941 | content: data.content, 942 | user_id: data.user_id, 943 | group_id: data.group_id, 944 | created_at: chrono::Utc::now(), 945 | }; 946 | 947 | socket 948 | .within(data.group_id.to_string()) 949 | .emit("message-back", resp) 950 | .ok(); 951 | } 952 | 953 | pub fn handle_join(socket: SocketRef, Data(data): Data<JoinRoom>) { 954 | info!("Received join: {:?}", data); 955 | 956 | let _ = socket.leave_all(); // before joining a new room, leave all rooms 957 | let _ = socket.join(data.room.to_string()); 958 | 959 | socket 960 | .within(data.room.to_string()) 961 | .emit("join-room-back", data) 962 | .ok(); 963 | } 964 | 965 | pub async fn handler_disconnect(socket: SocketRef, reason: DisconnectReason) { 966 | println!("Socket {} was disconnected because {} ", socket.id, reason); 967 | } 968 | 969 | ``` 970 | 971 | - Các handler function trong socket có thể `aysnc` hoặc `sync` và có thể nhận từ `0` đến `16` tham số 972 | - Trong trường hợp deserialization gặp lỗi thì handler sẽ không được gọi nên mọi người sử dụng `TryData` extractor để có thể xử lý lỗi thay vì `Data` extractor. Trong trường hợp này mình sẽ sử dụng `Data` extractor cho nhanh nhé. 973 | 974 | Có 2 event mình cần xử lý khi client gửi lên server 975 | 976 | - join 977 | 978 | ![Untitled](img/Untitled%203.png) 979 | 980 | Các socket nằm trong cùng 1 room thì khi server emit event đến room đó thì chỉ các socket trong room đó bắt được sự kiện do server phát ra 981 | 982 | - message 983 | 984 | Mọi người nhận thấy phần logic cho chức năng chat mình đã định nghĩa trước đó có thể tái sử dụng ở đây nên mình sẽ tách phần logic đó ra 1 file riêng là `src/features/chat/service.rs` 985 | 986 | Mọi người truy cập vào file `src/features/chat/handler.rs` và lấy phần xử lý logic đưa vào file `src/features/chat/service.rs` 987 | 988 | ```rust 989 | use entity::sea_orm_active_enums::MessageEnum; 990 | use sea_orm::ActiveValue::Set; 991 | use sea_orm::{ActiveModelTrait, DatabaseConnection}; 992 | 993 | use entity::{conversation, message}; 994 | 995 | use crate::error::{Error, Result}; 996 | 997 | use super::model::{Chat, MessageType}; 998 | 999 | pub async fn insert_chat(db_connection: DatabaseConnection, payload: Chat) -> Result<()> { 1000 | let message_type = match payload.message_type { 1001 | MessageType::File => MessageEnum::File, 1002 | MessageType::Text => MessageEnum::Text, 1003 | MessageType::Image => MessageEnum::Image, 1004 | }; 1005 | 1006 | let message_model = message::ActiveModel { 1007 | user_id: Set(payload.user_id), 1008 | content: Set(payload.content), 1009 | r#type: Set(Some(message_type)), 1010 | ..Default::default() 1011 | }; 1012 | 1013 | let message = message_model 1014 | .insert(&db_connection) 1015 | .await 1016 | .map_err(|e| Error::InsertFailed(e))?; 1017 | 1018 | let conversation_model = conversation::ActiveModel { 1019 | group_id: Set(payload.group_id), 1020 | msg_id: Set(message.id), 1021 | }; 1022 | 1023 | conversation_model 1024 | .insert(&db_connection) 1025 | .await 1026 | .map_err(|e| Error::InsertFailed(e))?; 1027 | 1028 | Ok(()) 1029 | } 1030 | ``` 1031 | 1032 | Sau đó mình sửa lại file `src/features/chat/handler.rs` 1033 | 1034 | ```rust 1035 | use axum::{http::StatusCode, response::IntoResponse, Extension, Json}; 1036 | 1037 | use sea_orm::DatabaseConnection; 1038 | use serde_json::json; 1039 | 1040 | use crate::error::Result; 1041 | 1042 | use super::model::Chat; 1043 | use super::service::insert_chat; 1044 | 1045 | pub async fn chat( 1046 | Extension(db_connection): Extension<DatabaseConnection>, 1047 | Json(payload): Json<Chat>, 1048 | ) -> Result<impl IntoResponse> { 1049 | insert_chat(db_connection, payload).await?; 1050 | 1051 | Ok(( 1052 | StatusCode::CREATED, 1053 | Json(json!( 1054 | { 1055 | "message": "Chat created successfully" 1056 | } 1057 | )), 1058 | )) 1059 | } 1060 | 1061 | ``` 1062 | 1063 | Mình cần thêm macro `Clone` cho struct `Chat` để có thể gọi được `clone()` và `Debug` để có thể in ra và để thêm được các macro đó cho Chat thì ta cũng phải thêm các macro đó cho `MessageType` 1064 | 1065 | ```rust 1066 | use serde::Deserialize; 1067 | use uuid::Uuid; 1068 | 1069 | #[derive(Deserialize, Debug, Clone)] 1070 | pub struct Chat { 1071 | pub user_id: Uuid, 1072 | pub content: String, 1073 | pub message_type: MessageType, 1074 | pub group_id: Uuid, 1075 | } 1076 | 1077 | #[derive(Deserialize, Debug, Clone)] 1078 | pub enum MessageType { 1079 | File, 1080 | Text, 1081 | Image, 1082 | } 1083 | ``` 1084 | 1085 | `src/socket/model.rs` 1086 | 1087 | ```rust 1088 | use serde::{Deserialize, Serialize}; 1089 | use uuid::Uuid; 1090 | 1091 | // for FE to render the message 1092 | #[derive(Debug, Serialize)] 1093 | pub struct MessageOut { 1094 | pub content: String, 1095 | pub user_id: Uuid, 1096 | pub group_id: Uuid, 1097 | pub created_at: chrono::DateTime<chrono::Utc>, 1098 | } 1099 | 1100 | #[derive(Debug, Serialize, Deserialize)] 1101 | pub struct JoinRoom { 1102 | pub room: Uuid, // group_id 1103 | } 1104 | ``` 1105 | 1106 | Mọi người có thể sử dụng Postman để kiểm tra Socket 1107 | 1108 | ![Untitled](img/Untitled%204.png) 1109 | 1110 | **Lưu ý:** nhớ set `JSON` để gửi data nhé không sẽ bị lỗi mà nó không hiển thị lỗi 1111 | 1112 | ## Github 1113 | 1114 | Mọi người có thể xem source code hoàn chỉnh ở đây nhé. 1115 | 1116 | [https://github.com/Learning-Tech-Workspace/learn-rust-backend](https://github.com/Learning-Tech-Workspace/learn-rust-backend) 1117 | 1118 | ## Postman 1119 | 1120 | [https://www.postman.com/navigation-astronaut-22006281/workspace/rust](https://www.postman.com/navigation-astronaut-22006281/workspace/rust) 1121 | 1122 | ## Tổng kết 1123 | 1124 | Trong bài viết này ta đã: 1125 | 1126 | - Xử lý lỗi 1127 | - Upload file - Serve static file 1128 | - Xây dựng middleware kiểm tra người dùng đã đăng nhập chưa 1129 | - WebSocket 1130 | 1131 | Đây là phần cuối cùng trong serise bài viết xây dựng ứng dụng realtime chat app với Rust nhé. Mình chủ yếu giới thiệu về mặt kỹ thuật nên các chức năng chưa hoàn chỉnh, mọi người có thể phát triển lên tiếp theo ý muốn của bản thân nhé. Cảm ơn mọi người đã theo dõi series. Hẹn mọi người ở những bài viết tiếp theo ^^ 1132 | 1133 | Mình chưa có quá nhiều kinh nghiệm với Rust trong việc xây dựng Backend. Trong bài viết có sai sót gì mọi người cùng thảo luận góp ý nhé. 1134 | 1135 | Cảm ơn mọi người đã đọc. -------------------------------------------------------------------------------- /rust-challenges/05-realtime-chatapp/part2/README.md: -------------------------------------------------------------------------------- 1 | # Xây dựng real time chat app (P2) 2 | 3 | Xin chào mọi người, trong bài viết lần trước thì mình đã làm được 4 | 5 | - Setup webserver sử dụng Axum 6 | - Tổ chức source code theo hướng feature 7 | - Cài đặt thành công chức năng đăng nhập với JWT và tạo mới user 8 | 9 | Trong bài này, mình sẽ hướng dẫn mọi người: 10 | 11 | - Thiết lập cơ sở dữ liệu với Docker 12 | - Tương tác với cơ sở dữ liệu sử dụng ORM 13 | 14 | # Kiến thức yêu cầu 15 | 16 | - Biết cơ bản về Docker (Docker Compose,…) 17 | - Biết cơ bản về SQL 18 | 19 | # Kiến thức đạt được sau bài viết này 20 | 21 | - Thiết kế database 22 | - Biết sử dụng sea-orm 23 | - Biết thêm một vài khái niệm mới của framework Axum như shared state giữa các handler function,… 24 | 25 | # Hướng dẫn 26 | 27 | ## Chạy Docker 28 | 29 | Trong bài viết này mình sẽ không nói quá chi tiết về Docker. Nếu mọi người chưa biết về Docker, mình recommend mọi người 2 video sau mình thấy minh họa khá trực quan và dễ hiểu: 30 | 31 | - [https://www.youtube.com/watch?v=pg19Z8LL06w](https://www.youtube.com/watch?v=pg19Z8LL06w) 32 | - [https://www.youtube.com/watch?v=SXwC9fSwct8](https://www.youtube.com/watch?v=SXwC9fSwct8) 33 | 34 | Mọi người có thể sẽ cần update version cho docker-compose. 35 | 36 | ```bash 37 | 38 | sudo apt-get update 39 | 40 | sudo apt-get install docker-compose-plugin 41 | 42 | ``` 43 | 44 | Mọi người chạy lệnh sau để kiểm tra version của docker compose nhé 45 | 46 | ```bash 47 | 48 | docker compose version 49 | 50 | ``` 51 | 52 | ![Untitled](img/Untitled15.png) 53 | 54 | Mọi người tạo file `docker-compose.yaml` ở thư mục gốc nhé 55 | 56 | ```rust 57 | version: "2.26.1" 58 | 59 | services: 60 | postgres-db: 61 | image: postgres 62 | ports: 63 | - 5432:5432 64 | environment: 65 | - POSTGRES_PASSWORD=admin 66 | - POSTGRES_USER=admin 67 | volumes: 68 | - postgres-db-data:/var/lib/postgresql/data 69 | 70 | pgadmin4: 71 | image: dpage/pgadmin4 72 | ports: 73 | - 5050:80 74 | environment: 75 | - PGADMIN_DEFAULT_EMAIL=admin@gmail.com 76 | - PGADMIN_DEFAULT_PASSWORD=admin 77 | depends_on: 78 | - "postgres-db" 79 | 80 | volumes: 81 | postgres-db-data: 82 | ``` 83 | 84 | Mình chạy 2 service 85 | 86 | - postgres-db: database server - port 5432 87 | - pgadmin4: UI quản lý postgres db - port 5050 88 | 89 | Để chạy các service ta dùng lệnh sau 90 | 91 | ```bash 92 | docker-compose -f docker-compose.yaml up 93 | ``` 94 | 95 | Sau khi chạy service nếu log của terminal hiển thị như vậy tức là các service đã chạy thành công 96 | 97 | ![Untitled](img/Untitled.png) 98 | 99 | Ta truy cập địa chỉ [`http://localhost:5050/`](http://localhost:5050/) và nhập các trường dữ liệu sau 100 | 101 | ``` 102 | Email: admin@gmail.com 103 | Password: admin 104 | ``` 105 | 106 | ![Untitled](img/Untitled%201.png) 107 | 108 | Sau đó ta nhấn vào `Add New Server` thì có 1 cửa sổ popup hiện lên 109 | 110 | ![Untitled](img/Untitled%202.png) 111 | 112 | Ta nhập `Name` (tên nào cũng được nhé). 113 | 114 | Sau đó qua tab `Connection` 115 | 116 | ![Untitled](img/Untitled%203.png) 117 | 118 | Ở tab này bạn phải nhập các trường dữ liệu sau: 119 | 120 | - Host name/address: postgres-db (service name trong docker-compose.yaml) 121 | - Username: admin (set trong docker-compose.yaml) 122 | - Password: admin (set trong docker-compose.yaml) 123 | 124 | Kết quả sau khi nhấn `Save` 125 | 126 | ![Untitled](img/Untitled%204.png) 127 | 128 | Tiếp theo ta tiến hành tạo database mới mà ta sẽ sử dụng cho project 129 | 130 | Nhấn chuột phải vào icon Databases → Create → Database…. 131 | 132 | ![Untitled](img/Untitled%205.png) 133 | 134 | Kết quả sau khi nhấn `Save` 135 | 136 | ![Untitled](img/Untitled%206.png) 137 | 138 | ## Thiết kế database 139 | 140 | [https://dbdiagram.io/](https://dbdiagram.io/): Đây là trang web hỗ trợ vẽ diagram khá tốt mà mình muốn recommend cho mọi người 141 | 142 | ![Untitled](img/Untitled%207.png) 143 | 144 | **Update**: Sau khi đọc lại bài viết thì mình thấy lược đồ cơ sở dữ liệu có vài chỗ chưa được hợp lý lắm. Đó là bảng Conversation vì 1 Group thì có N Message nhưng 1 Message thì chỉ thuộc về 1 Group mà thôi cho nên không cần bảng Conversation. 145 | 146 | ## Cài đặt các crate cần thiết 147 | 148 | Ta thêm các dòng sau vào `Cargo.toml` 149 | 150 | ```toml 151 | dotenv = "0.15.0" 152 | sea-orm = {version = "0.12.15", features = [ "sqlx-postgres", "runtime-tokio-native-tls", "macros", "debug-print" ]} 153 | ``` 154 | 155 | - dotenv: hỗ trợ load file `.env` 156 | - sea-orm 157 | 158 | ## SeaORM 159 | 160 | ### Giới thiệu 161 | 162 | sea-orm là 1 thư viện hỗ trợ ánh xạ từ code Rust sang code SQL 163 | 164 | **Table** map với object (hay là **Struct** bên Rust) 165 | 166 | **Column** được map với các **attribute** của struct 167 | 168 | Các khái niệm cơ bản 169 | 170 | - **Schema:** database (tập hợp các table) 171 | - **Entity:** table (support CRUD) 172 | - **EntityTrait:** cung cấp API để truy cập các thuộc tính của Entity (**Column**, **Relation**, **PrimaryKey**) 173 | - **Model**: struct lưu trữ tất cả thuộc tính và giá trị của chúng, read only 174 | - **ActiveModel**: insert, update, delete 175 | - **Migration**: quản lý các phiên bản database 176 | - MigrationTable: bảng quản lý việc thay đổi các version của db 177 | 178 | ### Connection and shared state 179 | 180 | `main.rs` 181 | 182 | ```rust 183 | pub mod enums; 184 | pub mod features; 185 | 186 | mod router; 187 | use std::env; 188 | 189 | use axum::Extension; 190 | use dotenv::dotenv; 191 | use router::create_router; 192 | use sea_orm::{ConnectOptions, Database}; 193 | 194 | #[tokio::main] 195 | async fn main() { 196 | // ... 197 | 198 | tracing_subscriber::fmt() 199 | .with_max_level(tracing::Level::DEBUG) 200 | .with_test_writer() 201 | .init(); 202 | 203 | dotenv().ok(); 204 | 205 | let mut opt = ConnectOptions::new(env::var("DATABASE_URL").unwrap()); 206 | 207 | let db_connection = match Database::connect(opt).await { 208 | Ok(conn) => conn, 209 | Err(e) => panic!("{}", format!("Database connection failed: {:?}", e)), 210 | }; 211 | 212 | // build our application with a route 213 | let app = create_router().layer(Extension(db_connection)); 214 | 215 | // ... 216 | } 217 | 218 | ``` 219 | 220 | - Setup `tracing_subscriber` để bắt được và hiển thị debug log 221 | - `dotenv().ok()` : load file `.env` 222 | - Để các handler function khác có thể truy cập biến db_connection, ta thêm middleware sau vào root router 223 | `.layer(Extension(db_connection))`: Extension là middleware thêm thông tin (biến db_connection) vào các request 224 | 225 | Các bạn tạo file`.env` ở thư mục gốc 226 | 227 | ```bash 228 | DATABASE_URL=postgres://admin:admin@localhost:5432/chat-app 229 | ``` 230 | 231 | ## SeaORM - Migration 232 | 233 | ### Tạo thư mục Migration 234 | 235 | [Setting Up Migration | SeaORM 🐚 An async & dynamic ORM for Rust (sea-ql.org)](https://www.sea-ql.org/SeaORM/docs/migration/setting-up-migration/#creating-migration-directory) 236 | 237 | Mọi người làm theo hướng dẫn phía trên phần `Creating Migration Directory` thôi nha. 238 | 239 | Để có thể chạy được migration thông qua `sea-orm-cli`, mọi người truy cập file `Cargo.toml` của folder `migration` và uncomment các dòng sau 240 | 241 | ```toml 242 | 243 | features = [ 244 | 245 | # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. 246 | 247 | # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. 248 | 249 | # e.g. 250 | 251 | "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature 252 | 253 | "sqlx-postgres", # `DATABASE_DRIVER` feature 254 | 255 | ] 256 | 257 | ``` 258 | 259 | Sau đó mọi người truy cập vào file `Cargo.toml` trong thư mục gốc và thêm dòng sau 260 | 261 | ```toml 262 | 263 | [workspace] 264 | 265 | members = [".", "migration"] 266 | 267 | ``` 268 | 269 | Workspace sử dụng để quản lý nhiều crate trong dự án trong trường hợp này gồm root crate (src) và crate migration 270 | 271 | Nếu không khai báo workspace thì Rust Analyzer sẽ không nhận diện được tất cả crate có trong project ⇒ Không báo lỗi,… 272 | 273 | Sau khi làm xong mọi người truy cập vào file `migration/src/m20220101_000001_create_table.rs` 274 | 275 | ```rust 276 | use sea_orm_migration::prelude::*; 277 | 278 | #[derive(DeriveMigrationName)] 279 | pub struct Migration; 280 | 281 | #[async_trait::async_trait] 282 | impl MigrationTrait for Migration { 283 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 284 | // Replace the sample below with your own migration scripts 285 | // todo!(); 286 | 287 | manager 288 | .create_table( 289 | Table::create() 290 | .table(Post::Table) 291 | .if_not_exists() 292 | .col( 293 | ColumnDef::new(Post::Id) 294 | .integer() 295 | .not_null() 296 | .auto_increment() 297 | .primary_key(), 298 | ) 299 | .col(ColumnDef::new(Post::Title).string().not_null()) 300 | .col(ColumnDef::new(Post::Text).string().not_null()) 301 | .to_owned(), 302 | ) 303 | .await 304 | } 305 | 306 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 307 | // Replace the sample below with your own migration scripts 308 | // todo!(); 309 | 310 | manager 311 | .drop_table(Table::drop().table(Post::Table).to_owned()) 312 | .await 313 | } 314 | } 315 | 316 | #[derive(DeriveIden)] 317 | enum Post { 318 | Table, 319 | Id, 320 | Title, 321 | Text, 322 | } 323 | ``` 324 | 325 | Mình sẽ giải thích qua một vài thành phần quan trọng 326 | 327 | - `enum Post`: định nghĩa table gồm các attribute nào (mỗi attribute là variant của enum) 328 | - `async fn up()`: tạo table trong database 329 | - Định nghĩa các Column dựa trên các attribute trong enum Post 330 | - Thiết lập kiểu dữ liệu 331 | - Thiết lập khóa chính (primary key), khóa ngoại (foreign key) 332 | - Thiết lập các ràng buộc (constraint) 333 | - `async fn down()`: xóa đi table trong database 334 | 335 | Mọi người nhớ xóa hoặc comment dòng lệnh `todo!()` để có thể chạy migration nhé. 336 | 337 | `migration/src/lib.rs` 338 | 339 | ```rust 340 | pub use sea_orm_migration::prelude::*; 341 | 342 | mod m20220101_000001_create_table; 343 | 344 | pub struct Migrator; 345 | 346 | #[async_trait::async_trait] 347 | impl MigratorTrait for Migrator { 348 | fn migrations() -> Vec<Box<dyn MigrationTrait>> { 349 | vec![Box::new(m20220101_000001_create_table::Migration)] 350 | } 351 | } 352 | 353 | ``` 354 | 355 | Khi chạy các lệnh liên quan đến migrate của `sea-orm-cli` thì sẽ gọi function này. 356 | 357 | Sau đó mọi người chạy lệnh sau 358 | 359 | ```bash 360 | sea-orm-cli migrate up 361 | # hoặc 362 | sea-orm-cli migrate fresh 363 | ``` 364 | 365 | ![Untitled](img/Untitled%208.png) 366 | 367 | Như vậy là chạy thành công nhé. Lúc này cơ sở dữ liệu của mọi người sẽ tạo các table được định nghĩa trong các file migration 368 | 369 | Mọi người vào UI quản lý PostgreSQL để kiểm tra nhé 370 | 371 | ![Untitled](img/Untitled%209.png) 372 | 373 | Ngoài table `post` được định nghĩa trong file migration thì có 1 table `seaql_migrations` được tạo ra (đây là MigrationTable mình có đề cập ở phần giới thiệu) 374 | 375 | ![Untitled](img/Untitled%2010.png) 376 | 377 | Nếu mình chạy `sea-orm-cli migrate up` một lần nữa thì kết quả sẽ như thế nào? 378 | 379 | ![Untitled](img/Untitled%2011.png) 380 | 381 | Không có gì xảy ra vì lúc này trong table `seaql_migrations` đã lưu record rằng file migration này đã được áp dụng cho cơ sở dữ liệu 382 | 383 | Còn nếu chạy `sea-orm-cli migrate fresh` ? 384 | 385 | ![Untitled](img/Untitled%2012.png) 386 | 387 | Như mọi người thấy thì nó sẽ xóa tất cả các table trong cơ sở dữ liệu và tiến hành chạy lại các file migration (gọi function up của mỗi file migration) 388 | 389 | ### Define Migration 390 | 391 | Trong phần thiết kế database, ta có 6 tables ⇒ Ta cần tạo 6 file migration 392 | 393 | ```bash 394 | sea-orm-cli migrate generate "create user" 395 | sea-orm-cli migrate generate "create group" 396 | sea-orm-cli migrate generate "create message" 397 | sea-orm-cli migrate generate "create file msg" 398 | sea-orm-cli migrate generate "create user group" 399 | sea-orm-cli migrate generate "create conversation" 400 | ``` 401 | 402 | Mọi người nên định nghĩa table theo thứ tự table độc lập (không phụ thuộc vào table nào) rồi đến table phụ thuộc vào table khác (khóa ngoại) 403 | 404 | Mình sẽ định nghĩa Table liên quan tới `User` trước 405 | 406 | ```rust 407 | use sea_orm_migration::prelude::*; 408 | 409 | #[derive(DeriveMigrationName)] 410 | pub struct Migration; 411 | 412 | #[async_trait::async_trait] 413 | impl MigrationTrait for Migration { 414 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 415 | manager 416 | .create_table( 417 | Table::create() 418 | .table(User::Table) 419 | .if_not_exists() 420 | .col( 421 | ColumnDef::new(User::Id) 422 | .uuid() 423 | .not_null() 424 | .primary_key() 425 | .default(Expr::cust("uuid_generate_v4()")), 426 | ) 427 | .col(ColumnDef::new(User::Name).string().not_null()) 428 | .col(ColumnDef::new(User::Email).string().not_null()) 429 | .col(ColumnDef::new(User::Password).string().not_null()) 430 | .col(ColumnDef::new(User::Avatar).string()) 431 | .col(ColumnDef::new(User::IsOnline).boolean().not_null()) 432 | .to_owned(), 433 | ) 434 | .await 435 | } 436 | 437 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 438 | manager 439 | .drop_table(Table::drop().table(User::Table).to_owned()) 440 | .await 441 | } 442 | } 443 | 444 | #[derive(DeriveIden)] 445 | pub enum User { 446 | Table, 447 | Id, 448 | Name, 449 | Email, 450 | Password, 451 | Avatar, 452 | IsOnline, 453 | } 454 | ``` 455 | 456 | Đoạn code này rõ ràng rồi ha. Riêng với Column id mình có thiết lập default value để tự động tạo uuid khi mình tạo mới user 457 | 458 | **_Lưu ý_**: chỉ dành cho **Postgres** 459 | 460 | Nếu bạn muốn set default như trên thì bạn phải create extension `uuid-ossp` để có thể sử dụng function `uuid_generate_v4()` ⇒ Mình tạo thêm 1 file migration 461 | 462 | ```rust 463 | sea-orm-cli migrate generate "create uuid extension" 464 | ``` 465 | 466 | ```rust 467 | use sea_orm_migration::prelude::*; 468 | 469 | #[derive(DeriveMigrationName)] 470 | pub struct Migration; 471 | 472 | #[async_trait::async_trait] 473 | impl MigrationTrait for Migration { 474 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 475 | let db = manager.get_connection(); 476 | 477 | db.execute_unprepared( 478 | "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"", 479 | ) 480 | .await?; 481 | 482 | Ok(()) 483 | } 484 | 485 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 486 | let db = manager.get_connection(); 487 | 488 | // Use `execute_unprepared` if the SQL statement doesn't have value bindings 489 | db.execute_unprepared( 490 | "DROP EXTENSION IF EXISTS \"uuid-ossp\"", 491 | ) 492 | .await?; 493 | 494 | Ok(()) 495 | } 496 | } 497 | ``` 498 | 499 | Mình sẽ định nghĩa hết các table còn lại 500 | 501 | `Group` 502 | 503 | ```rust 504 | use sea_orm_migration::prelude::*; 505 | 506 | #[derive(DeriveMigrationName)] 507 | pub struct Migration; 508 | 509 | #[async_trait::async_trait] 510 | impl MigrationTrait for Migration { 511 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 512 | manager 513 | .create_table( 514 | Table::create() 515 | .table(Group::Table) 516 | .if_not_exists() 517 | .col( 518 | ColumnDef::new(Group::Id) 519 | .uuid() 520 | .not_null() 521 | .primary_key() 522 | .default(Expr::cust("uuid_generate_v4()")), 523 | ) 524 | .col(ColumnDef::new(Group::Name).string().not_null()) 525 | .to_owned(), 526 | ) 527 | .await 528 | } 529 | 530 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 531 | manager 532 | .drop_table(Table::drop().table(Group::Table).to_owned()) 533 | .await 534 | } 535 | } 536 | 537 | #[derive(DeriveIden)] 538 | pub enum Group { 539 | Table, 540 | Id, 541 | Name, 542 | } 543 | 544 | ``` 545 | 546 | `Message` 547 | 548 | ```rust 549 | use sea_orm_migration::{ 550 | prelude::*, 551 | sea_orm::{EnumIter, Iterable}, 552 | sea_query::extension::postgres::Type, 553 | }; 554 | 555 | use crate::m20240520_104423_create_user::User; 556 | 557 | #[derive(DeriveMigrationName)] 558 | pub struct Migration; 559 | 560 | #[async_trait::async_trait] 561 | impl MigrationTrait for Migration { 562 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 563 | manager 564 | .create_type( 565 | Type::create() 566 | .as_enum(MessageEnum) 567 | .values(MessageVariants::iter()) 568 | .to_owned(), 569 | ) 570 | .await?; 571 | 572 | manager 573 | .create_table( 574 | Table::create() 575 | .table(Message::Table) 576 | .if_not_exists() 577 | .col( 578 | ColumnDef::new(Message::Id) 579 | .uuid() 580 | .not_null() 581 | .primary_key() 582 | .default(Expr::cust("uuid_generate_v4()")), 583 | ) 584 | .col(ColumnDef::new(Message::UserId).uuid().not_null()) 585 | .foreign_key( 586 | ForeignKey::create() 587 | .name("FK_message_user_userId") 588 | .from(Message::Table, Message::UserId) 589 | .to(User::Table, User::Id) 590 | .on_delete(ForeignKeyAction::Cascade) 591 | .on_update(ForeignKeyAction::Cascade), 592 | ) 593 | .col(ColumnDef::new(Message::Content).string().not_null()) 594 | .col( 595 | ColumnDef::new(Message::Type) 596 | .enumeration(MessageEnum, MessageVariants::iter()), 597 | ) 598 | .col( 599 | ColumnDef::new(Message::CreatedAt) 600 | .timestamp() 601 | .not_null() 602 | .default(SimpleExpr::Keyword(Keyword::CurrentTimestamp)), 603 | ) 604 | .to_owned(), 605 | ) 606 | .await 607 | } 608 | 609 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 610 | manager 611 | .drop_table(Table::drop().table(Message::Table).to_owned()) 612 | .await 613 | } 614 | } 615 | 616 | #[derive(DeriveIden)] 617 | pub enum Message { 618 | Table, 619 | Id, 620 | UserId, 621 | Content, 622 | Type, 623 | CreatedAt, 624 | } 625 | 626 | #[derive(DeriveIden)] 627 | struct MessageEnum; 628 | 629 | #[derive(Iden, EnumIter)] 630 | pub enum MessageVariants { 631 | #[iden = "File"] 632 | File, 633 | #[iden = "Image"] 634 | Image, 635 | #[iden = "Text"] 636 | Text, 637 | } 638 | ``` 639 | 640 | **Lưu ý:** 641 | 642 | - Chỉ PostgreSQL mới gọi được `create_type()` 643 | - Column làm khóa ngoại phải có cùng kiểu dữ liệu với Column làm khóa chính mà nó tham chiều tới 644 | - `.on_delete(ForeignKeyAction::Cascade)` cho phép bạn xóa record bên phía bảng 1 thì tất cả record bên phía bảng N sẽ bị xóa theo tương tự cho update 645 | 646 | `FileMsg` 647 | 648 | ```rust 649 | use sea_orm_migration::prelude::*; 650 | 651 | #[derive(DeriveMigrationName)] 652 | pub struct Migration; 653 | 654 | #[async_trait::async_trait] 655 | impl MigrationTrait for Migration { 656 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 657 | manager 658 | .create_table( 659 | Table::create() 660 | .table(FileMsg::Table) 661 | .if_not_exists() 662 | .col( 663 | ColumnDef::new(FileMsg::Id) 664 | .uuid() 665 | .not_null() 666 | .primary_key() 667 | .default(Expr::cust("uuid_generate_v4()")), 668 | ) 669 | .col(ColumnDef::new(FileMsg::MsgId).uuid().not_null()) 670 | .foreign_key( 671 | ForeignKey::create() 672 | .name("FK_fileMsg_msg_msgId") 673 | .from(FileMsg::Table, FileMsg::MsgId) 674 | .to(FileMsg::Table, FileMsg::Id) 675 | .on_delete(ForeignKeyAction::Cascade) 676 | .on_update(ForeignKeyAction::Cascade), 677 | ) 678 | .col(ColumnDef::new(FileMsg::FileName).string().not_null()) 679 | .to_owned(), 680 | ) 681 | .await 682 | } 683 | 684 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 685 | manager 686 | .drop_table(Table::drop().table(FileMsg::Table).to_owned()) 687 | .await 688 | } 689 | } 690 | 691 | #[derive(DeriveIden)] 692 | enum FileMsg { 693 | Table, 694 | Id, 695 | MsgId, 696 | FileName, 697 | } 698 | 699 | ``` 700 | 701 | `Conversation` 702 | 703 | ```rust 704 | use sea_orm_migration::prelude::*; 705 | 706 | use crate::{m20240520_104447_create_group::Group, m20240520_104508_create_message::Message}; 707 | 708 | #[derive(DeriveMigrationName)] 709 | pub struct Migration; 710 | 711 | #[async_trait::async_trait] 712 | impl MigrationTrait for Migration { 713 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 714 | manager 715 | .create_table( 716 | Table::create() 717 | .table(Conversation::Table) 718 | .if_not_exists() 719 | .col(ColumnDef::new(Conversation::GroupId).uuid().not_null()) 720 | .foreign_key( 721 | ForeignKey::create() 722 | .name("FK_conversation_group_groupId") 723 | .from(Conversation::Table, Conversation::GroupId) 724 | .to(Group::Table, Group::Id) 725 | .on_delete(ForeignKeyAction::Cascade) 726 | .on_update(ForeignKeyAction::Cascade), 727 | ) 728 | .col(ColumnDef::new(Conversation::MsgId).uuid().not_null()) 729 | .foreign_key( 730 | ForeignKey::create() 731 | .name("FK_conversation_message_msgId") 732 | .from(Conversation::Table, Conversation::MsgId) 733 | .to(Message::Table, Message::Id) 734 | .on_delete(ForeignKeyAction::Cascade) 735 | .on_update(ForeignKeyAction::Cascade), 736 | ) 737 | .primary_key( 738 | Index::create() 739 | .name("PK_conversation") 740 | .col(Conversation::GroupId) 741 | .col(Conversation::MsgId), 742 | ) 743 | .to_owned(), 744 | ) 745 | .await 746 | } 747 | 748 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 749 | manager 750 | .drop_table(Table::drop().table(Conversation::Table).to_owned()) 751 | .await 752 | } 753 | } 754 | 755 | #[derive(DeriveIden)] 756 | enum Conversation { 757 | Table, 758 | GroupId, 759 | MsgId, 760 | } 761 | ``` 762 | 763 | - Primary key gồm 2 column 764 | 765 | `UserGroup` 766 | 767 | ```rust 768 | use sea_orm_migration::prelude::*; 769 | 770 | use crate::{m20240520_104423_create_user::User, m20240520_104447_create_group::Group}; 771 | 772 | #[derive(DeriveMigrationName)] 773 | pub struct Migration; 774 | 775 | #[async_trait::async_trait] 776 | impl MigrationTrait for Migration { 777 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 778 | manager 779 | .create_table( 780 | Table::create() 781 | .table(UserGroup::Table) 782 | .if_not_exists() 783 | .col(ColumnDef::new(UserGroup::GroupId).uuid().not_null()) 784 | .foreign_key( 785 | ForeignKey::create() 786 | .name("FK_UserGroup_group_groupId") 787 | .from(UserGroup::Table, UserGroup::GroupId) 788 | .to(Group::Table, Group::Id) 789 | .on_delete(ForeignKeyAction::Cascade) 790 | .on_update(ForeignKeyAction::Cascade), 791 | ) 792 | .col(ColumnDef::new(UserGroup::UserId).uuid().not_null()) 793 | .foreign_key( 794 | ForeignKey::create() 795 | .name("FK_UserGroup_user_userId") 796 | .from(UserGroup::Table, UserGroup::UserId) 797 | .to(User::Table, User::Id) 798 | .on_delete(ForeignKeyAction::Cascade) 799 | .on_update(ForeignKeyAction::Cascade), 800 | ) 801 | .primary_key( 802 | Index::create() 803 | .name("PK_UserGroup") 804 | .col(UserGroup::GroupId) 805 | .col(UserGroup::UserId), 806 | ) 807 | .to_owned(), 808 | ) 809 | .await 810 | } 811 | 812 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 813 | manager 814 | .drop_table(Table::drop().table(UserGroup::Table).to_owned()) 815 | .await 816 | } 817 | } 818 | 819 | #[derive(DeriveIden)] 820 | enum UserGroup { 821 | Table, 822 | GroupId, 823 | UserId, 824 | } 825 | ``` 826 | 827 | `migration/src/lib.rs` 828 | 829 | ```rust 830 | pub use sea_orm_migration::prelude::*; 831 | 832 | mod m20240520_104423_create_user; 833 | mod m20240520_104447_create_group; 834 | mod m20240520_104508_create_message; 835 | mod m20240520_104520_create_file_msg; 836 | mod m20240520_104527_create_conversation; 837 | mod m20240520_111859_create_uuid_extension; 838 | mod m20240521_123300_create_user_group; 839 | 840 | pub struct Migrator; 841 | 842 | #[async_trait::async_trait] 843 | impl MigratorTrait for Migrator { 844 | fn migrations() -> Vec<Box<dyn MigrationTrait>> { 845 | vec![ 846 | Box::new(m20240520_104423_create_user::Migration), 847 | Box::new(m20240520_104447_create_group::Migration), 848 | Box::new(m20240520_104508_create_message::Migration), 849 | Box::new(m20240520_104520_create_file_msg::Migration), 850 | Box::new(m20240520_104527_create_conversation::Migration), 851 | Box::new(m20240520_111859_create_uuid_extension::Migration), 852 | Box::new(m20240521_123300_create_user_group::Migration), 853 | ] 854 | } 855 | } 856 | ``` 857 | 858 | Lưu ý: 859 | 860 | - Thứ tự chạy migration rất quan trọng 861 | 862 | Sau đó mọi người chạy lệnh `sea-orm-cli migrate fresh` và sử dụng `sea-orm-cli migrate status` để kiểm tra trạng thái các migration 863 | 864 | ![Untitled](img/Untitled%2013.png) 865 | 866 | Vậy là ta đã tạo thành công các table trong database tiếp đến ta sẽ tạo entities từ đó. 867 | 868 | ### Atomic migration (chỉ hoạt động với PostgreSQL) 869 | 870 | Khi chạy migration failed thì database sẽ được rolled back về trạng thái cũ (do migration script thực thi trong transaction) 871 | 872 | ### Schema First or Entity First 873 | 874 | Có 2 cách tiếp cận khi sử dụng sea-orm là `Schema First` và `Entity First` 875 | 876 | Trong hướng dẫn này thì mình tiếp cận theo hướng `Schema First` (chạy migration để tạo ra các table trong database rồi dựa vào đó để generate ra entity) 877 | 878 | ### sea-orm-cli migrate cheatsheet 879 | 880 | ```bash 881 | sea-orm-cli migrate init # tạo thư mục migration 882 | 883 | sea-orm-cli migrate generate NAME_OF_MIGRATION # tạo file migration 884 | 885 | sea-orm-cli migrate up # gọi function up được định nghĩa trong các file migration 886 | 887 | sea-orm-cli migrate fresh # xóa tất cả table trong db xong đó apply lại các migration (gọi function up của mỗi file migration) 888 | 889 | sea-orm-cli migrate status # kiểm tra trạng thái của các migration 890 | ``` 891 | 892 | ## SeaORM - Entity 893 | 894 | ### Tạo thư mục Entity 895 | 896 | Mọi người chạy command sau ở terminal nhé: 897 | 898 | ```bash 899 | sea-orm-cli generate entity -u postgres://admin:admin@localhost:5432/chat-app -o entity/src 900 | ``` 901 | 902 | Nó sẽ generate các entity dựa trên các table có trong database `chat-app` 903 | 904 | ![Untitled](img/Untitled%2014.png) 905 | 906 | Mọi người tạo thêm file `entity/Cargo.toml` 907 | 908 | ```toml 909 | [package] 910 | name = "entity" 911 | version = "0.1.0" 912 | edition = "2021" 913 | publish = false 914 | 915 | [lib] 916 | name = "entity" 917 | path = "src/mod.rs" 918 | 919 | [dependencies] 920 | serde = { version = "1", features = ["derive"] } 921 | 922 | [dependencies.sea-orm] 923 | version = "0.12.15" 924 | ``` 925 | 926 | ### Workspace Structure 927 | 928 | Mọi người làm theo hướng dẫn phía dưới nhé phần `Workspace Structure`. 929 | 930 | [Setting Up Migration | SeaORM 🐚 An async & dynamic ORM for Rust (sea-ql.org)](https://www.sea-ql.org/SeaORM/docs/migration/setting-up-migration/#workspace-structure) 931 | 932 | ### Entity Structure 933 | 934 | Trong bài viết này, mình sẽ không đi vào phần này bởi vì tất cả các entity đã được generate sẵn. Bây giờ mọi người chỉ cần biết thực hiện CRUD thông qua API có sẵn từ entity như thế nào. 935 | 936 | ## Xây dựng API 937 | 938 | ### Các thành phần cơ bản cho CRUD 939 | 940 | - Entity: cung cấp API để thực hiện CRUD 941 | - Mọi người có thể sẽ thắc mắc trong source code entity không có Entity nào được định nghĩa sao ta gọi được đó là nhờ macro `DeriveEntityModel` hỗ trợ generate entity từ `Model` 942 | - Mỗi dòng dữ liệu trong table ứng với `Model` 943 | - `ActiveValue`: enum để bắt được những thay đổi đối với attribute của ActiveModel 944 | - `ActiveModel` : có tất cả attributes (fields) của `Model` và các attribute này được theo dõi bởi ActiveValue 945 | 946 | ### User 947 | 948 | `src/features/users/handler.rs` 949 | 950 | ```rust 951 | use axum::{extract::Path, http::StatusCode, response::IntoResponse, Extension, Json}; 952 | 953 | use sea_orm::ActiveValue::Set; 954 | use sea_orm::{ 955 | ActiveModelTrait, ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter, 956 | }; 957 | use serde_json::json; 958 | use uuid::Uuid; 959 | 960 | use super::model::{CreateUser, UpdateUser, UserDTO}; 961 | 962 | use entity::user; 963 | 964 | pub async fn create_user( 965 | Extension(db_connection): Extension<DatabaseConnection>, 966 | Json(payload): Json<CreateUser>, 967 | ) -> impl IntoResponse { 968 | let user_model = user::ActiveModel { 969 | name: Set(payload.name), 970 | email: Set(payload.email), 971 | password: Set(payload.password), 972 | is_online: Set(payload.is_online), 973 | ..Default::default() 974 | }; 975 | 976 | user_model.insert(&db_connection).await.unwrap(); 977 | 978 | ( 979 | StatusCode::CREATED, 980 | Json(json!( 981 | { 982 | "message": "User created successfully" 983 | } 984 | )), 985 | ) 986 | } 987 | 988 | pub async fn get_user_by_id( 989 | Extension(db_connection): Extension<DatabaseConnection>, 990 | Path(id): Path<Uuid>, 991 | ) -> impl IntoResponse { 992 | let user = user::Entity::find() 993 | .filter(Condition::all().add(user::Column::Id.eq(id))) 994 | .one(&db_connection) 995 | .await 996 | .unwrap() 997 | .unwrap(); 998 | 999 | let result = UserDTO { 1000 | id: user.id, 1001 | name: user.name, 1002 | email: user.email, 1003 | avatar: user.avatar, 1004 | is_online: user.is_online, 1005 | }; 1006 | 1007 | (StatusCode::CREATED, Json(result)) 1008 | } 1009 | 1010 | pub async fn update_user( 1011 | Extension(db_connection): Extension<DatabaseConnection>, 1012 | Path(id): Path<Uuid>, 1013 | Json(payload): Json<UpdateUser>, 1014 | ) -> impl IntoResponse { 1015 | let mut user: user::ActiveModel = user::Entity::find() 1016 | .filter(Condition::all().add(user::Column::Id.eq(id))) 1017 | .one(&db_connection) 1018 | .await 1019 | .unwrap() 1020 | .unwrap() 1021 | .into(); 1022 | 1023 | user.name = Set(payload.name.unwrap()); 1024 | user.email = Set(payload.email.unwrap()); 1025 | user.avatar = Set(payload.avatar); 1026 | 1027 | user.update(&db_connection).await.unwrap(); 1028 | 1029 | ( 1030 | StatusCode::ACCEPTED, 1031 | Json(json!( 1032 | { 1033 | "message": "User updated successfully" 1034 | } 1035 | )), 1036 | ) 1037 | } 1038 | 1039 | pub async fn delete_user( 1040 | Extension(db_connection): Extension<DatabaseConnection>, 1041 | Path(id): Path<Uuid>, 1042 | ) -> impl IntoResponse { 1043 | let mut user = user::Entity::find() 1044 | .filter(Condition::all().add(user::Column::Id.eq(id))) 1045 | .one(&db_connection) 1046 | .await 1047 | .unwrap() 1048 | .unwrap(); 1049 | 1050 | user::Entity::delete_by_id(user.id) 1051 | .exec(&db_connection) 1052 | .await 1053 | .unwrap(); 1054 | 1055 | ( 1056 | StatusCode::ACCEPTED, 1057 | Json(json!( 1058 | { 1059 | "message": "User deleted successfully" 1060 | } 1061 | )), 1062 | ) 1063 | } 1064 | 1065 | pub async fn get_all_users( 1066 | Extension(db_connection): Extension<DatabaseConnection>, 1067 | ) -> impl IntoResponse { 1068 | let users: Vec<UserDTO> = user::Entity::find() 1069 | .all(&db_connection) 1070 | .await 1071 | .unwrap() 1072 | .into_iter() 1073 | .map(|user| UserDTO { 1074 | id: user.id, 1075 | name: user.name, 1076 | email: user.email, 1077 | avatar: user.avatar, 1078 | is_online: user.is_online, 1079 | }) 1080 | .collect(); 1081 | 1082 | (StatusCode::ACCEPTED, Json(users)) 1083 | } 1084 | 1085 | ``` 1086 | 1087 | Khi định nghĩa table thì mình có set default value cho vài Column nếu mình mình muốn set value default cho các attribute không được chỉ định thì dùng `..Default::default()` 1088 | 1089 | `..` : destrucutring để lấy ra các thuộc tính trong struct 1090 | 1091 | Tại sao không trả về `struct Model`? 1092 | 1093 | 1. Model chứa 1 vài thông tin quan trọng như password 1094 | 2. Model không có attribute Serialize (convert Rust type sang JSON). Ta có thể thêm vào nhưng không tốt lắm vì có thể trong tương lai ta sẽ thay đổi Schema (các file migration) thì khi ta generate entity dựa trên các table được tạo trong db nhờ chạy migration sẽ mất hết 1095 | 1096 | ⇒ Sử dụng DTO (Data Transfer Object) 1097 | 1098 | Nếu mọi người chưa biết về `into` thì mọi người nên đọc bài viết này 1099 | 1100 | [Rust-Developer-Vietnam/newbie/From_Into.md at main · openedu101/Rust-Developer-Vietnam (github.com)](https://github.com/openedu101/Rust-Developer-Vietnam/blob/main/newbie/From_Into.md) 1101 | 1102 | **_Lưu ý:_** 1103 | 1104 | - Có những function mà mình unwrap tận 2 lần thì đây là điều không nên. Trong bài viết sau thì mình sẽ hướng dẫn mọi người xử lý lỗi (Error Handling) 1105 | 1106 | `src/features/users/model.rs` 1107 | 1108 | ```rust 1109 | use serde::{Deserialize, Serialize}; 1110 | use uuid::Uuid; 1111 | 1112 | #[derive(Deserialize)] 1113 | pub struct CreateUser { 1114 | pub name: String, 1115 | pub email: String, 1116 | pub password: String, 1117 | pub avatar: Option<String>, 1118 | pub is_online: bool, 1119 | } 1120 | 1121 | #[derive(Deserialize)] 1122 | pub struct UpdateUser { 1123 | pub name: Option<String>, 1124 | pub email: Option<String>, 1125 | pub avatar: Option<String>, 1126 | } 1127 | 1128 | #[derive(Serialize, Deserialize)] 1129 | pub struct UserDTO { 1130 | pub id: Uuid, 1131 | pub name: String, 1132 | pub email: String, 1133 | pub avatar: Option<String>, 1134 | pub is_online: bool, 1135 | } 1136 | 1137 | ``` 1138 | 1139 | `src/features/users/router.rs` 1140 | 1141 | ```rust 1142 | use axum::{routing::get, Router}; 1143 | 1144 | use super::handler::{create_user, delete_user, get_all_users, get_user_by_id, update_user}; 1145 | 1146 | pub fn get_routes() -> Router { 1147 | Router::new() 1148 | .route("/", get(get_all_users).post(create_user)) 1149 | .route( 1150 | "/:id", 1151 | get(get_user_by_id).delete(delete_user).patch(update_user), 1152 | ) 1153 | } 1154 | ``` 1155 | 1156 | ### Group 1157 | 1158 | `src/features/group/handler.rs` 1159 | 1160 | ```rust 1161 | use axum::extract::Path; 1162 | use axum::{http::StatusCode, response::IntoResponse, Extension, Json}; 1163 | 1164 | use sea_orm::ActiveValue::Set; 1165 | use sea_orm::{ 1166 | ActiveModelTrait, ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter, 1167 | }; 1168 | use serde_json::json; 1169 | use uuid::Uuid; 1170 | 1171 | use crate::features::users::model::UserDTO; 1172 | 1173 | use super::model::{CreateGroup, GroupDTO}; 1174 | 1175 | use entity::{group, user, user_group}; 1176 | 1177 | pub async fn create_group( 1178 | Extension(db_connection): Extension<DatabaseConnection>, 1179 | Json(payload): Json<CreateGroup>, 1180 | ) -> impl IntoResponse { 1181 | let group_model = group::ActiveModel { 1182 | name: Set(payload.name), 1183 | ..Default::default() 1184 | }; 1185 | let new_group = group_model.insert(&db_connection).await.unwrap(); 1186 | 1187 | let records: Vec<user_group::ActiveModel> = payload 1188 | .user_ids 1189 | .into_iter() 1190 | .map(|user_id| user_group::ActiveModel { 1191 | group_id: Set(new_group.id), 1192 | user_id: Set(user_id), 1193 | }) 1194 | .collect(); 1195 | 1196 | user_group::Entity::insert_many(records) 1197 | .exec(&db_connection) 1198 | .await; 1199 | 1200 | ( 1201 | StatusCode::CREATED, 1202 | Json(json!( 1203 | { 1204 | "message": "Group created successfully" 1205 | } 1206 | )), 1207 | ) 1208 | } 1209 | 1210 | pub async fn get_group_by_id( 1211 | Extension(db_connection): Extension<DatabaseConnection>, 1212 | Path(id): Path<Uuid>, 1213 | ) -> impl IntoResponse { 1214 | let group = group::Entity::find() 1215 | .filter(Condition::all().add(group::Column::Id.eq(id))) 1216 | .one(&db_connection) 1217 | .await 1218 | .unwrap() 1219 | .unwrap(); 1220 | 1221 | let user_ids: Vec<Uuid> = user_group::Entity::find() 1222 | .filter(Condition::all().add(user_group::Column::GroupId.eq(group.id))) 1223 | .all(&db_connection) 1224 | .await 1225 | .unwrap() 1226 | .into_iter() 1227 | .map(|user_group_model| user_group_model.user_id) 1228 | .collect(); 1229 | 1230 | let mut users: Vec<UserDTO> = vec![]; 1231 | for user_id in user_ids.into_iter() { 1232 | let user = user::Entity::find() 1233 | .filter(Condition::all().add(user::Column::Id.eq(user_id))) 1234 | .one(&db_connection) 1235 | .await 1236 | .unwrap() 1237 | .unwrap(); 1238 | users.push(UserDTO { 1239 | id: user.id, 1240 | name: user.name, 1241 | email: user.email, 1242 | avatar: user.avatar, 1243 | is_online: user.is_online, 1244 | }); 1245 | } 1246 | 1247 | let result = GroupDTO { 1248 | id: group.id, 1249 | users, 1250 | }; 1251 | 1252 | (StatusCode::OK, Json(result)) 1253 | } 1254 | 1255 | ``` 1256 | 1257 | Function `get_group_by_id` chưa được tối ưu lắm nếu mọi người có cách nào hay hơn thì mình cùng thảo luận nhé 1258 | 1259 | `src/features/group/model.rs` 1260 | 1261 | ```rust 1262 | use serde::{Deserialize, Serialize}; 1263 | use uuid::Uuid; 1264 | 1265 | use crate::features::users::model::UserDTO; 1266 | 1267 | #[derive(Deserialize)] 1268 | pub struct CreateGroup { 1269 | pub name: String, 1270 | pub user_ids: Vec<Uuid>, 1271 | } 1272 | 1273 | #[derive(Serialize, Deserialize)] 1274 | pub struct GroupDTO { 1275 | pub id: Uuid, 1276 | pub users: Vec<UserDTO>, 1277 | } 1278 | 1279 | ``` 1280 | 1281 | `src/features/group/router.rs` 1282 | 1283 | ```rust 1284 | use axum::{ 1285 | routing::{get, post}, 1286 | Router, 1287 | }; 1288 | 1289 | use super::handler::{create_group, get_group_by_id}; 1290 | 1291 | pub fn get_routes() -> Router { 1292 | Router::new() 1293 | .route("/", post(create_group)) 1294 | .route("/:id", get(get_group_by_id)) 1295 | } 1296 | 1297 | ``` 1298 | 1299 | ### Chat 1300 | 1301 | `src/features/group/model.rs` 1302 | 1303 | ```rust 1304 | use serde::Deserialize; 1305 | use uuid::Uuid; 1306 | 1307 | #[derive(Deserialize)] 1308 | pub struct Chat { 1309 | pub user_id: Uuid, 1310 | pub content: String, 1311 | pub message_type: MessageType, 1312 | pub group_id: Uuid, 1313 | } 1314 | 1315 | #[derive(Deserialize)] 1316 | pub enum MessageType { 1317 | File, 1318 | Text, 1319 | Image, 1320 | } 1321 | ``` 1322 | 1323 | Ở đây mình định nghĩa thêm enum `MessageType` mặc dù đã có `MessageEnum` được generate bởi vì `MessageEnum` không có attribute `Deserialize` nên khi đưa vào struct `Chat` sẽ bị lỗi 1324 | 1325 | `src/features/chat/handler.rs` 1326 | 1327 | ```rust 1328 | use axum::{http::StatusCode, response::IntoResponse, Extension, Json}; 1329 | 1330 | use entity::sea_orm_active_enums::MessageEnum; 1331 | use sea_orm::ActiveValue::Set; 1332 | use sea_orm::{ 1333 | ActiveModelTrait, DatabaseConnection, 1334 | }; 1335 | use serde_json::json; 1336 | 1337 | use entity::{conversation, message}; 1338 | 1339 | use super::model::{Chat, MessageType}; 1340 | 1341 | pub async fn chat( 1342 | Extension(db_connection): Extension<DatabaseConnection>, 1343 | Json(payload): Json<Chat>, 1344 | ) -> impl IntoResponse { 1345 | let message_type = match payload.message_type { 1346 | MessageType::File => MessageEnum::File, 1347 | MessageType::Text => MessageEnum::Text, 1348 | MessageType::Image => MessageEnum::Image, 1349 | }; 1350 | 1351 | let message_model = message::ActiveModel { 1352 | user_id: Set(payload.user_id), 1353 | content: Set(payload.content), 1354 | r#type: Set(Some(message_type)), 1355 | ..Default::default() 1356 | }; 1357 | 1358 | let message = message_model.insert(&db_connection).await.unwrap(); 1359 | 1360 | let conversation_model = conversation::ActiveModel { 1361 | group_id: Set(payload.group_id), 1362 | msg_id: Set(message.id), 1363 | }; 1364 | 1365 | conversation_model.insert(&db_connection).await.unwrap(); 1366 | 1367 | ( 1368 | StatusCode::CREATED, 1369 | Json(json!( 1370 | { 1371 | "message": "Chat created successfully" 1372 | } 1373 | )), 1374 | ) 1375 | } 1376 | 1377 | ``` 1378 | 1379 | `r#type`: mình vô tình đặt tên thuộc tính trùng với các keyword trong Rust. Mình chỉ cần thêm `r#` phía trước là Rust có thể phân biệt giữa keyword với tên định nghĩa cho 1380 | 1381 | `src/features/group/router.rs` 1382 | 1383 | ```rust 1384 | use axum::{routing::post, Router}; 1385 | 1386 | use super::handler::chat; 1387 | 1388 | pub fn get_routes() -> Router { 1389 | Router::new().route("/", post(chat)) 1390 | } 1391 | 1392 | ``` 1393 | 1394 | `src/enums/routes.rs` 1395 | 1396 | ```rust 1397 | const AUTH_PATH: &str = "/auth"; 1398 | const USERS_PATH: &str = "/users"; 1399 | const GROUP_PATH: &str = "/group"; 1400 | const CHAT_PATH: &str = "/chat"; 1401 | 1402 | pub enum RoutePath { 1403 | AUTH, 1404 | USERS, 1405 | GROUP, 1406 | CHAT, 1407 | } 1408 | 1409 | impl RoutePath { 1410 | pub fn get_path(&self) -> &'static str { 1411 | match self { 1412 | RoutePath::AUTH => AUTH_PATH, 1413 | RoutePath::USERS => USERS_PATH, 1414 | RoutePath::GROUP => GROUP_PATH, 1415 | RoutePath::CHAT => CHAT_PATH, 1416 | } 1417 | } 1418 | } 1419 | 1420 | ``` 1421 | 1422 | `src/router.rs` 1423 | 1424 | ```rust 1425 | use axum::Router; 1426 | 1427 | use crate::{ 1428 | enums::routes::RoutePath, 1429 | features::{ 1430 | auth::routes::get_routes as get_auth_routes, chat::routes::get_routes as get_chat_routes, 1431 | group::routes::get_routes as get_group_routes, 1432 | users::routes::get_routes as get_user_routes, 1433 | }, 1434 | }; 1435 | 1436 | pub fn create_router() -> Router { 1437 | let auth_routes = get_auth_routes(); 1438 | let user_routes = get_user_routes(); 1439 | let group_routes = get_group_routes(); 1440 | let chat_routes = get_chat_routes(); 1441 | 1442 | let api_routes = Router::new() 1443 | .nest(RoutePath::AUTH.get_path(), auth_routes) 1444 | .nest(RoutePath::USERS.get_path(), user_routes) 1445 | .nest(RoutePath::GROUP.get_path(), group_routes) 1446 | .nest(RoutePath::CHAT.get_path(), chat_routes); 1447 | 1448 | Router::new().nest("/api", api_routes) 1449 | } 1450 | 1451 | ``` 1452 | 1453 | ## Github 1454 | 1455 | Mọi người có thể xem source code hoàn chỉnh ở đây nhé. 1456 | 1457 | [https://github.com/Learning-Tech-Workspace/learn-rust-backend](https://github.com/Learning-Tech-Workspace/learn-rust-backend) 1458 | 1459 | ## Postman 1460 | 1461 | [https://www.postman.com/navigation-astronaut-22006281/workspace/rust](https://www.postman.com/navigation-astronaut-22006281/workspace/rust) 1462 | 1463 | ## Tổng kết 1464 | 1465 | Trong bài viết này ta đã: 1466 | 1467 | - Thiết lập cơ sở dữ liệu với Docker 1468 | - Tương tác với cơ sở dữ liệu sử dụng sea-orm 1469 | - Nắm được các khái niệm cơ bản của sea-orm 1470 | 1471 | ## Bài viết tiếp theo 1472 | 1473 | - Xây dựng middleware 1474 | - Tiếp tục xây dựng API 1475 | - Sử dụng socket để làm real time 1476 | - Error Handling 1477 | 1478 | Mình chưa có quá nhiều kinh nghiệm với Rust trong việc xây dựng Backend. Trong bài viết có sai sót gì mọi người cùng thảo luận góp ý nhé. 1479 | 1480 | Cảm ơn mọi người đã đọc. 1481 | --------------------------------------------------------------------------------