The basic structure of most middleware is pretty simple:
-
An op dispatch function (a.k.a. handler
) that intercepts requests to the middleware
-
Some functions that do the actual work for each op and provide the responses
-
A middleware descriptor that specifies which middlewares are provided/required and documents the API of the middleware
Optionally you can have some dynamic vars for configuration purposes. Below is a complete example:
(ns nrepl.middleware.completion
"Code completion middleware."
(:require
[nrepl.util.completion :as complete]
[nrepl.middleware :as middleware :refer [set-descriptor!]]
[nrepl.misc :refer [response-for] :as misc]
[nrepl.transport :as t])
(:import nrepl.transport.Transport))
;; middleware configuration
(def ^:dynamic *complete-fn*
"Function to use for completion. Takes three arguments: `prefix`, the completion prefix,
`ns`, the namespace in which to look for completions, and `options`, a map of additional
options for the completion function."
complete/completions)
;; the business logic for the "completions" op
(defn completion-reply
[{:keys [session prefix ns complete-fn options] :as msg}]
(let [ns (if ns (symbol ns) (symbol (str (@session #'*ns*))))
completion-fn (or (and complete-fn (misc/requiring-resolve (symbol complete-fn))) *complete-fn*)]
(try
(response-for msg {:status :done :completions (completion-fn prefix ns options)})
(catch Exception e
(response-for msg {:status #{:done :completion-error :namespace-not-found}})))))
;; the handler
(defn wrap-completion
"Middleware that provides code completion.
It understands the following params:
* `prefix` - the prefix which to complete.
* `ns`- the namespace in which to do completion. Defaults to `*ns*`.
* `complete-fn` – a fully-qualified symbol naming a var whose function to use for
completion. Must point to a function with signature [prefix ns options].
* `options` – a map of options to pass to the completion function."
[h]
(fn [{:keys [op ^Transport transport] :as msg}]
(if (= op "completions")
(t/send transport (completion-reply msg))
(h msg))))
(set-descriptor! #'wrap-completion
{:requires #{"clone"}
:expects #{}
:handles {"completions"
{:doc "Provides a list of completion candidates."
:requires {"prefix" "The prefix to complete."}
:optional {"ns" "The namespace in which we want to obtain completion candidates. Defaults to `*ns*`."
"complete-fn" "The fully qualified name of a completion function to use instead of the default one (e.g. `my.ns/completion`)."
"options" "A map of options supported by the completion function."}
:returns {"completions" "A list of completion candidates. Each candidate is a map with `:candidate` and `:type` keys. Vars also have a `:ns` key."}}}})
|
Not all middleware respond directly to ops. You can have middleware that simply modify
requests or responses.
|
It’s important to make sure you don’t let some exceptions slide, as some clients might block
when waiting for a response (as some clients will try to handle nREPL messages in a synchronous
fashion due to various limitations).
Notice that you can have multiple statuses for one response - e.g. a combination of :done
and
some error status.
Also notice that the example middleware depends on the session
middleware, as it’s fetching
some data from the session. In practice this means that the session
middleware has already
been applied to the message for the completion
middleware before the message made its way to it,
and the required session data has been added to it.