This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2019-08-26
Channels
- # announcements (6)
- # beginners (88)
- # calva (12)
- # cider (13)
- # cljs-dev (27)
- # cljsrn (2)
- # clojure (68)
- # clojure-argentina (2)
- # clojure-dev (10)
- # clojure-europe (1)
- # clojure-greece (1)
- # clojure-italy (5)
- # clojure-nl (15)
- # clojure-spec (33)
- # clojure-switzerland (1)
- # clojure-uk (10)
- # clojurescript (121)
- # clojutre (3)
- # code-reviews (2)
- # core-async (1)
- # cursive (10)
- # data-science (1)
- # datomic (21)
- # emacs (10)
- # events (1)
- # fulcro (25)
- # graphql (6)
- # joker (4)
- # kaocha (12)
- # lambdaisland (3)
- # music (2)
- # off-topic (112)
- # om (2)
- # re-frame (25)
- # reagent (29)
- # reitit (93)
- # rewrite-clj (2)
- # shadow-cljs (18)
- # slack-help (4)
- # spacemacs (8)
- # tools-deps (1)
- # vim (2)
- # yada (5)
Newbie question about interceptors, are there examples how to test interceptors (call interceptor from test)? I just started to use Reitit.
@danielcompton so:
1) spec-tools defines transformations based on :type
, here’s the list of predefined types: https://github.com/metosin/spec-tools/blob/master/src/spec_tools/parse.cljc#L66-L86
2) reitit has custom string-transformer
and json-transformer
which set the transformation of type :map
to spec-tools.transform/strip-extra-keys-transformer
, here https://github.com/metosin/reitit/blob/master/modules/reitit-spec/src/reitit/coercion/spec.cljc#L12-L20, the code on spec-tools here: https://github.com/metosin/spec-tools/blob/master/src/spec_tools/transform.cljc#L184-L185, e.g. the :map
mapping is done on the spec-tools side (kinda hides the mapping, which is not good)
3) here’s a code to create a custom coercion: https://github.com/metosin/reitit/blob/master/modules/reitit-spec/src/reitit/coercion/spec.cljc#L137. e.g. create a new options map with modified string & json transformer.
Sorry for all the boilerplate. This should be much simpler, without resolving to global settings. ideas welcome.
also, as spec is not intended for the runtime transformations, the fail-on-extra-keys
is kinda hack and can’t report on the extra keys.
I think doing explicit closed
specs would be simpler and more explicit, using spec-tools.spell
, here are samples: https://github.com/metosin/spec-tools/blob/master/CHANGELOG.md#092-alpha1
When Spec2 arrives, there are more official ways to close spec, currently much WIP/TBD.
@jussip check out https://github.com/metosin/reitit/blob/master/test/cljc/reitit/interceptor_test.cljc
… about the coercion settings. I think the coercion instance could be optionally backed by a multimethod, which would dispatch on the :type
and would use that as an override to default. One could change the mapping like:
(defmulti my-transformer identity)
(defmethod my-transformer :map [_] spec-tools.transform/fail-on-extra-keys)
;; all mappings from the multimethod are collected at coercion creation time
(def my-spec-coercion (reitit.coercion.spec/coercion (assoc reitit.coercion.spec/default-options :transformer my-transformer))
no, the multimethod would be a bad idea. Explicit map is better in all possible ways.
Hi guys
how reitit handles back button?
i would like to set back button to specific page at some pages
What is the recommended way to parameterize your handler functions? Perhaps something like this:
(defn init-data-interceptor
[config]
{:enter (fn [ctx] (assoc ctx :config config))})
(defn new-handler
[config]
(http/ring-handler
(http/router
(routes)
{:interceptors [(init-data-interceptor config)]})
(ring/create-default-handler)
{:executor sieppari/executor}))
Then your routes can access config
in the ctx. The same could be done for DB connections, cache initialization, etc.@kenny we use the reloaded workflow and run reset
to rebuild all the things. It's few hundred millis with all the db pools, http server etc.
for your code, looks good. Another way is to use compile-time injection: interceptors can access the router opts and store any data from it in a closure, so you don't need to inject things into request at request-time. All the default interceptors are written like this.
@ikitommi Thanks! I've only used the auto-reload in the past. Could try the reset everything approach, I suppose. How do interceptors access the router opts? e.g. What key is that under?
all the keys, e.g. the whole router opts is passed as the sexond arg to the :compile
function, first one being the route data for the given route: https://github.com/metosin/reitit/blob/master/modules/reitit-http/src/reitit/http/coercion.cljc#L6-L26
each interceptor is compiled separately against all routes, so they can be optimized based on route data, can also be unmounted.
We thought that the compiling is good just for the library interceptors/middleware, but I think all our client projects use that now. So easy to reason about, expecially when you define the route data expectations in a spec. Can fail fast at router creation on invalid data, opposed to failing at runtime when a route is called.
Hmm, interesting. So you're suggesting to create a new key in the map passed to reitit.http/router
. I would then add keys to the routes themselves to mount the necessary interceptor?
This also implies that there's less of a reason to pass interceptors directly in a route's data.
Of you have a :db
you need to use in an interceptor, you either:
1) pass it to the function to create the interceptor (like you did) an use as a free variable in the interceptor functions (enter, leave, error)
2) put in into router options (could be just :db
or :system
key which has :db
under it) and use a :compile
hook. Can be combined with route data.
3) use request/context Injection, but it extra work at runtime
for example, authorization is a good candidate for the route data: add a authorization-interceptor
for all routes, make it read :roles
route data at :compile
: if that exists, return :enter
function that verifies that the context has the roles needed, if no :roles
are defined, unmount the interceptor, e.g. return nil
from compile. The interceptor needs most likely the db/ldap resource, can be passed via router options to :compile
or as a parameter to function that creates the interceptor.
Add a spec to the data (and attach that to the interceptor via :spec
key) and enable route data validation and you'll get expound-pretty errors at router creation time.
not listed, the interceptor docs are kinda non-existing. but there is :request
and :response
, that’s it I recall.
I think I must be misunderstanding something. I have a session interceptor:
(def session-interceptor
{:name ::session
:spec ::session-interceptor
:compile (fn [{:keys [session]} _]
(prn "here")
(when session
(let [options (default-session-options session)]
{:enter (fn session-interceptor-enter
[ctx]
(update ctx :request #(session-middleware/session-request % options)))
:leave (fn session-interceptor-leave
[ctx]
(update ctx :response #(session-middleware/session-response
%
(:request ctx)
options)))})))})
and some routes:
(def routes2
[["/ping" {:session {:store "asd"}
:get (fn [ctx]
(prn "ping")
{:status 200
:body "pong2"
:headers {}})}]])
and I create the handler here:
(http/ring-handler
(http/router
routes2
{:interceptors [interceptors/session-interceptor]
:validate reitit.ring.spec/validate
::rs/explain s/explain-str})
(ring/create-default-handler)
{:executor sieppari/executor})
The route data is invalid but I don't get a message printed out. Why not?Further, "here"
is never printed which must mean the session-interceptor :compile
function is never called.
Ohhh, all those options are supposed to be passed to ring-handler, not http/router haha
Well, some of them. Looks like the validate and rs/explain need to be passed to http/router
.
Ok, that rearrangement fixes the no "here" prints. Still not getting data validation errors printed though.
https://github.com/metosin/reitit/blob/master/examples/http-swagger/src/example/server.clj#L121
also, the one and only complication of compiling each interceptor per route is that if you have something like in-memory session storage, that is instantiated in an interceptor - there is session storage per route. here’s the issue showing how to do that (and a cause to write a built-in interceptor to make this go right for all users): https://github.com/metosin/reitit/issues/205
Oh I see what you mean. There will be only one session store for all routes under /api
. If there are other paths that require the session, you need to move the construction of the session middleware to contain both /api
and the new route.
Still no data validation message printed:
(http/ring-handler
(http/router
routes2
{:data {:interceptors [interceptors/session-interceptor]}
:validate reitit.ring.spec/validate
::rs/explain s/explain-str})
(ring/create-default-handler)
{:executor sieppari/executor})
The specs for the session-interceptor:
(s/def :session/store #(satisfies? session-store/SessionStore %))
(s/def :session/root string?)
(s/def :session/cookie-name string?)
(s/def :session/cookie-attrs map?)
(s/def ::session
(s/keys :opt-un [:session/store
:session/root
:session/cookie-name
:session/cookie-attrs]))
(s/def ::session-interceptor
(s/keys :opt-un [::session]))
@kenny :
(ns user
(:require [clojure.spec.alpha :as s]
[reitit.http :as http]
[reitit.ring :as ring]
[reitit.http.spec :as spec]
[reitit.interceptor.sieppari :as sieppari]
[spec-tools.spell :as spell]
[reitit.dev.pretty :as pretty]))
(s/def :session/store any?)
(s/def :session/root string?)
(s/def :session/cookie-name string?)
(s/def :session/cookie-attrs map?)
(s/def ::session
(s/keys :opt-un [:session/store
:session/root
:session/cookie-name
:session/cookie-attrs]))
(s/def ::session-interceptor
(s/keys :opt-un [::session]))
(def routes2
[["/ping" {:session {:store "asd"}
:get (fn [ctx]
(prn "ping")
{:status 200
:body "pong2"
:headers {}})}]])
(def session-interceptor
{:name ::session
:spec ::session-interceptor
:compile (fn [{:keys [session]} _]
(prn "here")
(when session
{:enter (fn session-interceptor-enter [ctx]
(println "enter")
ctx)
:leave (fn session-interceptor-leave [ctx]
(println "leave")
ctx
)}))})
(def app
(http/ring-handler
(http/router
routes2
{:data {:interceptors [session-interceptor]}
:reitit.spec/wrap spell/closed
:validate spec/validate})
(ring/create-default-handler)
{:executor sieppari/executor}))
; "here"
; "here"
; "here"
; "here"
(app {:request-method :get, :uri "/ping"})
; enter
; "ping"
; leave
; => {:status 200, :body "pong2", :headers {}}
failing spec:
(def app
(http/ring-handler
(http/router
["/ping" {:session "kikka"}]
{:data {:interceptors [session-interceptor]}
:exception pretty/exception
:reitit.spec/wrap spell/closed
:validate spec/validate})
(ring/create-default-handler)
{:executor sieppari/executor}))
there is a spec error in :session
, should fail with the given picture below (if the image was uploaded to slack)
I don't follow. I pasted that example in the REPL and still didn't get that validation exception printed.
did you paste the whole code above? it has all the imports & options in place. the latter is another test on top of that
This is what I get:
Loading src/compute/http_api/example.clj...
"here"
"here"
"here"
"here"
Loaded
(app {:request-method :get, :uri "/ping"})
enter
"ping"
leave
=> {:status 200, :body "pong2", :headers {}}
(def routes2
[["/ping" {:session {:store "asd"}
:get (fn [ctx]
(prn "ping")
{:status 200
:body "pong2"
:headers {}})}]])
That spec must be wrong. Should be:
(s/def :session/store #(satisfies? session-store/SessionStore %))
If I change routes2
to match your example:
(def routes2
[["/ping" {:session "asd"
:get (fn [ctx]
(prn "ping")
{:status 200
:body "pong2"
:headers {}})}]])
it fails too. It appears nested maps aren't getting checked?This should fail:
(def routes2
[["/ping" {:session {:store "asd"}
:get (fn [ctx]
(prn "ping")
{:status 200
:body "pong2"
:headers {}})}]])
with my data, that is ok, the other one isn’t:
(s/valid? ::session-interceptor {:store "asd"})
; => true
(s/valid? ::session-interceptor "asd")
; => false
if you change the spec back to the original, both should fail. Spec registry might have a stale entry if you redefine the spec back to the original, you should reload the whole ns to see it change.
Restarted the REPL and loaded that ns and got the invalid message. Must've been something like the stale spec thing. Still no messages getting printed in my actual ns though. There's gotta be one key somewhere misspelled or at the wrong level.
oh, they collect partial specs from different places. While reitit is no-defaults/all explicit/for people who have their own opinions, goal is to push a batteries-included “easy” stack built with reitit-http
out at some point. Will have all the “right” settings on by default. https://github.com/metosin/talvi
@kenny not sure, reitit-http optionally auto-generates OPTIONS
endpoint that might cause 2 invocations. 4 doesn’t sound good. You can inspect the expanded route-tree with: (->> app (reitit.http/get-router) (reitit.core/compiled-routes))
.
see https://github.com/metosin/reitit/blob/master/modules/reitit-http/src/reitit/http.cljc#L20
From the compile routes, it looks like there are only the two: one :get
that I defined and one generated :options
. Seems like it should get called 2 times, not 4.
@kenny it should do it twice. Could you write an issue out of that so it get's checked & fixed?