???
Query,Body,Path,
Header,Fom-parameters
Statuscodes
GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS
urisResources
functions,namespaces
&data
functions,namespaces
&data
Possible options• REST / web apis
– Beautiful apis, but for whom?– Extra abstracting to HTTP terms –wor th it?
• Just RPC– Simple, is it open enough (for 3rd par ties)?
• Data-driven (Falcor, Relay, Om Next)– Separate reads & mutations, bundled operations– Great for reads, controlled mutations? Maturity?
• Mix & match?– Commands & Queries, great api-docs– best par ts of data-driven
Kekkonen• Small library for generic message handling– “Functions processing requests via a dispatcher”
• Data-driven, no macros, wieldy via helpers• Uncoupled from HTTP/REST, just data & functions– Ring-adapter with Swagger-docs– Future: Web Sockets, messaging, command line, whatever, ...
• Key concepts:– Basics: Context, Handler, Dispatcher, Namespace, Interceptor– Adapters and APIs
Handlers 1/3• Purpose is to validate & process contexts• Internal representation is just data
{:name :plus:type :handler:description "Adds to numbers together":input {:data {:y s/Int
:x s/Ints/Keyword s/Any}
s/Keyword s/Any}:output s/Int:handler (fn [{{:keys [x y]} :data}]
(+ x y))}
Handlers 3/3• Plumbing fnk-notation with Schemas
(defnk ^:handler plus :- s/Int"Adds to numbers together"[[:data x - s/Int, y :- s/Int]](+ x y))
Dispatcher• Registry of handlers– Compiles handlers into dispatch-table & interceptor-chains
• “a better multimethod”• Functions to work with handlers
– check, validate, invoke– some-handler, all-handlers, available-handlers, dispatch-handlers
(defnk ^:handler increment"Stateful counter"[counter](swap! counter inc))
(defnk ^:handler plus"Adds two numbers together"[[:data x - s/Int, y :- s/Int]](+ x y))
(def d (k/dispatcher{:handlers {:math [#'increment #'plus]}:context {:counter (atom 0)}}))
(k/invoke d :math/plus) ; CoerceionError {:data missing-required-key}(k/invoke d :math/plus {:data {:x 1, :y 2}}) ; => 3
(k/invoke d :math/increment) ; => 1(k/invoke d :math/increment) ; => 2(k/invoke d :math/increment) ; => 3
(k/invoke d :math/increment {:counter (atom 41)}) ; => 42
Extending• Custom meta-data to handlers & namespaces– compile down to Interceptors
(defnk ^:command close-application"Closes the application”{:roles #{:applicant}:states #{:open :draft}:interceptors [notify-on-success]}
[db, [:data id :- s/Int]](success (application/close db id)))
The cool stuff• Validate handler input without executing body• Handler(mass-)availability with partial contexts• Speculative transactions*• Client-side bundled transactional contexts*• Extract handler-data to clients for local reasoning*• Safe and dynamic api-docs• Command-logging
*demo,notinthecoreyet
Problem• Digitalized building permits in
Finland• Multiple roles using the app,
collaborating in real-time– Single application
• Role-based authorization• Audit-trail
(defn create-api [{:keys [state chord]}](cqrs-api
{:swagger {:info {:title "Building Permit application":description "a complex simulated real-life
case example showcase project for http://kekkonen.io"}:securityDefinitions {:api_key {:type "apiKey", :name "x-apikey", :in "header"}}}
:swagger-ui {:validator-url nil:path "/api-docs"}
:core {:handlers {building-permit-ns 'backend.building-permitusers-ns 'backend.users:session 'backend.session}
:user [[:require-session app-session/require-session][:load-current-user app-session/load-current-user][:requires-role app-session/requires-role][::building-permit/retrieve-permit building-permit/retrieve-permit][::building-permit/requires-state building-permit/requires-state][::building-permit/requires-claim building-permit/requires-claim]]
:context {:state state:chord chord}}}))
Api
(defnk ^:command approve"Approve a permit"{:requires-role #{:authority}::requires-claim true::retrieve-permit true::requires-state #{:submitted}:interceptors [broadcast-update]}
[[:state permits archive-id-seq][:entities [:permit permit-id]]]
(swap! permits update permit-id assoc:state :approved:archive-id (swap! archive-id-seq inc))
(success {:status :ok}))
Command
Findings• Action availability logic on backend– Backend has all the facts, single query with Kekkonen– Not all data can be sent to client for local reasoning
• Modelling commands based on user intent– UI-actions map mostly 1:1 to api actions– Automatic audit trail
Links• https://github.com/metosin/kekkonen-building-
permit-example• https://building-permits.herokuapp.com/
Next steps?• Create handlers-trees from external sources / spec (db, file)• Kekkonen over Websockets• (ClojureScript) Api-docs beyond Swagger• Om Next Remotes• ClojureScript client• RE-Kekkonen• CQRS-template with Eventing• Handler mutations & hot-swapping• Graph-based dependency management• Pulsar-backend, extract api-docs, ping @andreiursan• Hiccup-style syntax for namespace-trees
Summary• Kekkonen is a fresh new api library for clj(s)• Simple, data-driven, free from the http– Your domain functions & data
• Enables cool new ways to interact with apis• Get involved– https://kekkonen.io & #kekkonen at Slack
Special thanks to• Prismatic Schema & Plumbing• Pedestal for Interceptors• Elegance of fnhouse• Ring-swagger• Best par ts of compojure-api• Schema-tools• Kebabs
Context• Execution context, client input under :data• Otherwise works mostly like in Pedestal
; simple context{:data {:x 1, :y 1}}
Handlers 2/3• Clojure functions with extra meta-data
(defn plus"Adds to numbers together"{:type :handler:input {:data {:y s/Int
:x s/Ints/Keyword s/Any}
s/Keyword s/Any}:output s/Int}
[{{:keys [x y]} :data}](+ x y))
(Vir tual) Namespace• Just like Clojure namespaces, but uncoupled to
allow internal refactoring
{:name :admin:type :namespace:description "Admin-operations":interceptors [[require-role :admin]]}
Interceptors• Like middleware in Ring• In the end, (mostly) everything is an interceptor• Pedestal <3
{:name "logging interceptor":enter (fn [ctx] (log/info ctx) ctx):leave (fn [ctx] (log/info ctx) ctx)}
Ring-adapter• Create a ring-handler from dispatcher & options
(def app(r/ring-handler
(k/dispatcher{:handlers {:math [#'increment #'plus]}:context {:counter (atom 0)}})))
(app {:uri "/":request-method :get}) => nil
(app {:uri "/math/plus":request-method :post:body-params {:x 1, :y 2}}) => 3
API• Public http-entrypoint in Kekkonen– Wires ring-adapter, middleware & swagger artifacts– Ships with good defaults
(def app(a/api
{:core {:handlers {:math [#'increment#'plus]}
:context {:counter (atom 0)}}}))
(server/run-server #'app {:port 5000})
source code for api
(defn api [options](s/with-fn-validation
(let [options (s/validate Options (kc/deep-merge +default-options+ options))swagger (merge (:swagger options) (mw/api-info (:mw options)))dispatcher (-> (k/dispatcher (:core options))
(k/inject (-> options :api :handlers))(k/inject (ks/swagger-handler swagger options)))]
(mw/wrap-api(r/routes
[(r/ring-handler dispatcher (:ring options))(ks/swagger-ui (:swagger-ui options))])
(:mw options)))))
Create your own api styles!• New styles via Dispatcher & Api configuration• Ships with RPC, HTTP and CQRS Api styles
(defn cqrs-api [options](a/api(kc/deep-merge
{:core {:type-resolver (k/type-resolver :command :query)}:swagger {:info {:title "Kekkonen CQRS API"}}:ring {:types {:query {:methods #{:get}
:parameters {[:data] [:request :query-params]}}:command {:methods #{:post}
:parameters {[:data] [:request :body-params]}}}}}options)))
(s/defschema Kebab{:id s/Int:name s/Str:type (s/enum :doner :shish :souvlaki)})
(s/defschema NewKebab(dissoc Kebab :id))
(defnk ^:query get-kebabs"Retrieves all kebabs"{:output [Kebab]}[db](success (vals @db)))
(defnk ^:command add-kebab"Adds an kebab to database"{:output Kebab}[db, ids, data :- NewKebab](let [item (assoc data :id (swap! ids inc))](swap! db assoc (:id item) item)(success item)))
(def app(cqrs-api{:core {:handlers {:kebabs [#'get-kebabs #'add-kebab]}
:context {:db (atom {}):ids (atom 0)}}}))
(server/run-server #'app {:port 4001})