Skip to content
Tommi Reiman edited this page Nov 2, 2015 · 7 revisions

Basics

Kekkonen offers a multimethod-like dispatch for tagged Clojure functions called the handlers. Handlers are grouped into virtual namespaces, which are registered into a dispatcher. Clients create an (invocation) context and invoke the handler via dispatcher with context and full path to handler, called action, as the dispatch value.

basics

The examples below might use the following namespace aliases:

(require '[kekkonen.core :as k])
(require '[schema.core :as s])
(require '[plumbing.core :as p])

For full set of features, read the source and the tests.

Context

Context is an Clojure Map, similar to the Ring request, containing all the needed data to invoke the handlers.

  • Keyword keys should be used - to get better support from Schema & Plumbing libraries
  • Client input should placed under :data

Dispatcher will add two special entries to the context for the handler to use:

  • :kekkonen.core/dispatcher, holding the current dispatcher instance
  • :kekkonen.core/handler holding the meta-data of the current handler instance

these can be used for context forwarding, creating external api-docs etc.

Handler

Handler is a Clojure function or a Var. Handlers are be 1-Arity, consuming the context map and can return anything. Handlers are registered to a dispatcher, which requires the handler to be identified with both name and type.

  • Handler name is resolved from the handler meta-data :name (a keyword or a symbol)
  • Type resolution is defined by the dispatcher

The default dispatcher allows only handlers of type :handler to be registered. Type is read either from the :type or :handler meta-data.

Dispatchers also read the following meta-data from handlers:

  • :input allowed input schema for the context
  • :output allowed output schema from the handler
  • :description description of the handler

Fn handler examples

Vanilla Clojure:

(with-meta
  (fn [context]
    (str "hello " context))
  {:name :my-handler
   :type :handler})

Using kekkonen.core/handler helper:

(k/handler
  {:name :my-handler}
  (fn [context]
    (str "hello " context)))

With input, output and description defined:

(k/handler
  {:name :fn-plus
   :description "a description"
   :input {:data {:x s/Int, :y s/Int}}
   :output {:result s/Int}}
  (fn [{{:keys [x y]} :data}]
    {:result (+ x y)}))

Var handler examples

When using Vars, meta-data should be set to Var instead of the Function. :name is set automatically by the Clojure compiler. Doc-string will be mapped to :description.

(defn ^:handler my-handler [context]
  (str "hello " context))

With input, output and description defined:

(defn ^:handler defn-plus 
  "a description"
  {:input {:data {:x s/Int, :y s/Int}}
   :output {:result s/Int}}
  [{{:keys [x y]} :data}]
  {:result (+ x y)})

Fnk handler examples

You can also use Plumbings Schema-aware concise destructuring.

(k/handler
  {:name :my-handler}
  (p/fnk [:as context]
    (str "hello " context)))

With fnk-input, output and description defined:

(k/handler
  {:name :fnk-plus
   :description "a description"
   :output {:result s/Int}}
  (p/fnk [[:data x :- s/Int, y :- s/Int]]
    {:result (+ x y)}))

With fnk-input, fnk-output and description defined (note: anonymous function requires a name here, a Plumbing feature):

(k/handler
  {:name :fnk-plus
   :description "a description"}
  (p/fnk f :- {:result s/Int} [[:data x :- s/Int, y :- s/Int]]
    {:result (+ x y)}))

Defnk handler examples

Defnk provides most terse syntax for handlers:

(p/defnk ^:handler my-handler [:as context]
  (str "hello " context))

With fnk-input, fnk-output and description defined:

(p/defnk ^:handler defnk-plus :- {:result s/Int}
  "a description"
  [[:data x :- s/Int, y :- s/Int]]
  {:result (+ x y)})

Namespaces

(Virtual) namespaces are used for organizing the handlers. Normal clojure namespaces are not used, as they would create a tight coupling between the client and the actual handler implementation and thus would make refactoring harder. Also, virtual namespaces allow the handlers to reside in multiple namespace branches.

Virtual namespaces are defined by the kekkonen.core/Namespace record, which has a keyword name and extra meta-data map on it. The whole namespace tree is a nested map with Namespaces as keys and either handlers or Namespaces as values. Namespaces can be created with kekkonen.core/namespace fn.

; a simple namespace
(def api-ns (k/namespace {:name :api}))

; namespace with custom meta-data (just for admins)
(def admin-ns (k/namespace {:name :admin ::roles #{:admin}}))

(def reset-db!
  (k/handler
    {:name :reset-db!}
    (fn [context] "resetted db!")))

; a namespace tree
(def handlers {api-ns {admin-ns reset-db!}})

Action

The fully qualified name of the handler instance, including the (virtual) namespace part and the handler name. Used in the handler dispatch.

From the previous Namespaces-example, there would be one action :api.admin/reset-db!.

Collecting handlers & namespaces

To make things easy, Dispatcher uses kekkonen.core/Collector protocol to process the namespace tree. The following extension are available to the protocol:

  • AFunction collects a anonymous function as a handler
  • Var collects a defn(k) handler
  • Symbol collects all public defn(k) handlers from a namespace
  • Keyword creates a Namespace out of the given value with empty meta-data
  • PersistentVector collects every (handler) on the vec
  • IPersistentMap collects keys as namespaces and values as what they represent
; namespace with custom meta-data (just for admins)
(def admin-ns (k/namespace {:name :admin ::roles #{:admin)

(def handlers 
  {:api 
    {:public 
      {:boss #'boss-move ; just one handler Var
       :user [#'add-user! #'remove-user! #'get-user] ; collect multiple handlers Vars
       :math 'my-app.handlers.math ; a namespace scan for handler Vars
       :common (k/handler 
                 {:name :ping) 
                 (fn [_] (ok {:ping "pong") ; inlined handler function
    admin-ns [#'add-user! reset-db! (k/handler {:name :identity} identity)]}}) ; mixing things

which would create the following fully-qualified actions (mapped to handlers):

:api.public.boss/boss-move
:api.public.user/add-user!
:api.public.user/remove-user!
:api.public.user/get-user
:api.public.math/... 
:api.public.common/ping
:admin/... ; all with extra meta {::roles #{:admin}}

Dispatcher

Dispatcher is registry holding the handlers and responsible for the handler dispatch. It's created with kekkonen.core/dispatcher function, returning a kekkonen.core/Dispatcher record. Dispatcher can be configured with the following options:

  • :handlers a map containing the namespace-tree of handlers
  • :context (optional) initial state for the dispatcher, request contexts are deep-merged with this
  • :type-resolver (optional) a function to verify if the proposed handler is accepted
  • :transformers (optional) request context transformers (function of context => context)
  • :coercion -> :input, :output (optional) coercion matchers for both input & output
  • :user (optional) a map of all valid extra meta-data keys and functions (value => context => context) how to process them

Both the schema and default for the options are found in kekkonen.core namespace.

Clients can interact with the individual handlers via the dispatcher using the following public functions in kekkonen.core. All take three parameters: dispatcher, action and the context.

  • check to check whether the handler is available - runs all rules from the namespaces & handler. Returns either nil (success) or fail with an exception.
  • validate to run both check and to validates context against the accumulated handler input-schema. Does not execute the actual handler body.
  • invoke to run both the check & validate and to actually run the handler body & the response validation.

Clients can also use the following dispatcher browsing functions:

  • some-handler returns details of a given handler
  • all-handlers returns a details of all registered handlers.
  • available-handlers returns a details of all handlers available (run with check) with the given context, optionally under a given namespace.
  • dispatch-handlers mass-evaluate handler rules either by check or validate, with the given context, optionally under a given namespace.

Putting it all together

Hello World

(def dispatcher 
  (k/dispatcher 
    {:handlers {:api (k/handler {:name :hello} (constantly "hello world"))}}))

(k/invoke dispatcher :api/hello)
; => "hello world"

More complete example

; define handlers
(p/defnk ^:command inc! [counter] (swap! counter inc))
(p/defnk ^:query plus [[:data x :- s/Int, y :- s/Int]] (+ x y))
(p/defnk ^:command reset-counter! [counter] (reset! counter 0))

; define custom namespaces
(def admin-ns (k/namespace {::admin true :name :admin}))

; define ::admin resolver
(defn admin-access-resolver [_] 
  (fn [ctx] (and (-> ctx ::admin) ctx)))

; the dispatcher
(def dispatcher 
  (k/dispatcher 
    {:handlers {:api {:math [#'inc! #'plus]
                      admin-ns #'reset-counter!}}
     :type-resolver (k/type-resolver :command :query)
     :context {:counter (atom 0)}}))

; invoking handlers
(k/invoke dispatcher :api.math/plus {:data {:x 1 :y 2}}) ; => 3

(k/invoke dispatcher :api.math/inc!) ;=> 1
(k/invoke dispatcher :api.math/inc!) ;=> 2
(k/invoke dispatcher :api.math/inc!) ;=> 3

(k/invoke dispatcher :api.admin/reset-counter!) ; => ExceptionInfo Invalid action
(k/invoke dispatcher :api.admin/reset-counter! {::admin true}) ; => 0

; checking handlers
(k/check dispatcher :invalid) ; => ExceptionInfo Invalid action
(k/check dispatcher :api.math/inc!) ;=> nil
(k/check dispatcher :api.admin/reset-counter!) ; => ExceptionInfo Invalid action
(k/check dispatcher :api.admin/reset-counter! {::admin true}) ; => nil

; validating handlers
(k/validate dispatcher :api.math/plus {:data {:x "1"}})
; => ExceptionInfo Coercion error
; {:type :kekkonen.core/request
;  :in nil
;  :value {:data {:x "1"}}
;  :schema {:data {:y s/Int, :x s/Int, s/Keyword s/Any}, s/Keyword s/Any}
;  :error {:data {:y 'missing-required-key, :x '(not (integer? "1"))}}}

(k/validate dispatcher :api.math/plus {:data {:x 1, :y 2}}) ; => nil

; TODO: browsing the handlers

; k/some-handler
; k/all-handlers
; k/available-handlers
; k/dispatch-handlers

Next up, Apis