Rust na prostym przykładzie
Język Rust reklamuje się hasłem szybki, niezawodny, wydajny - wybierz trzy
.
Został zaprojektowany jako język "systemowy" (czyli może zastąpić tradycyjnie używany C, za którym btw trochę tęsknię). Ponadto
kolejny rok z rzędu "wygrał" jako
najbardziej kochany język w ankiecie StackOverflow.
Wszystko krzyczy: "jestem dla Ciebie, weź mnie spróbuj".
No i tak sobie chodziliśmy koło siebie :) On w międzyczasie się rozwijał i parł do przodu, ja sobie czytałem, robiłem ćwiczenia, ale w końcu chyba udało się osiągnąć masę krytyczną potrzebną do wykonania kolejnego kroku. Na razie jestem na etapie tworzenia prostych narzędzi na własne potrzeby.
Tak więc w "menu" na dziś mamy proste narzędzie do znajdywania ostatniego buildu - przy okazji postaram się zaprezentować niektóre z cech języka oraz towarzyszącej infrastruktury.
Problem, cel, założenia
Problem:
- Jest sobie repozytorium buildów dla projektu (ew kilku projektów)
- Repozytorium wystawia buildy przez
http(s)
, jako zwykłe listy plików/katalogów. - Struktura katalogów jest hierarchiczna: lista branchy -> lista buildów -> lista platform -> lista plików wynikowych
Najprostszy możliwy przykład:
mkdir -p builds/master
mkdir -p builds/develop/b1/linux64
mkdir -p builds/develop/b2/linux64
mkdir -p builds/develop/b3/linux64
mkdir -p builds/develop/b1/win64
mkdir -p builds/develop/b2/win64
mkdir -p builds/develop/b3/win64
touch builds/develop/b3/linux64/findbuild_2019-09-28.snap
touch builds/develop/b3/linux64/findbuild.deb
touch builds/develop/b3/linux64/findbuild.rpm
python -m SimpleHTTPServer
pod adresem http://localhost:8000 będzie dostępne nasze repozytorium do testów.
Cel:
Znalezienie linka do pliku z ostatnim buildem z danego brancha.
Założenia
- dla każdego projektu z góry znamy url startowy
- dla każdego projektu znamy wzorzec nazywania buildów
- interesuje nas z góry określona platforma
- interesuje nas z góry określony plik wynikowy
Tworzenie projektu i jego struktura
Rust posiada własny program do zarządzania budowaniem projektów - taki odpowiednik maven
/gradle
.
Podobnie jak wymienionych istnieje możliwość stworzenia projektu z szablonu:
$ cargo new findbuild --bin
Created binary (application) `findbuild` package
tworzy projekt o nazwie findbuild
. --bin
oznacza, że będzie to aplikacja wykonywalna
(można jescze tworzyć projekt z biblioteką: --lib
). Wykonanie komendy tworzy katalog o nazwie podanego projektu wraz
z gotową strukturą (oraz repozytorium git
):
$ tree findbuild/
findbuild/
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
Sprawdzam czy to się w ogóle uruchamia:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/findbuild`
Hello, world!
Zawartośc pliku konfiguracji projektu Cargo.toml
:
[package]
name = "findbuild"
version = "0.1.0"
authors = ["Ihsahn <ihsahn@users.noreply.github.com>"]
[dependencies]
oraz zawartość jedynego pliku z kodem main.rs
:
fn main() {
println!("Hello, world!");
}
http get (razem z basic auth)
Skoro program ma operować na stronach html konieczne jest ich pobranie, czyli potrzebne nam narzędzie, które wykona HTTP GET
.
Szybkie wyszukiwanie i znalezione rzekomo najprostsze do użycia:
reqwest (i jego dokumentacja)
do sekcji [dependencies]
w Cargo.toml
dodajemy więc
reqwest = "0.9.13"
i juz można korzystać z nowej paczki w kodzie.
Po pierwsze potrzebujemy zmiennych na username, hasło oraz początkowy url:
let user = "username";
let passwd = "password";
let parent_path = "http://localhost:8000/builds/develop/";
Słowo kluczowe
let
służy do definiowania nowych zmiennych/stałych. Rust rozróżnia stałe (immutable variable
) tworzone właśnie przez użycielet
oraz takie które można modyfikować (let mut
)
Drugą cechą Rust widoczną w powyższym kodzie jest rozpoznawanie typów zmiennych na podstawie kodu/kontekstu (and
type interference
). W tym przypadku kompilator uzna, że zmienne są typu&str
Rust posiada dwa podstawowe typy określające ciąg znaków
String
oraz&str
. Ten pierwszy jest zapisany na stercie (heap
) i można go zmieniać, podczas gdy drugi jest niezmienny. Zainteresowanym szczegółami polecam bardzo fajny artykuł na ten temat: Rust: str vs String
Wykorzystanie nowej paczki możliwe jest po jej zaimportowaniu/zadeklarowaniu użycia:
extern crate reqwest;
no i teraz można już pobrać treść określonej strony
let mut response = reqwest::get("http://localhost:8000/builds/develop/")`
Operacje bedziemy powtarzać, więc dobrze by było mieć do tego osobną funkcję:
fn get_page(url: &str, user: &str, passwd: &str) -> String { //1
let client = reqwest::Client::new(); //2
let rsp = client //3
.get(url) //4
.basic_auth(user, Some(passwd)) //5
.send() //6
.unwrap(); //7
let body = rsp.text().unwrap(); //8
body //9
}
1 - definicja funkcji:
fn
określa, że będzie to funkcjaget_page
- nazwa funkcji(url: &str, user: &str, passwd: &str)
- definicja parametrów funkcji wraz z ich typami-> String
definicja typu zwracanego przez funkcję
2 - let client = reqwest::Client::new()
- stworzenie nowego klienta http i zapisanie go do zmiennej
3 - let rsp = client
- rozpoczynamy konfigurację klienta oraz pobranie danych (wynik zostanie zapisany do rsp
):
4 - get(url)
- będziemy wykonywać http get
z określonego url
5 - .basic_auth(user, Some(passwd))
- ustawiamy autentykację na basic auth
z podanym użytkownikiem i hasłem.
Hasło jest opcjonalne - typ
Option (działa to podobnie jak javowe
Optional).
W związku z tym trzeba podać, że jest tam konkretna wartość czyli Some(password)
6 - send()
- wysłanie http get
i pobranie wyniku
7 - .unwrap()
- Ponieważ wynikiem send
jest Result<Response,Error>
a nas interesuje samo Response
musimy je
jakoś wydobyć z Result
- właśnie do tego jest unwrap()
. Co prawda jeśli coś pójdzie nie tak nasz program się ładnie
wyłoży z komunikatem panic
, ale tym zajmiemy się jak już będzie działająca wersja.
8 - rsp.text().unwrap()
- pobieramy z Response
text i wykonujemy unwrap()
ponieważ ponownie musimy rozpakowac Result
.
9 - body
- zwracamy wartość body na zewnątrz (w Rust
ostatnia linia funkcji uważana jest za zwrócenie jej wartości,
dozwolone jest też użycie return
- zwłaszcza jeśli zwrot wartości miałby nastąpić we wcześniejszym fragmencie kodu)
Próba kompilacji i dostajemy błąd:
15 | let rsp = client
| --- help: consider changing this to be mutable: `mut rsp`
...
20 | let body = rsp.text().unwrap();
| ^^^ cannot borrow as mutable
No i udało nam się wpaść na to co decyduje o sile Rust
: system "własności" - to właśnie on zapewnia, że nie zrobimy
sobie krzywdy nadpisując po cichu dane w jednej funkcji a w drugiej jednocześnie czytając, oraz sprawia, że Rust
nie potrzebuje
Garbage Collectora
- ponieważ kompilator dokładnie śledzi użycia i własność obiektów i wie kiedy może je usunąć z pamięci
(bo właściciel przestał istnieć).
W tym przypadku ponieważ rsp.text()
jest zadeklarowane jako pub fn text(&mut self)
czyli wymaga zmiennej z dozwoloną modyfikacją
(operacja text()
może zmienić zawartość Response
na którym została wykonana) kompilator słusznie proponuje dodanie mut
do `rsp.
Na powyższym przykładzie widać jeszcze jedną cechę: błędy z kompilatora zazwyczaj są użyteczne
Jeszcze wykorzystanie nowej funkcji:
let page = get_page(parent_path, user, passwd);
println!("Page: {:?}", page);
pritln!
to makro (tak Rust
ma makra) do wypisywania tekstu
na stdout razem z opcjonalnym formatowaniem.
{:?}
- podajemy, że chcemy sformatować dany obiekt w celach debug
Pozostaje jeszcze jedna kwestia, pozbycie sie nadmiarowego reqwest::
z reqwest::Client
- w przypadku gdy nie mamy w
kodzie obiektów Client
z innych paczek możemy zadeklarować bezpośrednie użycie obiektu Client
poprzez dodanie
use reqwest::Client;
i usunięcie przedrostka w kodzie.
aktualny kod:
extern crate reqwest;
use reqwest::Client;
fn main() {
let user = "username";
let passwd = "password";
let parent_path = "http://localhost:8000/builds/develop/";
let page = get_page(parent_path, user, passwd);
println!("Page: {:?}", page);
}
fn get_page(url: &str, user: &str, passwd: &str) -> String {
let client = reqwest::Client::new();
let mut rsp = client
.get(url)
.basic_auth(user, Some(passwd))
.send()
.unwrap();
let body = rsp.text().unwrap();
body
}
Szukanie wersji/paczki na stronie (regexp)
Po ściągnięciu zawartości strony trzeba na niej znaleźć albo ostatni build albo katalog odpowiedni dla naszej platformy albo konkretną paczkę. Wszystkie te użycia można zaimplementować za jednym razem przy użyciu wyrażeń regularnych.
Ponownie szybkie wyszukanie odpowiedniej paczki: regex (i jego dokumentacja)
Podobnie jak poprzednio trzeba podać, że nasz projekt będzie z tej paczki korzystał:
do [dependencies]
w Cargo.toml
dopisujemy:
regex = "1.1.5"
oraz definiujemy jej użycie w kodzie:
extern crate regex;
use regex::Regex;
Na początek prosty regexp, który znajdzie katalog z ostatnim buildem, gdzie nazwa zaczyna się od b
a następnie jest
stale rosnący numer kompilacji:
let version_pattern = "(b\\d+)";
Tworzymy obiekt Regex
:
let regexp = Regex::new(version_pattern).unwrap();
do wyszukiwania wszystkich pasujących grup w danych wejściowych dostępne są dwie metody:
captures_iter
lub find_iter
, przy czym ta pierwsza (captures_iter
) zwraca iterator po
Captures
które dopiero trzeba rozpakować, żeby dostać się do pasującego tekstu.
W przypadku find_iter
dostajemy iterator po "pojedyńczych" trafieniach Match
Kod który będzie wyciągał żądaną przez nas wartość:
let value = regexp.find_iter(page.as_str()) //1
.last() //2
.unwrap() //3 Match
.as_str();//4
1 - regexp.find_iter(page.as_str())
wykonanie regexp i pobranie interatora
2 - last()
- wersje na stronie powinny być ułożone rosnąco, w związku z tym interesuje nas ostatni pasujący wynik
3 - unwrap()
- podobnie jak wcześniej musimy rozpakować Option
4 - as_str()
- pobranie z Match
wartości jako typ &str
Oczywiście pobieranie różnych tekstów będziemy powtarzać, więc potrzebujemy z powyższego kodu zrobić osobą funkcję
fn find_last_matching_url(page: String, version_pattern: &str) -> &str {
let regexp = Regex::new(version_pattern).unwrap();
let value = regexp.find_iter(page.as_str())
.last()
.unwrap()
.as_str();
value
}
oraz wywołanie:
let result = find_last_matching_url(page, version_pattern);
println!("Last build: {:?}", result);
próba kompilacji i błąd:
|
34 | let value = regexp.find_iter(page.as_str())
| ---- `page` is borrowed here
...
39 | value
| ^^^^^ returns a value referencing data owned by the current function
Tłumacząc na nasz: zwracane value
(które jest typu &str
) to najprawdopodobniej referencja do części wejściowego page
. Definiując
parametr po prostu jako page: String
pozwalamy myśleć kompilatorowi, że własnośc zmiennej powinna zostać przeniesiona do wewnątrz funkcji.
Natomast na zewnątrz zwracamy tylko wskazanie do kawałka tej zmiennej, więc kompilator nie będzie wiedział kiedy powinien zwolnić całą pamięć dla page
.
Spróbujmy więc dać na wejściu referencję:
fn find_last_matching_url(page: &String, version_pattern: &str) -> &str {
oraz subtelna zmiana w wywołaniu (dochodzi &
przed page
):
let result = find_last_matching_url(&page, version_pattern);
i kolejny błąd kompilacji:
|
32 | fn find_last_matching_url(page: &String, version_pattern: &str) -> &str {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `page` or `version_pattern`
No i tu już wszystko jasne: kompilator rozpoznał, że zwracamy coś co jest pochodną jednego z parametrów wejściowych,
ale nie jest na tyle inteligentny żeby samemu rozpoznać z którego. Musimy mu w tym pomóc: do określania jak długo dane
referencje są "ważne" (jak długi jest czas ich życia)
służą adnotacje zwane lifetimes
.
Funkcję trzeba oznaczć ile różnych lifetimes
bedzie posiadać (w naszym przypadku będzie to jedno lifetime
o nazwie a
):
fn find_last_matching_url<'a>
. Potem trzeba dodać oznaczenie do wybranego parametru, oraz do definicji zwracanego typu:
fn find_last_matching_url<'a>(page: &'a String, version_pattern: &str) -> &'a str {
i .. nie ma błędów kompilacji!
aktualny kod:
extern crate reqwest;
extern crate regex;
use reqwest::Client;
use regex::Regex;
fn main() {
let user = "username";
let passwd = "password";
let parent_path = "http://localhost:8000/builds/develop/";
let version_pattern = "(b\\d+)";
let page = get_page(parent_path, user, passwd);
println!("Page: {:?}", page);
let result = find_last_matching_url(&page, version_pattern);
println!("Last build: {:?}", result);
}
fn get_page(url: &str, user: &str, passwd: &str) -> String {
let client = Client::new();
let mut rsp = client
.get(url)
.basic_auth(user, Some(passwd))
.send()
.unwrap();
let body = rsp.text().unwrap();
body
}
fn find_last_matching_url<'a>(page: &'a String, version_pattern: &str) -> &'a str {
let regexp = Regex::new(version_pattern).unwrap();
let value = regexp.find_iter(page.as_str())
.last()
.unwrap()
.as_str();
value
}
URL konkretnej paczki buildowej
Mamy już katalog z buildem - potrzebujemy jeszcze URL konkretnej paczki buildowej.
- Po pierwsze, aktualny
result
to będzie naszebuild_version_name
- w naszym przykładzie będzie tob3
(ostatni build zdevelop
) - Wiemy, że interesują nas paczki dla
linux64
, więc dodajęlet platform = "linux64";
- Potrzebujemy teraz URL gdzie będziemy szukać konkretnej paczki:
let platform_path = parent_path + build_version_name + "/" + platform;
co oczywiście znów kończy się błędem kompilacji
|
21 | let platform_path = parent_path + build_version_name + "/" + platform;
| ----------- ^ ------------------ &str
| | |
| | `+` cannot be used to concatenate two `&str` strings
| &str
help: `to_owned()` can be used to create an owned `String` from a string reference. String concatenation appends the string on the right to the string on the left and may require reallocation. This requires ownership of the string on the left
|
21 | let platform_path = parent_path.to_owned() + build_version_name + "/" + platform;
| ^^^^^^^^^^^^^^^^^^^^^^
Znów wszystko ładnie wytłumaczone:
+
nie może zostać użyty do połączenia dwóch typów&str
(czyli referencji do części innego String)- proponuje użyć metody
to_owned()
aby zamienićparent_path
naString
(zwróćcie uwagę że kompilator podaje też powód takiej zmiany: połączenie ciągów znaków wymaga realokacji pamięci a więc również "własności" zmiennej)
Poprawnie kompilująca się wersja:
let platform_path = parent_path.to_owned() + build_version_name + "/" + platform;
Szukamy paczek snap, więc dodajemy jescze definicje regexp pasującą do nazwy paczki
let package_pattern = "(findbuild_.*?snap)";
, a następnie potrzebujemy ponownie pobrać treść strony i znaleźć na niej dane według wzorca:
let platform_page = get_page(platform_path.as_str(), user, passwd);
let package_name = find_last_matching_url(&platform_page, package_pattern);
println!("Package name: {:?}", package_name);
para wywołań get_page
i find_mathing_url
powtarza się drugi raz więc zamienimy ją na jedną funkcję:
fn get_page_and_find_matching(user: &str, passwd: &str, pattern: &str, path: &String) -> &str {
let platform_page = get_page(path.as_str(), user, passwd);
let build_package_name = find_last_matching_url(&platform_page, pattern);
build_package_name
}
Niestety kompilator protestuje, że nie wie do czego "przypiąć" zwracaną referencję &str
:
|
29 | fn get_page_and_find_matching(user: &str, passwd: &str, pattern: &str, path: &String) -> &str {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `user`, `passwd`, `pattern`, or `path`
Sytuacja jest o tyle skomplikowana, że zwracana referencja pochodzi do obiektu który jest stworzony przez get_page
w
związku z tym, można spokojnie przekazać jego "własność" na zewnątrz funkcji:
fn get_page_and_find_matching(user: &str, passwd: &str, pattern: &str, path: &str) -> String {
let platform_page = get_page(path, user, passwd);
let build_package_name = find_last_matching_url(&platform_page, pattern);
build_package_name.to_string()
}
Pozostaje nam podmienić wywołania, oraz skomponować końcowy adres paczki.
aktualny kod:
extern crate reqwest;
extern crate regex;
use reqwest::Client;
use regex::Regex;
fn main() {
let user = "username";
let passwd = "password";
let parent_path = "http://localhost:8000/builds/develop/";
let version_pattern = "(b\\d+)";
let platform = "linux64";
let package_pattern = "(findbuild_.*?snap)";
let build_version_name = get_page_and_find_matching(user, passwd, version_pattern, parent_path);
let platform_path = parent_path.to_owned() + build_version_name.as_str() + "/" + platform;
let package_name = get_page_and_find_matching(user, passwd, package_pattern, &platform_path);
let specific_build_url = platform_path + "/" + &package_name;
println!("Full url: {:?}", specific_build_url);
}
fn get_page_and_find_matching(user: &str, passwd: &str, pattern: &str, path: &str) -> String {
let platform_page = get_page(path, user, passwd);
let build_package_name = find_last_matching_url(&platform_page, pattern);
build_package_name.to_string()
}
fn get_page(url: &str, user: &str, passwd: &str) -> String {
let client = Client::new();
let mut rsp = client
.get(url)
.basic_auth(user, Some(passwd))
.send()
.unwrap();
let body = rsp.text().unwrap();
body
}
fn find_last_matching_url<'a>(page: &'a String, version_pattern: &str) -> &'a str {
let regexp = Regex::new(version_pattern).unwrap();
let value = regexp.find_iter(page.as_str())
.last()
.unwrap()
.as_str();
value
}
W kodzie powyżej zaszła jeszcze jedna ważna zmiana: build_version_name
zwracane przez get_page_and_find_matching
jest teraz typu String
w związku z tym, podczas sklejania stringów konwertujemy go na &str
: build_version_name.as_str()
Pobranie paczki z buildem
Do pobrania paczki potrzebujemy "goły" response z serwera, w związku z tym z metody get_page
wydzielę get
:
fn get(url: &str, user: &str, passwd: &str) -> Response {
let client = Client::new();
let rsp = client
.get(url)
.basic_auth(user, Some(passwd))
.send()
.unwrap();
rsp
}
fn get_page(url: &str, user: &str, passwd: &str) -> String {
let mut rsp = get(url, user, passwd);
let body = rsp.text().unwrap();
body
}
w methodzie
get
zmiennarsp
nie musi być oznaczona jakomut
ponieważ w wykonujemy na niej żadnych operacji (w tej metodzie)
Pobranie odpowiedzi z serwera z paczką:
let mut package_response = get(specific_build_url.as_str(), user, passwd);
Stworzenie pliku wynikowego:
let mut out = File::create(package_name).unwrap();
... i przepisanie zawartości z odpowiedzi do pliku
io::copy(&mut package_response, &mut out).unwrap();
aktualny kod:
extern crate reqwest;
extern crate regex;
use reqwest::{Client, Response};
use regex::Regex;
use std::fs::File;
use std::io;
fn main() {
let user = "username";
let passwd = "password";
let parent_path = "http://localhost:8000/builds/develop/";
let version_pattern = "(b\\d+)";
let platform = "linux64";
let package_pattern = "(findbuild_.*?snap)";
let build_version_name = get_page_and_find_matching(user, passwd, version_pattern, parent_path);
let platform_path = parent_path.to_owned() + build_version_name.as_str() + "/" + platform;
let package_name = get_page_and_find_matching(user, passwd, package_pattern, &platform_path);
let specific_build_url = platform_path + "/" + &package_name;
println!("Full url: {:?}", specific_build_url);
let mut package_response = get(specific_build_url.as_str(), user, passwd);
let mut out = File::create(package_name).unwrap();
io::copy(&mut package_response, &mut out).unwrap();
}
fn get_page_and_find_matching(user: &str, passwd: &str, pattern: &str, path: &str) -> String {
let platform_page = get_page(path, user, passwd);
let build_package_name = find_last_matching_url(&platform_page, pattern);
build_package_name.to_string()
}
fn get(url: &str, user: &str, passwd: &str) -> Response {
let client = Client::new();
let rsp = client
.get(url)
.basic_auth(user, Some(passwd))
.send()
.unwrap();
rsp
}
fn get_page(url: &str, user: &str, passwd: &str) -> String {
let mut rsp = get(url, user, passwd);
let body = rsp.text().unwrap();
body
}
fn find_last_matching_url<'a>(page: &'a String, version_pattern: &str) -> &'a str {
let regexp = Regex::new(version_pattern).unwrap();
let value = regexp.find_iter(page.as_str())
.last()
.unwrap()
.as_str();
value
}
także na findbuild github repo.
Podstawowe funkcjonalności działają, plik się ściąga, więc na dziś to wszystko...
todo
- pobieranie danych (adres, hasło, etc) z lini komend i/lub zmiennych środowiskowych
- obsługa błędów
- testy
tak więc... ciąg dalszy nastąpi.