babashka/doc/pods.md

213 lines
6.9 KiB
Markdown
Raw Normal View History

2020-05-06 19:14:14 +00:00
# Pods
2020-05-07 18:05:42 +00:00
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](https://nrepl.org/) and
[LSP](https://microsoft.github.io/language-server-protocol/) protocols,
[bencode](https://en.wikipedia.org/wiki/Bencode),
[JSON](https://www.json.org/json-en.html),
[EDN](https://github.com/edn-format/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:
- [pod-babashka-hsqldb](https://github.com/borkdude/pod-babashka-hsqldb): a pod
that allows you to create and fire queries at a
[HSQLDB](http://www.hsqldb.org/) database.
## Implementing your own pod
### Examples
Eductional examples of pods can be found [here](examples/pods):
- [pod-babashka-hsqldb](examples/pods/pod-babashka-hsqldb): a pod that allows
you to create and fire queries at a [HSQLDB](http://www.hsqldb.org/)
database. Implemented in Clojure.
- [pod-lispyclouds-sqlite](examples/pods/pod-lispyclouds-sqlite): a pod that
allows you to create and fire queries at a [sqlite](https://www.sqlite.org/)
database. Implemented in Python.
- [pod-babashka-filewatcher](examples/pods/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:
- [pod-lispyclouds-sqlite](examples/pods/pod-lispyclouds-sqlite): a pod to
communicate with [sqlite](https://www.sqlite.org/), provided by
[@lispyclouds](https://github.com/lispyclouds).
Pods created by the babashka maintainers use the identifier `babashka`:
- [pod-babashka-hsqldb](https://github.com/borkdude/pod-babashka-hsqldb): a pod
to communicate with [HSQLDB](http://www.hsqldb.org/)
### The protocol
#### Message and payload format
Exchange of _messages_ between babashka and the pod happens in the
[bencode](https://en.wikipedia.org/wiki/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](https://github.com/nrepl/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:
``` clojure
{"op" "describe"}
```
Encoded in bencode this looks like:
``` clojure
(bencode/write-bencode System/out {"op" "describe"})
;;=> d2:op8:describee
```
The pod should reply to this request with a message in the vein of:
``` clojure
{"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:
``` clojure
(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:
``` clojure
{"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:
``` clojure
{"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`.
``` clojure
{"id" "1d17f8fe-4f70-48bf-b6a9-dc004e52d056"
"out" "hello"}
```
``` clojure
{"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:
``` clojure
{"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:
``` clojure
(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"]
```