babashka/doc/pods.md
Michiel Borkent 1d46e68dda
Pod docs
2020-05-07 20:05:42 +02:00

6.9 KiB

Pods

Pods are standalone programs that can expose namespaces with functions to babashka. Pods can be created independently from babashka. Any program can be invoked as a pod, as long as it implements a protocol, the so called pod protocol. The pod protocol is influenced by and built upon battle tested technologies: the nREPL and LSP protocols, bencode, JSON, EDN and composition of UNIX command line tools in via good old stdin and stdout.

Pods are a brand new way to extend babashka and you should consider the protocol alpha for now. Breaking changes may occur as we discover better ways of doing things. Pods were introduced in babashka version 0.0.92.

Pods you can try today:

Implementing your own pod

Examples

Eductional examples of pods can be found here:

  • pod-babashka-hsqldb: a pod that allows you to create and fire queries at a HSQLDB database. Implemented in Clojure.

  • pod-lispyclouds-sqlite: a pod that allows you to create and fire queries at a sqlite database. Implemented in Python.

  • pod-babashka-filewatcher: a filewatcher pod. It exposes one function pod-babashka-filewatcher/watch and return a core.async channel to listen for change events for a file path. Implemented in Rust.

Naming

When choosing a name for your pod, considering the following naming scheme:

pod-<user-id>-<pod-name>

where <user-id> is your Github, Gitlab, etc. handle and <pod-name> describes the intent of your pod.

Examples:

Pods created by the babashka maintainers use the identifier babashka:

The protocol

Message and payload format

Exchange of messages between babashka and the pod happens in the bencode format. Bencode is a bare-bones format that only has four types:

  • integers
  • lists
  • dictionaries (maps)
  • byte strings

Additionally, payloads like args (arguments) or value (a function return value) are encoded in either JSON or EDN.

So remember: messages are in bencode, payloads (particular fields in the message) are in either JSON or EDN.

Bencode is chosen as the message format because it is a light-weight format which can be implemented in 200-300 lines of code in most languages. If pods are implemented in Clojure, they only need to depend on the bencode library and use pr-str and edn/read-string for encoding and decoding payloads. Then why not use EDN as the message format? Assuming EDN (or JSON for that matter) as the message and payload format for all pods is too constraining: other languages might already have built-in JSON support and there might not be a good EDN library available. More payload formats might be added in the future (e.g. transit).

When calling the babashka.pods/load-pod function, babashka will start the pod and leave the pod running throughout the duration of a babashka script.

describe

The first message that babashka will send to the pod on its stdin is:

{"op" "describe"}

Encoded in bencode this looks like:

(bencode/write-bencode System/out {"op" "describe"})
;;=> d2:op8:describee

The pod should reply to this request with a message in the vein of:

{"format" "json"
 "namespaces"
 [{"name" "pod.lispyclouds.sqlite"
   "vars" [{"name" "execute!"}]}]}

In this reply, the pod declares that payloads will be encoded and decoded using JSON. It also declares that the pod exposes one namespace, pod.lispyclouds.sqlite with one var execute!.

Upon receiving this message, babashka creates these namespaces and vars.

The user can load your pod with:

(require '[babashka.pods :as pods])
(pods/load-pod "pod-lispyclouds-sqlite")
(some? (find-ns 'pod.lispyclouds.sqlite)) ;;=> true
(require '[pod.lispyclouds.sqlite :as sql])

invoke

When invoking var that is related to the pod, let's call it a proxy var, babashka reaches out to the pod with the arguments encoded in JSON or EDN. The pod will then respond with a return value encoded in JSON or EDN. Babashka will then decode the return value and present the user with that.

Example: the user invokes (sql/execute! "select * from foo"). Babashka sends this message to the pod:

{"id" "1d17f8fe-4f70-48bf-b6a9-dc004e52d056"
 "var" "pod.lispyclouds.sqlite/execute!"
 "args" "[\"select * from foo\"]"

The id is unique identifier generated by babashka which correlates this request with a response from the pod.

An example response from the pod could look like:

{"id" "1d17f8fe-4f70-48bf-b6a9-dc004e52d056"
 "value" "[[1] [2]]"
 "status" "[\"done\"]"

Here, the value payload is the return value payload. The field status contains "done" so babashka knows that this is the last message related to the request with id 1d17f8fe-4f70-48bf-b6a9-dc004e52d056.

Now you know most there is to know about the pod protocol!

out and err

Pods may send messages with an out and err string value. Babashka prints these messages to *out* and *err*. Stderr from the pod is redirected to System/err.

{"id" "1d17f8fe-4f70-48bf-b6a9-dc004e52d056"
 "out" "hello"}
{"id" "1d17f8fe-4f70-48bf-b6a9-dc004e52d056"
 "err" "debug"}

Error handling

Responses may contain an ex-message string and ex-data payload string (JSON or EDN) along with an "error" value in status. This will cause babashka to throw an ex-info with the associated values.

async

Pods may implement async functions that return one or more values at any time in the future. This must be declared as part of the describe response:

{"format" "json"
 "namespaces"
 [{"name" "pod.babashka.filewatcher"
   "vars" [{"name" "watch" "async" "true"}]}]}

When calling this function from babashka, the return value is a core.async channel on which the values will be received:

(pods/load-pod "target/release/pod-babashka-filewatcher")
(def chan (pod.babashka.filewatcher/watch "/tmp"))
(require '[clojure.core.async :as async])
(loop [] (prn (async/<!! chan)) (recur))
;;=> ["changed" "/tmp"]
;;=> ["changed" "/tmp"]