8.17.2. JSONRPC-02 — Implementing a Server with dispatch_line

This tutorial shows the server side of daslib/jsonrpc: the dispatch_line convenience that wraps parsing, envelope building, notification suppression, and batch fan-out in one call. The lower-level parse_request + envelope-builder path is also covered for cases where dispatch_line doesn’t fit.

8.17.2.1. The dispatcher block

dispatch_line(line, strict, dispatcher_block) parses a wire line and invokes the dispatcher block once per well-formed request — notifications included. The block receives (method, params_json) and returns the raw JSON result string; the library wraps that result in a JSON-RPC envelope for every request that carries an id.

require daslib/jsonrpc

def dispatch(method, params_json : string) : string {
    if (method == "ping") return "\"pong\""
    if (method == "echo") return jsonrpc::compact_json_whitespace(params_json)
    return "\{\"error\":\"unknown: {method}\"}"
}

[export]
def main() {
    let resp = jsonrpc::dispatch_line("\{\"id\":1,\"method\":\"ping\"}", false) $(m, p) {
        return dispatch(m, p)
    }
    print("{resp}\n")
    // → {"jsonrpc":"2.0","id":1,"result":"pong"}
}

8.17.2.2. Notification semantics

For a notification (no id), the dispatcher still runs (so side effects happen) but the return value is discarded and dispatch_line returns "". Per JSON-RPC 2.0 §4.1, the wire MUST stay silent.

8.17.2.3. Top-level parse failure

If the wire line isn’t valid JSON, dispatch_line returns a pre-built PARSE_ERROR envelope and the dispatcher is never called. Same for top-level §6 violations such as the empty array [].

8.17.2.4. Strict vs permissive

dispatch_line(line, strict=false) is permissive: the jsonrpc field is optional and any string value is accepted. Pass strict=true to enforce §4 — a missing or non-"2.0" jsonrpc field yields INVALID_REQUEST.

8.17.2.5. When dispatch_line doesn’t fit

dispatch_line wraps all dispatcher results in a success envelope. When you need to emit specific error codes per method (METHOD_NOT_FOUND, INVALID_PARAMS), drop down to parse_request + envelope builders directly:

def handle(line : string) : string {
    var req = parse_request(line)
    var wire = ""
    if (!empty(req.error_envelope)) {
        wire = req.error_envelope
    } elif (req.method != "ping" && req.method != "echo") {
        wire = error(req.id_str, METHOD_NOT_FOUND, "no such method: {req.method}")
    } else {
        wire = response(req.id_str, dispatch(req.method, req.params_json))
    }
    free_request(req)
    return wire
}

parse_request hands back the parse tree it built in req.document, because req.params is a view into it. Close the request scope with free_request once you are done reading; the strings on ParsedRequest outlive the tree, so the assembled response is still valid afterwards. dispatch_line owns that scope itself, which is why the high-level path above frees nothing.

8.17.2.6. Running the tutorial

daslang.exe tutorials/jsonrpc/02_dispatch_line.das

Full source: tutorials/jsonrpc/02_dispatch_line.das

8.17.2.7. See also