Dokumentasi dan Doc Test di Rust
Setiap halaman di docs.rs dan setiap halaman dokumentasi standard library ditulis oleh seorang programmer, langsung di dalam kode sumber, memakai komentar khusus. Rust mengubah komentar-komentar itu menjadi sebuah website hanya dengan satu perintah. Kamu bisa melakukan hal yang sama persis untuk kodemu sendiri.
Ada alasan kedua kenapa ini penting. Contoh kode di dalam dokumentasimu adalah tes sungguhan. Saat kamu menjalankan cargo test, Rust meng-compile dan menjalankannya. Kalau kamu mengubah fungsi lalu lupa memperbarui contohnya, tesnya gagal. Dokumentasimu tidak bisa diam-diam jadi usang. Ini jarang ada di bahasa pemrograman lain, dan termasuk salah satu hal paling menyenangkan dari Rust.
Doc comment: /// dan //!
Section titled “Doc comment: /// dan //!”Kamu sudah bertemu komentar biasa di halaman Komentar. Doc comment (komentar dokumentasi) mirip, tapi memakai tiga karakter:
///mendokumentasikan item di bawahnya: fungsi, struct, enum, field, atau modul.//!mendokumentasikan item tempat ia berada. Di bagian paling atassrc/lib.rsatausrc/main.rs, ia menjelaskan seluruh crate.
//! Program resep yang mungil.
/// Membagi `total` gram secara merata menjadi `portions` porsi.fn split_evenly(total: u32, portions: u32) -> u32 { total / portions}Baris pertama doc comment sebaiknya berupa satu kalimat ringkasan yang pendek. Berbagai tool menampilkan baris itu di daftar dan hasil pencarian, jadi tulislah sebaik mungkin.
Doc comment tidak mengubah apa yang dilakukan programmu. Berikut fungsi yang sudah didokumentasikan dan dipakai dari main:
/// Membagi `total` gram secara merata menjadi `portions` porsi.////// Sisa gramnya dibuang, jadi 10 gram dibagi 3 porsi hasilnya 3.////// # Panics////// Panic jika `portions` bernilai nol.fn split_evenly(total: u32, portions: u32) -> u32 { if portions == 0 { panic!("portions harus lebih besar dari nol"); } total / portions}
fn main() { println!("{}", split_evenly(600, 3)); println!("{}", split_evenly(10, 3));}Output:
2003Di kebanyakan editor, kalau kamu mengarahkan kursor ke split_evenly di dalam main, teks ini muncul di pop-up. Itu saja sudah sepadan dengan usahanya.
Markdown di doc comment
Section titled “Markdown di doc comment”Doc comment ditulis dalam Markdown, format yang sama dengan yang dipakai di GitHub dan banyak aplikasi chat. Kamu bisa memakai `code`, **tebal**, *miring*, daftar, link, dan judul:
/// Mengembalikan suhu oven untuk sebuah masakan, dalam **derajat Celsius**.////// Masakan yang didukung:////// - `"bread"`: 220 derajat/// - `"cake"`: 180 derajat/// - selain itu: 200 derajat////// *Selalu* cek oven kamu sendiri, karena setiap oven berbeda./// Lihat [Celsius di Wikipedia](https://id.wikipedia.org/wiki/Celsius).pub fn oven_temperature(dish: &str) -> u32 { match dish { "bread" => 220, "cake" => 180, _ => 200, }}Sisipkan baris /// kosong di antara paragraf, sama seperti baris kosong di file Markdown biasa. Bungkus nama fungsi, tipe, dan nilai dengan backtick supaya tampil sebagai kode.
Bagian-bagian standar
Section titled “Bagian-bagian standar”Programmer Rust memakai beberapa judul yang sama berulang kali, supaya pembaca tahu harus melihat ke mana. Kamu sudah melihat salah satunya. Judul-judul ini biasanya ditulis dalam bahasa Inggris, karena begitulah konvensinya di seluruh ekosistem Rust. Ini yang paling umum:
# Examples: cara memakai item tersebut. Hampir setiap item publik sebaiknya punya bagian ini.# Panics: kapan fungsi bisa panic, supaya pemanggil bisa menghindarinya.# Errors: kapan fungsi yang mengembalikanResultmemberikanErr, dan kenapa.# Safety: untukunsafe fn, aturan yang wajib dipatuhi pemanggil (lihat Unsafe Rust).
Berikut # Errors dan # Safety di sebuah file library, src/lib.rs:
/// Membaca takaran seperti `"250g"` dan mengembalikan jumlah gramnya.////// # Errors////// Mengembalikan error jika teksnya tidak diakhiri `g`,/// atau jika bagian sebelumnya bukan bilangan bulat.pub fn parse_grams(text: &str) -> Result<u32, String> { let number = text .strip_suffix('g') .ok_or(format!("'{text}' harus diakhiri g"))?; number .parse() .map_err(|_| format!("'{number}' bukan bilangan bulat"))}
/// Mengembalikan bahan pertama tanpa memeriksa apakah daftarnya kosong.////// # Safety////// Pemanggil harus memastikan `ingredients` tidak kosong./// Memanggil fungsi ini pada slice kosong adalah undefined behavior.pub unsafe fn first_unchecked(ingredients: &[String]) -> &String { unsafe { ingredients.get_unchecked(0) }}Perhatikan bahwa judulnya memakai satu #. Di dalam doc comment, # adalah judul Markdown, bukan atribut Rust.
Membuat dokumentasi dengan cargo doc
Section titled “Membuat dokumentasi dengan cargo doc”Di sisa halaman ini kita akan mengerjakan sebuah library kecil. Buat dengan cargo new --lib kitchen. Lalu, di dalam proyek itu, jalankan:
cargo doc --open Documenting kitchen v0.1.0 (/path/to/kitchen) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.91s Generated /path/to/kitchen/target/doc/kitchen/index.htmlCargo membangun sebuah website HTML di target/doc/, dan --open membukanya di browser. Tampilannya persis seperti halaman di docs.rs: ada sidebar, kotak pencarian, dan satu halaman untuk setiap fungsi dan tipe. Cargo juga mendokumentasikan setiap crate yang kamu pakai sebagai dependensi, yang sangat berguna saat kamu sedang offline.
Secara default, hanya item publik (yang diberi pub) yang muncul, karena dokumentasi ini ditujukan untuk orang yang memakai library-mu. Tambahkan --document-private-items untuk menyertakan semuanya.
Doc test: contoh yang benar-benar dijalankan
Section titled “Doc test: contoh yang benar-benar dijalankan”Berikut bagian awal src/lib.rs di library kitchen. Fungsinya punya bagian # Examples berisi blok kode di dalam doc comment:
//! Fungsi-fungsi bantu kecil untuk dapur.//!//! `kitchen` mengonversi suhu oven, membagi makanan menjadi beberapa porsi,//! dan membaca takaran seperti `"250g"`.
/// Mengonversi suhu dari Celsius ke Fahrenheit.////// # Examples////// ```/// let f = kitchen::celsius_to_fahrenheit(180.0);/// assert_eq!(f, 356.0);/// ```pub fn celsius_to_fahrenheit(celsius: f64) -> f64 { celsius * 9.0 / 5.0 + 32.0}Sekarang jalankan cargo test:
Doc-tests kitchen
running 1 testtest src/lib.rs - celsius_to_fahrenheit (line 10) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sCargo menemukan blok kode di baris 10, mengubahnya menjadi program kecil, meng-compile, lalu menjalankannya. assert_eq! memeriksa jawabannya. Ada beberapa hal yang perlu diperhatikan:
- Contohnya memanggil
kitchen::celsius_to_fahrenheit, lengkap dengan nama crate. Doc test di-compile seolah-olah ia adalah program milik orang lain yang memakai library-mu, jadi ia hanya bisa melihat itempub, dan kamu perlu path lengkap atau barisuse kitchen::...;. - Kamu tidak perlu menulis
fn main. Rustdoc membungkus contohmu di dalammainsecara otomatis. - Pakai
assert!danassert_eq!(dari halaman Testing) untuk menunjukkan sekaligus memeriksa hasilnya.
Bagaimana kalau contohnya salah? Ganti 356.0 menjadi 350.0, lalu jalankan tesnya lagi:
running 1 testtest src/lib.rs - celsius_to_fahrenheit (line 10) ... FAILED
failures:
---- src/lib.rs - celsius_to_fahrenheit (line 10) stdout ----Test executable failed (exit status: 101).
stderr:assertion `left == right` failed left: 356.0 right: 350.0
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sPesan dari Rust memang selalu dalam bahasa Inggris. Di sini ia bilang assertion left == right gagal: nilai sebenarnya (left) adalah 356.0, sedangkan yang ditulis di contoh (right) adalah 350.0.
Itulah intinya. Contoh yang salah langsung ketahuan, alih-alih membingungkan pembaca berbulan-bulan kemudian. Untuk menjalankan doc test saja, pakai cargo test --doc.
Menyembunyikan baris persiapan dengan #
Section titled “Menyembunyikan baris persiapan dengan #”Kadang sebuah contoh butuh baris-baris yang bisa mengalihkan perhatian pembaca. Baris di doc test yang diawali # (tanda pagar dan spasi) tetap di-compile, tapi tidak ditampilkan di dokumentasi yang dihasilkan.
Pemakaian paling umum: kamu ingin menunjukkan operator ?, tapi ? hanya bisa dipakai di dalam fungsi yang mengembalikan Result. Jadi kamu menyembunyikan main buatanmu sendiri:
/// Membaca takaran seperti `"250g"` dan mengembalikan jumlah gramnya.////// # Examples////// ```/// use kitchen::parse_grams;////// # fn main() -> Result<(), String> {/// let flour = parse_grams("250g")?;/// assert_eq!(flour, 250);////// assert!(parse_grams("banyak").is_err());/// # Ok(())/// # }/// ```pub fn parse_grams(text: &str) -> Result<u32, String> { let number = text .strip_suffix('g') .ok_or(format!("'{text}' harus diakhiri g"))?; number .parse() .map_err(|_| format!("'{number}' bukan bilangan bulat"))}Di browser, pembaca hanya melihat empat baris yang menarik: use, pemanggilan dengan ?, dan dua assert. Tesnya tetap meng-compile semuanya.
ignore, no_run, dan should_panic
Section titled “ignore, no_run, dan should_panic”Tidak semua contoh bisa langsung dijalankan begitu saja. Kamu bisa menaruh sebuah kata setelah backtick pembuka untuk memberi tahu rustdoc apa yang harus dilakukan:
```no_run: compile contohnya, tapi jangan jalankan. Cocok untuk kode yang membaca file, membuka koneksi jaringan, atau berulang selamanya.```should_panic: jalankan dan harapkan terjadi panic. Tesnya gagal kalau ternyata tidak panic.```ignore: jangan di-compile sama sekali. Pakai untuk sketsa dan pseudo-code. Cobano_rundulu, karena contoh yang di-ignore bisa jadi usang tanpa ada yang sadar.```text: sama sekali bukan kode Rust, cuma teks (misalnya contoh output).
/// Membagi `total` gram secara merata menjadi `portions` porsi.////// # Examples////// ```/// assert_eq!(kitchen::split_evenly(600, 3), 200);/// ```////// Nol porsi tidak masuk akal, jadi kode ini panic:////// ```should_panic/// kitchen::split_evenly(600, 0);/// ```pub fn split_evenly(total: u32, portions: u32) -> u32 { if portions == 0 { panic!("portions harus lebih besar dari nol"); } total / portions}
/// Memuat daftar belanja dari file, satu barang per baris.////// ```no_run/// let items = kitchen::load_list("shopping.txt").unwrap();/// println!("Ada {} barang yang harus dibeli", items.len());/// ```pub fn load_list(path: &str) -> std::io::Result<Vec<String>> { let text = std::fs::read_to_string(path)?; Ok(text.lines().map(String::from).collect())}Dengan semua contoh ini di kitchen, ditambah satu sketsa ignore di komentar //! milik crate, cargo test melaporkan:
running 7 teststest src/lib.rs - (line 8) ... ignoredtest src/lib.rs - load_list (line 87) - compile ... oktest src/lib.rs - split_evenly (line 34) ... oktest src/lib.rs - split_evenly (line 40) - should panic ... oktest src/lib.rs - parse_grams (line 59) ... oktest src/lib.rs - celsius_to_fahrenheit (line 16) ... oktest src/lib.rs - Recipe (line 104) ... ok
test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.01sTes no_run bertuliskan - compile: contoh itu hanya di-build. Urutan baris-barisnya bisa berubah setiap kali dijalankan, karena tes berjalan secara paralel.
Membuat link ke item lain
Section titled “Membuat link ke item lain”Di dalam doc comment, tulis sebuah nama di dalam backtick dan kurung siku, seperti [`Vec`], dan rustdoc mengubahnya menjadi link ke halaman item tersebut. Ini disebut intra-doc link. Cara ini bisa dipakai untuk item buatanmu sendiri maupun apa pun di standard library:
//! Program resep yang mungil.
/// Sebuah resep: nama dan daftar bahan beserta beratnya dalam gram.////// Buat dengan [`Recipe::new`] lalu isi dengan [`Recipe::add`]./// Bahan-bahannya disimpan dalam [`Vec`], sesuai urutan kamu menambahkannya.struct Recipe { /// Nama yang ditampilkan di bagian atas resep. name: String, /// Setiap bahan beserta beratnya dalam gram. ingredients: Vec<(String, u32)>,}
impl Recipe { /// Membuat resep kosong dengan nama yang diberikan. fn new(name: &str) -> Recipe { Recipe { name: name.to_string(), ingredients: Vec::new() } }
/// Menambahkan bahan ke akhir daftar. fn add(&mut self, ingredient: &str, grams: u32) { self.ingredients.push((ingredient.to_string(), grams)); }
/// Menjumlahkan berat semua bahan. fn total_grams(&self) -> u32 { self.ingredients.iter().map(|(_, grams)| grams).sum() }}
fn main() { let mut pancakes = Recipe::new("Panekuk"); pancakes.add("tepung", 200); pancakes.add("susu", 300); pancakes.add("telur", 50);
println!("{}", pancakes.name); for (ingredient, grams) in &pancakes.ingredients { println!("- {ingredient}: {grams} g"); } println!("Total: {} g", pancakes.total_grams());}Output:
Panekuk- tepung: 200 g- susu: 300 g- telur: 50 gTotal: 550 gField dan method juga bisa punya komentar /// sendiri, dan masing-masing muncul di halaman struct tersebut. Kalau sebuah link menunjuk ke sesuatu yang tidak ada, cargo doc akan memperingatkanmu. Di sini Recipe::push adalah salah ketik dari Recipe::add:
warning: unresolved link to `Recipe::push` --> src/lib.rs:100:51 |100 | /// Buat dengan [`Recipe::new`] lalu isi dengan [`Recipe::push`]. | ^^^^^^^^^^^^ the struct `Recipe` has no field or associated item named `push` | = note: `#[warn(rustdoc::broken_intra_doc_links)]` on by defaultArtinya: link ke Recipe::push tidak bisa ditemukan, karena struct Recipe tidak punya field atau item bernama push.
Mewajibkan dokumentasi dengan missing_docs
Section titled “Mewajibkan dokumentasi dengan missing_docs”Untuk library yang akan dipakai orang lain, kamu bisa meminta compiler mengingatkanmu tentang setiap item publik yang belum didokumentasikan. Taruh baris ini di bagian paling atas src/lib.rs:
#![warn(missing_docs)]Sekarang, lupa menulis doc comment akan memunculkan peringatan:
warning: missing documentation for a function --> src/lib.rs:138:1 |138 | pub fn grams_to_ounces(grams: f64) -> f64 { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |note: the lint level is defined here --> src/lib.rs:1:9 | 1 | #![warn(missing_docs)] | ^^^^^^^^^^^^Peringatan ini bilang ada fungsi tanpa dokumentasi, dan note di bawahnya menunjukkan di mana pemeriksaan ini dinyalakan. #! (dengan tanda seru) berarti atributnya berlaku untuk seluruh crate. Pemeriksaan ini juga meminta komentar //! untuk crate itu sendiri. Item privat tidak pernah dilaporkan, jadi fungsi bantu internal tidak akan merepotkanmu.
docs.rs: hosting gratis untuk dokumentasimu
Section titled “docs.rs: hosting gratis untuk dokumentasimu”Saat kamu memublikasikan crate ke crates.io (lihat Cargo dan Crate), docs.rs otomatis membangun dokumentasinya dan meng-host-nya secara gratis, untuk setiap versi. Kamu tidak perlu melakukan apa-apa. Itulah sebabnya hampir setiap crate Rust punya dokumentasi yang tampilannya seragam dan mudah dicari.
Dua tips lagi:
- Jalankan
rustup doc --stduntuk membuka dokumentasi standard library secara offline di browser. - Di halaman docs.rs atau std mana pun, klik link Source untuk membaca kode di balik sebuah fungsi. Ini cara belajar yang sangat bagus.
Kesalahan umum
Section titled “Kesalahan umum”Lupa menulis nama crate di doc test. Doc test berada di luar crate-mu, jadi kode ini gagal:
error[E0425]: cannot find function `celsius_to_fahrenheit` in this scope --> src/lib.rs:17:9 |17 | let f = celsius_to_fahrenheit(180.0); | ^^^^^^^^^^^^^^^^^^^^^ not found in this scopeCompiler tidak menemukan fungsi celsius_to_fahrenheit di scope (cakupan) contoh itu. Solusinya: tulis kitchen::celsius_to_fahrenheit(180.0), atau tambahkan baris use kitchen::celsius_to_fahrenheit; di bagian atas contoh (boleh disembunyikan dengan # kalau mau).
Doc comment yang tidak mendokumentasikan apa pun. /// harus berada tepat sebelum sebuah item. Di dalam badan fungsi, pakai //:
error[E0585]: found a documentation comment that doesn't document anything --> src/main.rs:3:5 |3 | /// Bagi makanan menjadi tiga porsi. | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: doc comments must come before what they document, if a comment was intended use `//`Compiler menemukan doc comment yang tidak menjelaskan apa-apa. Bagian help mengingatkan bahwa doc comment harus ditulis sebelum hal yang didokumentasikannya, dan kalau yang kamu maksud adalah komentar biasa, pakai //.
Mengira doc test di program biner ikut dijalankan. Doc test hanya berjalan untuk crate library (src/lib.rs). Contoh di doc comment src/main.rs tidak dites. Kalau programmu makin besar, pindahkan logikanya ke src/lib.rs dan biarkan main.rs tetap kecil.
Menulis dokumentasi yang cuma mengulang kode. “Menjumlahkan dua angka” di atas fn add(a, b) tidak membantu. Jelaskan hal yang tidak terlihat oleh pembaca: satuan (gram? detik?), kasus-kasus khusus, dan kapan fungsinya gagal.
Ringkasan
Section titled “Ringkasan”///mendokumentasikan item berikutnya, dan//!mendokumentasikan crate atau modul tempat ia berada. Keduanya memakai Markdown.- Pakai bagian-bagian standar:
# Examples,# Panics,# Errors, dan# Safety. cargo doc --openmembangun situs HTML untuk crate-mu beserta dependensinya.- Blok kode di doc comment adalah doc test:
cargo testmeng-compile dan menjalankannya. Sembunyikan baris persiapan dengan#, dan pakaino_run,should_panic, atauignorebila perlu. [`Name`]membuat intra-doc link, dan#![warn(missing_docs)]mengingatkanmu tentang item publik yang belum didokumentasikan.- Crate yang dipublikasikan ke crates.io otomatis mendapat hosting dokumentasi di docs.rs.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Komentar mana yang mendokumentasikan seluruh crate jika ditaruh di bagian atas src/lib.rs?
2. Apa yang dilakukan cargo test terhadap blok kode di dalam komentar /// sebuah library?
3. Di sebuah doc test, apa fungsi baris yang diawali # (tanda pagar dan spasi)?
4. Sebuah contoh membuka koneksi jaringan. Kamu ingin contoh itu di-compile tapi tidak dijalankan. Anotasi mana yang kamu pakai?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- The rustdoc book (bahasa Inggris): panduan resmi untuk tool dokumentasi Rust.
- Documentation tests — the rustdoc book (bahasa Inggris): semua opsi doc test, termasuk baris tersembunyi dan
compile_fail. - Linking to items by name — the rustdoc book (bahasa Inggris): semua cara menulis intra-doc link.
- Publishing a crate — The Rust Book (bahasa Inggris): menulis doc comment yang berguna sebelum memublikasikan crate.
- Documentation — Rust API Guidelines (bahasa Inggris): daftar periksa tentang apa saja yang perlu ada di dokumentasi crate yang baik.