Compare commits

..

2 commits

Author SHA1 Message Date
Michiel Borkent
252be31a74 wip 2020-10-13 19:39:44 +02:00
Michiel Borkent
fec742194c wip 2020-10-13 11:29:41 +02:00
21 changed files with 390 additions and 1346 deletions

View file

@ -1,28 +0,0 @@
version: 2.1
jobs:
test:
docker:
- image: clojure:openjdk-11-tools-deps-1.10.3.1087-slim-bullseye
working_directory: ~/repo
environment:
LEIN_ROOT: "true"
BABASHKA_PLATFORM: linux
resource_class: large
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "deps.edn" }}
# fallback to using latest cache if no exact match is found
- v1-dependencies-
- run: |
script/test
- save_cache:
paths:
- ~/.m2
key: v1-dependencies-{{ checksum "deps.edn" }}
workflows:
version: 2
ci:
jobs:
- test

4
.gitignore vendored
View file

@ -3,7 +3,3 @@
.lein-failures .lein-failures
/pom.xml /pom.xml
.lein-repl-history .lein-repl-history
.cache
.clj-kondo/babashka
.clj-kondo/rewrite-clj
src/scratch.clj

View file

@ -1,21 +0,0 @@
# Changelog
## Unreleased
- [#63](https://github.com/babashka/pods/issues/63): create directory before un-tarring
- [#59](https://github.com/babashka/pods/issues/59): delete port file on exit
- [#65](https://github.com/babashka/pods/issues/65): fix warnings when defining var with core name in JVM
- [#66](https://github.com/babashka/pods/issues/66): Allow metadata on fn arguments for transit+json
## v0.2.0
- [#61](https://github.com/babashka/pods/issues/61): add transit as explicit JVM dependency
- [#60](https://github.com/babashka/pods/issues/60): transform pod reader error into exception of caller
- Switch "out" and "err" messages to print and flush instead of `println` ([@justone](https://github.com/justone))
- Set TCP_NODELAY on transport socket ([@retrogradeorbit](https://github.com/retrogradeorbit))
- Allow env vars OS_NAME & OS_ARCH to override os props ([@cap10morgan](https://github.com/cap10morgan))
- [#49](https://github.com/babashka/pods/issues/49): don't log socket closed exception
## v0.1.0
Initial version

128
README.md
View file

@ -1,11 +1,9 @@
# Babashka pods # babashka.pods
[![Clojars Project](https://img.shields.io/clojars/v/babashka/babashka.pods.svg)](https://clojars.org/babashka/babashka.pods)
Babashka pods are programs that can be used as Clojure libraries by babashka. Babashka pods are programs that can be used as Clojure libraries by babashka.
This is the library to load babashka pods. It is used by This is the library to load babashka pods. It is used by
[babashka](https://github.com/babashka/babashka/) but also usable from the JVM [babashka](https://github.com/borkdude/babashka/) but also usable from the JVM
and [sci](https://github.com/borkdude/sci)-based projects other than babashka. and [sci](https://github.com/borkdude/sci)-based projects other than babashka.
<a title="Wa17gs / CC BY-SA (https://creativecommons.org/licenses/by-sa/4.0)" href="https://commons.wikimedia.org/wiki/File:Below_Golden_Gate_Bridge.jpeg"><img width="512" alt="Below Golden Gate Bridge" src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Below_Golden_Gate_Bridge.jpeg/512px-Below_Golden_Gate_Bridge.jpeg"></a> <a title="Wa17gs / CC BY-SA (https://creativecommons.org/licenses/by-sa/4.0)" href="https://commons.wikimedia.org/wiki/File:Below_Golden_Gate_Bridge.jpeg"><img width="512" alt="Below Golden Gate Bridge" src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Below_Golden_Gate_Bridge.jpeg/512px-Below_Golden_Gate_Bridge.jpeg"></a>
@ -26,7 +24,7 @@ babashka is the pod client. When a JVM invokes a pod, the JVM is the pod client.
- _message_: a message sent from the pod client to the pod or vice versa, - _message_: a message sent from the pod client to the pod or vice versa,
encoded in [bencode](https://en.wikipedia.org/wiki/Bencode) format. encoded in [bencode](https://en.wikipedia.org/wiki/Bencode) format.
- _payload_: a particular field of a _message_ encoded in a _payload format_ - _payload_: a particular field of a _message_ encoded in a _payload format_
(currently JSON, EDN or Transit JSON). Examples are `args`, `value` and `ex-data`. _ (currently only JSON or EDN). Examples are `args`, `value` and `ex-data`. _
- _pod protocol_: the documented way of exchanging messages between a _pod - _pod protocol_: the documented way of exchanging messages between a _pod
client_ and _pod_. client_ and _pod_.
@ -48,7 +46,7 @@ _below_ in Polish and Russian. In Romanian it means _bridge_
## Available pods ## Available pods
For a list of available pods, take a look For a list of available pods, take a look
[here](https://github.com/babashka/babashka/blob/master/doc/projects.md#pods). [here](https://github.com/borkdude/babashka/blob/master/doc/libraries.md#pods).
## Status ## Status
@ -72,47 +70,6 @@ On the JVM:
;;=> [#:next.jdbc{:update-count 0}] ;;=> [#:next.jdbc{:update-count 0}]
``` ```
### Where does the pod come from?
When calling `load-pod` with a string or vector of strings (or declaring it in your `bb.edn`),
the pod is looked up on the local file system (either using the PATH, or using an absolute path).
When it is called with a qualified symbol and a version - like `(load-pod 'org.babashka/aws "0.0.5")`
then it will be looked up in and downloaded from the [pod-registry](https://github.com/babashka/pod-registry). You can customize the file system location that `load-pod` will use by setting the `BABASHKA_PODS_DIR` environment variable.
By default babashka will search for a pod binary matching your system's OS and arch. If you want to download
pods for a different OS / arch (e.g. for deployment to servers), you can set one or both of the following
environment variables:
- `BABASHKA_PODS_OS_NAME=Linux` (or `Mac OS X` or any other value returned by Java's `os.name` property)
- `BABASHKA_PODS_OS_ARCH=aarch64` (or `amd64` or any other value returned by Java's `os.arch` property)
### In a babashka project
As of babashka 0.8.0 you can declare the pods your babashka project uses in your `bb.edn` file like so:
```clojure
:pods {org.babashka/hsqldb {:version "0.1.0"} ; will be downloaded from the babashka pod registry
my.local/pod {:path "../pod-my-local/my-pod-binary"
:cache false}} ; optionally disable namespace caching if you're actively working on this pod
```
Then you can just require the pods in your code like any other clojure lib:
```clojure
(ns my.project
(:require [pod.babashka.hsqldb :as sql]
[my.local.pod :as my-pod]))
(def db "jdbc:hsqldb:mem:testdb;sql.syntax_mys=true")
(sql/execute! db ["create table foo ( foo int );"])
;;=> [#:next.jdbc{:update-count 0}]
(my-pod/do-a-thing "foo")
;;=> "something"
```
The pods will then be loaded on demand when you require them. No need to call `load-pod` explicitly.
## Sci ## Sci
To use pods in a [sci](https://github.com/borkdude/sci) based project, see To use pods in a [sci](https://github.com/borkdude/sci) based project, see
@ -130,8 +87,8 @@ light weight replacement for native interop (JNI, JNA, etc.).
### Examples ### Examples
Beyond the already available pods mentioned above, educational examples of pods Beyond the already available pods mentioned above, eductional examples of pods
can be found [here](examples): can be found [here](../examples/pods):
- [pod-lispyclouds-sqlite](examples/pod-lispyclouds-sqlite): a pod that - [pod-lispyclouds-sqlite](examples/pod-lispyclouds-sqlite): a pod that
allows you to create and fire queries at a [sqlite](https://www.sqlite.org/) allows you to create and fire queries at a [sqlite](https://www.sqlite.org/)
@ -179,10 +136,10 @@ format that only has four types:
- byte strings - byte strings
Additionally, _payloads_ like `args` (arguments) or `value` (a function return Additionally, _payloads_ like `args` (arguments) or `value` (a function return
value) are encoded in either EDN, JSON or Transit JSON. value) are encoded in either JSON or EDN.
So remember: messages are in bencode, payloads (particular fields in the So remember: messages are in bencode, payloads (particular fields in the
message) are in either EDN, JSON or Transit JSON. message) are in either JSON or EDN.
Bencode is chosen as the message format because it is a light-weight format 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 which can be implemented in 200-300 lines of code in most languages. If pods are
@ -191,7 +148,7 @@ implemented in Clojure, they only need to depend on the
`edn/read-string` for encoding and decoding payloads. `edn/read-string` for encoding and decoding payloads.
So we use bencode as the first encoding and choose one of multiple richer encodings on top of this, similar to how the nREPL protocol is implemented. More So we use bencode as the first encoding and choose one of multiple richer encodings on top of this, similar to how the nREPL protocol is implemented. More
payload formats might be added in the future. payload formats might be added in the future (e.g. transit).
Other languages typically use a bencode library + a JSON library to encode payloads. Other languages typically use a bencode library + a JSON library to encode payloads.
When calling the `babashka.pods/load-pod` function, the pod client will start When calling the `babashka.pods/load-pod` function, the pod client will start
@ -212,7 +169,7 @@ Encoded in bencode this looks like:
;;=> d2:op8:describee ;;=> d2:op8:describee
``` ```
The pod should reply to this request with a message similar to: The pod should reply to this request with a message in the vein of:
``` clojure ``` clojure
{"format" "json" {"format" "json"
@ -226,9 +183,7 @@ In this reply, the pod declares that payloads will be encoded and decoded using
JSON. It also declares that the pod exposes one namespace, JSON. It also declares that the pod exposes one namespace,
`pod.lispyclouds.sqlite` with one var `execute!`. `pod.lispyclouds.sqlite` with one var `execute!`.
To encode payloads in EDN use `"edn"` and for Transit JSON use `"transit+json"`. The pod encodes the above map to bencode and writes it to stdoud. The pod client
The pod encodes the above map to bencode and writes it to stdout. The pod client
reads this message from the pod's stdout. reads this message from the pod's stdout.
Upon receiving this message, the pod client creates these namespaces and vars. Upon receiving this message, the pod client creates these namespaces and vars.
@ -254,8 +209,8 @@ As a pod user, you can load the pod with:
#### invoke #### invoke
When invoking a var that is related to the pod, let's call it a _proxy var_, the When invoking a var that is related to the pod, let's call it a _proxy var_, the
pod client reaches out to the pod with the arguments encoded in EDN, JSON or Transit JSON. The pod client reaches out to the pod with the arguments encoded in JSON or EDN. The
pod will then respond with a return value encoded in EDN, JSON or Transit JSON. The pod client pod will then respond with a return value encoded in JSON or EDN. The pod client
will then decode the return value and present the user with that. will then decode the return value and present the user with that.
Example: the user invokes `(sql/execute! "select * from foo")`. The pod client Example: the user invokes `(sql/execute! "select * from foo")`. The pod client
@ -374,55 +329,6 @@ In the pod client:
nil nil
``` ```
#### Metadata
**From pod to pod client**
*Fixed Metadata on vars*
Pods may attach metadata to functions and macros by sending data to the pod client
in a `"meta"` field as part of a `"var"` section. The metadata must be an appropriate
map, encoded as an EDN string. This is only applicable to vars in the pod and will be
ignored if the var refers to Client-side code, since metadata can already be defined
in those code blocks (see 'Dynamic Metadata' below to enable the encoding of metadata).
For example, a pod can define a function called `add`:
``` clojure
{"format" "json"
"namespaces"
[{"name" "pod.babashka.demo"
"vars" [{"name" "add"
"meta" "{:doc \"arithmetic addition of 2 arguments\" :arglists ([a b])}"}]}]}
```
*Dynamic Metadata*
Pods may send metadata on values returned to the client if metadata encoding is enabled
for the particular transport format used by the pod.
For example, if your pod uses `:transit+json` as its format, you can enable metadata
encoding by adding `:transform transit/write-meta` (or whatever transit is aliased to)
to the optional map passed to `transit/writer`. e.g.:
````clojure
(transit/writer baos :json {:transform transit/write-meta})
````
##### From pod client to pod
Currently sending metadata on arguments passed to a pod function is available only for the
`transit+json` format and can be enabled on a per var basis.
A pod can enable metadata to be read on arguments by sending the "arg-meta" field to "true"
for the var representing that function. For example:
````clojure
{:format :transit+json
:namespaces [{:name "pod.babashka.demo"
:vars [{"name" "round-trip" "arg-meta" "true"}]}]}
````
#### Deferred namespace loading #### Deferred namespace loading
When your pod exposes multiple namespaces that can be used independently from When your pod exposes multiple namespaces that can be used independently from
@ -497,7 +403,7 @@ The arguments to `babashka.pods/invoke` are:
- a pod identifier string derived from the first described namespace. - a pod identifier string derived from the first described namespace.
- the symbol of the var to invoke - the symbol of the var to invoke
- the arguments to the var - the arguments to the var
- an opts map containing `:handlers` containing callback functions: `:success`, `:error` and `:done` - an opts map containing `:handler` containing callback functions: `:success`, `:error` and `:done`
The return value of `babashka.pods/invoke` is a map containing `:result`. When The return value of `babashka.pods/invoke` is a map containing `:result`. When
not using callbacks, this is the return value from the pod var invocation. When not using callbacks, this is the return value from the pod var invocation. When
@ -523,9 +429,9 @@ callback is only called if no errors were sent by the pod.
In the above example the wrapper function calls the pod identified by In the above example the wrapper function calls the pod identified by
`"pod.babashka.filewatcher"`. It calls the var `"pod.babashka.filewatcher"`. It calls the var
`pod.babashka.filewatcher/watch*`. In `:success` it pulls out received `pod.babashka.filewatcher/watch*`. In `:on-success` it pulls out received
values, passing them to the user-provided callback. Additionally, it prints any values, passing them to the user-provided callback. Additionally, it prints any
errors received from the pod library in `:error` to `*err*`. errors received from the pod library in `:on-error` to `*err*`.
A user will then use `pod.babashka.filewatcher/watch` like this: A user will then use `pod.babashka.filewatcher/watch` like this:
@ -542,7 +448,7 @@ user=> (fw/watch "/tmp" (fn [result] (prn "result" result)))
nil nil
user=> (spit "/tmp/foobar123.txt" "foo") user=> (spit "/tmp/foobar123.txt" "foo")
nil nil
user=> "result" {:path "/private/tmp/foobar123.txt", :type :create} user=> "result" {:path "/private/tmp/foobar123.txt", :type "create"}
``` ```
## Run tests ## Run tests

2
bb.edn
View file

@ -1,2 +0,0 @@
{:tasks {test {:doc "Run tests"
:task (shell "script/test")}}}

View file

@ -1,15 +1,13 @@
{:deps {nrepl/bencode {:mvn/version "1.1.0"} {:deps {nrepl/bencode {:mvn/version "1.1.0"}
cheshire/cheshire {:mvn/version "5.10.0"} cheshire {:mvn/version "5.10.0"}}
com.cognitect/transit-clj {:mvn/version "1.0.324"}
babashka/fs {:mvn/version "0.1.6"}}
:aliases :aliases
{:sci {:sci
{:extra-deps {:extra-deps
{borkdude/sci {:git/url "https://github.com/borkdude/sci" {borkdude/sci {:git/url "https://github.com/borkdude/sci"
:sha "5aa9031eb3692a2207106076088fcab7347c2299"}}} :sha "a7f8d05f08ab150621c2403dacdd57c47ea09ff4"}}}
:test :test
{:extra-deps {:extra-deps
{cognitect/test-runner {test-runner
{:git/url "https://github.com/cognitect-labs/test-runner" {:git/url "https://github.com/cognitect-labs/test-runner"
:sha "cb96e80f6f3d3b307c59cbeb49bb0dcb3a2a780b"}} :sha "cb96e80f6f3d3b307c59cbeb49bb0dcb3a2a780b"}}
:extra-paths ["test"] :extra-paths ["test"]
@ -19,5 +17,5 @@
{lambdaisland/kaocha {:mvn/version "1.0.632"}} {lambdaisland/kaocha {:mvn/version "1.0.632"}}
:main-opts ["-m" "kaocha.runner"]} :main-opts ["-m" "kaocha.runner"]}
:test-pod :test-pod
{:extra-paths ["src" "test-pod"] {:paths ["src" "test-pod"]
:main-opts ["-m" "pod.test-pod"]}}} :main-opts ["-m" "pod.test-pod"]}}}

View file

@ -1,17 +1,15 @@
(defproject babashka/babashka.pods "0.2.0" (defproject babashka/babashka.pods "0.0.1-SNAPSHOT"
:description "babashka pods" :description "babashka pods"
:url "https://github.com/babashka/babashka.pods" :url "https://github.com/babashka/babashka.pods"
:scm {:name "git" :scm {:name "git"
:url "https://github.com/babashka/babashka.pods"} :url "https://github.com/babashka/babashka.pods"}
:license {:name "EPL-1.0" :license {:name "EPL-1.0"
:url "https://www.eclipse.org/legal/epl-1.0/"} :url "https://www.eclipse.org/legal/epl-1.0/"}
:dependencies [[org.clojure/clojure "1.10.3"] :dependencies [[org.clojure/clojure "1.10.2-alpha1"]
[nrepl/bencode "1.1.0"] [nrepl/bencode "1.1.0"]
[cheshire "5.10.0"] [cheshire "5.10.0"]]
[babashka/fs "0.1.6"]
[com.cognitect/transit-clj "1.0.329"]]
:deploy-repositories [["clojars" {:url "https://clojars.org/repo" :deploy-repositories [["clojars" {:url "https://clojars.org/repo"
:username :env/clojars_user :username :env/babashka_nrepl_clojars_user
:password :env/clojars_pass :password :env/babashka_nrepl_clojars_pass
:sign-releases false}]] :sign-releases false}]]
:profiles {:test {:dependencies [[borkdude/sci "0.2.4"]]}}) :profiles {:test {:dependencies [[borkdude/sci "0.0.13-alpha.27"]]}})

View file

@ -1,17 +0,0 @@
#!/usr/bin/env bb
(ns changelog
(:require [clojure.string :as str]))
(let [changelog (slurp "CHANGELOG.md")
replaced (str/replace changelog
#" #(\d+)"
(fn [[_ issue after]]
(format " [#%s](https://github.com/babashka/pods/issues/%s)%s"
issue issue (str after))))
replaced (str/replace replaced
#"@([a-zA-Z0-9-_]+)([, \.)])"
(fn [[_ name after]]
(format "[@%s](https://github.com/%s)%s"
name name after)))]
(spit "CHANGELOG.md" replaced))

View file

@ -1,32 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -eou pipefail clojure -A:test -n babashka.pods.jvm-test
#clojure -A:test:kaocha --focus babashka.pods.jvm-test
export BABASHKA_POD_TEST_FORMAT clojure -A:sci:test -n babashka.pods.sci-test
export BABASHKA_POD_TEST_SOCKET #clojure -A:sci:test:kaocha --focus babashka.pods.sci-test
# format = edn
BABASHKA_POD_TEST_FORMAT=edn
echo "Testing edn"
clojure -M:test -n babashka.pods.jvm-test
clojure -M:sci:test -n babashka.pods.sci-test
clojure -M:test -n babashka.pods.impl-test
# format = json
BABASHKA_POD_TEST_FORMAT=json
echo "Testing json"
clojure -M:test -n babashka.pods.jvm-test
clojure -M:sci:test -n babashka.pods.sci-test
# format = json
BABASHKA_POD_TEST_FORMAT="transit+json"
echo "Testing transit"
clojure -M:test -n babashka.pods.jvm-test
clojure -M:sci:test -n babashka.pods.sci-test
# socket = true
unset BABASHKA_POD_TEST_FORMAT
BABASHKA_POD_TEST_SOCKET=true
echo "Testing socket"
clojure -M:test -n babashka.pods.jvm-test
clojure -M:sci:test -n babashka.pods.sci-test

View file

@ -3,29 +3,12 @@
(defn load-pod (defn load-pod
([pod-spec] (load-pod pod-spec nil)) ([pod-spec] (load-pod pod-spec nil))
([pod-spec version opts]
(load-pod pod-spec (assoc opts :version version)))
([pod-spec opts] (jvm/load-pod pod-spec opts))) ([pod-spec opts] (jvm/load-pod pod-spec opts)))
(defn unload-pod (defn unload-pod
([pod-id-or-pod] (unload-pod pod-id-or-pod {})) ([pod-id] (unload-pod pod-id {}))
([pod-id-or-pod opts] (jvm/unload-pod pod-id-or-pod opts))) ([pod-id opts] (jvm/unload-pod pod-id opts)))
(defn invoke (defn invoke
([pod-id-or-pod sym args] (invoke pod-id-or-pod sym args {})) ([pod-id sym args] (invoke pod-id sym args {}))
([pod-id-or-pod sym args opts] (jvm/invoke pod-id-or-pod sym args opts))) ([pod-id sym args opts] (jvm/invoke pod-id sym args opts)))
(defmacro copy-var [name var]
`(do (def ~name ~var)
(let [m# (meta (var ~var))
doc# (:doc m#)
arglists# (:arglists m#)]
(alter-meta! (var ~name) assoc
:arglists arglists#
:doc doc#))))
#_:clj-kondo/ignore
(do
(copy-var add-transit-read-handler! jvm/add-transit-read-handler!)
(copy-var add-transit-write-handler! jvm/add-transit-write-handler!)
(copy-var set-default-transit-write-handler! jvm/set-default-transit-write-handler!))

View file

@ -1,13 +1,10 @@
(ns babashka.pods.impl (ns babashka.pods.impl
{:no-doc true} {:no-doc true}
(:refer-clojure :exclude [read]) (:refer-clojure :exclude [read])
(:require [babashka.pods.impl.resolver :as resolver] (:require [bencode.core :as bencode]
[bencode.core :as bencode]
[cheshire.core :as cheshire] [cheshire.core :as cheshire]
[clojure.edn :as edn] [clojure.edn :as edn]
[clojure.java.io :as io] [clojure.java.io :as io])
[clojure.string :as str]
[cognitect.transit :as transit])
(:import [java.io PushbackInputStream] (:import [java.io PushbackInputStream]
[java.net Socket])) [java.net Socket]))
@ -28,9 +25,6 @@
(defn bytes->string [^"[B" bytes] (defn bytes->string [^"[B" bytes]
(String. bytes)) (String. bytes))
(defn bytes->boolean [^"[B" bytes]
(= "true" (String. bytes)))
(defn get-string [m k] (defn get-string [m k]
(-> (get m k) (-> (get m k)
bytes->string)) bytes->string))
@ -39,66 +33,9 @@
(some-> (get m k) (some-> (get m k)
bytes->string)) bytes->string))
(defn get-maybe-boolean [m k]
(some-> (get m k)
bytes->boolean))
(defn next-id [] (defn next-id []
(str (java.util.UUID/randomUUID))) (str (java.util.UUID/randomUUID)))
(def ^:dynamic *pod-id* nil)
(defonce transit-read-handlers (atom {}))
(defonce transit-read-handler-maps (atom {}))
(defn update-transit-read-handler-map []
(swap! transit-read-handler-maps assoc *pod-id*
(transit/read-handler-map (get @transit-read-handlers *pod-id*))))
(defn transit-json-read [pod-id ^String s]
(with-open [bais (java.io.ByteArrayInputStream. (.getBytes s "UTF-8"))]
(let [r (transit/reader bais :json {:handlers (get @transit-read-handler-maps pod-id)})]
(transit/read r))))
;; https://www.cognitect.com/blog/2015/9/10/extending-transit
(defn add-transit-read-handler!
([tag fn]
(let [rh (transit/read-handler fn)]
(swap! transit-read-handlers assoc-in [*pod-id* tag] rh)
(update-transit-read-handler-map)
nil)))
(defonce transit-write-handlers (atom {}))
(defonce transit-write-handler-maps (atom {}))
(defn update-transit-write-handler-map []
(swap! transit-write-handler-maps assoc *pod-id*
(transit/write-handler-map (get @transit-write-handlers *pod-id*))))
;; https://www.cognitect.com/blog/2015/9/10/extending-transit
(defn add-transit-write-handler!
[classes tag fn]
(let [rh (transit/write-handler tag fn)]
(doseq [class classes]
(swap! transit-write-handlers assoc-in [*pod-id* class] rh)))
(update-transit-write-handler-map)
nil)
(defonce transit-default-write-handlers (atom {}))
(defn set-default-transit-write-handler! [tag-fn val-fn]
(let [wh (transit/write-handler tag-fn val-fn)]
(swap! transit-default-write-handlers assoc *pod-id* wh)))
(defn transit-json-write
[pod-id ^String s metadata?]
(with-open [baos (java.io.ByteArrayOutputStream. 4096)]
(let [w (transit/writer baos :json (cond-> {:handlers (get @transit-write-handler-maps pod-id)
:default-handler (get @transit-default-write-handlers pod-id)}
metadata? (assoc :transform transit/write-meta)))]
(transit/write w s)
(str baos))))
(defn invoke [pod pod-var args opts] (defn invoke [pod pod-var args opts]
(let [handlers (:handlers opts) (let [handlers (:handlers opts)
stream (:stdin pod) stream (:stdin pod)
@ -106,8 +43,7 @@
chans (:chans pod) chans (:chans pod)
write-fn (case format write-fn (case format
:edn pr-str :edn pr-str
:json cheshire/generate-string :json cheshire/generate-string)
:transit+json #(transit-json-write (:pod-id pod) % (:arg-meta opts)))
id (next-id) id (next-id)
chan (if handlers handlers chan (if handlers handlers
(promise)) (promise))
@ -131,18 +67,11 @@
#(Boolean/parseBoolean %)) #(Boolean/parseBoolean %))
name-sym (symbol name) name-sym (symbol name)
sym (symbol ns-name-str name) sym (symbol ns-name-str name)
code (get-maybe-string var "code") code (get-maybe-string var "code")]
vmeta (some-> (get var "meta")
bytes->string
edn/read-string)
name-sym (if vmeta
(with-meta name-sym vmeta)
name-sym)
metadata? (get-maybe-boolean var "arg-meta")]
[name-sym [name-sym
(or code (or code
(fn [& args] (fn [& args]
(let [res (invoke pod sym args {:async async? :arg-meta metadata?})] (let [res (invoke pod sym args {:async async?})]
res)))])) res)))]))
vars)) vars))
@ -165,39 +94,24 @@
(catch Exception e (catch Exception e
(binding [*out* *err*] (binding [*out* *err*]
(println "Cannot read JSON: " (pr-str s)) (println "Cannot read JSON: " (pr-str s))
(throw e)))))
:transit+json
(fn [s]
(try (transit-json-read (:pod-id pod) s)
(catch Exception e
(binding [*out* *err*]
(println "Cannot read Transit JSON: " (pr-str s))
(throw e))))))] (throw e))))))]
(binding [*pod-id* (:pod-id pod)]
(try (try
(loop [] (loop []
(let [reply (try (read stdout) (let [reply (try (read stdout)
(catch java.io.EOFException _ (catch java.io.EOFException _
::EOF) ::EOF))]
(catch java.net.SocketException e
(if (= "Socket closed" (ex-message e))
::EOF
(throw e))))]
(when-not (identical? ::EOF reply) (when-not (identical? ::EOF reply)
(let [id (get reply "id") (let [id (get reply "id")
id (bytes->string id) id (bytes->string id)
value* (find reply "value") value* (find reply "value")
[exception value] (try (some->> value* value (some-> value*
second second
bytes->string bytes->string
read-fn read-fn)
(vector nil))
(catch Exception e
[e nil]))
status (get reply "status") status (get reply "status")
status (set (map (comp keyword bytes->string) status)) status (set (map (comp keyword bytes->string) status))
error? (or exception (contains? status :error)) error? (contains? status :error)
done? (or error? exception (contains? status :done)) done? (or error? (contains? status :done))
[ex-message ex-data] [ex-message ex-data]
(when error? (when error?
[(or (some-> (get reply "ex-message") [(or (some-> (get reply "ex-message")
@ -215,9 +129,8 @@
:vars (bencode->vars pod name-str v)})) :vars (bencode->vars pod name-str v)}))
chan (get @chans id) chan (get @chans id)
promise? (instance? clojure.lang.IPending chan) promise? (instance? clojure.lang.IPending chan)
exception (or exception exception (when (and promise? error?)
(when (and promise? error?) (ex-info ex-message ex-data))
(ex-info ex-message ex-data)))
;; NOTE: if we need more fine-grained handlers, we will add ;; NOTE: if we need more fine-grained handlers, we will add
;; a :raw handler that will just get the bencode message's raw ;; a :raw handler that will just get the bencode message's raw
;; data ;; data
@ -229,16 +142,6 @@
bytes->string) bytes->string)
err (some-> (get reply "err") err (some-> (get reply "err")
bytes->string)] bytes->string)]
;; NOTE: write to out and err before delivering promise for making
;; listening to output synchronous.
(when out
(binding [*out* out-stream]
(print out)
(.flush ^java.io.Writer out-stream)))
(when err
(binding [*out* err-stream]
(print err)
(.flush ^java.io.Writer err-stream)))
(when (or value* error? namespace) (when (or value* error? namespace)
(cond promise? (cond promise?
(deliver chan (cond error? exception (deliver chan (cond error? exception
@ -253,39 +156,39 @@
(when promise? (when promise?
(deliver chan nil)) (deliver chan nil))
(when done-handler (when done-handler
(done-handler)))) (done-handler)))
(when out
(binding [*out* out-stream]
(println out)))
(when err (binding [*out* err-stream]
(println err))))
(recur)))) (recur))))
(catch Exception e (catch Exception e
(binding [*out* *err* #_err-stream] (binding [*out* *err* #_err-stream]
(prn e))))))) (prn e))))))
(def pods (atom {})) (def pods (atom {}))
(defn get-pod-id [x]
(if (map? x)
(:pod/id x)
x))
(defn lookup-pod [pod-id] (defn lookup-pod [pod-id]
(get @pods pod-id)) (get @pods pod-id))
(defn destroy* [{:keys [:stdin :process :ops]}] (defn destroy [pod-id]
(if (contains? ops :shutdown) (when-let [pod (lookup-pod pod-id)]
(do (write stdin (if (contains? (:ops pod) :shutdown)
(do (write (:stdin pod)
{"op" "shutdown" {"op" "shutdown"
"id" (next-id)}) "id" (next-id)})
(.waitFor ^Process process)) (.waitFor ^Process (:process pod)))
(.destroy ^Process process))) (.destroy ^Process (:process pod)))
(defn destroy [pod-id-or-pod]
(let [pod-id (get-pod-id pod-id-or-pod)]
(when-let [pod (lookup-pod pod-id)]
(destroy* pod)
(when-let [rns (:remove-ns pod)] (when-let [rns (:remove-ns pod)]
(doseq [[ns-name _] (:namespaces pod)] (doseq [[ns-name _] (:namespaces pod)]
(rns ns-name)))) (rns ns-name)))))
(swap! pods dissoc pod-id)
nil)) (def next-pod-id
(let [counter (atom 0)]
(fn []
(let [[o _] (swap-vals! counter inc)]
o))))
(def bytes->symbol (def bytes->symbol
(comp symbol bytes->string)) (comp symbol bytes->string))
@ -309,125 +212,54 @@
the socket is connected." the socket is connected."
^Socket ^Socket
[^String hostname ^Integer port] [^String hostname ^Integer port]
(doto (Socket. hostname port) (Socket. hostname port))
(.setTcpNoDelay true)))
(defn close-socket (defn close-socket
"Close the socket, and also closes its input and output streams." "Close the socket, and also closes its input and output streams."
[^Socket socket] [^Socket socket]
(try (.close socket) (.close socket))
nil
(catch java.net.SocketException _ nil)))
(defn port-file [pid] (defn gobbler [^java.io.InputStream is]
(doto (io/file (str ".babashka-pod-" pid ".port")) (future
(.deleteOnExit)))
(defn read-port [^java.io.File port-file]
(loop [] (loop []
(let [f port-file] (let [v (.read is)]
(if-let [s (when (.exists f) (when-not (= -1 v)
(let [s (slurp f)] (print (char v))
(when (str/ends-with? s "\n") (recur))))))
(str/trim s))))]
(Integer/parseInt s) (defn read-port [pid]
1888 #_(loop []
(let [f (io/file (str ".babashka/pods/" pid ".port"))]
(if (.exists f)
(edn/read-string (slurp f))
(recur))))) (recur)))))
(defn debug [& strs]
(binding [*out* *err*]
(println (str/join " " (map pr-str strs)))))
(defn resolve-pod [pod-spec {:keys [:version :path :force] :as opts}]
(when (qualified-symbol? pod-spec)
(when (and (not version) (not path))
(throw (IllegalArgumentException. "Version or path must be provided")))
(when (and version path)
(throw (IllegalArgumentException. "You must provide either version or path, not both"))))
(let [resolved (when (and (qualified-symbol? pod-spec) version)
(resolver/resolve pod-spec version force))
opts (if resolved
(if-let [extra-opts (:options resolved)]
(merge opts extra-opts)
opts)
opts)
pod-spec (cond
resolved [(:executable resolved)]
path [path]
(string? pod-spec) [pod-spec]
:else pod-spec)]
{:pod-spec pod-spec, :opts opts}))
(defn run-pod [pod-spec {:keys [:transport] :as _opts}]
(let [pb (ProcessBuilder. ^java.util.List pod-spec)
socket? (identical? :socket transport)
_ (if socket?
(.inheritIO pb)
(.redirectError pb java.lang.ProcessBuilder$Redirect/INHERIT))
_ (cond-> (doto (.environment pb)
(.put "BABASHKA_POD" "true"))
socket? (.put "BABASHKA_POD_TRANSPORT" "socket"))
p (.start pb)
port-file (when socket? (port-file (.pid p)))
socket-port (when socket? (read-port port-file))
[socket stdin stdout]
(if socket?
(let [^Socket socket
(loop []
(if-let [sock (try (create-socket "localhost" socket-port)
(catch java.net.ConnectException _
nil))]
sock
(recur)))]
[socket
(.getOutputStream socket)
(PushbackInputStream. (.getInputStream socket))])
[nil (.getOutputStream p) (java.io.PushbackInputStream. (.getInputStream p))])]
{:process p
:socket socket
:stdin stdin
:stdout stdout}))
(defn describe-pod [{:keys [:stdin :stdout]}]
(write stdin {"op" "describe"
"id" (next-id)})
(read stdout))
(defn describe->ops [describe-reply]
(some->> (get describe-reply "ops") keys (map keyword) set))
(defn describe->metadata [describe-reply resolve-fn]
(let [format (-> (get describe-reply "format") bytes->string keyword)
ops (describe->ops describe-reply)
readers (when (identical? :edn format)
(read-readers describe-reply resolve-fn))]
{:format format, :ops ops, :readers readers}))
(defn run-pod-for-metadata [pod-spec opts]
(let [running-pod (run-pod pod-spec opts)
describe-reply (describe-pod running-pod)
ops (describe->ops describe-reply)]
(destroy* (assoc running-pod :ops ops))
describe-reply))
(defn load-pod-metadata [unresolved-pod-spec {:keys [:download-only] :as opts}]
(let [{:keys [:pod-spec :opts]} (resolve-pod unresolved-pod-spec opts)]
(if download-only
(resolver/warn "Not running pod" unresolved-pod-spec "to pre-cache metadata because OS and/or arch are different than system")
(run-pod-for-metadata pod-spec opts))))
(defn load-pod (defn load-pod
([pod-spec] (load-pod pod-spec nil)) ([pod-spec] (load-pod pod-spec nil))
([pod-spec opts] ([pod-spec {:keys [:remove-ns :resolve :socket :inherit-io]}]
(let [{:keys [:pod-spec :opts]} (resolve-pod pod-spec opts) (let [pod-spec (if (string? pod-spec) [pod-spec] pod-spec)
{:keys [:remove-ns :resolve]} opts pb (ProcessBuilder. ^java.util.List pod-spec)
_ (if inherit-io
{p :process, stdin :stdin, stdout :stdout, socket :socket (.inheritIO pb)
:as running-pod} (.redirectError pb java.lang.ProcessBuilder$Redirect/INHERIT))
(run-pod pod-spec opts) _ (doto (.environment pb)
(.put "BABASHKA_POD" "true"))
reply (or (:metadata opts) p (.start pb)
(describe-pod running-pod)) pid (.pid p)
{:keys [:format :ops :readers]} (describe->metadata reply resolve) socket-port (when socket (read-port pid))
[stdin stdout _gobbler]
(if socket
(let [socket (create-socket "localhost" socket-port)]
[(.getOutputStream socket)
(PushbackInputStream. (.getInputStream socket))])
[(.getOutputStream p) (java.io.PushbackInputStream. (.getInputStream p))])
_ (write stdin {"op" "describe"
"id" (next-id)})
reply (read stdout)
format (-> (get reply "format") bytes->string keyword)
ops (some->> (get reply "ops") keys (map keyword) set)
readers (when (identical? :edn format)
(read-readers reply resolve))
pod {:process p pod {:process p
:pod-spec pod-spec :pod-spec pod-spec
:stdin stdin :stdin stdin
@ -439,16 +271,11 @@
:err *err* :err *err*
:remove-ns remove-ns :remove-ns remove-ns
:readers readers} :readers readers}
_ (add-shutdown-hook! #(destroy pod))
pod-namespaces (get reply "namespaces") pod-namespaces (get reply "namespaces")
pod-id (or (when-let [ns (first pod-namespaces)] pod-id (or (when-let [ns (first pod-namespaces)]
(get-string ns "name")) (get-string ns "name"))
(next-id)) (next-id))
_ (add-shutdown-hook! #(do
(destroy pod-id)
(when socket
;; this probably isn't necessary because we
;; killed the process, but anyway
(close-socket socket))))
pod (assoc pod :pod-id pod-id) pod (assoc pod :pod-id pod-id)
pod-namespaces (mapv #(bencode->namespace pod %) pod-namespaces (mapv #(bencode->namespace pod %)
pod-namespaces) pod-namespaces)
@ -468,9 +295,8 @@
@prom)) @prom))
(defn invoke-public [pod-id fn-sym args opts] (defn invoke-public [pod-id fn-sym args opts]
(let [pod-id (get-pod-id pod-id) (let [pod (lookup-pod pod-id)]
pod (lookup-pod pod-id)]
(invoke pod fn-sym args opts))) (invoke pod fn-sym args opts)))
(defn unload-pod [pod-id-or-pod] (defn unload-pod [pod-id]
(destroy pod-id-or-pod)) (destroy pod-id))

View file

@ -1,225 +0,0 @@
(ns babashka.pods.impl.resolver
{:no-doc true}
(:refer-clojure :exclude [resolve])
(:require [clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.java.shell :refer [sh]]
[clojure.string :as str])
(:import [java.net URL HttpURLConnection]
[java.nio.file Files]))
(set! *warn-on-reflection* true)
(defn normalize-arch [arch]
(if (= "amd64" arch)
"x86_64"
arch))
(defn normalize-os [os]
(-> os str/lower-case (str/replace #"\s+" "_")))
(def os
(delay
{:os/name (or (System/getenv "BABASHKA_PODS_OS_NAME")
(System/getProperty "os.name"))
:os/arch (let [arch (or (System/getenv "BABASHKA_PODS_OS_ARCH")
(System/getProperty "os.arch"))]
(normalize-arch arch))}))
(defn warn [& strs]
(binding [*out* *err*]
(apply println strs)))
(defn match-artifacts
([package] (match-artifacts package (:os/arch @os)))
([package arch]
(let [artifacts (:pod/artifacts package)
res (filter (fn [{os-name :os/name
os-arch :os/arch}]
(let [os-arch (normalize-arch os-arch)]
(and (re-matches (re-pattern os-name) (:os/name @os))
(re-matches (re-pattern os-arch)
arch))))
artifacts)]
(if (empty? res)
(if (and (= "Mac OS X" (:os/name @os))
(= "aarch64" (:os/arch @os)))
;; Rosetta2 fallback on Apple M1 machines
(match-artifacts package "x86_64")
(throw (IllegalArgumentException. (format "No executable found for pod %s (%s) and OS %s/%s"
(:pod/name package)
(:pod/version package)
(:os/name @os)
(:os/arch @os)))))
res))))
(defn unzip [{:keys [^java.io.File zip-file
^java.io.File destination-dir
verbose]}]
(when verbose (warn "Unzipping" (.getPath zip-file) "to" (.getPath destination-dir)))
(let [output-path (.toPath destination-dir)
zip-file (io/file zip-file)
_ (.mkdirs (io/file destination-dir))]
(with-open
[fis (Files/newInputStream (.toPath zip-file) (into-array java.nio.file.OpenOption []))
zis (java.util.zip.ZipInputStream. fis)]
(loop []
(let [entry (.getNextEntry zis)]
(when entry
(let [entry-name (.getName entry)
new-path (.resolve output-path entry-name)]
(if (.isDirectory entry)
(Files/createDirectories new-path ^"[Ljava.nio.file.attribute.FileAttribute;"
(into-array java.nio.file.attribute.FileAttribute []))
(Files/copy ^java.io.InputStream zis
new-path
^"[Ljava.nio.file.CopyOption;"
(into-array
java.nio.file.CopyOption
[java.nio.file.StandardCopyOption/REPLACE_EXISTING]))))
(recur)))))))
(defn un-tgz [^java.io.File zip-file ^java.io.File destination-dir verbose?]
(when verbose? (warn "Unzipping" (.getPath zip-file) "to" (.getPath destination-dir)))
(let [tmp-file (java.io.File/createTempFile "glam" ".tar")
output-path (.toPath tmp-file)]
(with-open
[fis (Files/newInputStream (.toPath zip-file) (into-array java.nio.file.OpenOption []))
zis (java.util.zip.GZIPInputStream. fis)]
(Files/copy ^java.io.InputStream zis
output-path
^"[Ljava.nio.file.CopyOption;"
(into-array
[java.nio.file.StandardCopyOption/REPLACE_EXISTING])))
(.mkdirs destination-dir)
(let [res (sh "tar" "xf" (.getPath tmp-file) "--directory" (.getPath destination-dir))]
(when-not (zero? (:exit res))
(throw (ex-info (:err res) res))))
(.delete tmp-file)))
(defn make-executable [dest-dir executables verbose?]
(doseq [e executables]
(let [f (io/file dest-dir e)]
(when verbose? (warn "Making" (.getPath f) "executable."))
(.setExecutable f true))))
(defn download [source ^java.io.File dest verbose?]
(when verbose? (warn "Downloading" source "to" (.getPath dest)))
(let [source (URL. source)
dest (io/file dest)
conn ^HttpURLConnection (.openConnection ^URL source)]
(.setInstanceFollowRedirects conn true)
(.connect conn)
(io/make-parents dest)
(with-open [is (.getInputStream conn)]
(io/copy is dest))))
(defn repo-dir []
(io/file (if-let [pods-dir (System/getenv "BABASHKA_PODS_DIR")]
(io/file pods-dir)
(io/file (or
(System/getenv "XDG_DATA_HOME")
(System/getProperty "user.home"))
".babashka"
"pods"))
"repository"))
(def pods-repo-dir
;; wrapped in delay for GraalVM native-image
(delay
(repo-dir)))
(defn github-url [qsym version]
(format
"https://raw.githubusercontent.com/babashka/pod-registry/master/manifests/%s/%s/manifest.edn"
qsym version))
(defn pod-manifest
[qsym version force?]
(let [f (io/file @pods-repo-dir (str qsym) (str version) "manifest.edn")]
(if (and (not force?)
(.exists f))
(edn/read-string (slurp f))
(do (download (github-url qsym version) f false)
(edn/read-string (slurp f))))))
(defn cache-dir
^java.io.File
[{pod-name :pod/name
pod-version :pod/version}]
(let [base-file
(if-let [pods-dir (System/getenv "BABASHKA_PODS_DIR")]
(io/file pods-dir)
(io/file (or
(System/getenv "XDG_CACHE_HOME")
(System/getProperty "user.home"))
".babashka"
"pods"))]
(io/file base-file
"repository"
(str pod-name)
pod-version
(normalize-os (:os/name @os))
(:os/arch @os))))
(defn data-dir
^java.io.File
[{pod-name :pod/name
pod-version :pod/version}]
(io/file @pods-repo-dir
(str pod-name)
pod-version
(normalize-os (:os/name @os))
(:os/arch @os)))
(defn sha256 [file]
(let [buf (byte-array 8192)
digest (java.security.MessageDigest/getInstance "SHA-256")]
(with-open [bis (io/input-stream (io/file file))]
(loop []
(let [count (.read bis buf)]
(when (pos? count)
(.update digest buf 0 count)
(recur)))))
(-> (.encode (java.util.Base64/getEncoder)
(.digest digest))
(String. "UTF-8"))))
(defn resolve [qsym version force?]
(when-not (string? version)
(throw (IllegalArgumentException. "Version must be provided for resolving from pod registry!")))
(when-let [manifest (pod-manifest qsym version force?)]
(let [artifacts (match-artifacts manifest)
cdir (cache-dir manifest)
ddir (data-dir manifest)
execs (mapv (fn [artifact]
(let [url (:artifact/url artifact)
file-name (last (str/split url #"/"))
cache-file (io/file cdir file-name)
executable (io/file ddir (:artifact/executable artifact))]
(when (or force? (not (.exists executable)))
(warn (format "Downloading pod %s (%s)" qsym version))
(download url cache-file false)
(when-let [expected-sha (:artifact/hash artifact)]
(let [sha (sha256 cache-file)]
(when-not (= (str/replace expected-sha #"^sha256:" "")
sha)
(throw (ex-info (str "Wrong SHA-256 for file" (str cache-file))
{:sha sha
:expected-sha expected-sha})))))
(let [filename (.getName cache-file)]
(cond (str/ends-with? filename ".zip")
(unzip {:zip-file cache-file
:destination-dir ddir
:verbose false})
(or (str/ends-with? filename ".tgz")
(str/ends-with? filename ".tar.gz"))
(un-tgz cache-file ddir
false))
(.delete cache-file))
(make-executable ddir [(:artifact/executable artifact)] false)
(warn (format "Successfully installed pod %s (%s)" qsym version))
(io/file ddir (:artifact/executable artifact)))
(io/file ddir (:artifact/executable artifact)))) artifacts)]
{:executable (.getAbsolutePath ^java.io.File (first execs))
:options (:pod/options manifest)})))

View file

@ -12,12 +12,11 @@
(defn- process-namespace [{:keys [:name :vars]}] (defn- process-namespace [{:keys [:name :vars]}]
(binding [*ns* (load-string (format "(ns %s) *ns*" name))] (binding [*ns* (load-string (format "(ns %s) *ns*" name))]
(doseq [[var-sym v] vars] (doseq [[var-sym v] vars]
(when-let [maybe-core (some-> (ns-resolve *ns* var-sym) meta :ns str symbol)]
(when (= 'clojure.core maybe-core)
(ns-unmap *ns* var-sym)))
(cond (cond
(ifn? v) (ifn? v)
(intern name var-sym v) (do
(ns-unmap *ns* var-sym)
(intern name var-sym v))
(string? v) (string? v)
(load-string v))))) (load-string v)))))
@ -34,20 +33,15 @@
(defn load-pod (defn load-pod
([pod-spec] (load-pod pod-spec nil)) ([pod-spec] (load-pod pod-spec nil))
([pod-spec version opts] (load-pod pod-spec (assoc opts :version version))) ([pod-spec _opts]
([pod-spec opts] (let [pod (impl/load-pod
(let [opts (if (string? opts)
{:version opts}
opts)
pod (impl/load-pod
pod-spec pod-spec
(merge {:remove-ns remove-ns {:remove-ns remove-ns
:resolve (fn [sym] :resolve (fn [sym]
(or (resolve sym) (or (resolve sym)
(intern (intern
(create-ns (symbol (namespace sym))) (create-ns (symbol (namespace sym)))
(symbol (name sym)))))} (symbol (name sym)))))})
opts))
namespaces (:namespaces pod)] namespaces (:namespaces pod)]
(swap! namespaces-to-load (swap! namespaces-to-load
merge merge
@ -56,10 +50,9 @@
(when defer? (when defer?
[ns-name pod])) [ns-name pod]))
namespaces))) namespaces)))
(binding [impl/*pod-id* (:pod-id pod)]
(doseq [[ns-sym vars lazy?] namespaces (doseq [[ns-sym vars lazy?] namespaces
:when (not lazy?)] :when (not lazy?)]
(process-namespace {:name ns-sym :vars vars}))) (process-namespace {:name ns-sym :vars vars}))
(future (impl/processor pod)) (future (impl/processor pod))
{:pod/id (:pod-id pod)}))) {:pod/id (:pod-id pod)})))
@ -71,18 +64,3 @@
(defn invoke (defn invoke
([pod-id sym args] (invoke pod-id sym args {})) ([pod-id sym args] (invoke pod-id sym args {}))
([pod-id sym args opts] (impl/invoke-public pod-id sym args opts))) ([pod-id sym args opts] (impl/invoke-public pod-id sym args opts)))
(defmacro copy-var [name var]
`(do (def ~name ~var)
(let [m# (meta (var ~var))
doc# (:doc m#)
arglists# (:arglists m#)]
(alter-meta! (var ~name) assoc
:arglists arglists#
:doc doc#))))
#_:clj-kondo/ignore
(do
(copy-var add-transit-read-handler! impl/add-transit-read-handler!)
(copy-var add-transit-write-handler! impl/add-transit-write-handler!)
(copy-var set-default-transit-write-handler! impl/set-default-transit-write-handler!))

View file

@ -1,12 +1,6 @@
(ns babashka.pods.sci (ns babashka.pods.sci
(:require [babashka.pods.impl :as impl] (:require [babashka.pods.impl :as impl]
[sci.core :as sci] [sci.core :as sci]))
[clojure.java.io :as io]
[babashka.pods.impl.resolver :as resolver]
[babashka.fs :as fs])
(:import (java.io PushbackInputStream File)))
(set! *warn-on-reflection* true)
(defn- process-namespace [ctx {:keys [:name :vars]}] (defn- process-namespace [ctx {:keys [:name :vars]}]
(let [env (:env ctx) (let [env (:env ctx)
@ -17,71 +11,24 @@
(swap! env update-in [:namespaces ns-name] (swap! env update-in [:namespaces ns-name]
(fn [ns-map] (fn [ns-map]
(if ns-map ns-map {:obj sci-ns}))) (if ns-map ns-map {:obj sci-ns})))
(doseq [[var-name var-value :as var] vars] (doseq [[var-name var-value] vars]
(cond (ifn? var-value) (cond (ifn? var-value)
(swap! env assoc-in [:namespaces ns-name var-name] (swap! env assoc-in [:namespaces ns-name var-name]
(sci/new-var (sci/new-var
(symbol (str ns-name) (str var-name)) var-value (meta var-name))) (symbol (str ns-name) (str var-name)) var-value))
(string? var-value) (string? var-value)
(sci/eval-string* ctx var-value)))))) (sci/eval-string* ctx var-value))))))
(defn metadata-cache-file ^File [^File bb-edn-file pod-spec {:keys [:version :path]}] (def load-pod
(if version (with-meta
(io/file (resolver/cache-dir {:pod/name pod-spec :pod/version version}) (fn
"metadata.cache")
(let [config-dir (.getParentFile bb-edn-file)
cache-dir (io/file config-dir ".babashka")
pod-file (-> path io/file .getName)
cache-file (io/file cache-dir (str pod-file ".metadata.cache"))]
cache-file)))
(defn load-metadata-from-cache [bb-edn-file pod-spec opts]
(let [cache-file (metadata-cache-file bb-edn-file pod-spec opts)]
(when (.exists cache-file)
(with-open [r (PushbackInputStream. (io/input-stream cache-file))]
(impl/read r)))))
(defn load-pod-metadata* [bb-edn-file pod-spec {:keys [:version :cache] :as opts}]
(let [metadata (impl/load-pod-metadata pod-spec opts)
cache-file (when (and metadata cache)
(metadata-cache-file bb-edn-file pod-spec opts))]
(when cache-file
(io/make-parents cache-file)
(when (fs/writable? (fs/parent cache-file))
(with-open [w (io/output-stream cache-file)]
(impl/write w metadata))))
metadata))
(defn load-pod-metadata
([pod-spec opts] (load-pod-metadata nil pod-spec opts))
([bb-edn-file pod-spec {:keys [:cache] :as opts}]
(let [metadata
(if-let [cached-metadata (when cache
(load-metadata-from-cache bb-edn-file
pod-spec
opts))]
cached-metadata
(load-pod-metadata* bb-edn-file pod-spec opts))]
(reduce
(fn [pod-namespaces ns]
(let [ns-sym (-> ns (get "name") impl/bytes->string symbol)]
(assoc pod-namespaces ns-sym {:pod-spec pod-spec
:opts (assoc opts :metadata metadata)})))
{} (get metadata "namespaces")))))
(defn load-pod
([ctx pod-spec] (load-pod ctx pod-spec nil)) ([ctx pod-spec] (load-pod ctx pod-spec nil))
([ctx pod-spec version opts] (load-pod ctx pod-spec (assoc opts :version version))) ([ctx pod-spec _opts]
([ctx pod-spec opts] (let [env (:env ctx)
(let [opts (if (string? opts)
{:version opts}
opts)
env (:env ctx)
pod (binding [*out* @sci/out pod (binding [*out* @sci/out
*err* @sci/err] *err* @sci/err]
(impl/load-pod (impl/load-pod
pod-spec pod-spec
(merge
{:remove-ns {:remove-ns
(fn [sym] (fn [sym]
(swap! env update :namespaces dissoc sym)) (swap! env update :namespaces dissoc sym))
@ -95,8 +42,7 @@
(let [v (sci/new-var sym {:predefined true})] (let [v (sci/new-var sym {:predefined true})]
(swap! env assoc-in [:namespaces sym-ns sym-name] (swap! env assoc-in [:namespaces sym-ns sym-name]
v) v)
v))))} v))))}))
opts)))
namespaces (:namespaces pod) namespaces (:namespaces pod)
namespaces-to-load (set (keep (fn [[ns-name _ defer?]] namespaces-to-load (set (keep (fn [[ns-name _ defer?]]
(when defer? (when defer?
@ -115,12 +61,12 @@
(when prev-load-fn (when prev-load-fn
(prev-load-fn m))))] (prev-load-fn m))))]
(swap! env assoc :load-fn new-load-fn))) (swap! env assoc :load-fn new-load-fn)))
(binding [impl/*pod-id* (:pod-id pod)]
(doseq [[ns-name vars lazy?] namespaces (doseq [[ns-name vars lazy?] namespaces
:when (not lazy?)] :when (not lazy?)]
(process-namespace ctx {:name ns-name :vars vars}))) (process-namespace ctx {:name ns-name :vars vars}))
(sci/future (impl/processor pod)) (sci/future (impl/processor pod))
{:pod/id (:pod-id pod)}))) {:pod/id (:pod-id pod)})))
{:sci.impl/op :needs-ctx}))
(defn unload-pod (defn unload-pod
([pod-id] (unload-pod pod-id {})) ([pod-id] (unload-pod pod-id {}))
@ -130,18 +76,3 @@
(defn invoke (defn invoke
([pod-id sym args] (invoke pod-id sym args {})) ([pod-id sym args] (invoke pod-id sym args {}))
([pod-id sym args opts] (impl/invoke-public pod-id sym args opts))) ([pod-id sym args opts] (impl/invoke-public pod-id sym args opts)))
(defmacro copy-var [name var]
`(do (def ~name ~var)
(let [m# (meta (var ~var))
doc# (:doc m#)
arglists# (:arglists m#)]
(alter-meta! (var ~name) assoc
:arglists arglists#
:doc doc#))))
#_:clj-kondo/ignore
(do
(copy-var add-transit-read-handler! impl/add-transit-read-handler!)
(copy-var add-transit-write-handler! impl/add-transit-write-handler!)
(copy-var set-default-transit-write-handler! impl/set-default-transit-write-handler!))

View file

@ -3,10 +3,8 @@
(:require [bencode.core :as bencode] (:require [bencode.core :as bencode]
[cheshire.core :as cheshire] [cheshire.core :as cheshire]
[clojure.edn :as edn] [clojure.edn :as edn]
[clojure.java.io :as io] [clojure.java.io :as io])
[cognitect.transit :as transit]) (:import [java.io PushbackInputStream])
(:import [java.io PushbackInputStream]
[java.net ServerSocket])
(:gen-class)) (:gen-class))
(def debug? false) (def debug? false)
@ -16,15 +14,17 @@
(binding [*out* (io/writer "/tmp/log.txt" :append true)] (binding [*out* (io/writer "/tmp/log.txt" :append true)]
(apply println args)))) (apply println args))))
(defn write [stream v] (def stdin (PushbackInputStream. System/in))
(bencode/write-bencode stream v)
(flush)) (defn write [v]
(bencode/write-bencode System/out v)
(.flush System/out))
(defn read-string [^"[B" v] (defn read-string [^"[B" v]
(String. v)) (String. v))
(defn read [stream] (defn read []
(bencode/read-bencode stream)) (bencode/read-bencode stdin))
(def dependents (def dependents
(for [i (range 10)] (for [i (range 10)]
@ -34,70 +34,19 @@
(format "(def x%s (inc x%s))" i (dec i)) (format "(def x%s (inc x%s))" i (dec i))
"(def x0 0)")})) "(def x0 0)")}))
(defn transit-json-read [^String s]
(with-open [bais (java.io.ByteArrayInputStream. (.getBytes s "UTF-8"))]
(let [r (transit/reader bais :json {:handlers
{"local-date-time"
(transit/read-handler
(fn [s]
(java.time.LocalDateTime/parse s)))
"java.array"
(transit/read-handler
(fn [v]
(into-array v)))}})]
(transit/read r))))
(defn transit-json-write [s]
(with-open [baos (java.io.ByteArrayOutputStream. 4096)]
(let [w (transit/writer baos :json {:handlers
{java.time.LocalDateTime
(transit/write-handler
"local-date-time"
str)}
:default-handler
(transit/write-handler
(fn [x] (when (.isArray (class x)) "java.array"))
vec)})]
(transit/write w s)
(str baos))))
(defn transit-json-write-meta [s]
(with-open [baos (java.io.ByteArrayOutputStream. 4096)]
(let [w (transit/writer baos :json {:transform transit/write-meta})]
(transit/write w s)
(str baos))))
(defn run-pod [cli-args] (defn run-pod [cli-args]
(let [format (cond (contains? cli-args "--json") :json (let [format (if (contains? cli-args "--json")
(contains? cli-args "--transit+json") :transit+json :json
:else :edn) :edn)
write-fn (case format write-fn (if (identical? :json format)
:edn pr-str cheshire/generate-string
:json cheshire/generate-string pr-str)
:transit+json transit-json-write) read-fn (if (identical? :json format)
read-fn (case format #(cheshire/parse-string % true)
:edn edn/read-string edn/read-string)]
:json #(cheshire/parse-string % true)
:transit+json transit-json-read)
socket (= "socket" (System/getenv "BABASHKA_POD_TRANSPORT"))
[in out] (if socket
(let [server (ServerSocket. 0)
port (.getLocalPort server)
pid (.pid (java.lang.ProcessHandle/current))
port-file (io/file (str ".babashka-pod-" pid ".port"))
_ (.addShutdownHook (Runtime/getRuntime)
(Thread. (fn [] (.delete port-file))))
_ (spit port-file
(str port "\n"))
socket (.accept server)
in (PushbackInputStream. (.getInputStream socket))
out (.getOutputStream socket)]
[in out])
[(PushbackInputStream. System/in)
System/out])]
(try (try
(loop [] (loop []
(let [message (try (read in) (let [message (try (read)
(catch java.io.EOFException _ (catch java.io.EOFException _
::EOF))] ::EOF))]
(when-not (identical? ::EOF message) (when-not (identical? ::EOF message)
@ -106,18 +55,16 @@
op (keyword op)] op (keyword op)]
(case op (case op
:describe :describe
(do (write out {"format" (case format (do (write {"format" (if (= format :json)
:edn "edn" "json"
:json "json" "edn")
:transit+json "transit+json")
"readers" {"my/tag" "identity" "readers" {"my/tag" "identity"
;; NOTE: this function is defined later, ;; NOTE: this function is defined later,
;; which should be supported ;; which should be supported
"my/other-tag" "pod.test-pod/read-other-tag"} "my/other-tag" "pod.test-pod/read-other-tag"}
"namespaces" "namespaces"
[{"name" "pod.test-pod" [{"name" "pod.test-pod"
"vars" (into [{"name" "add-sync" "vars" (into [{"name" "add-sync"}
"meta" "{:doc \"add the arguments\"}"}
{"name" "range-stream" {"name" "range-stream"
"async" "true"} "async" "true"}
{"name" "assoc"} {"name" "assoc"}
@ -134,35 +81,7 @@
{"name" "other-tag"} {"name" "other-tag"}
;; reads thing with other tag ;; reads thing with other tag
{"name" "read-other-tag" {"name" "read-other-tag"
"code" "(defn read-other-tag [x] [x x])" "code" "(defn read-other-tag [x] [x x])"}]
"meta" "{:doc \"unread\"}"}
{"name" "round-trip-meta"
"arg-meta" "true"}
{"name" "dont-round-trip-meta"
"arg-meta" "false"}
{"name" "-local-date-time"}
{"name" "transit-stuff"
"code" "
(babashka.pods/add-transit-read-handler! \"local-date-time\"
(fn [s] (java.time.LocalDateTime/parse s)))
(babashka.pods/add-transit-write-handler! #{java.time.LocalDateTime}
\"local-date-time\"
str )
(defn local-date-time [x]
(-local-date-time x))
;; serialize Java arrays as vectors with tag java.array
(babashka.pods/set-default-transit-write-handler!
(fn [x] (when (.isArray (class x)) \"java.array\"))
vec)
(babashka.pods/add-transit-read-handler! \"java.array\"
into-array)
"}
{"name" "incorrect-edn"}]
dependents)} dependents)}
{"name" "pod.test-pod.loaded" {"name" "pod.test-pod.loaded"
"defer" "true"} "defer" "true"}
@ -185,12 +104,12 @@
(case var (case var
pod.test-pod/add-sync pod.test-pod/add-sync
(try (let [ret (apply + args)] (try (let [ret (apply + args)]
(write out (write
{"value" (write-fn ret) {"value" (write-fn ret)
"id" id "id" id
"status" ["done"]})) "status" ["done"]}))
(catch Exception e (catch Exception e
(write out (write
{"ex-data" (write-fn {:args args}) {"ex-data" (write-fn {:args args})
"ex-message" (.getMessage e) "ex-message" (.getMessage e)
"status" ["done" "error"] "status" ["done" "error"]
@ -198,79 +117,53 @@
pod.test-pod/range-stream pod.test-pod/range-stream
(let [rng (apply range args)] (let [rng (apply range args)]
(doseq [v rng] (doseq [v rng]
(write out (write
{"value" (write-fn v) {"value" (write-fn v)
"id" id}) "id" id})
(Thread/sleep 100)) (Thread/sleep 100))
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id})) "id" id}))
pod.test-pod/assoc pod.test-pod/assoc
(write out (write
{"value" (write-fn (apply assoc args)) {"value" (write-fn (apply assoc args))
"status" ["done"] "status" ["done"]
"id" id}) "id" id})
pod.test-pod/error pod.test-pod/error
(write out (write
{"ex-data" (write-fn {:args args}) {"ex-data" (write-fn {:args args})
"ex-message" (str "Illegal arguments") "ex-message" (str "Illegal arguments")
"status" ["done" "error"] "status" ["done" "error"]
"id" id}) "id" id})
pod.test-pod/print pod.test-pod/print
(do (write out (do (write
{"out" (with-out-str (prn args)) {"out" (pr-str args)
"id" id}) "id" id})
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id})) "id" id}))
pod.test-pod/print-err pod.test-pod/print-err
(do (write out (do (write
{"err" (with-out-str (prn args)) {"err" (pr-str args)
"id" id}) "id" id})
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id})) "id" id}))
pod.test-pod/return-nil pod.test-pod/return-nil
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id "id" id
"value" (write-fn nil)}) "value" "nil"})
pod.test-pod/reader-tag pod.test-pod/reader-tag
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id "id" id
"value" "#my/tag[1 2 3]"}) "value" "#my/tag[1 2 3]"})
pod.test-pod/other-tag pod.test-pod/other-tag
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id "id" id
"value" "#my/other-tag[1]"}) "value" "#my/other-tag[1]"}))
pod.test-pod/round-trip-meta
(write out
{"status" ["done"]
"id" id
"value"
(case format
:transit+json (transit-json-write-meta (first args))
(write-fn (first args)))})
pod.test-pod/dont-round-trip-meta
(write out
{"status" ["done"]
"id" id
"value"
(case format
:transit+json (transit-json-write-meta (first args))
(write-fn (first args)))})
pod.test-pod/-local-date-time
(write out
{"status" ["done"]
"id" id
"value" (write-fn (first args))})
pod.test-pod/incorrect-edn
(write out
{"status" ["done"]
"id" id
"value" (write-fn {(keyword "foo bar") 1})}))
(recur)) (recur))
:shutdown (System/exit 0) :shutdown (System/exit 0)
:load-ns (let [ns (-> (get message "ns") :load-ns (let [ns (-> (get message "ns")
@ -280,14 +173,14 @@
read-string)] read-string)]
(case ns (case ns
pod.test-pod.loaded pod.test-pod.loaded
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id "id" id
"name" "pod.test-pod.loaded" "name" "pod.test-pod.loaded"
"vars" [{"name" "loaded" "vars" [{"name" "loaded"
"code" "(defn loaded [x] (inc x))"}]}) "code" "(defn loaded [x] (inc x))"}]})
pod.test-pod.loaded2 pod.test-pod.loaded2
(write out (write
{"status" ["done"] {"status" ["done"]
"id" id "id" id
"name" "pod.test-pod.loaded2" "name" "pod.test-pod.loaded2"
@ -301,7 +194,5 @@
(prn e)))))) (prn e))))))
(defn -main [& args] (defn -main [& args]
#_(binding [*out* *err*]
(prn :args args))
(when (= "true" (System/getenv "BABASHKA_POD")) (when (= "true" (System/getenv "BABASHKA_POD"))
(run-pod (set args)))) (run-pod (set args))))

View file

@ -1,11 +0,0 @@
(require '[babashka.pods :as pods])
(pods/load-pod 'org.babashka/buddy "0.1.0")
(require '[pod.babashka.buddy.codecs :as codecs]
'[pod.babashka.buddy.hash :as hash])
(println (-> (hash/sha256 "foobar")
(codecs/bytes->hex)))
(pods/load-pod 'org.babashka/etaoin) ;; should cause error when version & path are missing

View file

@ -1,19 +1,5 @@
(require '[babashka.pods :as pods]) (require '[babashka.pods :as pods])
(def pod-id (:pod/id (pods/load-pod ["clojure" "-A:test-pod"])))
(def fmt (or (System/getenv "BABASHKA_POD_TEST_FORMAT")
"edn"))
(def socket (System/getenv "BABASHKA_POD_TEST_SOCKET"))
(def cmd (cond-> ["clojure" "-M:test-pod"]
(= "json" fmt) (conj "--json")
(= "transit+json" fmt) (conj "--transit+json")))
;; (.println System/err cmd)
(def pod-id (:pod/id (pods/load-pod cmd
{:socket (boolean socket)})))
(require '[pod.test-pod :as pod]) (require '[pod.test-pod :as pod])
(def pod-ns-name (ns-name (find-ns 'pod.test-pod))) (def pod-ns-name (ns-name (find-ns 'pod.test-pod)))
@ -55,60 +41,19 @@
(def add-result (pod.test-pod/add-sync 1 2 3)) (def add-result (pod.test-pod/add-sync 1 2 3))
(def nil-result (pod.test-pod/return-nil)) (def nil-result (pod.test-pod/return-nil))
(def add-sync-meta (:doc (meta #'pod.test-pod/add-sync)))
(def error-meta (:doc (meta #'pod.test-pod/error)))
(def read-other-tag-meta (:doc (meta #'pod.test-pod/read-other-tag)))
(def x9 pod.test-pod/x9) (def x9 pod.test-pod/x9)
(def tagged (if (= "edn" fmt) (def tagged (pod/reader-tag))
(pod/reader-tag) (def other-tagged (pod/other-tag))
[1 2 3]))
(def other-tagged
(if (= "edn" fmt)
(pod/other-tag)
[[1] [1]]))
(def fn-called (pod.test-pod/fn-call inc 2)) (def fn-called (pod.test-pod/fn-call inc 2))
;; (.println System/err (str :fmt " " fmt))
(def local-date-time
(if (= "transit+json" fmt)
(instance? java.time.LocalDateTime (pod.test-pod/local-date-time (java.time.LocalDateTime/now)))
true))
(def assoc-string-array
(if (= "transit+json" fmt)
(let [v (:a (pod.test-pod/assoc {} :a (into-array String ["foo"])))]
(.isArray (class v)))
true))
(def round-trip-meta
(if (= "transit+json" fmt)
(= {:my-meta 2} (meta (pod.test-pod/round-trip-meta (with-meta [2] {:my-meta 2}))))
true))
(def round-trip-meta-nested
(if (= "transit+json" fmt)
(= {:my-meta 3} (meta (first (pod.test-pod/round-trip-meta [(with-meta [3] {:my-meta 3})]))))
true))
(def dont-round-trip-meta
(if (= "transit+json" fmt)
(= nil (meta (pod.test-pod/dont-round-trip-meta (with-meta [2] {:my-meta 2}))))
true))
(require '[pod.test-pod.only-code :as only-code]) (require '[pod.test-pod.only-code :as only-code])
(def should-be-1 (only-code/foo)) (def should-be-1 (only-code/foo))
(require '[pod.test-pod.loaded2 :as loaded2]) (require '[pod.test-pod.loaded2 :as loaded2])
(def loaded (loaded2/loaded 1)) (def loaded (loaded2/loaded 1))
(def incorrect-edn-response
(try (pod.test-pod/incorrect-edn)
(catch Exception e (ex-message e))))
(pods/unload-pod pod-id) (pods/unload-pod pod-id)
(def successfully-removed (nil? (find-ns 'pod.test-pod))) (def successfully-removed (nil? (find-ns 'pod.test-pod)))
@ -129,13 +74,4 @@
other-tagged other-tagged
loaded loaded
fn-called fn-called
local-date-time should-be-1]
assoc-string-array
round-trip-meta
round-trip-meta-nested
dont-round-trip-meta
should-be-1
add-sync-meta
error-meta
read-other-tag-meta
incorrect-edn-response]

View file

@ -1,11 +0,0 @@
(ns babashka.pods.impl-test
(:require [clojure.test :refer :all]
[babashka.pods.impl :refer :all]))
(deftest load-pod-test
(testing "resolve fn gets called when pod has EDN data readers"
(let [resolved? (atom false)
test-resolve (fn [_sym]
(reset! resolved? true))]
(load-pod ["clojure" "-M:test-pod"] {:resolve test-resolve})
(is @resolved?))))

View file

@ -1,8 +1,6 @@
(ns babashka.pods.jvm-test (ns babashka.pods.jvm-test
(:require [babashka.pods.test-common :refer [test-program assertions (:require [babashka.pods.test-common :refer [test-program assertions]]
pod-registry]] [clojure.test :refer [deftest]]))
[clojure.string :as str]
[clojure.test :refer [deftest is]]))
(deftest jvm-test (deftest jvm-test
(let [out (java.io.StringWriter.) (let [out (java.io.StringWriter.)
@ -14,15 +12,3 @@
(catch Exception e (prn e))))] (catch Exception e (prn e))))]
(assertions out err ret))) (assertions out err ret)))
(deftest pod-registry-test
(let [out (java.io.StringWriter.)
err (java.io.StringWriter.)
ex (binding [*out* out
*err* err]
(try (load-string
pod-registry)
(catch Exception e
e)))]
(is (str/includes? (str out) "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"))
(is (str/includes? (pr-str ex) "Version or path must be provided"))))

View file

@ -1,41 +1,18 @@
(ns babashka.pods.sci-test (ns babashka.pods.sci-test
(:require [babashka.pods.sci :as pods] (:require [babashka.pods.sci :as pods]
[babashka.pods.test-common :refer [test-program assertions pod-registry]] [babashka.pods.test-common :refer [test-program assertions]]
[clojure.string :as str] [clojure.test :refer [deftest]]
[clojure.test :refer [deftest is]]
[sci.core :as sci])) [sci.core :as sci]))
(deftest sci-test (deftest sci-test
(let [out (java.io.StringWriter.) (let [out (java.io.StringWriter.)
err (java.io.StringWriter.) err (java.io.StringWriter.)
ctx-ref (volatile! nil)
ctx (sci/init {:namespaces {'babashka.pods
{'load-pod (fn [& args]
(apply pods/load-pod @ctx-ref args))
'invoke pods/invoke
'unload-pod pods/unload-pod
'add-transit-read-handler! pods/add-transit-read-handler!
'add-transit-write-handler! pods/add-transit-write-handler!
'set-default-transit-write-handler! pods/set-default-transit-write-handler!}}
:classes {'System System
'java.time.LocalDateTime java.time.LocalDateTime
'java.lang.Class Class}})
_ (vreset! ctx-ref ctx)
ret (sci/binding [sci/out out ret (sci/binding [sci/out out
sci/err err] sci/err err]
(binding [*out* out (sci/eval-string
*err* err] test-program
(sci/eval-string* ctx test-program)))] {:namespaces {'babashka.pods
{'load-pod pods/load-pod
'invoke pods/invoke
'unload-pod pods/unload-pod}}}))]
(assertions out err ret))) (assertions out err ret)))
(deftest pod-registry-test
(let [out (java.io.StringWriter.)
err (java.io.StringWriter.)
ex (binding [*out* out
*err* err]
(try (load-string
pod-registry)
(catch Exception e
e)))]
(is (str/includes? (str out) "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"))
(is (str/includes? (pr-str ex) "Version or path must be provided"))))

View file

@ -1,27 +1,18 @@
(ns babashka.pods.test-common (ns babashka.pods.test-common
(:require [clojure.java.io :as io] (:require [clojure.java.io :as io]
[clojure.string :as str]
[clojure.test :refer [is]])) [clojure.test :refer [is]]))
(def test-program (slurp (io/file "test-resources" "test_program.clj"))) (def test-program (slurp (io/file "test-resources" "test_program.clj")))
(defn assertions [out err ret] (defn assertions [out err ret]
;; (.println System/err ret)
;; (.println System/err out)
;; (.println System/err err)
(doseq [[expected actual] (doseq [[expected actual]
(map vector (replace (map vector '["pod.test-pod"
{::edn-error (if (= "edn"
(System/getenv "BABASHKA_POD_TEST_FORMAT"))
"Map literal must contain an even number of forms"
::dont-care)}
'["pod.test-pod"
pod.test-pod pod.test-pod
{:a 1, :b 2} {:a 1, :b 2}
6 6
3 3
[1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9]
#"Illegal arguments / \{:args [\(\[]1 2 3[\)\]]\}" "Illegal arguments / {:args (1 2 3)}"
nil nil
3 3
#"cast" #"cast"
@ -32,23 +23,10 @@
[[1] [1]] [[1] [1]]
2 2
3 3
true ;; local-date 1]
true ;; roundtrip string array
true ;; roundtrip metadata
true ;; roundtrip metadata nested
true ;; dont roundtrip metadata (when arg-meta "false"/ absent)
1
"add the arguments"
nil
nil
::edn-error])
(concat ret (repeat ::nil)))] (concat ret (repeat ::nil)))]
(cond (instance? java.util.regex.Pattern expected) (if (instance? java.util.regex.Pattern expected)
(is (re-find expected actual)) (is (re-find expected actual))
(= ::dont-care expected) nil
:else
(is (= expected actual)))) (is (= expected actual))))
(is (= "(\"hello\" \"print\" \"this\" \"debugging\" \"message\")\n:foo\n:foo\n" (str out))) (is (= "(\"hello\" \"print\" \"this\" \"debugging\" \"message\")\n:foo\n:foo\n" (str out)))
(is (str/starts-with? (str err) "(\"hello\" \"print\" \"this\" \"error\")" ))) (is (= "(\"hello\" \"print\" \"this\" \"error\")\n" (str err))))
(def pod-registry (slurp (io/file "test-resources" "pod_registry.clj")))