Guestbook: append-only signed entries, rendered as a small HTML page
The form POSTs name+message; entries are stored in an immutable log. curl -X POST localhost:8080/sign -d '{"name":"Ada","message":"hi"}'
A Soma program from the web part of the verified corpus: 66 lines, cells Guestbook, GuestbookTest. It passes soma check with soma 3.2.4.
How it is built
The cell Guestbook declares 3 signals in its face: request(method: String, path: String, body: String) -> String, sign(name: String, message: String) -> Map and entries() -> List; keeps 1 memory slot: book (List<String>, persistent, immutable and retain). Handlers: sign (line 17), entries (line 24) and request (line 44). Private helpers: _render.
The file also holds 1 test cell (GuestbookTest) with 4 assertions, run by soma test.
The source
soma.toml
[package]
name = "web-corpus"
version = "0.1.0"
entry = "todo_state.cell"
[verify]
cells = ["TodoService"]
deadlock_free = true
eventually = ["Done", "Cancelled"]guestbook.cell
// Guestbook: append-only signed entries, rendered as a small HTML page.
// The form POSTs name+message; entries are stored in an immutable log.
// curl -X POST localhost:8080/sign -d '{"name":"Ada","message":"hi"}'
cell Guestbook {
face {
signal request(method: String, path: String, body: String) -> String
signal sign(name: String, message: String) -> Map
signal entries() -> List
promise all_persistent
}
memory {
book: List<String> [persistent, immutable, retain(5years)]
}
on sign(name: String, message: String) {
if trim(name) == "" { return response(400, map("error", "name required")) }
let entry = map("name", escape_html(name), "message", escape_html(message), "at", now())
book.push(to_json(entry))
return entry
}
on entries() {
let out = list()
for raw in book.all {
out = push(out, from_json(raw))
}
return out
}
on _render() {
let rows = ""
for raw in book.all {
let e = from_json(raw)
let name = e.name
let message = e.message
rows = concat(rows, "<li><b>{name}</b>: {message}</li>")
}
if rows == "" { rows = "<li><i>be the first to sign</i></li>" }
return concat("<h1>Guestbook</h1><ul>", concat(rows, "</ul>"))
}
on request(method: String, path: String, body: String) {
let req = map("method", method, "path", path)
match req {
{method: "GET", path: "/"} -> html(_render())
{method: "POST", path: "/sign"} -> {
let d = from_json(body)
sign(d.name, d.message)
redirect("/")
}
_ -> response(404, "not found")
}
}
}
cell test GuestbookTest {
rules {
assert sign("Ada", "hello").name == "Ada"
assert sign("<b>x</b>", "hi").name == "<b>x</b>"
assert len(entries()) == 2
assert sign("", "no name")._status == 400
}
}What the toolchain says
$ soma check guestbook.cell
warning: `rows` grows by concatenation inside a loop — each step copies the whole value, so the loop is quadratic (8 MB took 32 s, a list + join() 30 ms): push the pieces to a List and `join(parts, "")` once
--> guestbook.cell:38:13
|
38 | rows = concat(rows, "<li><b>{name}</b>: {message}</li>")
| ^
note: 'immutable' on 'book' implies 'consistent' (added automatically)
--> guestbook.cell:14:9
|
14 | book: List<String> [persistent, immutable, retain(5years)]
| ^
✓ 1 warning(s), no errors.$ soma test guestbook.cell
test GuestbookTest ...
✓ assert sign("Ada", "hello").name == "Ada"
✓ assert sign("<b>x</b>", "hi").name == "<b>x</b>"
✓ assert len(entries()) == 2
✓ assert sign("", "no name")._status == 400
4 tests: 4 passed, 0 failed$ soma verify guestbook.cell
Verifying guestbook.cell...
No state machine in this program: nothing to prove beyond `soma check` (invariants and lifecycles are what verify proves).
note: the soma.toml beside this file declares [verify] properties; none applies here (no state machine)
VERIFY OK (vacuous — no state machine)Builtins used
concat, escape_html, from_json, html, len, list, map, now, push, redirect, response, to_json, trim