commit 0c76ea8b82b9d5f72359186f2321d4257a917cb9 from: Omar Polo date: Fri Oct 15 21:33:26 2021 UTC rewrite APIs the old API were a thin layer on top of the java class, this is a beginning to use a more clojure-ish ones. commit - 958b98ffc8f0bd8a11fbf894dfced6fb3914c0a2 commit + 0c76ea8b82b9d5f72359186f2321d4257a917cb9 blob - 433d9d6f88de57d8f81b651ec51f38fe0d2de6db blob + d6893054dd424cf0413db01feac1c84bf0d1594f --- src/gemini/core.clj +++ src/gemini/core.clj @@ -2,54 +2,13 @@ (:require [clojure.java.io :as io]) (:import + (java.net URI) (com.omarpolo.gemini Request))) -(defmacro ^:private request->map [& args] - `(try - (let [req# (Request. ~@args)] - {:request req# - :code (.getCode req#) - :meta (.getMeta req#) - :body (.body req#)}) - (catch Throwable e# - {:error e#}))) +(comment + (set! *warn-on-reflection* true) +) -(defn fetch - "Make a gemini request. `uri` may be a URI, URL or string, and - represent the request to perform. `host` and `port` are extracted - from the given `uri` in not given, and port defaults to 1965. The - returned request needs to be closed when done." - ([uri] - (request->map uri)) - ([host uri] - (fetch host 1965 uri)) - ([host port uri] - (request->map host port uri))) - -(defn body-as-string! - "Read all the response into a strings and returns it. The request - will be closed." - [{r :request}] - (let [sw (java.io.StringWriter.)] - (with-open [r r] - (io/copy (.body r) sw) - (.toString sw)))) - -(defn close - "Close a request." - [{r :request}] - (.close r)) - -(defmacro with-request - "Make a request, eval `body` when it succeed and automatically close - the request, or throw an exception if the request fails." - [[var req] & body] - `(let [~var ~req] - (when-let [e# (:error ~var)] - (throw e#)) - (with-open [req# (:request ~var)] - ~@body))) - ;; helpers @@ -80,3 +39,103 @@ (defn is-temporary-failure? [{c :code}] (= 4 (/ c 10))) (defn is-permanent-failure? [{c :code}] (= 5 (/ c 10))) (defn is-client-cert-required? [{c :code}] (= 6 (/ c 10))) + + + +(defn- parse-params [{:keys [proxy request follow-redirects?]}] + (when (and (:host proxy) + (not (:port proxy))) + (throw (ex-info "invalid proxy definition" {:got proxy + :reason "missing proxy host"}))) + {:host (:host proxy) + :port (:port proxy) + :request (or request + (throw (ex-info ":request is nil" {}))) + :follow-redirects? (case follow-redirects? + nil 0 + false 0 + true 5 + follow-redirects?)}) + +(defn- resolve-uri [request meta] + (let [uri (URI. request) + rel (URI. meta)] + (str (.resolve uri rel)))) + +(defn- fetch' [host port uri] + (try + (let [req (cond + host (Request. ^String host ^int port ^String uri) + :else (Request. ^String uri))] + {:uri uri + :request req + :code (.getCode req) + :meta (.getMeta req) + :body (.body req)}) + (catch Throwable e + {:error e}))) + +(defn fetch + "Make a gemini request. `params` is a map with the following + keys (only `:request` is mandatory): + + - `:proxy`: a map of `:host` and `:port`, identifies the server to + send the requests to. This allows to use a gemini server as a + proxy, it doesn't do any other kind of proxying (e.g. SOCK5.) + + - `:request` the URI (as string) to require. + + - `:follow-redirects?` if `false` or `nil` don't follow redirects, + if `true` follow up to 5 redirects, or the number of redirects to + follow. + + Return a map with `:request`, `:code`, `:meta`, `:body` on success + or `:error` on failure. The request needs to be closed when done + usign `close`." + [params] + (let [{:keys [host port request follow-redirects?] :as orig} (parse-params params)] + (loop [n follow-redirects? + request request + redirected? false] + (let [res (fetch' host port request) + redirect? (and (not (:error res)) + (is-redirect? res))] + (cond + (:error res) res + (= follow-redirects? 0) res + (and (= 0 n) + redirect?) (do (.close ^Request (:request res)) + (throw (ex-info "too many redirects" + {:original orig + :redirects follow-redirects? + :code (:code res) + :meta (:meta res)}))) + redirect? (do (.close ^Request (:request res)) + (recur (dec n) + (resolve-uri request (:meta res)) + true)) + :else res))))) + +(defn body-as-string! + "Read all the response into a strings and returns it. The request + will be closed." + [{r :request}] + (let [sw (java.io.StringWriter.)] + (with-open [r ^Request r] + (io/copy (.body r) sw) + (.toString sw)))) + +(defn close + "Close a request." + [{r :request}] + (.close ^Request r)) + +(defmacro with-request + "Make a request, eval `body` when it succeed and automatically close + the request, or throw an exception if the request fails." + [[var req] & body] + `(let [~var ~req] + (when-let [e# (:error ~var)] + (throw e#)) + (with-open [req# (:request ~var)] + ~@body)))