開發程式的時候,常會需要讓程式能與使用者或其它程式互動,透過文字介面、圖形介面或是通訊協定標準,從外部取得資料來進行更進一步的處理。尤其是當使用者在使用我們的程式時,由於我們沒有辦法限制他們輸入的資料一定要符合程式設計的格式,程式很可能就會因錯誤的輸入而造成錯誤的輸出,甚至對整個系統的安全性造成威脅。所以通常我們在處理外部進來的資料時,會先檢查它們的格式後再進行處理。



Validators

「Validators」是筆者開發的套件,提供了統一的用法來驗證多種不同的資料格式。比如要驗證的資料格式為「XXX」(XXX可以是網址、網域、電子郵件、Base64字串等等),「Validators」套件就會提供這個「XXX」格式的「驗證器」(命名為「XXXValidator」),用來設定這個格式允許或必須啟用的項目,像是網址需不需要包含通訊協定和連接埠等等。這些設定項目都是「ValidatorOption」列舉的值,有「Must」、「Allow」、「NotAllow」三個變體,分別表示該項目是否「必要」、「允許」還是「不允許」。也就是說,就算都是符合「XXX」格式的資料,可能會因「XXXValidator」的選項設定不同,而保有細節上的差異。

也是因為有這些細節上的差異,每種「XXX」格式,也有其包裝格式(Wrappers)。例如「Domain」(網域)這個格式,它的驗證器「DomainValidator」,可以設定這個網域資料能不能包含連接埠,和其網域名稱能不能是本地端名稱「localhost」,所以「Domain」格式就擁有「DomainLocalhostableAllowPort」、「DomainLocalhostableWithPort」、「DomainLocalhostableWithoutPort」、「DomainUnlocalhostableAllowPort」、「DomainUnlocalhostableWithPort」、「DomainUnlocalhostableWithoutPort」這六種不同的包裝格式。包裝格式可以直接被用來檢查輸入資料,並建立出可序列化與反序列化的資料模型。

Crates.io

https://crates.io/crates/validators

Cargo.toml

validators = "*"

使用方法

「validators」這個crate下,以不同的模組來區分不同的資料格式,例如「Domain」(網域)這個格式和其相關的驗證器和包裝格式,就是屬於「domain」模組。用法如下:

extern crate validators;

use validators::domain::{DomainLocalhostableWithPort};

let domain = "tool.magiclen.org:8080".to_string();

let domain = DomainLocalhostableWithPort::from_string(domain).unwrap();

assert_eq!("tool.magiclen.org:8080", domain.get_full_domain());
assert_eq!("tool.magiclen.org", domain.get_full_domain_without_port());
assert_eq!("org", domain.get_top_level_domain().unwrap());
assert_eq!("tool", domain.get_sub_domain().unwrap());
assert_eq!("magiclen", domain.get_domain());
assert_eq!(8080, domain.get_port()); // This function does not use `Option` as its return value, because the struct `DomainLocalhostableWithPort` has already made sure the input must have a port number!
自定義的格式

「Validators」套件當然不可能包山包海,把全部的資料格式都包成模組。會包成模組的主要還是如網址、網域、電子郵件、Base64字串等等常見的資料格式。但「Validators」套件也不是不能處理其它不在模組內的資料格式,只是這些格式必須由各位程式設計師來定義其反序列化的方式了!針對可轉為字串、數值和Vec結構體(也就是陣列)的資料,「Validators」套件提供了一些方便的巨集,可以快速產生出他們的包裝格式。

像是要建立出一個只接受「Hi」或「Hello」字串的包裝格式,我們可以利用「validated_customized_regex_string」巨集,搭配正規表達式「^(Hi|Hello)$」,撰寫出以下程式:

#[macro_use] extern crate validators;

validated_customized_regex_string!(Greet, "^(Hi|Hello)$");

let s = Greet::from_str("Hi").unwrap();

以上程式中,我們實作出一個「Greet」結構體,並限制這個結構體只能表示「Hi」或「Hello」字串。

「validated_customized_regex_string」巨集雖然很方便,但由於程式要使用正規表達式時,需要先耗費額外的硬體資源來編譯它,因此在比較頻繁使用狀況下,「validated_customized_regex_string」巨集所實作出來的結構體之效能可能會不盡理想。此時我們可以考慮用全域靜態變數的方式,重複使用同一個已編譯好的正規表達式實體。程式可改寫如下:

#[macro_use] extern crate validators;
#[macro_use] extern crate lazy_static;
extern crate regex;

use regex::Regex;

lazy_static! {
    static ref RE_GREET: Regex = {
        Regex::new("^(Hi|Hello)$").unwrap()
    };
}

validated_customized_regex_string!(Greet, ref RE_GREET);

let s = Greet::from_str("Hi").unwrap();

將「validated_customized_regex_string」巨集的第二個參數,從正規表達式,改為「ref」關鍵字再加上靜態變數的名稱,就可以重複使用該靜態變數所儲存的正規表達式實體了!

如果要讓我們自定義的包裝格式變成公開的結構體,只需要在名稱前加上「pub」關鍵字即可,程式如下:

#[macro_use] extern crate validators;

validated_customized_regex_string!(pub Greet, "^(Hi|Hello)$");

let s = Greet::from_str("Hi").unwrap();

至於數值資料,若我們要求資料只能夠是0到100(包含0和100)的u8數值,來表示滿分為100的分數。可以利用「validated_customized_ranged_number」巨集寫出以下程式:

#[macro_use] extern crate validators;

validated_customized_ranged_number!(Score, u8, 0, 100);

let score = Score::from_str("80").unwrap();

如果是陣列資料,可以使用「validated_customized_ranged_length_vec」巨集再搭配其它的包裝格式,來快速實作出有限制數量範圍的陣列資料格式。如下:

#[macro_use] extern crate validators;

validated_customized_regex_string!(Name, "^[A-Z][a-zA-Z]*( [A-Z][a-zA-Z]*)*$");
validated_customized_ranged_length_vec!(Names, 1, 5);

let mut names = Vec::new();

names.push(Name::from_str("Ron").unwrap());
names.push(Name::from_str("Magic Len").unwrap());

let names = Names::from_vec(names).unwrap();

如果是集合資料(不重複的資料),可以使用「validated_customized_ranged_length_hash_set」巨集再搭配其它的包裝格式,來快速實作出有限制數量範圍的HashSet資料格式。如下:

#[macro_use] extern crate validators;

use std::collections::HashSet;

validated_customized_regex_string!(Name, "^[A-Z][a-zA-Z]*( [A-Z][a-zA-Z]*)*$");
validated_customized_ranged_length_hash_set!(Names, 1, 5);

let mut names = HashSet::new();

names.insert(Name::from_str("Ron").unwrap());
names.insert(Name::from_str("Magic Len").unwrap());

let names = Names::from_hash_set(names).unwrap();

上面所提到的只是常用巨集的部份,另外還有其它比較低階的巨集,如果有需要的話就照著套件的文件來用吧!

電話號碼支援

「Validators」套件也有支援不同國家電話號碼的驗證,透過「validated_customized_phone_number」巨集可以針對不同國家建立出電話號碼的驗證器。不過想要用這個功能的話,必須開啟「phone-number」特色。「Cargo.toml」設定檔的寫法如下:

[dependencies.validators]
version = "*"
features = ["phone-number"]

用法如下:

#[macro_use] extern crate validators;

use validators::PhoneNumberCountry;

validated_customized_phone_number!(P1, PhoneNumberCountry::TW);
validated_customized_phone_number!(pub P2, PhoneNumberCountry::CN, PhoneNumberCountry::US);

let phone_number = P1::from_str("0912345678").unwrap();
assert_eq!("0912345678", phone_number.get_full_phone_number());
assert!(phone_number.get_countries().contains(&PhoneNumberCountry::TW));

let phone_number = P2::from_str("626-555-1212").unwrap();
assert_eq!("626-555-1212", phone_number.get_full_phone_number());
assert!(phone_number.get_countries().contains(&PhoneNumberCountry::US));

Rocket框架支援

Rocket是Rust的一個Web框架(Web Framework),「Validators」套件所有的包裝格式接有實作Rocket的「FromFormValue」和「FromParam」特性。如果要啟用「Validators」套件Rocket框架支援,只需要開啟「rocketly」特色,「Cargo.toml」設定檔的寫法如下:

[dependencies.validators]
version = "*"
features = ["rocketly"]

開啟「rocketly」特色後,「Validators」套件的包裝格式就是可以直接當作資料模型,用在Rocket框架中了!

用法如下:

#![feature(plugin)]
#![feature(custom_derive)]
#![plugin(rocket_codegen)]

#[macro_use] extern crate validators;

extern crate rocket;

use rocket::request::Form;

use validators::http_url::HttpUrlUnlocalableWithProtocol;
use validators::email::Email;

validated_customized_ranged_number!(PersonID, u8, 0, 100);
validated_customized_regex_string!(Name, r"^[\S ]{1,80}$");
validated_customized_ranged_number!(PersonAge, u8, 0, 130);

#[derive(Debug, FromForm)]
struct ContactModel {
    name: Name,
    age: Option<PersonAge>,
    email: Email,
    url: Option<HttpUrlUnlocalableWithProtocol>
}

#[post("/contact/<id>", data = "<model>")]
fn contact(id: PersonID, model: Form<ContactModel>) -> &'static str {
    println!("{}", id);
    println!("{:?}", model.get());
    "do something..."
}

Serde框架支援

Serde是Rust的一個資料序列化和反序列化的框架,「Validators」套件所有的包裝格式接有實作Serde的「Serialize」和「Deserialize」特性。如果要啟用「Validators」套件Serde框架支援,只需要開啟「serdely」特色,「Cargo.toml」設定檔的寫法如下:

[dependencies.validators]
version = "*"
features = ["serdely"]

開啟「serdely」特色後,「Validators」套件的包裝格式就是可以直接用於Serde框架中,在不同格式間進行序列化與反序列化了!

用法如下:

#[macro_use] extern crate validators;
#[macro_use] extern crate serde_json;

validated_customized_regex_string!(Name, "^[A-Z][a-zA-Z]*( [A-Z][a-zA-Z]*)*$");
validated_customized_ranged_length_vec!(Names, 1, 5);

let mut names = Vec::new();

names.push(Name::from_str("Ron").unwrap());
names.push(Name::from_str("Magic Len").unwrap());

let names = Names::from_vec(names).unwrap();

assert_eq!("[\"Ron\",\"Magic Len\"]", json!(names).to_string());