From 13aaf74c4b51439f6c2294d62fdd48c1ac4cb248 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Mon, 22 Jun 2026 13:54:04 -0400 Subject: [PATCH] Host completeness for clj-http-lite: with-open/slurp on shims, charset, exc classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gaps the clj-http-lite suite hit running over jolt-lang/http-client: - with-open now closes a tagged-table stream shim via its .close method (was a :close field lookup that only matched the Janet-era tables). - slurp accepts a bytevector / jolt byte-array / a byte input-stream shim and honors :encoding — clj-http-lite slurps response bodies and :as :stream bodies. - (.getBytes s charset) and Charset/forName respect the charset (ISO-8859-1 etc., one byte per char), not always UTF-8; Charset/forName returns the name string. - (class e) reads the JVM :class off a throwable tagged-table, so clojure.test's (thrown? Class …) / (= Class (class e)) match a library's typed exceptions. - Registered the HTTP/IO exception class names (IOException, UnknownHostException, SocketTimeoutException, SSLException, …) so the FQ literals self-evaluate. All runtime .ss shims, no re-mint. Full gate green. --- host/chez/host-class.ss | 16 ++++++++++++++++ host/chez/host-static.ss | 4 +++- host/chez/host-table.ss | 9 +++++++++ host/chez/io.ss | 25 +++++++++++++++++++++++++ host/chez/natives-str.ss | 12 +++++++++++- 5 files changed, 64 insertions(+), 2 deletions(-) diff --git a/host/chez/host-class.ss b/host/chez/host-class.ss index 9d9a333..ba67878 100644 --- a/host/chez/host-class.ss +++ b/host/chez/host-class.ss @@ -49,7 +49,16 @@ ("Charset" . "java.nio.charset.Charset") ("Base64" . "java.util.Base64") ("Exception" . "java.lang.Exception") ("IllegalArgumentException" . "java.lang.IllegalArgumentException") + ("IllegalStateException" . "java.lang.IllegalStateException") + ("RuntimeException" . "java.lang.RuntimeException") + ("UnsupportedOperationException" . "java.lang.UnsupportedOperationException") ("InterruptedException" . "java.lang.InterruptedException") + ("IOException" . "java.io.IOException") + ("UnknownHostException" . "java.net.UnknownHostException") + ("ConnectException" . "java.net.ConnectException") + ("SocketTimeoutException" . "java.net.SocketTimeoutException") + ("MalformedURLException" . "java.net.MalformedURLException") + ("SSLException" . "javax.net.ssl.SSLException") ("Throwable" . "java.lang.Throwable"))) (for-each (lambda (pair) (def-var! "clojure.core" (car pair) (cdr pair))) @@ -74,4 +83,11 @@ '("java.lang.Long" "java.lang.Integer" "java.lang.Double" "java.lang.Float" "java.lang.Number" "java.lang.String" "java.lang.Boolean" "java.lang.Character" "java.lang.Object" + ;; exception classes compared against (class e): (= java.net.SocketTimeoutException (class e)) + "java.lang.Exception" "java.lang.Throwable" "java.lang.RuntimeException" + "java.lang.IllegalArgumentException" "java.lang.IllegalStateException" + "java.lang.UnsupportedOperationException" "java.io.IOException" + "java.net.UnknownHostException" "java.net.ConnectException" + "java.net.SocketTimeoutException" "java.net.MalformedURLException" + "javax.net.ssl.SSLException" "clojure.lang.Keyword" "clojure.lang.Symbol" "clojure.lang.Ratio" "clojure.lang.Atom")) diff --git a/host/chez/host-static.ss b/host/chez/host-static.ss index 1c574d3..5904f4c 100644 --- a/host/chez/host-static.ss +++ b/host/chez/host-static.ss @@ -583,7 +583,9 @@ (let loop ((l lst) (i 0)) (if (null? l) bv (begin (bytevector-u8-set! bv i (car l)) (loop (cdr l) (+ i 1))))))) (register-class-statics! "URLEncoder" (list (cons "encode" url-encode))) (register-class-statics! "URLDecoder" (list (cons "decode" url-decode))) -(register-class-statics! "Charset" (list (cons "forName" (lambda (nm) (make-jhost "charset" (list (cons 'name nm))))))) +;; Charset/forName yields the canonical name STRING (not an opaque object) so it +;; threads straight into (.getBytes s cs) / (String. bytes cs), which take a name. +(register-class-statics! "Charset" (list (cons "forName" (lambda (nm) (jolt-str-render-one nm))))) ;; ---- Base64 (RFC 4648) ------------------------------------------------------ (define b64-alphabet "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") diff --git a/host/chez/host-table.ss b/host/chez/host-table.ss index 39fcdbf..b155dba 100644 --- a/host/chez/host-table.ss +++ b/host/chez/host-table.ss @@ -176,3 +176,12 @@ ((htable-sorted-set? obj) '("PersistentTreeSet" "Sorted" "IPersistentSet" "Set" "java.util.Set" "Collection" "IPersistentCollection" "Object")) (else (%h-value-host-tags obj))))) + +;; (class e) on a throwable tagged-table (a library's ex-info envelope carrying a +;; JVM :class, e.g. jolt-lang/http-client's UnknownHostException) reads that +;; class name, so clojure.test's (thrown? Class …) / (= Class (class e)) match. +(define %h-class jolt-class) +(set! jolt-class (lambda (x) + (let ((c (and (htable? x) (hashtable-ref (htable-h x) "class" #f)))) + (if (and c (string? c)) c (%h-class x))))) +(def-var! "clojure.core" "class" jolt-class) diff --git a/host/chez/io.ss b/host/chez/io.ss index 766bb28..fc7bfe3 100644 --- a/host/chez/io.ss +++ b/host/chez/io.ss @@ -151,10 +151,32 @@ ((reader-jhost? rdr) (drain-reader rdr)) (else (jolt-str-render-one rdr)))))) +;; (slurp src :encoding "...") — pull the charset from the trailing kwargs. +(define (slurp-encoding opts) + (let loop ((o opts)) + (cond ((or (null? o) (null? (cdr o))) '()) + ((and (keyword-t? (car o)) (string=? (keyword-t-name (car o)) "encoding")) + (list (jolt-str-render-one (cadr o)))) + (else (loop (cddr o)))))) +;; drain a byte input-stream shim (tagged-table) one byte at a time to a bytevector. +(define (drain-byte-stream src) + (let loop ((acc '())) + (let ((b (record-method-dispatch src "read" jolt-nil))) + (if (or (jolt-nil? b) (and (number? b) (< b 0))) + (u8-list->bytevector (reverse acc)) + (loop (cons (bitwise-and (jnum->exact b) #xff) acc)))))) (define (jolt-slurp src . opts) (cond ((jfile? src) (read-file-string (jfile-path src))) ((reader-jhost? src) (drain-reader src)) + ;; bytes (a bytevector or a jolt byte-array): decode with :encoding (UTF-8 + ;; default). clj-http-lite slurps response-body byte arrays. + ((bytevector? src) (decode-bytevector src (slurp-encoding opts))) + ((and (jolt-array? src) (eq? (jolt-array-kind src) 'byte)) + (decode-bytevector (na-bytearray->bv src) (slurp-encoding opts))) + ;; a byte input-stream shim (e.g. clj-http-lite's :as :stream body): drain it. + ((and (htable? src) (jolt-truthy? (jolt-ref-get src (keyword "jolt" "input-stream")))) + (decode-bytevector (drain-byte-stream src) (slurp-encoding opts))) ((string? src) (read-file-string src)) (else (error #f "slurp: unsupported source" src)))) @@ -230,6 +252,9 @@ ((jolt-nil? x) jolt-nil) ((and (jhost? x) (member (jhost-tag x) '("string-reader" "pushback-reader" "writer"))) (record-method-dispatch x "close" jolt-nil) jolt-nil) + ;; a library's stream shim (tagged-table) closes via its registered .close + ;; method (a no-op for in-memory streams); absent method -> no-op. + ((htable? x) (guard (e (#t jolt-nil)) (record-method-dispatch x "close" jolt-nil)) jolt-nil) ((jfile? x) jolt-nil) (else (let ((closef (jolt-get x (keyword #f "close") jolt-nil))) diff --git a/host/chez/natives-str.ss b/host/chez/natives-str.ss index ac09f65..20d18ad 100644 --- a/host/chez/natives-str.ss +++ b/host/chez/natives-str.ss @@ -117,7 +117,17 @@ (string=? (ascii-string-down s) (ascii-string-down (arg 0)))) ((string=? method "compareTo") (let ((o (arg 0))) (cond ((string? s o) 1.0) (else 0.0)))) - ((string=? method "getBytes") (string->utf8 s)) + ((string=? method "getBytes") + ;; (.getBytes s) / (.getBytes s charset). UTF-8 default; ISO-8859-1/latin1/ + ;; ascii encode one byte per char (clj-http-lite's body-encoding path). + (if (null? rest) + (string->utf8 s) + (let ((cn (ascii-string-down (if (string? (arg 0)) (arg 0) (jolt-str-render-one (arg 0)))))) + (if (member cn '("iso-8859-1" "latin1" "iso8859-1" "us-ascii" "ascii")) + (let* ((n (string-length s)) (bv (make-bytevector n))) + (do ((i 0 (+ i 1))) ((= i n) bv) + (bytevector-u8-set! bv i (bitwise-and (char->integer (string-ref s i)) #xff)))) + (string->utf8 s))))) ((string=? method "matches") (if (irregex-match (str-irx (arg 0)) s) #t #f)) ((string=? method "replaceAll") (irregex-replace/all (str-irx (arg 0)) s (arg 1))) ((string=? method "replaceFirst") (irregex-replace (str-irx (arg 0)) s (arg 1)))