Lezione 7 di 7 · 35 min di lettura
Riepilogo e sfida finale
HTTP, mist, wisp, strade, middleware, moduli, JSON e attori in una pagina, tre domande di controllo, e un libro degli ospiti vero, con una pagina per le persone e un'API per i programmi.
Guarda quanta strada
In questo modulo hai scritto il programma che sta dall’altra parte del browser. Adesso sai:
- com’è fatta una richiesta HTTP e la sua risposta, e maneggiarle con
gleam_http; - avviare un server con mist e wisp, e parlarci con il browser e con curl;
- smistare le richieste con un
casesu percorso e metodo, e provare un gestore conwisp/simulate, senza server; - avvolgere un gestore con dei middleware, costruire pagine HTML sicure e leggere i moduli;
- scrivere un’API JSON, con il codice di stato giusto per ogni errore;
- usare un attore, passato in un contesto, per uno stato condiviso da tutte le richieste.
La pagina da tenere accanto
| Cosa | Come si scrive |
|---|---|
| Pacchetti | gleam add gleam_http wisp mist (più gleam_erlang, e gleam_json per le API) |
| Avviare il server | wisp_mist.handler(gestore, chiave) \|> mist.new \|> mist.port(8000) \|> mist.start, poi process.sleep_forever() |
| Un gestore | fn handle_request(request: Request) -> Response |
| Strade | case wisp.path_segments(request) { [] -> ..., ["books", id] -> ..., _ -> wisp.not_found() } |
| Metodi | request.method; use <- wisp.require_method(request, Get); wisp.method_not_allowed([Get, Post]) |
| Query | wisp.get_query(request) \|> list.key_find("name") |
| Risposte | wisp.ok(), wisp.created(), wisp.no_content(), wisp.redirect(to: "/"), wisp.bad_request(d), wisp.not_found() |
| Corpo | wisp.html_response(html, 200), wisp.json_response(json, 200), \|> wisp.string_body(testo) |
| Middleware | fn m(next: fn() -> Response) -> Response, usato con use <- m |
| Log ed errori | wisp.configure_logger(); use <- wisp.log_request(request); use <- wisp.rescue_crashes |
| HTML sicuro | wisp.escape_html(testo) |
| Moduli e JSON | use form <- wisp.require_form(request), form.values; use data <- wisp.require_json(request) |
| Provare | simulate.request(Get, "/"), simulate.form_body, simulate.json_body, simulate.read_body |
| Stato condiviso | un attore, in un Context; a mist: fn(request) { handle_request(request, context) } |
Trovi tutto anche nel Codex (tasto K).
Quiz di controllo
Quiz
Il tuo gestore riceve DELETE /books, ma la strada /books accetta solo GET e POST. Quale risposta è giusta?
Quiz
Un'API riceve un POST con il corpo {"title": "Dune" (manca la graffa finale) e l'intestazione content-type: application/json. Cosa risponde require_json?
Quiz
Il server conta le visite in un attore. Lo spegni e lo riaccendi. Da che numero riparte il contatore?
La sfida: il libro degli ospiti
Il libro degli ospiti è un classico dei primi siti web: una pagina dove chiunque passi può lasciare il suo nome e un messaggio, e leggere quelli degli altri. Il nostro avrà:
GET /: una pagina HTML con un modulo (nome e messaggio) e l’elenco dei messaggi, dal più recente;POST /entries: riceve il modulo, salva il messaggio e rimanda il browser alla pagina principale; se il nome o il messaggio sono vuoti (anche solo spazi), risponde400;GET /api/entries: gli stessi messaggi in JSON, per i programmi;405per i metodi sbagliati sulle tre strade,404per tutto il resto;- un log per ogni richiesta, e un
500ordinato se qualcosa si schianta.
I messaggi vivono in un attore: tutti i visitatori vedono gli stessi.
Esercizio · sul tuo computer
Il libro degli ospiti
Crea un progetto nuovo accanto a exercises, nella cartella learn-gleam:
gleam new guestbook
cd guestbook
gleam add gleam_http gleam_erlang gleam_otp gleam_json wisp mistOrganizza il codice in tre moduli, come lo scaffale del modulo 8:
src/guestbook/store.gleam: l’attore. Un tipo Entry(name: String, text: String), un Message opaco, e le funzioni start(), add(store, entry) e all(store), che restituisce i messaggi dal più recente. Un alias pub type Store = Subject(Message) rende le firme più leggibili.
src/guestbook/router.gleam: il tipo Context(store: Store) e handle_request(request, context), con i middleware e le strade della sfida.
src/guestbook.gleam: il main, che configura il log, avvia l’attore e il server sulla porta 8000.
Prima di accenderlo, prova il router senza server, con un modulo src/try_guestbook.gleam che usa simulate. Poi avvialo con gleam run, apri http://localhost:8000 e lascia un paio di messaggi dal browser. Infine, riavvia il server (così parte vuoto), e dal secondo terminale:
curl -d 'name=Ada&text=Lovely place!' localhost:8000/entries
curl -d 'name=Joe&text=Hello, <b>world</b>' localhost:8000/entries
curl -d 'name= &text=Who am I?' localhost:8000/entries
curl localhost:8000/api/entriesYou are being redirected: /
You are being redirected: /
Bad request: name and text are required
[{"name":"Joe","text":"Hello, <b>world</b>"},{"name":"Ada","text":"Lovely place!"}]Incolla qui l’output dell’ultimo comando, curl localhost:8000/api/entries.
Mostra una soluzione (prima prova da solo!)
L’attore, con lo stesso schema del contatore della lezione precedente. I messaggi nuovi si aggiungono in testa alla lista, che quindi è già in ordine dal più recente:
import gleam/erlang/process.{type Subject}
import gleam/otp/actor
pub type Entry {
Entry(name: String, text: String)
}
pub opaque type Message {
Add(entry: Entry)
All(reply_to: Subject(List(Entry)))
}
pub type Store =
Subject(Message)
pub fn start() -> Result(Store, actor.StartError) {
case actor.new([]) |> actor.on_message(handle_message) |> actor.start {
Ok(started) -> Ok(started.data)
Error(error) -> Error(error)
}
}
pub fn add(store: Store, entry: Entry) -> Nil {
actor.send(store, Add(entry))
}
/// The entries, newest first.
pub fn all(store: Store) -> List(Entry) {
actor.call(store, waiting: 1000, sending: All)
}
fn handle_message(
entries: List(Entry),
message: Message,
) -> actor.Next(List(Entry), Message) {
case message {
Add(entry) -> actor.continue([entry, ..entries])
All(reply_to) -> {
actor.send(reply_to, entries)
actor.continue(entries)
}
}
}Il router, con i middleware in cima e le strade in un case su percorso e metodo:
import gleam/http.{Get, Post}
import gleam/json
import gleam/list
import gleam/result
import gleam/string
import guestbook/store.{type Entry, type Store, Entry}
import wisp.{type Request, type Response}
pub type Context {
Context(store: Store)
}
pub fn handle_request(request: Request, context: Context) -> Response {
use <- wisp.log_request(request)
use <- wisp.rescue_crashes
case wisp.path_segments(request), request.method {
[], Get -> home_page(context)
["entries"], Post -> add_entry(request, context)
["api", "entries"], Get -> list_entries(context)
[], _ -> wisp.method_not_allowed([Get])
["entries"], _ -> wisp.method_not_allowed([Post])
["api", "entries"], _ -> wisp.method_not_allowed([Get])
_, _ -> wisp.not_found()
}
}
fn home_page(context: Context) -> Response {
let items =
store.all(context.store)
|> list.map(fn(entry) {
"<li><b>"
<> wisp.escape_html(entry.name)
<> "</b>: "
<> wisp.escape_html(entry.text)
<> "</li>"
})
|> string.concat
let html = "<h1>Guestbook</h1>
<form method='post' action='/entries'>
<input name='name' placeholder='Your name'>
<input name='text' placeholder='Your message'>
<button>Sign</button>
</form>
<ul>" <> items <> "</ul>"
wisp.html_response(html, 200)
}
fn add_entry(request: Request, context: Context) -> Response {
use form <- wisp.require_form(request)
let entry = {
use name <- result.try(field(form.values, "name"))
use text <- result.try(field(form.values, "text"))
Ok(Entry(name:, text:))
}
case entry {
Ok(entry) -> {
store.add(context.store, entry)
wisp.redirect(to: "/")
}
Error(_) -> wisp.bad_request("name and text are required")
}
}
/// A form field, trimmed, that must not be empty.
fn field(values: List(#(String, String)), name: String) -> Result(String, Nil) {
use value <- result.try(list.key_find(values, name))
case string.trim(value) {
"" -> Error(Nil)
value -> Ok(value)
}
}
fn list_entries(context: Context) -> Response {
store.all(context.store)
|> json.array(entry_to_json)
|> json.to_string
|> wisp.json_response(200)
}
fn entry_to_json(entry: Entry) -> json.Json {
json.object([
#("name", json.string(entry.name)),
#("text", json.string(entry.text)),
])
}Il modulo principale, che mette insieme i pezzi:
import gleam/erlang/process
import guestbook/router.{Context}
import guestbook/store
import mist
import wisp
import wisp/wisp_mist
pub fn main() -> Nil {
wisp.configure_logger()
let assert Ok(entries) = store.start()
let context = Context(store: entries)
let handler = fn(request) { router.handle_request(request, context) }
let assert Ok(_) =
wisp_mist.handler(handler, wisp.random_string(64))
|> mist.new
|> mist.port(8000)
|> mist.start
process.sleep_forever()
}E la prova senza server, con un attore tutto suo:
import gleam/http.{Get, Post}
import gleam/int
import gleam/io
import gleam/list
import guestbook/router.{Context}
import guestbook/store
import wisp/simulate
pub fn main() -> Nil {
let assert Ok(entries) = store.start()
let context = Context(store: entries)
[
simulate.request(Post, "/entries")
|> simulate.form_body([#("name", "Ada"), #("text", "Lovely place!")]),
simulate.request(Post, "/entries")
|> simulate.form_body([#("name", "Joe"), #("text", "Hello, <b>world</b>")]),
simulate.request(Post, "/entries")
|> simulate.form_body([#("name", " "), #("text", "Who am I?")]),
simulate.request(Get, "/api/entries"),
simulate.request(Get, "/"),
simulate.request(http.Delete, "/entries"),
simulate.request(Get, "/admin"),
]
|> list.each(fn(request) {
let response = router.handle_request(request, context)
io.println(
http.method_to_string(request.method)
<> " "
<> request.path
<> " -> "
<> int.to_string(response.status),
)
io.println(simulate.read_body(response))
})
}POST /entries -> 303
You are being redirected: /
POST /entries -> 303
You are being redirected: /
POST /entries -> 400
Bad request: name and text are required
GET /api/entries -> 200
[{"name":"Joe","text":"Hello, <b>world</b>"},{"name":"Ada","text":"Lovely place!"}]
GET / -> 200
<h1>Guestbook</h1>
<form method='post' action='/entries'>
<input name='name' placeholder='Your name'>
<input name='text' placeholder='Your message'>
<button>Sign</button>
</form>
<ul><li><b>Joe</b>: Hello, <b>world</b></li><li><b>Ada</b>: Lovely place!</li></ul>
DELETE /entries -> 405
Method not allowed
GET /admin -> 404
Not foundQualche dettaglio da notare:
- Nel JSON il messaggio di Joe resta
Hello, <b>world</b>: i dati si conservano come sono arrivati, e l’escape si fa solo quando diventano HTML. Nella pagina, infatti, il<b>compare come testo, non come grassetto. Chi legge l’API deciderà da sé come mostrarli. try_guestbooknon stampa le righe di log, perché non chiamawisp.configure_logger(): senza, Erlang mostra solo i messaggi più gravi. Il server vero, invece, scriveINFO 303 POST /entriese compagnia.fieldfa due controlli in uno: il campo c’è, e tolti gli spazi non è vuoto. Per questo il terzoPOST, con un nome fatto solo di spazi, riceve400.storenon sa niente di HTTP, erouternon sa che dietroStorec’è un attore: sa solo chiamarestore.addestore.all. Se un giorno i messaggi finissero in un database, cambierebbe solostore.gleam.
Dove andare da qui
Con questo modulo hai visto il giro completo di un programma Gleam vero: tipi e funzioni, errori come valori, processi e attori, dati dal mondo esterno, e un server che li mette al lavoro per tanti utenti insieme.
Da qui, la strada migliore è costruire qualcosa di tuo: un piccolo sito, un’API per un’idea che hai in testa, uno strumento da riga di comando. Quando ti serve qualcosa di nuovo, i pacchetti si cercano su packages.gleam.run, la documentazione di ognuno sta su hexdocs.pm, e gli esempi ufficiali di wisp mostrano come si fanno cose che qui non abbiamo visto, come i cookie, i file caricati e i database. E per ogni dubbio sul linguaggio, la fonte è sempre la stessa: la documentazione ufficiale di Gleam.