From b00133ca0547493cd18443f0d861a8e96cdc7263 Mon Sep 17 00:00:00 2001 From: Jude Payne Date: Fri, 12 May 2023 15:09:40 +0100 Subject: [PATCH] Fix #66: opt-in metadata (#67) --- CHANGELOG.md | 1 + README.md | 33 +++++++- src/babashka/pods/impl.clj | 24 ++++-- test-pod/pod/test_pod.clj | 120 ++++++++++++++++++----------- test-resources/test_program.clj | 18 +++++ test/babashka/pods/test_common.clj | 3 + 6 files changed, 144 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bffa21d..88a9616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - [#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 diff --git a/README.md b/README.md index 4ecb804..7d742cc 100644 --- a/README.md +++ b/README.md @@ -376,11 +376,15 @@ 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. +in those code blocks (see 'Dynamic Metadata' below to enable the encoding of metadata). For example, a pod can define a function called `add`: @@ -392,6 +396,33 @@ For example, a pod can define a function called `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 When your pod exposes multiple namespaces that can be used independently from diff --git a/src/babashka/pods/impl.clj b/src/babashka/pods/impl.clj index b2b14e2..5d54b66 100644 --- a/src/babashka/pods/impl.clj +++ b/src/babashka/pods/impl.clj @@ -28,6 +28,9 @@ (defn bytes->string [^"[B" bytes] (String. bytes)) +(defn bytes->boolean [^"[B" bytes] + (= "true" (String. bytes))) + (defn get-string [m k] (-> (get m k) bytes->string)) @@ -36,6 +39,10 @@ (some-> (get m k) bytes->string)) +(defn get-maybe-boolean [m k] + (some-> (get m k) + bytes->boolean)) + (defn next-id [] (str (java.util.UUID/randomUUID))) @@ -83,10 +90,12 @@ (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] +(defn transit-json-write + [pod-id ^String s metadata?] (with-open [baos (java.io.ByteArrayOutputStream. 4096)] - (let [w (transit/writer baos :json {:handlers (get @transit-write-handler-maps pod-id) - :default-handler (get @transit-default-write-handlers pod-id)})] + (let [w (transit/writer baos :json (merge {:handlers (get @transit-write-handler-maps pod-id) + :default-handler (get @transit-default-write-handlers pod-id)} + (when metadata? {:transform transit/write-meta})))] (transit/write w s) (str baos)))) @@ -97,8 +106,8 @@ chans (:chans pod) write-fn (case format :edn pr-str - :json cheshire/generate-string - :transit+json #(transit-json-write (:pod-id pod) %)) + :json cheshire/generate-string + :transit+json #(transit-json-write (:pod-id pod) % (:arg-meta opts))) id (next-id) chan (if handlers handlers (promise)) @@ -128,11 +137,12 @@ edn/read-string) name-sym (if vmeta (with-meta name-sym vmeta) - name-sym)] + name-sym) + metadata? (get-maybe-boolean var "arg-meta")] [name-sym (or code (fn [& args] - (let [res (invoke pod sym args {:async async?})] + (let [res (invoke pod sym args {:async async? :arg-meta metadata?})] res)))])) vars)) diff --git a/test-pod/pod/test_pod.clj b/test-pod/pod/test_pod.clj index 3fb5b5c..9f98c90 100644 --- a/test-pod/pod/test_pod.clj +++ b/test-pod/pod/test_pod.clj @@ -61,6 +61,12 @@ (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] (let [format (cond (contains? cli-args "--json") :json (contains? cli-args "--transit+json") :transit+json @@ -130,6 +136,10 @@ {"name" "read-other-tag" "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" " @@ -176,65 +186,81 @@ pod.test-pod/add-sync (try (let [ret (apply + args)] (write out - {"value" (write-fn ret) - "id" id - "status" ["done"]})) + {"value" (write-fn ret) + "id" id + "status" ["done"]})) (catch Exception e (write out - {"ex-data" (write-fn {:args args}) - "ex-message" (.getMessage e) - "status" ["done" "error"] - "id" id}))) + {"ex-data" (write-fn {:args args}) + "ex-message" (.getMessage e) + "status" ["done" "error"] + "id" id}))) pod.test-pod/range-stream (let [rng (apply range args)] (doseq [v rng] (write out - {"value" (write-fn v) - "id" id}) + {"value" (write-fn v) + "id" id}) (Thread/sleep 100)) (write out - {"status" ["done"] - "id" id})) + {"status" ["done"] + "id" id})) pod.test-pod/assoc (write out - {"value" (write-fn (apply assoc args)) - "status" ["done"] - "id" id}) + {"value" (write-fn (apply assoc args)) + "status" ["done"] + "id" id}) pod.test-pod/error (write out - {"ex-data" (write-fn {:args args}) - "ex-message" (str "Illegal arguments") - "status" ["done" "error"] - "id" id}) + {"ex-data" (write-fn {:args args}) + "ex-message" (str "Illegal arguments") + "status" ["done" "error"] + "id" id}) pod.test-pod/print (do (write out - {"out" (with-out-str (prn args)) - "id" id}) + {"out" (with-out-str (prn args)) + "id" id}) (write out - {"status" ["done"] - "id" id})) + {"status" ["done"] + "id" id})) pod.test-pod/print-err (do (write out - {"err" (with-out-str (prn args)) - "id" id}) + {"err" (with-out-str (prn args)) + "id" id}) (write out - {"status" ["done"] - "id" id})) + {"status" ["done"] + "id" id})) pod.test-pod/return-nil (write out - {"status" ["done"] - "id" id - "value" (write-fn nil)}) + {"status" ["done"] + "id" id + "value" (write-fn nil)}) pod.test-pod/reader-tag (write out - {"status" ["done"] - "id" id - "value" "#my/tag[1 2 3]"}) + {"status" ["done"] + "id" id + "value" "#my/tag[1 2 3]"}) pod.test-pod/other-tag (write out - {"status" ["done"] - "id" id - "value" "#my/other-tag[1]"}) + {"status" ["done"] + "id" id + "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"] @@ -255,20 +281,20 @@ (case ns pod.test-pod.loaded (write out - {"status" ["done"] - "id" id - "name" "pod.test-pod.loaded" - "vars" [{"name" "loaded" - "code" "(defn loaded [x] (inc x))"}]}) + {"status" ["done"] + "id" id + "name" "pod.test-pod.loaded" + "vars" [{"name" "loaded" + "code" "(defn loaded [x] (inc x))"}]}) pod.test-pod.loaded2 (write out - {"status" ["done"] - "id" id - "name" "pod.test-pod.loaded2" - "vars" [{"name" "x" - "code" "(require '[pod.test-pod.loaded :as loaded])"} - {"name" "loaded" - "code" "(defn loaded [x] (loaded/loaded x))"}]})) + {"status" ["done"] + "id" id + "name" "pod.test-pod.loaded2" + "vars" [{"name" "x" + "code" "(require '[pod.test-pod.loaded :as loaded])"} + {"name" "loaded" + "code" "(defn loaded [x] (loaded/loaded x))"}]})) (recur))))))) (catch Exception e (binding [*out* *err*] diff --git a/test-resources/test_program.clj b/test-resources/test_program.clj index 476b062..6bef8d1 100644 --- a/test-resources/test_program.clj +++ b/test-resources/test_program.clj @@ -84,6 +84,21 @@ (.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]) (def should-be-1 (only-code/foo)) @@ -116,6 +131,9 @@ fn-called local-date-time assoc-string-array + round-trip-meta + round-trip-meta-nested + dont-round-trip-meta should-be-1 add-sync-meta error-meta diff --git a/test/babashka/pods/test_common.clj b/test/babashka/pods/test_common.clj index 3a793b4..e0b7d59 100644 --- a/test/babashka/pods/test_common.clj +++ b/test/babashka/pods/test_common.clj @@ -34,6 +34,9 @@ 3 true ;; local-date 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