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:

  1. Zvládnout základní webový server v Rustu (axum).
  2. Napsat si vlastní LRU mechanismus nad Vec (bez HashMap, VecDeque, LinkedHashMap apod.) — cvičení na algoritmizaci a práci s indexy/vlastnictvím dat.
  3. 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í obsah
    • mimetype — 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/json se stejnou strukturou ({ "content": "...", "mimetype": "..." })
  • 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 mimetype se 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-cmark a vrátí se jako text/html
      • Bloky kódu (```lang) obarvit přes syntect (syntax highlighting)
      • Bloky ```mermaid vyrenderovat jako SVG diagram (mermaid-svg nebo ekvivalent)
    • 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):

    1. Najdi položku, která byla nejdéle neprohlížená (viz interpretace níže).
    2. Této položce sniž počítadlo o 1.
    3. 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čí.
    4. 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í na 0 a 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, PriorityQueue atd.). 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

ÚčelKnihovna
Web frameworkaxum
Markdown → HTMLpulldown-cmark
Syntax highlightingsyntect
Mermaid diagramy → SVGmermaid-svg (nebo ekvivalentní crate renderující mermaid do SVG)
UUIDuuid (v4)
Serializaceserde, serde_json
Runtimetokio

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)

MetodaCestaPopis
GET/HTML formulář pro vložení nové paste
POST/pasteVytvoří novou paste (form-urlencoded nebo JSON), vrátí UUID
GET/paste/:uuidZobrazí obsah dle mimetype, zvýší hit počítadlo

Akceptační kritéria

  1. Lze vytvořit paste přes formulář i přes JSON request a dostanu zpět platné UUID.
  2. Zobrazení text/markdown paste správně vyrenderuje HTML, obarví kód a vykreslí mermaid diagram jako SVG.
  3. 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.
  4. Eviktovaná paste vrací po dotazu 404.
  5. Implementace úložiště a LRU logiky používá výhradně Vec (žádné asociativní kolekce).
  6. Žá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 mut nebo globální proměnnou s unsafe — 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>>>.
  • 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 axum State, 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 Vec a lineární hledání — má jen volat metody typu store.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: podle MimeKind se vybere jiný algoritmus vykreslení.
  • V Rustu to typicky není potřeba řešit přes trait objekty (dyn Trait) — stačí match nad enumem MimeKind, 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žadovat dyn/Box<dyn Trait>.

Newtype pattern

  • Doporuč obalit Uuid do 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ěj axum::response::IntoResponse.
  • Handlery pak vrací Result<T, AppError> místo unwrap()/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č Mutex a ne RwLock (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 → RwLock by nepřinesl výhodu).

SOLID principy relevantní k úkolu

  • SRP (Single Responsibility): PasteStore se stará jen o data + evikci, render.rs jen o převod obsahu, handlery jen o HTTP vstup/výstup.
  • OCP (Open/Closed): přidání nového MimeKind (např. text/csv v 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ř je Vec. Umožňuje to zítra vyměnit Vec za jinou strukturu bez zásahu do handlerů.

Testovatelnost

  • Díky Repository patternu a oddělení store.rs od 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:

  1. 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 0 a musí se pokračovat na dalšího kandidáta,
    • ověření, že po zobrazení (touch/get) položky se zvýší její hits a aktualizuje last_seen_tick,
    • hraniční případy: MAX_PASTES == 1, všechny položky se stejným last_seen_tick, evikce, když je store prázdný.
  2. 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.
  3. 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,
    • 404 na 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.rs a render.rs (čistá logika, bez HTTP — rychlé, deterministické).
  • Integrační testy pro routes.rs, ideálně přes axum::body a tower::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_PASTES a portu serveru.