Aturan bisnis seperti “alamat email harus berformat sah” dan “sandi minimal delapan karakter” biasanya hidup sebagai pemeriksaan yang tersebar: setiap fungsi yang menerima string email merasa wajib memvalidasinya lagi, dan fungsi yang lupa memvalidasi diam-diam menerima data kotor. Newtype memutus penyebaran tersebut dengan menyimpan aturan di dalam tipe. Sebuah struct Email yang field-nya privat hanya bisa lahir lewat konstruktor Email::new() yang memvalidasi input dan mengembalikan Result — keberhasilan menghasilkan nilai bertipe Email adalah bukti bahwa aturan sudah terpenuhi. Setelah itu, setiap fungsi dengan parameter bertipe Email tidak perlu memvalidasi apa pun, karena compiler menolak string mentah yang mencoba masuk. Pada chapter ini kita membangun newtype Email dan Password untuk alur pendaftaran akun, lalu membuktikan bahwa upaya melewati validasi ditolak di pintu kompilasi.
Validasi yang Terserah Pemanggil #
Mari kita lihat dulu bentuk lama masalahnya. Fungsi pendaftaran dengan tanda tangan fn register(email: &str, password: &str) tidak memuat satu pun informasi aturan bisnis. Tipe &str menerima "[email protected]" dan "budi#example.com" dengan rasa yang sama, sehingga setiap lapisan yang ingin aman harus memvalidasi sendiri: handler HTTP memvalidasi, service memvalidasi lagi, dan fungsi penyimpanan tetap waswas. Validasi yang berulang tiga kali bukan tanda hati-hati, melainkan tanda tidak adanya jaminan — kalau setiap lapisan harus memeriksa, tidak ada lapisan yang bisa berhenti memeriksa.
Masalahnya lebih dalam dari sekadar duplikasi. Nilai &str tidak membawa sejarah: string yang sudah dua kali lolos pemeriksaan dan string yang belum pernah diperiksa sama-sama bertipe &str, jadi compiler tidak bisa membantu membedakannya. Konsep newtype yang sudah kita kenal dari fondasi Course menjadi kunci: bungkus nilai mentah ke dalam tipe baru, jadikan field-nya privat, dan pastikan satu-satunya jalan menciptakan tipe tersebut adalah konstruktor yang memvalidasi.
Newtype dengan Konstruktor Tervalidasi #
Program berikut mendefinisikan dua newtype domain — Email dan Password — masing-masing dengan error enum sendiri, lalu menyusun fungsi register yang menerima kedua tipe tersebut. Perhatikan bahwa seluruh validasi hanya terjadi di konstruktor:
use std::fmt;
pub struct Email {
// field privat: satu-satunya pintu masuk adalah Email::new
value: String,
}
pub enum EmailError {
Malformed,
MissingDomainDot,
}
impl fmt::Display for EmailError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
EmailError::Malformed => write!(f, "format email tidak valid"),
EmailError::MissingDomainDot => {
write!(f, "domain harus memuat titik, misalnya example.com")
}
}
}
}
impl Email {
pub fn new(input: &str) -> Result<Self, EmailError> {
let value = input.trim();
let parts: Vec<&str> = value.split('@').collect();
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
return Err(EmailError::Malformed);
}
if !parts[1].contains('.') {
return Err(EmailError::MissingDomainDot);
}
Ok(Self { value: value.to_string() })
}
pub fn as_str(&self) -> &str {
&self.value
}
}
pub struct Password {
value: String,
}
pub enum PasswordError {
TooShort { received: usize },
MissingDigit,
}
impl fmt::Display for PasswordError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PasswordError::TooShort { received } => write!(
f,
"sandi minimal 8 karakter, diterima {} karakter",
received
),
PasswordError::MissingDigit => write!(f, "sandi harus memuat minimal satu angka"),
}
}
}
impl Password {
pub fn new(input: &str) -> Result<Self, PasswordError> {
let panjang = input.chars().count();
if panjang < 8 {
return Err(PasswordError::TooShort { received: panjang });
}
if !input.chars().any(|c| c.is_ascii_digit()) {
return Err(PasswordError::MissingDigit);
}
Ok(Self { value: input.to_string() })
}
// getter tanpa awalan get_, sesuai konvensi Rust
pub fn length(&self) -> usize {
self.value.chars().count()
}
}
// Pemanggil menerima jaminan: parameter sudah tervalidasi
fn register(email: Email, password: Password) -> String {
format!("Akun {} terdaftar (sandi {} karakter)", email.as_str(), password.length())
}
fn main() {
let inputs = [
("[email protected]", "rahasia123"),
("budi#example.com", "rahasia123"),
("[email protected]", "rahasia"),
("[email protected]", "sandikuat"),
];
for (raw_email, raw_password) in inputs {
let email = match Email::new(raw_email) {
Ok(email) => email,
Err(error) => {
println!("Pendaftaran '{}' gagal: {}", raw_email, error);
continue;
}
};
let password = match Password::new(raw_password) {
Ok(password) => password,
Err(error) => {
println!("Pendaftaran '{}' gagal: {}", raw_email, error);
continue;
}
};
println!("{}", register(email, password));
}
}Output yang Dihasilkan:
Akun [email protected] terdaftar (sandi 10 karakter)
Pendaftaran 'budi#example.com' gagal: format email tidak valid
Pendaftaran '[email protected]' gagal: sandi minimal 8 karakter, diterima 7 karakter
Pendaftaran '[email protected]' gagal: sandi harus memuat minimal satu angkaMari kita bedah kode di atas:
- Baris 3-6 (
pub struct Email { value: String }): newtype membungkus satu field. Fieldvaluetidak diberipub, sehingga kode di luar modul tempatEmaildidefinisikan tidak bisa membaca apalagi menulis field tersebut secara langsung. Inilah yang membuat konstruktor menjadi pintu tunggal. - Baris 25 (
pub fn new(input: &str) -> Result<Self, EmailError>): konstruktor menerima string mentah dan mengembalikanResult. Kegagalan validasi adalah bagian dari tanda tangan, bukan panic tersembunyi — pemanggil wajib menangani kedua kemungkinan, sesuai pola section Penanganan Error. - Baris 26-27 (
let value = input.trim(); let parts ... split('@').collect()): validasi memecah teks pada tanda@. Email yang sah harus menghasilkan tepat dua bagian: nama lokal dan domain. - Baris 29-35 (dua pemeriksaan
if): aturan domain dinyatakan eksplisit — tepat satu@, kedua bagian tidak kosong, dan domain memuat titik. Setiap pelanggaran kembali sebagai varianEmailErroryang berbeda, sehingga pemanggil bisa menampilkan pesan yang tepat. - Baris 37 (
Ok(Self { value: value.to_string() })): nilai baru hanya diciptakan setelah semua pemeriksaan lolos. Mulai baris ini, nilai bertipeEmailadalah bukti berjalan bahwa formatnya sah. - Baris 68-79 (
impl Password { pub fn new ... }):Passwordmenerapkan pola yang sama untuk aturan berbeda — panjang minimal delapan karakter dan memuat angka. VarianTooShort { received: usize }membawa data konteks berapa panjang yang diterima, pola enum error kustom yang sudah kita kenal. - Baris 82-84 (
pub fn length(&self) -> usize): newtype menyembunyikan representasi, jadi akses data diberikan lewat method. Namalength()tanpa awalanget_mengikuti konvensi getter komunitas Rust — kita bahas lengkap pada chapter Rust API Guidelines. - Baris 88 (
fn register(email: Email, password: Password)): tanda tangan inilah hasil akhirnya.registertidak memvalidasi apa pun karena validasi sudah melekat pada tipe parameter — fungsi ini secara harfiah tidak bisa menerima sandi tujuh karakter. - Baris 92-117 (
fn main()): empat kombinasi data diuji; satu kombinasi lolos, tiga gagal di tempat yang berbeda. Pemanggilan validasi terjadi sekali per nilai, bukan sekali per fungsi penerima.
Menutup Pintu Bypass #
Field privat hanya bermakna bila ada batas yang dijaga. Aturan privasi Rust bekerja per modul, jadi contoh pembuktian berikut menempatkan Email di dalam modul email dan mencoba membangun nilai lewat struct literal dari luar modul tersebut. Simpan sebagai main.rs, lalu jalankan rustc --edition 2021 main.rs:
mod email {
pub struct Email {
// field privat: hanya kode di dalam modul email yang boleh menyentuh
value: String,
}
impl Email {
pub fn new(input: &str) -> Result<Self, String> {
if input.contains('@') {
Ok(Self { value: input.to_string() })
} else {
Err(String::from("format email tidak valid"))
}
}
pub fn as_str(&self) -> &str {
&self.value
}
}
}
fn main() {
// Jalur sah: lewat konstruktor yang memvalidasi
match email::Email::new("[email protected]") {
Ok(email) => println!("Email sah: {}", email.as_str()),
Err(error) => println!("Ditolak: {}", error),
}
// Upaya melewati validasi lewat struct literal
let gelap = email::Email { value: String::from("admin@root") };
println!("Email gelap: {}", gelap.as_str());
}Pesan error kompilator:
error[E0451]: field `value` of struct `Email` is private
--> main.rs:30:32
|
30 | let gelap = email::Email { value: String::from("admin@root") };
| ^^^^^ private field
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0451`.Error E0451 menutup pintu belakang: konstruktor literal email::Email { value: ... } ditolak karena field value hanya terlihat dari dalam modul email. Nilai "admin@root" yang tidak akan pernah lolos validasi new() juga tidak akan pernah masuk lewat jalur ini. Pada program contoh satu file, seluruh kode berada di modul crate root sehingga privasi berlaku hanya setelah kita memisahkan modul — pada crate nyata, newtype domain hidup di modulnya sendiri dan seluruh batas ini berlaku penuh, sesuai materi section Modul dan Visibilitas.
Jaminan yang Mengalir ke Seluruh Sistem #
Kekuatan newtype baru terasa ketika jaminannya mengalir ke tanda tangan pemanggil lain. Fungsi save_contact(email: Email), send_welcome(email: &Email), dan register(email: Email, password: Password) semuanya menerima dokumentasi yang ditegakkan compiler: parameter tersebut sudah tervalidasi. Pemanggil yang mencoba mengirim "budi#example.com" langsung ditolak dengan error tipe mismatch — E0308, error yang sudah kita kenal sejak chapter Referensi dan Borrowing — karena &str dan Email adalah tipe yang berbeda. Bandingkan dengan versi &str: satu-satunya penjaga adalah kebiasaan tiap fungsi memvalidasi sendiri.
Ringkasan #
Newtype untuk validasi domain bekerja dengan tiga lapis: field privat menutup akses langsung, konstruktor new() -> Result menjadi satu-satunya pintu masuk yang memeriksa aturan bisnis, dan tanda tangan fungsi bertipe Email atau Password menyebarkan jaminan ke seluruh sistem tanpa validasi ulang. Upaya bypass lewat struct literal ditolak compiler dengan E0451 selama newtype didefinisikan di modul terpisah dari pemanggilnya. Validasi yang tadinya tersebar kini terkumpul di satu titik pada batas sistem. Satu bidang desain API yang belum kita rapihkan adalah nasib kegagalan itu sendiri: ketika error akhirnya tampil ke pengguna, pesan apa yang boleh dibocorkan — pertanyaan yang kita jawab pada chapter Desain Error pada API.