Skip to content

Commit

Permalink
Fix #302: babashka compatibility
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
borkdude committed Jul 1, 2022
1 parent bb52213 commit 1ec8582
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 32 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/clojure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
<a href="https://babashka.org" rel="nofollow"><img src="https://github.com/babashka/babashka/raw/master/logo/badge.svg" alt="bb compatible" style="max-width: 100%;"></a>

Data-driven Schemas for Clojure/Script.
Data-driven Schemas for Clojure/Script and [babashka](#babashka).

**STATUS**: well matured [*alpha*](#alpha)

Expand Down Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions bb.edn
Original file line number Diff line number Diff line change
@@ -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}}}
2 changes: 1 addition & 1 deletion deps.edn
Original file line number Diff line number Diff line change
@@ -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
Expand Down
24 changes: 17 additions & 7 deletions src/malli/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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))))

Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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)))
Expand All @@ -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))))
Expand Down Expand Up @@ -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)))))
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions src/malli/impl/util.cljc
Original file line number Diff line number Diff line change
@@ -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)))
Expand All @@ -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
Expand Down
19 changes: 11 additions & 8 deletions src/malli/sci.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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!)))))
40 changes: 40 additions & 0 deletions test/bb_test_runner.clj
Original file line number Diff line number Diff line change
@@ -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))))
21 changes: 12 additions & 9 deletions test/malli/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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?]]
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion test/malli/generator_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

Expand Down
15 changes: 12 additions & 3 deletions test/malli/util_test.cljc
Original file line number Diff line number Diff line change
@@ -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)))

Expand Down Expand Up @@ -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)))))))))

0 comments on commit 1ec8582

Please sign in to comment.