From 1ec8582e3a39c8d170cf5f8a7482386bf66370fe Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Fri, 1 Jul 2022 11:52:22 +0200 Subject: [PATCH] Fix #302: babashka compatibility This commit adds compatibility with babashka. Fixes #302. Notes: - To make malli work well with bb, the :bb reader conditionals rely less on internal details of Clojure and use core functions instead, similar to what happens in :cljs branches. As discussed with @puredanger, I didn't add LazilyPersistentVector to bb as that was too far off from what is considered a public API. - Upgraded borkdude/dynaload which was made compatible with bb - Add bb test runner to ensure no breakage happens with future changes to malli - The evaluation part is implemented by just using load-string in bb, since SCI itself isn't exposed yet (might happen in the future). SCI also has eval-form which lets you skip the string parsing bit, that might be more performant, but we can address that in a future PR. --- .github/workflows/clojure.yml | 22 +++++++++++++++++++ README.md | 28 +++++++++++++++++++++++- bb.edn | 20 +++++++++++++++++ deps.edn | 2 +- src/malli/core.cljc | 24 ++++++++++++++------ src/malli/impl/util.cljc | 6 +++-- src/malli/sci.cljc | 19 +++++++++------- test/bb_test_runner.clj | 40 ++++++++++++++++++++++++++++++++++ test/malli/core_test.cljc | 21 ++++++++++-------- test/malli/generator_test.cljc | 3 ++- test/malli/util_test.cljc | 15 ++++++++++--- 11 files changed, 168 insertions(+), 32 deletions(-) create mode 100644 bb.edn create mode 100644 test/bb_test_runner.clj diff --git a/.github/workflows/clojure.yml b/.github/workflows/clojure.yml index bba617c2e..bfce804c0 100644 --- a/.github/workflows/clojure.yml +++ b/.github/workflows/clojure.yml @@ -59,3 +59,25 @@ jobs: run: npm ci - name: Run tests run: bin/node + + build-bb: + name: Babashka + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Java 11 + uses: actions/setup-java@v3.4.0 + with: + distribution: zulu + java-version: 11 + cache: maven + - name: Setup Clojure + uses: DeLaGuardo/setup-clojure@master + with: + cli: latest + # bb: latest + - name: Download bb master + run: bash <(curl -s https://raw.githubusercontent.com/babashka/babashka/master/install) --version 0.8.157-SNAPSHOT + - name: Run tests + run: bb test-bb diff --git a/README.md b/README.md index 12bb480ce..3a330398f 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ [![cljdoc badge](https://cljdoc.org/badge/metosin/malli)](https://cljdoc.org/d/metosin/malli/) [![Clojars Project](https://img.shields.io/clojars/v/metosin/malli.svg)](https://clojars.org/metosin/malli) [![Slack](https://img.shields.io/badge/clojurians-malli-blue.svg?logo=slack)](https://clojurians.slack.com/messages/malli/) +bb compatible -Data-driven Schemas for Clojure/Script. +Data-driven Schemas for Clojure/Script and [babashka](#babashka). **STATUS**: well matured [*alpha*](#alpha) @@ -2899,6 +2900,31 @@ With sci (18Mb): ./demosci '[:fn (fn [x] (and (int? x) (> x 10)))]]' '12' ``` +## Babashka + +Since version 0.8.9 malli is compatible with [babashka](https://babashka.org/), +a native, fast starting Clojure interpreter for scripting. + +You can add malli to `bb.edn`: + +``` clojure +{:deps {metosin/malli {:mvn/version "0.8.9"}}} +``` + +or directly in a babashka script: + +``` clojure +(ns bb-malli + (:require [babashka.deps :as deps])) + +(deps/add-deps '{:deps {metosin/malli {:mvn/version "0.8.9"}}}) + +(require '[malli.core :as malli]) + +(prn (malli/validate [:map [:a [:int]]] {:a 1})) +(prn (malli/explain [:map [:a [:int]]] {:a "foo"})) +``` + ## 3rd party libraries - [Aave](https://github.com/teknql/aave), a code checking tool for Clojure. diff --git a/bb.edn b/bb.edn new file mode 100644 index 000000000..7f80c53fd --- /dev/null +++ b/bb.edn @@ -0,0 +1,20 @@ +{:deps {current/deps {:local/root "."}} + :tasks + {test-clj {:doc "Run JVM Clojure tests with kaocha" + :task (apply clojure (str "-A:" (System/getenv "CLOJURE")) + "-M:test" "-m" "kaocha.runner" *command-line-args*)} + + test-cljs {:doc "Run ClojureScript tests" + :task (do + (println "Running CLJS tests without optimizations") + (apply clojure "-M:test:cljs-test-runner" "-c" "{:optimizations :none, :preloads [sci.core]}" + *command-line-args*) + (println "Running CLJS tests with optimizations advanced") + (apply clojure "-M:test:cljs-test-runner" "-c" "{:optimizations :none, :preloads [sci.core]}" + *command-line-args*))} + + test-bb {:doc "Run Babashka tests" + :extra-deps {org.babashka/spec.alpha {:git/url "https://github.com/babashka/spec.alpha" + :git/sha "1a841c4cc1d4f6dab7505a98ed2d532dd9d56b78"}} + :extra-paths ["src" "test"] + :task bb-test-runner/run-tests}}} diff --git a/deps.edn b/deps.edn index aecd2223c..b62f0e242 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,6 @@ {:paths ["src" "resources"] :deps {org.clojure/clojure {:mvn/version "1.11.1"} - borkdude/dynaload {:mvn/version "0.2.2"} + borkdude/dynaload {:mvn/version "0.3.4"} borkdude/edamame {:mvn/version "1.0.0"} org.clojure/test.check {:mvn/version "1.1.1"} ;; pretty errors, optional deps diff --git a/src/malli/core.cljc b/src/malli/core.cljc index 74c738a9e..b9fad1387 100644 --- a/src/malli/core.cljc +++ b/src/malli/core.cljc @@ -7,7 +7,8 @@ [malli.impl.util :as miu] [malli.registry :as mr] [malli.sci :as ms]) - #?(:clj (:import (clojure.lang Associative IPersistentCollection MapEntry IPersistentVector LazilyPersistentVector PersistentArrayMap) + #?(:clj (:import #?(:bb (clojure.lang Associative IPersistentCollection MapEntry IPersistentVector PersistentArrayMap) + :clj (clojure.lang Associative IPersistentCollection MapEntry IPersistentVector LazilyPersistentVector PersistentArrayMap)) (java.util.concurrent.atomic AtomicReference) (java.util.regex Pattern)))) @@ -449,8 +450,11 @@ (-fail! ::invalid-ref {:ref e}))))) (defn -eager-entry-parser [children props options] - (letfn [(-vec [^objects arr] #?(:clj (LazilyPersistentVector/createOwning arr), :cljs (vec arr))) - (-map [^objects arr] #?(:clj (PersistentArrayMap/createWithCheck arr) + (letfn [(-vec [^objects arr] #?(:bb (vec arr) :clj (LazilyPersistentVector/createOwning arr), :cljs (vec arr))) + (-map [^objects arr] #?(:bb (let [m (apply array-map arr)] + (when-not (= (* 2 (count m)) (count arr)) + (-fail! ::duplicate-keys)) m) + :clj (PersistentArrayMap/createWithCheck arr) :cljs (let [m (apply array-map arr)] (when-not (= (* 2 (count m)) (count arr)) (-fail! ::duplicate-keys)) m))) @@ -504,7 +508,11 @@ (-intercepting parent-transformer child-transformer))) (defn -map-transformer [ts] - #?(:clj (apply -comp (map (fn child-transformer [[k t]] + #?(:bb (fn [x] (reduce (fn child-transformer [m [k t]] + (if-let [entry (find m k)] + (assoc m k (t (val entry))) + m)) x ts)) + :clj (apply -comp (map (fn child-transformer [[k t]] (fn [^Associative x] (if-let [e ^MapEntry (.entryAt x k)] (.assoc x k (t (.val e))) x))) (rseq ts))) @@ -516,7 +524,8 @@ (defn -tuple-transformer [ts] (fn [x] (reduce-kv -update x ts))) (defn -collection-transformer [t empty] - #?(:clj (fn [x] (let [i (.iterator ^Iterable x)] + #?(:bb (fn [x] (into (when x empty) (map t) x)) + :clj (fn [x] (let [i (.iterator ^Iterable x)] (loop [x ^IPersistentCollection empty] (if (.hasNext i) (recur (.cons x (t (.next i)))) @@ -977,7 +986,8 @@ (fn [[key {:keys [optional]} value]] (let [valid? (-validator value) default (boolean optional)] - #?(:clj (fn [^Associative m] (if-let [map-entry (.entryAt m key)] (valid? (.val map-entry)) default)) + #?(:bb (fn [m] (if-let [map-entry (find m key)] (valid? (val map-entry)) default)) + :clj (fn [^Associative m] (if-let [map-entry (.entryAt m key)] (valid? (.val map-entry)) default)) :cljs (fn [m] (if-let [map-entry (find m key)] (valid? (val map-entry)) default))))) (-children this)) closed (conj (fn [m] (reduce (fn [acc k] (if (contains? keyset k) acc (reduced false))) true (keys m))))) @@ -1976,7 +1986,7 @@ (into-schema? ?schema) (-into-schema ?schema nil nil options) (vector? ?schema) (let [v #?(:clj ^IPersistentVector ?schema, :cljs ?schema) t #?(:clj (.nth v 0), :cljs (nth v 0)) - n #?(:clj (.count v), :cljs (count v)) + n #?(:bb (count v) :clj (.count v), :cljs (count v)) ?p (when (> n 1) #?(:clj (.nth v 1), :cljs (nth v 1)))] (if (or (nil? ?p) (map? ?p)) (into-schema t ?p (when (< 2 n) (subvec ?schema 2 n)) options) diff --git a/src/malli/impl/util.cljc b/src/malli/impl/util.cljc index f0d2898a4..6124343ee 100644 --- a/src/malli/impl/util.cljc +++ b/src/malli/impl/util.cljc @@ -1,5 +1,6 @@ (ns malli.impl.util - #?(:clj (:import (clojure.lang MapEntry LazilyPersistentVector) + #?(:clj (:import #?(:bb (clojure.lang MapEntry) + :clj (clojure.lang MapEntry LazilyPersistentVector)) (java.util.concurrent TimeoutException TimeUnit FutureTask)))) (def ^:const +max-size+ #?(:clj Long/MAX_VALUE, :cljs (.-MAX_VALUE js/Number))) @@ -22,7 +23,8 @@ (if-not (zero? c) (let [oa (object-array c), iter (.iterator ^Iterable os)] (loop [n 0] (when (.hasNext iter) (aset oa n (f (.next iter))) (recur (unchecked-inc n)))) - (LazilyPersistentVector/createOwning oa)) [])) + #?(:bb (vec oa) + :clj (LazilyPersistentVector/createOwning oa))) [])) :cljs (into [] (map f) os)))) #?(:clj diff --git a/src/malli/sci.cljc b/src/malli/sci.cljc index 2ea9adc0c..c4fb24aed 100644 --- a/src/malli/sci.cljc +++ b/src/malli/sci.cljc @@ -2,11 +2,14 @@ (:require [borkdude.dynaload :as dynaload])) (defn evaluator [options fail!] - (let [eval-string* (dynaload/dynaload 'sci.core/eval-string* {:default nil}) - init (dynaload/dynaload 'sci.core/init {:default nil}) - fork (dynaload/dynaload 'sci.core/fork {:default nil})] - (fn [] (if (and @eval-string* @init @fork) - (let [ctx (init options)] - (eval-string* ctx "(alias 'm 'malli.core)") - (fn eval [s] (eval-string* (fork ctx) (str s)))) - fail!)))) + #?(:bb (fn [] + (fn [form] + (load-string (str "(ns user (:require [malli.core :as m]))\n" form)))) + :default (let [eval-string* (dynaload/dynaload 'sci.core/eval-string* {:default nil}) + init (dynaload/dynaload 'sci.core/init {:default nil}) + fork (dynaload/dynaload 'sci.core/fork {:default nil})] + (fn [] (if (and @eval-string* @init @fork) + (let [ctx (init options)] + (eval-string* ctx "(alias 'm 'malli.core)") + (fn eval [s] (eval-string* (fork ctx) (str s)))) + fail!))))) diff --git a/test/bb_test_runner.clj b/test/bb_test_runner.clj new file mode 100644 index 000000000..f273299a0 --- /dev/null +++ b/test/bb_test_runner.clj @@ -0,0 +1,40 @@ +(ns bb-test-runner + (:require + [clojure.test :as t] + [malli.clj-kondo-test] + [malli.core-test] + [malli.destructure-test] + [malli.dot-test] + [malli.error-test] + [malli.experimental-test] + [malli.generator-test] + [malli.instrument-test] + [malli.json-schema-test] + [malli.plantuml-test] + [malli.provider-test] + [malli.registry-test] + [malli.swagger-test] + [malli.transform-test] + [malli.util-test])) + +(defn run-tests [& _args] + (let [{:keys [fail error]} + (t/run-tests + 'malli.core-test + 'malli.clj-kondo-test + 'malli.destructure-test + 'malli.dot-test + 'malli.error-test + 'malli.experimental-test + 'malli.instrument-test + 'malli.json-schema-test + ;; 'malli.generator-test ;; skipped for now due to test.chuck incompatibility + 'malli.plantuml-test + 'malli.provider-test + 'malli.registry-test + 'malli.swagger-test + 'malli.transform-test + 'malli.util-test)] + (when (or (pos? fail) + (pos? error)) + (System/exit 1)))) diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index 3f4c63105..079042f05 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -92,14 +92,16 @@ (is (= ['int? 'string?] (map m/form (m/eval "(m/children [:or {:some \"props\"} int? string?])")))) (is (schema= [[:x [::m/val 'int?]] [:y [::m/val 'string?]]] (m/eval "(m/entries [:map [:x int?] [:y string?]])"))) (is (schema= [[:x nil 'int?] [:y nil 'string?]] (m/eval "(m/children [:map [:x int?] [:y string?]])")))) - (testing "with options" - (testing "disabling sci" - (is (= 2 ((m/eval inc {::m/disable-sci true}) 1))) - (is (thrown? #?(:clj Exception, :cljs js/Error) ((m/eval 'inc {::m/disable-sci true}) 1)))) - (testing "custom bindings" - (let [f '(fn [schema] (m/form schema))] - (is (thrown? #?(:clj Exception, :cljs js/Error) ((m/eval f) :string))) - (is (= :string ((m/eval f {::m/sci-options {:namespaces {'malli.core {'form m/form}}}}) :string))))))) + #?(:bb nil + :default + (testing "with options" + (testing "disabling sci" + (is (= 2 ((m/eval inc {::m/disable-sci true}) 1))) + (is (thrown? #?(:clj Exception, :cljs js/Error) ((m/eval 'inc {::m/disable-sci true}) 1)))) + (testing "custom bindings" + (let [f '(fn [schema] (m/form schema))] + (is (thrown? #?(:clj Exception, :cljs js/Error) ((m/eval f) :string))) + (is (= :string ((m/eval f {::m/sci-options {:namespaces {'malli.core {'form m/form}}}}) :string)))))))) (deftest into-schema-test (is (form= [:map {:closed true} [:x int?]] @@ -837,7 +839,8 @@ #?(:clj (testing "non-terminating functions DO NOT fail fast" (let [schema [:fn '(fn [x] (< x (apply max (range))))]] - (is (= ::miu/timeout (miu/-run (fn [] (m/validate schema 1)) 100))) + #?(:bb nil ;; Graalvm doesn't support calling .stop on Threads since that is deprecated + :clj (is (= ::miu/timeout (miu/-run (fn [] (m/validate schema 1)) 100)))) #_(is (false? (m/validate schema 1))) #_(is (results= {:schema schema :value 1 diff --git a/test/malli/generator_test.cljc b/test/malli/generator_test.cljc index 6d6c201dd..e3598dbdd 100644 --- a/test/malli/generator_test.cljc +++ b/test/malli/generator_test.cljc @@ -150,7 +150,8 @@ [:int [:map [:type [:= :int]] [:int int?]]] [:multi [:map [:type [:= :multi]] [:multi {:optional true} [:ref ::multi]]]]]}} ::multi]))) - #?(:clj (testing "regex" + #?(:bb nil ;; test.chuck doesn't work in bb + :clj (testing "regex" (let [re #"^\d+ \d+$"] (m/validate re (mg/generate re))) diff --git a/test/malli/util_test.cljc b/test/malli/util_test.cljc index e499cd1b2..0d7b4fb0f 100644 --- a/test/malli/util_test.cljc +++ b/test/malli/util_test.cljc @@ -1,11 +1,20 @@ (ns malli.util-test (:require [clojure.test :refer [are deftest is testing]] - #?(:clj [jsonista.core :as json]) + #?(:bb [cheshire.core :as json] + :clj [jsonista.core :as json]) [malli.core :as m] [malli.impl.util :as miu] [malli.registry :as mr] [malli.util :as mu])) +#?(:clj (defn from-json [s] + #?(:bb (json/parse-string s) + :clj (json/read-value s)))) + +#?(:clj (defn to-json [x] + #?(:bb (json/generate-string x) + :clj (json/write-value-as-string x)))) + (defn form= [& ?schemas] (apply = (map #(if (m/schema? %) (m/form %) %) ?schemas))) @@ -1048,11 +1057,11 @@ "value" 1}] "schema" ["map" ["a" ["vector" ["maybe" "string"]]]] "value" {"a" 1}} - (json/read-value (json/write-value-as-string (mu/explain-data schema input-1))))) + (from-json (to-json (mu/explain-data schema input-1))))) (is (= {"errors" [{"in" ["a" 0] "path" ["a" 0 0] "schema" "string" "value" true}] "schema" ["map" ["a" ["vector" ["maybe" "string"]]]] "value" {"a" [true]}} - (json/read-value (json/write-value-as-string (mu/explain-data schema input-2))))))))) + (from-json (to-json (mu/explain-data schema input-2)))))))))