From 59cbb2568871cf542ea259ad04c063750b4efc85 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 13 Aug 2018 08:17:17 +0300 Subject: [PATCH] initial http-handler --- .../reitit-core/src/reitit/interceptor.cljc | 7 +- modules/reitit-http/project.clj | 10 + modules/reitit-http/src/reitit/http.cljc | 198 ++++++++++++++++++ modules/reitit-ring/src/reitit/ring.cljc | 2 +- .../reitit-swagger/src/reitit/swagger.cljc | 2 +- 5 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 modules/reitit-http/project.clj create mode 100644 modules/reitit-http/src/reitit/http.cljc diff --git a/modules/reitit-core/src/reitit/interceptor.cljc b/modules/reitit-core/src/reitit/interceptor.cljc index 8f9cfd0c..1e5af68d 100644 --- a/modules/reitit-core/src/reitit/interceptor.cljc +++ b/modules/reitit-core/src/reitit/interceptor.cljc @@ -112,8 +112,7 @@ (defn compile-result ([route opts] (compile-result route opts nil)) - ([[path {:keys [interceptors handler] :as data}] opts scope] - (ensure-handler! path data scope) + ([[_ {:keys [interceptors handler] :as data}] opts _] (map->Endpoint {:interceptors (chain interceptors handler data opts) :data data}))) @@ -131,8 +130,8 @@ Options: - | key | description | - | --------------------------------|-------------| + | key | description + | --------------------------------|------------- | `:reitit.interceptor/transform` | Function of [Interceptor] => [Interceptor] to transform the expanded Interceptors (default: identity). | `:reitit.interceptor/registry` | Map of `keyword => IntoInterceptor` to replace keyword references into Interceptor diff --git a/modules/reitit-http/project.clj b/modules/reitit-http/project.clj new file mode 100644 index 00000000..9adf42ac --- /dev/null +++ b/modules/reitit-http/project.clj @@ -0,0 +1,10 @@ +(defproject metosin/reitit-http "0.2.0-SNAPSHOT" + :description "Reitit: HTTP routing with interceptors" + :url "https://github.com/metosin/reitit" + :license {:name "Eclipse Public License" + :url "http://www.eclipse.org/legal/epl-v10.html"} + :plugins [[lein-parent "0.3.2"]] + :parent-project {:path "../../project.clj" + :inherit [:deploy-repositories :managed-dependencies]} + :dependencies [[metosin/reitit-core] + [metosin/reitit-ring]]) diff --git a/modules/reitit-http/src/reitit/http.cljc b/modules/reitit-http/src/reitit/http.cljc new file mode 100644 index 00000000..08d812f1 --- /dev/null +++ b/modules/reitit-http/src/reitit/http.cljc @@ -0,0 +1,198 @@ +(ns reitit.http + (:require [meta-merge.core :refer [meta-merge]] + [reitit.interceptor :as interceptor] + [reitit.ring :as ring] + [reitit.core :as r] + [reitit.impl :as impl])) + +(defrecord Endpoint [data handler path method interceptors]) + +(defn http-handler + "Creates a ring-handler out of a http-router and + an interceptor runner. + Optionally takes a ring-handler which is called + in no route matches." + ([router runner] + (http-handler router runner nil)) + ([router runner default-handler] + (let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))] + (with-meta + (fn [request] + (if-let [match (r/match-by-path router (:uri request))] + (let [method (:request-method request) + path-params (:path-params match) + result (:result match) + interceptors (-> result method :interceptors) + request (-> request + (impl/fast-assoc :path-params path-params) + (impl/fast-assoc ::r/match match) + (impl/fast-assoc ::r/router router))] + (:response (runner interceptors request))) + (default-handler request))) + {::r/router router})))) + +(defn get-router [handler] + (-> handler meta ::r/router)) + +(defn get-match [request] + (::r/match request)) + +(defn coerce-handler [[path data] {:keys [expand] :as opts}] + [path (reduce + (fn [acc method] + (if (contains? acc method) + (update acc method expand opts) + acc)) data ring/http-methods)]) + +(defn compile-result [[path data] opts] + (let [[top childs] (ring/group-keys data) + ->handler (fn [handler] + (if handler + (fn [ctx] + (->> ctx :request handler (assoc ctx :response))))) + compile (fn [[path data] opts scope] + (let [data (update data :handler ->handler)] + (interceptor/compile-result [path data] opts scope))) + ->endpoint (fn [p d m s] + (-> (compile [p d] opts s) + (map->Endpoint) + (assoc :path p) + (assoc :method m))) + ->methods (fn [any? data] + (reduce + (fn [acc method] + (cond-> acc + any? (assoc method (->endpoint path data method nil)))) + (ring/map->Methods {}) + ring/http-methods))] + (if-not (seq childs) + (->methods true top) + (reduce-kv + (fn [acc method data] + (let [data (meta-merge top data)] + (assoc acc method (->endpoint path data method method)))) + (->methods (:handler top) data) + childs)))) + +(defn router + "Creates a [[reitit.core/Router]] from raw route data and optionally an options map with + support for http-methods and Interceptors. See [docs](https://metosin.github.io/reitit/) + for details. + + Example: + + (router + [\"/api\" {:interceptors [format-i oauth2-i]} + [\"/users\" {:get get-user + :post update-user + :delete {:interceptors [delete-i] + :handler delete-user}}]]) + + See router options from [[reitit.core/router]] and [[reitit.middleware/router]]." + ([data] + (router data nil)) + ([data opts] + (let [opts (meta-merge {:coerce coerce-handler, :compile compile-result} opts)] + (r/router data opts)))) + +(ns reitit.interceptor.simple) + +(defn enqueue [ctx interceptors] + (update ctx :queue (fnil into clojure.lang.PersistentQueue/EMPTY) interceptors)) + + +(defn leave [ctx stack key] + (let [it (clojure.lang.RT/iter stack)] + (loop [ctx ctx, key key] + (if (.hasNext it) + (if-let [f (-> it .next key)] + (let [[ctx key] (try + [(f ctx)])]) + (recur + (try + (leave ctx))) + (recur ctx)) + ctx)))) + +(defn try-f [ctx f] + (try + (f ctx) + (catch Exception e + (assoc ctx :error e)))) + +(defn enter [ctx] + (let [queue ^clojure.lang.PersistentQueue (:queue ctx) + stack (:stack ctx) + error (:error ctx) + interceptor (peek queue)] + (cond + + ;; all done + (not interceptor) + (leave ctx stack :leave) + + ;; error + error + (leave (assoc ctx :queue nil) stack :error) + + ;; continue + :else + (let [queue (pop queue) + stack (conj stack interceptor) + f (or (:enter interceptor) identity) + ctx (-> ctx (assoc :queue queue) (assoc :stack stack) (try-f f))] + (recur ctx))))) + +(defrecord Context [queue stack request response]) + +(defn context [interceptors request] + (->Context (into clojure.lang.PersistentQueue/EMPTY interceptors) nil request nil)) + +(defn execute [interceptors request] + (enter (context interceptors request))) + +(ns user) + +(require '[reitit.http :as http]) +(require '[reitit.interceptor.simple :as simple]) + +(def i (fn [value] + {:enter (fn [ctx] + (update-in ctx [:request :enter] (fnil conj []) value)) + :leave (fn [ctx] + (update-in ctx [:response :body :leave] (fnil conj []) value))})) + +(def f (fn [key] {key (fn [ctx] (throw (ex-info "fail" {})))})) + +(def app + (http/http-handler + (http/router + ["/api" + {:interceptors [(i 1) (i 2)]} + ["/ipa" + {:interceptors [(i 3) (f :enter) (i 4)] + :handler (fn [{:keys [enter]}] + {:status 200 + :body {:enter enter}})}]]) + simple/execute)) + +(app {:request-method :get, :uri "/api/ipa"}) +; => {:status 200, :body {:enter [1 2 3 4], :leave [4 3 2 1]}} + +(def app2 + (http/http-handler + (http/router + ["/api" + {:interceptors [(i 1) (i 2)]} + ["/ipa" + {:interceptors [(i 3) (i 4) (fn [ctx] + (assoc ctx + :response + {:status 200 + :body {:enter (-> ctx + :request + :enter)}}))]}]]) + simple/execute)) + +(app2 {:request-method :get, :uri "/api/ipa"}) +; => {:status 200, :body {:enter [1 2 3 4], :leave [4 3 2 1]}} diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index 1c03e4f0..c01b1434 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -12,7 +12,7 @@ (defrecord Methods [get head post put delete connect options trace patch]) (defrecord Endpoint [data handler path method middleware]) -(defn- group-keys [data] +(defn ^:no-wiki group-keys [data] (reduce-kv (fn [[top childs] k v] (if (http-methods k) diff --git a/modules/reitit-swagger/src/reitit/swagger.cljc b/modules/reitit-swagger/src/reitit/swagger.cljc index c17f4fb5..bb92607f 100644 --- a/modules/reitit-swagger/src/reitit/swagger.cljc +++ b/modules/reitit-swagger/src/reitit/swagger.cljc @@ -13,7 +13,7 @@ (s/def ::summary string?) (s/def ::description string?) -(s/def ::swagger (s/keys :req-un [::id])) +(s/def ::swagger (s/keys :opt-un [::id])) (s/def ::spec (s/keys :opt-un [::swagger ::no-doc ::tags ::summary ::description])) (def swagger-feature