Topik ini merancang arsitektur proyek arsip-suhu yang lingkupnya sudah terkunci, yaitu membagi kode menjadi crate pustaka dan crate biner dengan modul terpisah, merancang tipe error ArsipError beserta rantai penyebabnya, dan merancang trait SumberCatatan yang memisahkan asal data dari pengolahannya. Seluruh rancangan langsung ditulis sebagai kode yang dapat dibangun, bukan sekadar diagram, sehingga pada akhir topik proyek multi-berkas sudah berjalan terhadap arsip contoh beserta jalur error-nya. Setelah membacanya, keputusan penempatan kode pada proyek serupa dapat diambil dengan alasan yang sama, dan kegagalan dari lapisan mana pun dapat dilaporkan dari satu titik terluar.
Pembagian modul dan aliran data#
Pembagian modul mengikuti tanggung jawab, bukan urutan penulisan. Crate pustaka pada src/lib.rs memuat tiga modul, yaitu parser yang mengubah teks menjadi catatan, statistik yang merangkum catatan menjadi laporan, dan error yang mendefinisikan kegagalan bersama. Crate biner pada src/main.rs hanya membaca argumen baris perintah, memanggil pustaka, lalu mencetak hasilnya. Pemisahan ini membuat logika pengolahan dapat diuji tanpa menjalankan program utama, dan biner tetap tipis karena tidak menyimpan aturan domain apa pun.
arsip-suhu/
├── Cargo.toml
├── catatan.txt
└── src/
├── lib.rs
├── error.rs
├── parser.rs
├── statistik.rs
└── main.rsAliran data berjalan satu arah dari kiri ke kanan, dan seluruh langkah mengembalikan Result dengan tipe error yang sama. Diagram berikut memetakan modul beserta nilai yang melintas di antara modul tersebut.
Baris komentar pada awal setiap berkas Rust pada topik ini menyatakan bahwa berkas itu tidak dapat dikompilasi sendirian oleh rustc, karena modul-modulnya saling merujuk melalui crate. Pembangunan lengkap dilakukan Cargo pada proyek di luar pohon konten situs dengan berkas yang disalin dari bundle topik.
Desain tipe error#
Tipe error dirancang lebih dahulu karena semua modul memakainya. Empat cara gagal pada proyek ini dipetakan menjadi empat variant ArsipError, yaitu berkas yang tidak terbaca, baris yang tidak sesuai bentuk, suhu di luar jangkauan, dan arsip tanpa satu pun catatan. Variant menyimpan konteksnya sebagai field bertipe, misalnya nomor baris dan salinan baris mentah, sehingga pesan akhir dapat menunjuk baris sumber tanpa perlu backtrace.
// tidak-dijalankan sendirian oleh rustc karena bagian dari pustaka arsip_suhu.
//! Tipe error untuk seluruh langkah pengolahan arsip.
use std::fmt;
use std::io;
/// Kegagalan yang terjadi saat memuat atau mengolah arsip catatan.
#[derive(Debug)]
pub enum ArsipError {
/// Berkas tidak dapat dibaca oleh sistem berkas.
BacaGagal {
/// Error I/O dari sistem berkas.
sumber: io::Error,
},
/// Baris tidak memuat tepat dua kolom atau suhunya bukan angka.
BarisTidakSah {
/// Nomor baris mulai dari satu.
nomor: usize,
/// Isi baris yang ditolak.
baris: String,
},
/// Nilai suhu berada di luar jangkauan sensor.
SuhuDiluarJangkauan {
/// Nomor baris mulai dari satu.
nomor: usize,
/// Nilai suhu yang ditolak.
nilai: f64,
},
/// Arsip tidak memuat satu pun catatan.
TanpaData,
}
impl fmt::Display for ArsipError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ArsipError::BacaGagal { .. } => write!(f, "berkas tidak dapat dibaca"),
ArsipError::BarisTidakSah { nomor, baris } => {
write!(f, "baris ke-{nomor} tidak sah: \"{baris}\"")
}
ArsipError::SuhuDiluarJangkauan { nomor, nilai } => {
write!(
f,
"suhu {nilai} pada baris ke-{nomor} di luar jangkauan -60..60"
)
}
ArsipError::TanpaData => write!(f, "arsip tidak memuat catatan"),
}
}
}
impl std::error::Error for ArsipError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ArsipError::BacaGagal { sumber } => Some(sumber),
_ => None,
}
}
}
impl From<io::Error> for ArsipError {
fn from(sumber: io::Error) -> Self {
ArsipError::BacaGagal { sumber }
}
}Dua implementasi menuntaskan kontraknya sebagai tipe error Rust. Display menyusun kalimat singkat untuk manusia, sedangkan source hanya menautkan io::Error dari variant BacaGagal sebagai penyebab, dan variant lain mengembalikan None karena memang tidak punya lapisan di bawahnya. From<io::Error> hadir supaya operator ? pada read_to_string dapat mengangkat error I/O menjadi ArsipError secara otomatis. Dengan konversi itu, kode pengambil data tidak perlu memetakan error secara manual pada setiap pemanggilan.
Trait sumber catatan#
Penguraian tidak boleh terikat pada berkas, karena pengujian yang memakai berkas sementara menjadi lambat dan rapuh. Trait SumberCatatan memisahkan asal data dengan menyatakan satu kemampuan, yaitu memuat daftar catatan, lalu menyediakan dua implementasi. Implementasi untuk Path membaca berkas lalu mendelegasikan penguraian, sedangkan implementasi untuk str mengurai teks yang sudah berada di memori. Kedua implementasi dideklarasikan pada src/lib.rs bersama re-export antarmuka utama, sehingga akar pustaka sekaligus menjadi daftar isi API publiknya.
// tidak-dijalankan sendirian oleh rustc karena modul parser dan statistik berada di berkas lain.
//! Pustaka arsip-suhu: pengolah arsip catatan sensor suhu harian.
pub mod error;
pub mod parser;
pub mod statistik;
pub use error::ArsipError;
pub use parser::Catatan;
pub use statistik::Statistik;
use std::path::Path;
/// Sumber catatan yang dapat dimuat menjadi daftar [`Catatan`].
///
/// Dua implementasi tersedia: `Path` membaca berkas lalu mengurainya,
/// sedangkan `str` mengurai teks yang sudah tersimpan di memori. Pengujian
/// cukup memakai implementasi `str` tanpa perlu berkas sementara.
pub trait SumberCatatan {
/// Memuat seluruh catatan dari sumber ini.
fn muat(&self) -> Result<Vec<Catatan>, ArsipError>;
}
impl SumberCatatan for Path {
fn muat(&self) -> Result<Vec<Catatan>, ArsipError> {
let teks = std::fs::read_to_string(self)?;
<str as SumberCatatan>::muat(&teks)
}
}
impl SumberCatatan for str {
fn muat(&self) -> Result<Vec<Catatan>, ArsipError> {
parser::urai(self)
}
}Implementasi untuk str memanggil fungsi yang sama dengan implementasi Path, sehingga hanya ada satu tempat penguraian. Program biner memakai jalur Path, pengujian memakai jalur str dengan teks yang tertulis di dalam kode pengujian, dan perbedaan keduanya berhenti pada cara memperoleh teksnya.
Pengurai dan statistik#
Modul parser menerapkan aturan bentuk baris yang ditetapkan pada tahap pemilihan lingkup. Baris kosong dilewati, baris dengan jumlah kolom selain dua ditolak, suhu yang bukan bilangan ditolak, dan nilai di luar jangkauan -60 sampai 60 dianggap bacaan rusak. Fungsi urai mengembalikan Vec<Catatan> milik pemanggilnya sepenuhnya, sehingga tanggal yang tersimpan sebagai String ikut berpindah kepemilikan tanpa pinjaman yang tersisa.
// tidak-dijalankan sendirian oleh rustc karena bagian dari pustaka arsip_suhu.
//! Penguraian teks catatan sensor menjadi daftar [`Catatan`].
use crate::error::ArsipError;
/// Satu baris catatan harian: tanggal dan suhu.
#[derive(Debug, Clone, PartialEq)]
pub struct Catatan {
/// Tanggal sebagaimana tertulis, misalnya "2026-01-01".
pub tanggal: String,
/// Suhu dalam derajat Celsius.
pub suhu: f64,
}
/// Jangkauan suhu yang dianggap masuk akal bagi sensor ruangan.
const JANGKAUAN: f64 = 60.0;
/// Menguraikan teks menjadi daftar catatan.
///
/// Setiap baris memuat tanggal dan suhu yang dipisahkan spasi, contohnya
/// `2026-01-01 26.5`. Baris kosong dilewati, sedangkan baris berisi kolom
/// selain dua, suhu bukan angka, atau suhu di luar jangkauan menghentikan
/// penguraian dengan error yang menyebut nomor barisnya.
///
/// ```
/// use arsip_suhu::parser::urai;
///
/// let catatan = urai("2026-01-01 26.5\n\n2026-01-02 27.0").unwrap();
/// assert_eq!(catatan.len(), 2);
/// assert_eq!(catatan[0].tanggal, "2026-01-01");
/// assert_eq!(catatan[0].suhu, 26.5);
/// ```
pub fn urai(teks: &str) -> Result<Vec<Catatan>, ArsipError> {
let mut hasil = Vec::new();
for (i, mentah) in teks.lines().enumerate() {
let baris = mentah.trim();
if baris.is_empty() {
continue;
}
let mut kolom = baris.split_whitespace();
let (tanggal, suhu) = match (kolom.next(), kolom.next(), kolom.next()) {
(Some(tanggal), Some(suhu), None) => (tanggal, suhu),
_ => return Err(tidak_sah(i + 1, baris)),
};
let suhu: f64 = match suhu.parse() {
Ok(nilai) => nilai,
Err(_) => return Err(tidak_sah(i + 1, baris)),
};
if !(-JANGKAUAN..=JANGKAUAN).contains(&suhu) {
return Err(ArsipError::SuhuDiluarJangkauan {
nomor: i + 1,
nilai: suhu,
});
}
hasil.push(Catatan {
tanggal: tanggal.to_string(),
suhu,
});
}
Ok(hasil)
}
/// Membungkus baris mentah menjadi error dengan nomor barisnya.
fn tidak_sah(nomor: usize, baris: &str) -> ArsipError {
ArsipError::BarisTidakSah {
nomor,
baris: baris.to_string(),
}
}Modul statistik menerima potongan catatan sebagai pinjaman baca, menghitung seluruh nilai dalam satu lintasan, lalu mengembalikan struct Statistik yang menyalin nilainya. Daftar kosong dijawab dengan error TanpaData karena rata-rata tidak bermakna tanpa satu pun nilai, dan penolakan itu menjaga fungsi tetap jujur alih-alih mengembalikan nol yang tampak sah. Implementasi Display pada struct yang sama memformat laporan empat baris dengan lebar label seragam, sehingga biner cukup mencetaknya tanpa mengenal isi formatnya.
// tidak-dijalankan sendirian oleh rustc karena bagian dari pustaka arsip_suhu.
//! Statistik arsip: jumlah, rata-rata, tertinggi, dan terendah.
use std::fmt;
use crate::error::ArsipError;
use crate::parser::Catatan;
/// Ringkasan statistik atas sekumpulan catatan.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Statistik {
/// Banyaknya catatan yang dihitung.
pub jumlah: usize,
/// Nilai rata-rata seluruh suhu.
pub rata_rata: f64,
/// Suhu tertinggi.
pub tertinggi: f64,
/// Suhu terendah.
pub terendah: f64,
}
impl Statistik {
/// Menghitung statistik dalam satu lintasan atas daftar catatan.
///
/// Daftar kosong menghasilkan error `TanpaData` karena rata-rata,
/// tertinggi, dan terendah tidak memiliki makna tanpa satu pun nilai.
///
/// ```
/// use arsip_suhu::parser::urai;
/// use arsip_suhu::statistik::Statistik;
///
/// let catatan = urai("2026-01-01 26.0\n2026-01-02 28.0").unwrap();
/// let s = Statistik::dari(&catatan).unwrap();
/// assert_eq!(s.jumlah, 2);
/// assert_eq!(s.tertinggi, 28.0);
/// assert_eq!(s.terendah, 26.0);
/// ```
pub fn dari(catatan: &[Catatan]) -> Result<Statistik, ArsipError> {
let mut sisip = catatan.iter();
let Some(pertama) = sisip.next() else {
return Err(ArsipError::TanpaData);
};
let mut jumlah = 1;
let mut total = pertama.suhu;
let mut tertinggi = pertama.suhu;
let mut terendah = pertama.suhu;
for c in sisip {
jumlah += 1;
total += c.suhu;
tertinggi = tertinggi.max(c.suhu);
terendah = terendah.min(c.suhu);
}
Ok(Statistik {
jumlah,
rata_rata: total / jumlah as f64,
tertinggi,
terendah,
})
}
}
impl fmt::Display for Statistik {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "jumlah baris : {}", self.jumlah)?;
writeln!(f, "rata-rata : {:.2} °C", self.rata_rata)?;
writeln!(f, "tertinggi : {:.2} °C", self.tertinggi)?;
write!(f, "terendah : {:.2} °C", self.terendah)
}
}Contoh pada komentar dokumentasi urai dan Statistik::dari ditulis sebagai blok kode yang benar-benar dapat dikompilasi. Kedua contoh itu menjadi doc test yang dijalankan cargo test, sehingga dokumentasi antarmuka ikut diperiksa setiap kali pengujian berjalan.
Biner tipis dan jalur error#
Biner memegang tiga keputusan yang memang miliknya, yaitu membaca argumen, memilih sumber data, dan menentukan bentuk pelaporan kegagalan. Manifest proyek pada tahap ini masih polos karena seluruh kode memakai pustaka standar saja, dan berkas catatan dari tahap pemilihan lingkup tetap menjadi data uji.
[package]
name = "arsip-suhu"
version = "0.1.0"
edition = "2021"2026-01-01 24.1
2026-01-02 25.6
2026-01-03 26.5
2026-01-04 28.4
2026-01-05 27.3// tidak-dijalankan sendirian oleh rustc karena memakai crate pustaka arsip_suhu dari berkas lain.
//! Biner arsip-suhu: membaca satu arsip lalu mencetak laporan statistiknya.
use std::path::Path;
use arsip_suhu::{Statistik, SumberCatatan};
fn main() {
if let Err(e) = jalankan() {
eprintln!("error: {e}");
let mut sumber = std::error::Error::source(&e);
while let Some(s) = sumber {
eprintln!("sebab: {s}");
sumber = s.source();
}
std::process::exit(1);
}
}
fn jalankan() -> Result<(), arsip_suhu::ArsipError> {
let Some(nama) = std::env::args().nth(1) else {
eprintln!("pemakaian: arsip-suhu <berkas-catatan>");
std::process::exit(2);
};
let catatan = Path::new(&nama).muat()?;
println!("{nama}: {} baris", catatan.len());
println!();
println!("{}", Statistik::dari(&catatan)?);
Ok(())
}Fungsi main menjadi satu-satunya titik yang mencetak kegagalan. Fungsi itu mencetak pesan Display terlebih dahulu, lalu menyusuri rantai source dan mencetak setiap penyebab dengan awalan sebab. Dengan bentuk itu, error dari lapisan mana pun keluar sebagai laporan berlapis tanpa setiap modul perlu tahu cara pencetakannya. Eksekusi contoh dilakukan pada proyek di luar pohon konten situs.
# siapkan proyek percobaan yang bersih di luar pohon konten
rm -rf /tmp/proyek-arsip-arsitektur
cargo new /tmp/proyek-arsip-arsitektur --bin -q --vcs none
# salin berkas proyek dari bundle ke posisinya masing-masing
cp Cargo.toml /tmp/proyek-arsip-arsitektur/Cargo.toml
cp src/lib.rs /tmp/proyek-arsip-arsitektur/src/lib.rs
cp src/error.rs /tmp/proyek-arsip-arsitektur/src/error.rs
cp src/parser.rs /tmp/proyek-arsip-arsitektur/src/parser.rs
cp src/statistik.rs /tmp/proyek-arsip-arsitektur/src/statistik.rs
cp src/main.rs /tmp/proyek-arsip-arsitektur/src/main.rs
# bangun kedua crate lalu jalankan terhadap arsip contoh
cargo run --manifest-path /tmp/proyek-arsip-arsitektur/Cargo.toml -q -- catatan.txtcatatan.txt: 5 baris
jumlah baris : 5
rata-rata : 26.38 °C
tertinggi : 28.40 °C
terendah : 24.10 °CAngka pada laporan sama persis dengan keluaran purwarupa pada tahap pemilihan lingkup, dan itu bukan kebetulan, karena data ujinya sama dan aturan hitungnya sama. Perbedaannya ada pada jalur kegagalan yang kini teruji. Jalur pertama menguji berkas yang tidak ada.
# jalankan terhadap berkas yang tidak ada untuk memeriksa jalur error I/O
cargo run --manifest-path /tmp/proyek-arsip-arsitektur/Cargo.toml -q -- /tmp/tidak-ada.txt 2>&1error: berkas tidak dapat dibaca
sebab: No such file or directory (os error 2)Dua baris keluaran itu memperlihatkan rantai penyebab bekerja, yaitu pesan Display milik ArsipError diikuti pesan io::Error dari source. Jalur kedua menguji pemakaian program tanpa argumen, dan kode keluar bash ikut ditampilkan sebagai bukti konvensi kode keluar.
# jalankan tanpa argumen lalu tampilkan kode keluarnya
cargo run --manifest-path /tmp/proyek-arsip-arsitektur/Cargo.toml -q 2>&1
echo "kode keluar: $?"pemakaian: arsip-suhu <berkas-catatan>
kode keluar: 2Kesalahan umum#
Merancang tipe error setelah seluruh modul jadi. Tipe error yang ditulis belakangan cenderung menjadi kantong string karena setiap modul sudah punya pesan sendiri. Rancang variantnya lebih dahulu bersama konteks yang dibawanya, lalu tulis modul yang mengembalikan tipe itu.
Mengikat penguraian pada operasi berkas. Fungsi yang membuka berkas di dalam tubuh pengurainya tidak dapat diuji tanpa berkas sementara dan tidak dapat dipakai ulang untuk teks dari sumber lain. Pisahkan pengambilan teks dari penguraian, misalnya lewat trait sumber seperti SumberCatatan, agar pengujian cukup memakai teks yang tertulis di dalam kode.
Mencetak error dari dalam pustaka. Pustaka yang mencetak atau menggugurkan dirinya sendiri merebut keputusan pelaporan dari biner, dan pemanggil kehilangan kesempatan memilih bentuk laporan. Kembalikan Result hingga titik terluar, lalu biarkan satu fungsi pelaporan di biner yang mencetaknya.
Ringkasan#
- Proyek dibagi menjadi crate pustaka berisi modul
parser,statistik, danerror, serta crate biner tipis yang hanya membaca argumen dan mencetak hasil. ArsipErrormemetakan empat cara gagal ke empat variant dengan konteks bertipe, mengimplementasikanDisplaydansource, serta menyediakanFrom<io::Error>untuk operator?.- Trait
SumberCatatanmemisahkan asal data dari penguraian lewat implementasiPathdanstr, sehingga pengujian tidak memerlukan berkas sementara. - Biner melaporkan kegagalan dari satu titik terluar dengan menyusuri rantai
source, dan memakai kode keluar 0, 1, serta 2 menurut jenis keberhasilannya.
Referensi#
- Separating Modules into Different Files — The Rust Book — Aturan pencarian berkas untuk deklarasi modul pada proyek multi-berkas.
- Error Handling — The Rust Book
— Filosofi
Result, operator?, dan pemilihan antara panic dengan pemulihan. - std::error::Error — The Rust Standard Library
— Kontrak
Display,Debug, dansourcebagi tipe error buatan sendiri. - The Cargo Book: Package Layout — Struktur direktori baku paket Cargo beserta peran tiap direktori.