From 635cab0e494c0a5bebc722284c620534c6cf6105 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Thu, 25 Jun 2026 04:42:35 -0400 Subject: [PATCH 1/3] host interop + deps fixes for running ring-defaults on jolt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shaken out getting ring-defaults (and its ring-core/anti-forgery/session stack) to load and serve static resources on jolt. All general fixes, all runtime: - Class/forName throws a catchable ClassNotFoundException for a class jolt can't back (it returned a broken truthy value for any name, and crashed on use). Lets the common (try (Class/forName "optional.Dep") (catch ...)) probe libraries use to detect an absent dependency work — e.g. ring's joda-time check. - deps: reconcile native libs (and source roots) in one step, deduped by library identity, instead of the ad-hoc distinct at each call site. An app pulling two libs that declare the same shared object (libcrypto via both jolt-crypto and http-client) now includes and loads it once. - io: a File answers getProtocol ("file") / getFile so resource-serving middleware that expects io/resource to hand back a file: URL works; the classloader gains getResources (every source root holding the resource). - clojure.string/replace accepts a char match/replacement, like the JVM. JVM-certified corpus rows for the Class/forName and string/replace behavior. --- host/chez/host-static-methods.ss | 23 ++++++++++++++------ host/chez/io.ss | 16 ++++++++++++++ host/chez/natives-str.ss | 6 ++++-- jolt-core/jolt/deps.clj | 37 ++++++++++++++++++++++++++------ test/chez/corpus.edn | 4 ++++ 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/host/chez/host-static-methods.ss b/host/chez/host-static-methods.ss index 500a423..da65e1b 100644 --- a/host/chez/host-static-methods.ss +++ b/host/chez/host-static-methods.ss @@ -206,13 +206,24 @@ (register-class-statics! "NumberFormat" nf-statics) (register-class-statics! "java.text.NumberFormat" nf-statics)) +;; Class.forName: an array descriptor ("[C") is its own class token; a class Jolt +;; can back (registered statics/ctor, or a java.*/clojure.* core class) yields a +;; class object; anything else throws a catchable ClassNotFoundException, like the +;; JVM — so the common `(try (Class/forName "optional.Dep") (catch …))` probe a +;; library uses to detect an absent dependency works (e.g. ring's joda-time check). +(define (forname-known? nm) + (or (lookup-class class-statics-tbl nm) + (lookup-class class-ctors-tbl nm) + (let ((pre? (lambda (p) (and (>= (string-length nm) (string-length p)) + (string=? (substring nm 0 (string-length p)) p))))) + (or (pre? "java.") (pre? "clojure.") (pre? "jolt."))))) (register-class-statics! "Class" - ;; an array descriptor ("[C", "[I", …) is its own class token (so instance? and - ;; class compare equal); other names become a class jhost. - (list (cons "forName" (lambda (nm) - (if (and (> (string-length nm) 0) (char=? (string-ref nm 0) #\[)) - nm - (make-jhost "class" (list (cons 'name nm)))))))) + (list (cons "forName" + (lambda (nm . _) + (cond + ((and (> (string-length nm) 0) (char=? (string-ref nm 0) #\[)) nm) + ((forname-known? nm) (make-class-obj nm)) + (else (jolt-throw (jolt-host-throwable "java.lang.ClassNotFoundException" nm)))))))) ;; ---- System helpers (defined before use above via top-level order) ---------- ;; os.name reflects the actual platform (Chez's machine-type names it): a *osx diff --git a/host/chez/io.ss b/host/chez/io.ss index f08caa6..9bd9454 100644 --- a/host/chez/io.ss +++ b/host/chez/io.ss @@ -131,6 +131,11 @@ ((string=? name "getCanonicalPath")(list (jfile-abs fp))) ((string=? name "toURI") (list (string-append "file:" (jfile-abs fp)))) ((string=? name "toURL") (list (make-url (string-append "file:" (jfile-abs fp))))) + ;; io/resource returns a File where the JVM returns a file: URL; answer the + ;; two URL methods resource-serving middleware (ring) calls on the result, so + ;; it sees a "file" protocol and a path without changing the return type. + ((string=? name "getProtocol") (list "file")) + ((string=? name "getFile") (list (jfile-abs fp))) ((string=? name "exists") (list (if (file-exists? fp) #t #f))) ((string=? name "isDirectory") (list (if (file-directory? fp) #t #f))) ((string=? name "isFile") (list (if (and (file-exists? fp) (not (file-directory? fp))) #t #f))) @@ -433,8 +438,19 @@ ((file-exists? (string-append (car roots) "/" nm)) (make-url (string-append "file:" (car roots) "/" nm))) (else (loop (cdr roots))))))) +;; getResources: every source root that holds the named resource, as file: URLs +;; (enumeration-seq just calls seq, so a list serves). ring's static-resource +;; symlink check enumerates these to confirm a served file sits under a root. +(define (cl-get-resources self name) + (let ((nm (jolt-str-render-one name))) + (let loop ((roots (get-source-roots)) (acc '())) + (cond ((null? roots) (list->cseq (reverse acc))) + ((file-exists? (string-append (car roots) "/" nm)) + (loop (cdr roots) (cons (make-url (string-append "file:" (car roots) "/" nm)) acc))) + (else (loop (cdr roots) acc)))))) (register-host-methods! "classloader" (list (cons "getResource" cl-get-resource) + (cons "getResources" cl-get-resources) (cons "getResourceAsStream" (lambda (self name) (let ((u (cl-get-resource self name))) diff --git a/host/chez/natives-str.ss b/host/chez/natives-str.ss index cfa4fb2..474f631 100644 --- a/host/chez/natives-str.ss +++ b/host/chez/natives-str.ss @@ -303,7 +303,9 @@ (define (str-replace-all pat repl s) (if (jolt-regex? pat) (re-replace (regex-t-irx pat) s repl #t) - (str-replace-literal s pat repl))) + ;; literal match: a char/number match or replacement (str/replace s \a \b) + ;; coerces to a string, as on the JVM. + (str-replace-literal s (str-needle pat) (str-needle repl)))) (define (str-replace-literal-first s a b) (let ((alen (string-length a)) (i (str-index-of s a 0))) (if (fx O(n^2)). @@ -91,7 +115,7 @@ roots [] natives []] (if (>= i (count queue)) - {:roots (distinct roots) :natives natives} + {:roots roots :natives natives} (let [[coord spec bd] (nth queue i) i (inc i)] (if (contains? seen coord) @@ -125,7 +149,8 @@ project-roots (map #(abspath project-dir %) project-paths) all-deps (merge (:deps edn) extra-deps) {dep-roots :roots dep-natives :natives} (resolve-deps all-deps project-dir)] - {:roots (vec (distinct (concat project-roots dep-roots))) + ;; reconcile: the project's own roots/natives + every dep's, deduped once. + {:roots (dedup-by identity (concat project-roots dep-roots)) :main-opts main-opts ;; the project's own paths (relative to project-dir) and absolute resource ;; roots, plus its :jolt/build options — `jolt build` uses these to bundle @@ -136,7 +161,7 @@ :build (:jolt/build edn) :embed-dirs (mapv #(abspath project-dir %) (:embed (:jolt/build edn))) :tasks (:tasks edn) - :natives (vec (distinct (concat (:jolt/native edn) dep-natives))) + :natives (dedup-by native-key (concat (:jolt/native edn) dep-natives)) ;; nREPL middleware a library contributes (jolt.nrepl composes them over its ;; built-in handler) — symbols resolving to a middleware fn or a vector of them. :nrepl-middleware (:nrepl/middleware edn)}))) diff --git a/test/chez/corpus.edn b/test/chez/corpus.edn index 0159023..0afe82b 100644 --- a/test/chez/corpus.edn +++ b/test/chez/corpus.edn @@ -1,4 +1,8 @@ [ + {:suite "interop / Class.forName" :label "unknown class throws ClassNotFoundException" :expected ":nf" :actual "(try (Class/forName \"no.such.Klass\") (catch ClassNotFoundException _ :nf))"} + {:suite "interop / Class.forName" :label "known class resolves" :expected "\"java.lang.String\"" :actual "(.getName (Class/forName \"java.lang.String\"))"} + {:suite "clojure.string / replace" :label "char match and replacement" :expected "\"a-b-c\"" :actual "(clojure.string/replace \"a/b/c\" \\/ \\-)"} + {:suite "clojure.string / replace" :label "char match, string replacement" :expected "\"x_y\"" :actual "(clojure.string/replace \"x.y\" \\. \"_\")"} {:suite "interop-fixes / deprecated #^ metadata reader" :label "#^ type hint on a param" :expected "\"x\"" :actual "(do (defn f1 [#^String s] s) (f1 \"x\"))"} {:suite "interop-fixes / deprecated #^ metadata reader" :label "#^\"[B\" array hint" :expected "[1 2]" :actual "(do (defn f2 [#^\"[B\" b] b) (f2 [1 2]))"} {:suite "interop-fixes / deprecated #^ metadata reader" :label "#^ is equivalent to ^" :expected "true" :actual "(= (meta (with-meta [] {:tag (quote String)})) {:tag (quote String)})"} From 65c8072ec8925032185703c203517f843c8ffa2a Mon Sep 17 00:00:00 2001 From: Yogthos Date: Thu, 25 Jun 2026 06:29:57 -0400 Subject: [PATCH 2/3] io/copy: write to a byte-output-stream shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit io/copy handled file/stream/writer targets but not a host byte-output-stream table (jolt-lang/http-client's ByteArrayOutputStream shim, :jolt/output-stream), erroring 'don't know how to write to'. Dispatch through the shim's .write method, byte-exact for a byte source — the JVM's io/copy writes to any OutputStream. --- host/chez/io-streams.ss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/host/chez/io-streams.ss b/host/chez/io-streams.ss index 00a843b..3eb9782 100644 --- a/host/chez/io-streams.ss +++ b/host/chez/io-streams.ss @@ -296,6 +296,14 @@ (let ((port (open-file-output-port (path-of output) (file-options no-fail) (buffer-mode block)))) (put-bytevector port bv) (close-port port)) (jolt-spit output (input-text input))))) + ;; a byte-output-stream shim (a host tagged-table with :jolt/output-stream, + ;; e.g. http-client's ByteArrayOutputStream): write through its .write method, + ;; byte-exact for a byte source. + ((and (htable? output) (jolt-truthy? (jolt-ref-get output (keyword "jolt" "output-stream")))) + (let ((bv (input-bytes input))) + (record-method-dispatch output "write" + (list->cseq (list (if bv (make-jolt-array (list->vector (bytevector->u8-list bv)) 'byte) + (input-text input))))))) (else (error #f "io/copy: don't know how to write to" output))) jolt-nil) (def-var! "clojure.java.io" "copy" jio-copy) From b2f671989de697ab6d1022fa63e2fea607c3b23e Mon Sep 17 00:00:00 2001 From: Yogthos Date: Thu, 25 Jun 2026 06:37:27 -0400 Subject: [PATCH 3/3] docs: list ring-defaults (via jolt-crypto) --- docs/libraries.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/libraries.md b/docs/libraries.md index f2fc671..9a6308e 100644 --- a/docs/libraries.md +++ b/docs/libraries.md @@ -14,6 +14,10 @@ the [examples](https://github.com/jolt-lang/examples), e.g. the * [ring-core](https://github.com/ring-clojure/ring) — via `:deps/root "ring-core"`, on the ring-app example * [ring-codec](https://github.com/ring-clojure/ring-codec) — URL/form encoding +* [ring-defaults](https://github.com/ring-clojure/ring-defaults) — the standard + middleware stack (params, static resources + content-type, session, security + headers); its session/CSRF crypto comes from + [jolt-lang/jolt-crypto](https://github.com/jolt-lang/jolt-crypto) (OpenSSL) * [reitit-core](https://github.com/metosin/reitit) — data-driven routing; the `reitit.Trie` Java class is mirrored by [jolt-lang/router](https://github.com/jolt-lang/router). `JOLT_FEATURES` `clj`.