soma.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"}'

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.

Generated from soma describe on this file.

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 == "&lt;b&gt;x&lt;/b&gt;"
        assert len(entries()) == 2
        assert sign("", "no name")._status == 400
    }
}

What the toolchain says

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ 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 == "&lt;b&gt;x&lt;/b&gt;"
  ✓ 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

Source file: /corpus/web/guestbook.cell · examples/corpus/web/guestbook.cell