Zadání: Paste server (Rust)
Cíl úkolu
Naprogramuj jednoduchý webový server pro sdílení textového/binárního obsahu (obdoba pastebin.com / hastebin), s omezenou kapacitou úložiště řízenou vlastní implementací LRU (Least Recently Used) evikce.
Účelem cvičení je:
- Zvládnout základní webový server v Rustu (axum).
- Napsat si vlastní LRU mechanismus nad
Vec(bezHashMap,VecDeque,LinkedHashMapapod.) — cvičení na algoritmizaci a práci s indexy/vlastnictvím dat. - Umět zpracovat formulářová i JSON data a vyrenderovat výstup podle typu obsahu (plain / HTML / Markdown / binární data).
Funkční požadavky
1. Vytvoření nové položky (paste)
- Endpoint pro zobrazení formuláře (GET), jednoduchý HTML formulář s poli:
content— textový/binární obsahmimetype— výběr z:text/plain,text/html,text/markdown,application/octet-stream
- Endpoint pro zpracování formuláře (POST), musí umět přijmout:
- klasický
application/x-www-form-urlencoded(odeslání z formuláře) application/jsonse stejnou strukturou ({ "content": "...", "mimetype": "..." })
- klasický
- Po úspěšném uložení se vygeneruje nové UUID v4 pro danou položku a vrátí se uživateli (např. jako přesměrování na zobrazovací endpoint, nebo v JSON odpovědi, pokud přišel JSON request).
2. Zobrazení uložené položky
- Endpoint typu
GET /paste/{uuid}. - Podle uloženého
mimetypese zvolí způsob vykreslení:text/plain→ vrátí se jako čistý text (Content-Type: text/plain)text/html→ vrátí se přímo jako HTML (opatrně, žádný sanitizing se v tomto cvičení neřeší, ale zmiň to v komentáři jako known limitation)text/markdown→ převede se na HTML pomocípulldown-cmarka vrátí se jakotext/html- Bloky kódu (
```lang) obarvit přessyntect(syntax highlighting) - Bloky
```mermaidvyrenderovat jako SVG diagram (mermaid-svgnebo ekvivalent)
- Bloky kódu (
application/octet-stream→ vrátí se jako binární data ke stažení (Content-Disposition: attachment)
- Pokud UUID neexistuje (buď nikdy neexistovalo, nebo bylo mezitím vyhozeno z LRU), vrátit
404.
3. Limit kapacity + vlastní LRU
- Server má konfigurovatelný limit počtu uložených položek (
MAX_PASTES, např. přes konstantu nebo env proměnnou). - Každá položka v sobě nese čítač/skóre používaný pro LRU rozhodování (viz níže — nejde o klasické "timestamp LRU", ale o countdown/decrement LRU, popsané zadavatelem).
Chování:
-
Při zobrazení položky (GET
/paste/{uuid}) se jí zvýší počítadlo (např.hits += 1). -
Při vytvoření nové položky, když je úložiště plné (dosažen
MAX_PASTES):- Najdi položku, která byla nejdéle neprohlížená (viz interpretace níže).
- Této položce sniž počítadlo o 1.
- Pokud počítadlo kleslo na
0, tato položka je evikována (odstraněna) a na její místo (nebo jen do Vecu) se vloží nová položka. Tím cyklus končí. - Pokud počítadlo po snížení není
0, opakuj krok 1–3 znovu (najdi další "nejdéle neprohlíženou" položku — typicky další v pořadí) — dokud se nějaká položka nevyprázdní na0a neuvolní místo.
Poznámka pro junior programátora: toto je odlišné od klasického LRU, kde se rovnou vyhazuje nejstarší záznam. Tady místo okamžité eviction "trestáš" postupně nejstarší kandidáty snížením počítadla, dokud se některý nevynuluje. Promysli si, jak si u každé položky efektivně pamatovat pořadí posledního přístupu (např. pomocí "generace"/tick counteru, který se při každém přístupu zvyšuje a ukládá se do položky spolu s daty) — a jak z toho ve
Vecu bez pomocných kolekcí najít kandidáta s nejnižší hodnotou "naposledy viděno". -
Omezení implementace: Na úložiště paste dat (i pro LRU logiku) je povoleno použít pouze
Vec(žádnýHashMap,BTreeMap,VecDeque,PriorityQueueatd.). Vyhledávání podle UUID tedy bude lineární — to je záměr cvičení.
4. Souběžnost / uložení dat
- Server běží čistě in-memory (žádná databáze, žádný soubor na disku).
- Server je synchronní v přístupu k datům — tzn. přístup ke sdílenému úložišti (Vec s pastami) musí být chráněn (např.
Mutex/RwLock), i když samotný webserver (axum/tokio) běží asynchronně. Zdůrazni v kódu / komentáři, že úmyslně nepoužíváš lock-free ani sharded přístupy — jde o jednoduchost, ne o výkon.
Doporučený tech stack
| Účel | Knihovna |
|---|---|
| Web framework | axum |
| Markdown → HTML | pulldown-cmark |
| Syntax highlighting | syntect |
| Mermaid diagramy → SVG | mermaid-svg (nebo ekvivalentní crate renderující mermaid do SVG) |
| UUID | uuid (v4) |
| Serializace | serde, serde_json |
| Runtime | tokio |
Datový model (návrh, lze upravit)
struct Paste { id: Uuid, content: Vec<u8>, // binární i textový obsah mimetype: MimeKind, // enum: PlainText, Html, Markdown, OctetStream hits: u32, // počítadlo pro LRU last_seen_tick: u64, // "generace" naposledy zobrazeno, pro nalezení kandidáta k evikci } enum MimeKind { PlainText, Html, Markdown, OctetStream, }
Endpointy (návrh)
| Metoda | Cesta | Popis |
|---|---|---|
| GET | / | HTML formulář pro vložení nové paste |
| POST | /paste | Vytvoří novou paste (form-urlencoded nebo JSON), vrátí UUID |
| GET | /paste/:uuid | Zobrazí obsah dle mimetype, zvýší hit počítadlo |
Akceptační kritéria
- Lze vytvořit paste přes formulář i přes JSON request a dostanu zpět platné UUID.
- Zobrazení
text/markdownpaste správně vyrenderuje HTML, obarví kód a vykreslí mermaid diagram jako SVG. - Po naplnění kapacity (
MAX_PASTES) se při vložení nové paste korektně provede popsaný decrement-LRU cyklus a nejméně jedna položka je eviktována. - Eviktovaná paste vrací po dotazu
404. - Implementace úložiště a LRU logiky používá výhradně
Vec(žádné asociativní kolekce). - Žádný panic při běžném provozu (neplatné UUID, neplatný mimetype, chybějící pole ve formuláři → vracet smysluplné HTTP chybové kódy, ne pád serveru).
Doporučené návrhové vzory a principy
Toto není povinná checklist, ale seznam konceptů, které se na tento úkol přirozeně hodí a junior by je měl znát/vyzkoušet. U každého je uvedeno kde a proč se hodí.
Singleton (přes sdílený stav, ne přes globální proměnnou)
- In-memory "databáze" (Vec pastů) musí existovat jen jednou po celou dobu běhu serveru a být sdílená napříč všemi request handlery.
- V Rustu se Singleton nedělá přes
static mutnebo globální proměnnou sunsafe— správný idiomatický ekvivalent je:- vytvořit stav (
Arc<Mutex<PasteStore>>) jednou při startu (main), - předat ho do axum přes
.with_state(...), - v handlerech ho přijímat přes extractor
State<Arc<Mutex<PasteStore>>>.
- vytvořit stav (
- Tím dostaneš efekt Singletonu (jedna instance, sdílený přístup), ale bez globálního stavu a bez
unsafe— je to spíš Dependency Injection stylem axumState, který Singleton nahrazuje bezpečněji.
Repository pattern
- Odděl logiku ukládání/vyhledávání/evikce pastů (repository) od HTTP handlerů (axum route funkce).
- Handler by neměl vůbec vědět, že uvnitř je
Veca lineární hledání — má jen volat metody typustore.insert(...),store.get(&uuid),store.touch(&uuid). - Výhoda: LRU logiku můžeš testovat samostatně (unit testy na
PasteStore), bez nutnosti běžícího HTTP serveru.
Strategy pattern (výběr renderovací strategie podle mimetype)
- Renderování obsahu (
plain/html/markdown/octet-stream) je klasický případ na Strategy: podleMimeKindse vybere jiný algoritmus vykreslení. - V Rustu to typicky není potřeba řešit přes trait objekty (
dyn Trait) — stačímatchnad enumemMimeKind, kde každá větev volá samostatnou funkci (render_markdown,render_plain, ...). Je to pořád Strategy, jen řešená staticky, ne přes dynamický dispatch — je dobré junior naučit, že vzor nemusí vždy vyžadovatdyn/Box<dyn Trait>.
Newtype pattern
- Doporuč obalit
Uuiddo vlastního typu (např.struct PasteId(Uuid);), aby se v kódu nedalo omylem zaměnit s jiným UUID (pokud by jich v budoucnu bylo víc druhů). Levné cvičení na typovou bezpečnost.
Separation of concerns / vrstvení aplikace
Doporučená struktura modulů, aby handler, doménová logika a HTTP vrstva nebyly semlety v jednom souboru:
src/
main.rs // sestavení app, spuštění serveru
routes.rs // axum handlery (tenká vrstva, jen převod HTTP <-> domain)
store.rs // PasteStore + LRU logika (čistá logika, žádné axum typy)
render.rs // markdown/syntax/mermaid rendering
model.rs // Paste, MimeKind, error typy
Error handling – vlastní chybový typ + IntoResponse
- Vytvoř vlastní enum
AppError(např.NotFound,InvalidMimeType,PayloadTooLarge,LockPoisoned) a implementuj pro nějaxum::response::IntoResponse. - Handlery pak vrací
Result<T, AppError>místounwrap()/panic!()— přímo naplňuje akceptační kritérium "žádný panic při běžném provozu".
Interior mutability a vlastnictví
Arc<Mutex<PasteStore>>je zde vzorové využití interior mutability — sdílený stav mezi async tasky, který potřebuje mutovatelný přístup i přes nemutovatelné sdílení (Arc).- Junior by měl umět vysvětlit, proč
Mutexa neRwLock(zápisy jsou tu časté a krátké — insert i get obě měníhits/last_seen_tick, takže čtení stejně potřebuje zápis →RwLockby nepřinesl výhodu).
SOLID principy relevantní k úkolu
- SRP (Single Responsibility):
PasteStorese stará jen o data + evikci,render.rsjen o převod obsahu, handlery jen o HTTP vstup/výstup. - OCP (Open/Closed): přidání nového
MimeKind(např.text/csvv budoucnu) by nemělo vyžadovat zásah do LRU logiky — jen přidání nové varianty enumu a nové renderovací větve. - DIP (Dependency Inversion): handlery závisí na abstrakci
PasteStore(jejím veřejném API), ne na detailu, že uvnitř jeVec. Umožňuje to zítra vyměnitVecza jinou strukturu bez zásahu do handlerů.
Testovatelnost
- Díky Repository patternu a oddělení
store.rsod HTTP vrstvy by mělo jít napsat unit testy na LRU cyklus bez nutnosti startovat server (např. simulovat naplnění store nad limit a ověřit, že padla očekávaná položka).
Testování — POVINNÉ, ne bonus
Testy nejsou volitelná položka. Testy tvoří tvrdý požadavek na cca 60 % objemu zdrojového kódu (poměr řádků testů : produkčního kódu přibližně 6:4). Bez odpovídajícího pokrytí testy se úkol nepovažuje za splněný, bez ohledu na to, jak dobře funguje aplikace samotná.
Co musí být pokryto testy:
- LRU/evikční logika (
store.rs) — nejvyšší priorita:- vložení pod limit kapacity (žádná evikce),
- vložení přesně na limit,
- vložení nad limit → ověření, že se sníží počítadlo správné položky (nejdéle neprohlížené),
- opakovaný decrement cyklus, kdy první kandidát nespadne na
0a musí se pokračovat na dalšího kandidáta, - ověření, že po zobrazení (
touch/get) položky se zvýší jejíhitsa aktualizujelast_seen_tick, - hraniční případy:
MAX_PASTES == 1, všechny položky se stejnýmlast_seen_tick, evikce, když je store prázdný.
- Rendering (
render.rs):- markdown → HTML převod (základní formátování),
- syntax highlighting bloku kódu (ověřit, že výstup obsahuje očekávané HTML/CSS třídy pro zvýraznění),
- mermaid blok → validní SVG výstup,
- plain text a octet-stream se nerenderují, jen procházejí beze změny.
- HTTP vrstva (
routes.rs) — integrační testy:- vytvoření paste přes form-urlencoded i JSON a ověření navráceného UUID,
- zobrazení existující paste pro každý
MimeKind, 404na neexistující/evikované UUID,- chybové stavy (neplatný mimetype, chybějící pole) vrací smysluplný stavový kód, ne panic.
Doporučený přístup:
- Unit testy pro
store.rsarender.rs(čistá logika, bez HTTP — rychlé, deterministické). - Integrační testy pro
routes.rs, ideálně přesaxum::bodyatower::ServiceExt::oneshot, aby se nemusel startovat skutečný TCP listener. - Testy piš průběžně s implementací (ne až na konci) — u LRU logiky to navíc pomůže odhalit chyby v návrhu dřív, než se promítnou do zbytku aplikace.
Bonus (nepovinné, pokud zbyde čas)
- Endpoint na smazání paste podle UUID.
- Limit velikosti jednotlivé paste (např. max. X kB) s vracením
413 Payload Too Large. - CLI/env konfigurace
MAX_PASTESa portu serveru.