soma.cell

Form handler with server-side validation

POST /contact validates each field and either stores the submission or returns a 422 with field errors.

A Soma program from the web part of the verified corpus: 54 lines, cells ContactForm, ContactFormTest. It passes soma check with soma 3.2.4.

How it is built

The cell ContactForm declares 3 signals in its face: request(method: String, path: String, body: String) -> Map, submit(data: Map) -> Map and validate(data: Map) -> List; keeps 1 memory slot: submissions (Map<String, Map>, persistent and consistent). Handlers: validate (line 16), submit (line 27) and request (line 36).

The file also holds 1 test cell (ContactFormTest) 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"]

contact_form.cell

// Form handler with server-side validation. POST /contact validates each
// field and either stores the submission or returns a 422 with field errors.

cell ContactForm {
    face {
        signal request(method: String, path: String, body: String) -> Map
        signal submit(data: Map) -> Map
        signal validate(data: Map) -> List
        promise all_persistent
    }

    memory {
        submissions: Map<String, Map> [persistent, consistent]
    }

    on validate(data: Map) {
        let errors = list()
        let name = data.name ?? ""
        let email = data.email ?? ""
        let message = data.message ?? ""
        if trim(name) == "" { errors = push(errors, "name is required") }
        if contains(email, "@") == false { errors = push(errors, "email is invalid") }
        if len(trim(message)) < 5 { errors = push(errors, "message too short") }
        return errors
    }

    on submit(data: Map) {
        let errors = validate(data)
        if len(errors) > 0 { return response(422, map("ok", false, "errors", errors)) }
        let id = to_string(next_id())
        let rec = map("id", id, "name", data.name, "email", data.email, "message", data.message)
        submissions.set(id, rec)
        return map("ok", true, "id", id)
    }

    on request(method: String, path: String, body: String) {
        let req = map("method", method, "path", path)
        match req {
            {method: "GET",  path: "/"}        -> html("<form method='post' action='/contact'><input name='name'><input name='email'><textarea name='message'></textarea><button>Send</button></form>")
            {method: "POST", path: "/contact"} -> submit(from_json(body))
            _                                  -> response(404, map("error", "no route"))
        }
    }
}

cell test ContactFormTest {
    rules {
        assert len(validate(map("name", "Ada", "email", "[email protected]", "message", "hello there"))) == 0
        assert len(validate(map("name", "", "email", "bad", "message", "x"))) == 3
        assert submit(map("name", "Ada", "email", "[email protected]", "message", "hello there")).ok == true
        assert len(validate(map("name", "Ada", "email", "[email protected]", "message", "x"))) == 1
    }
}

What the toolchain says

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

$ soma check contact_form.cell
✓ All checks passed.
$ soma test contact_form.cell
test ContactFormTest ...
  ✓ assert len(validate(map("name", "Ada", "email", "[email protected]", "message", "hello there"))) == 0
  ✓ assert len(validate(map("name", "", "email", "bad", "message", "x"))) == 3
  ✓ assert submit(map("name", "Ada", "email", "[email protected]", "message", "hello there")).ok == true
  ✓ assert len(validate(map("name", "Ada", "email", "[email protected]", "message", "x"))) == 1

4 tests: 4 passed, 0 failed
$ soma verify contact_form.cell
Verifying contact_form.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

contains, from_json, html, len, list, map, next_id, push, response, to_string, trim

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