Permalink
Browse files

"describe" op and middleware for introspection of operations availabl…

…e from an nREPL endpoint; fixes NREPL-25
  • Loading branch information...
1 parent 68bac3f commit 96b58c07857ff3cc35a6a7b7d89c942fc4c04110 @cemerick cemerick committed Aug 14, 2012
@@ -0,0 +1,49 @@
+(ns clojure.tools.nrepl.middleware
+ (:require clojure.tools.nrepl
+ [clojure.tools.nrepl.transport :as transport]
+ [clojure.tools.nrepl.misc :as misc]))
+
+(defn- var-name
+ [^clojure.lang.Var v]
+ (str (.ns v) \/ (.sym v)))
+
+(defn- wrap-conj-descriptor
+ [descriptor-map h]
+ (fn [{:keys [op descriptors] :as msg}]
+ (h (if-not (= op "describe")
+ msg
+ (assoc msg :descriptors (merge descriptor-map descriptors))))))
+
+(defn set-descriptor!
+ "Sets the given [descriptor] map as the ::descriptor metadata on
+ the provided [middleware-var], after assoc'ing in the var's
+ fully-qualified name as the descriptor's \"implemented-by\" value."
+ [middleware-var descriptor]
+ (let [descriptor (assoc descriptor "implemented-by" (var-name middleware-var))]
+ (alter-meta! middleware-var assoc ::descriptor descriptor)
+ (alter-var-root middleware-var #(comp (partial wrap-conj-descriptor (:handles descriptor)) %))))
+
+(defn- safe-version
+ [m]
+ (into {} (filter (fn [[_ v]] (or (number? v) (string? v))) m)))
+
+(defn wrap-describe
+ [h]
+ (fn [{:keys [op descriptors verbose? transport] :as msg}]
+ (if (= op "describe")
+ (transport/send transport (misc/response-for msg
+ {:ops (if verbose?
+ descriptors
+ (into {} (map #(vector (key %) {}) descriptors)))
+ :versions {:nrepl (safe-version clojure.tools.nrepl/version)
+ :clojure (safe-version *clojure-version*)}
+ :status :done}))
+ (h msg))))
+
+(set-descriptor! #'wrap-describe
+ {:handles {"describe"
+ {:doc "Produce a machine- and human-readable directory and documentation for the operations supported by an nREPL endpoint."
+ :requires {}
+ :optional {"verbose?" "Include informational detail for each \"op\"eration in the return message."}
+ :returns {"ops" "Map of \"op\"erations supported by this nREPL endpoint"
+ "versions" "Map containing version maps (like *clojure-version*, e.g. major, minor, incremental, and qualifier keys) for values, component names as keys. Common keys include \"nrepl\" and \"clojure\"."}}}})
@@ -2,7 +2,8 @@
clojure.tools.nrepl.middleware.interruptible-eval
(:require [clojure.tools.nrepl.transport :as t]
clojure.main)
- (:use [clojure.tools.nrepl.misc :only (response-for returning)])
+ (:use [clojure.tools.nrepl.misc :only (response-for returning)]
+ [clojure.tools.nrepl.middleware :only (set-descriptor!)])
(:import clojure.lang.LineNumberingPushbackReader
(java.io StringReader Writer)
java.util.concurrent.atomic.AtomicLong
@@ -188,3 +189,17 @@
(h msg))))
+(set-descriptor! #'interruptible-eval
+ {:handles {"eval"
+ {:doc "Evaluates code."
+ :requires {"code" "The code to be evaluated."
+ "session" "The ID of the session within which to evaluate the code."}
+ :optional {"id" "An opaque message ID that will be included in responses related to the evaluation, and which may be used to restrict the scope of a later \"interrupt\" operation."}
+ :returns {}}
+ "interrupt"
+ {:doc "Attempts to interrupt some code evaluation."
+ :requires {"session" "The ID of the session used to start the evaluation to be interrupted."}
+ :optional {"interrupt-id" "The opaque message ID sent with the original \"eval\" request."}
+ :returns {"status" "'interrupted' if an evaluation was identified and interruption will be attempted
+'session-idle' if the session is not currently evaluating any code
+'interrupt-id-mismatch' if the session is currently evaluating code sent using a different ID than specified by the \"interrupt-id\" value "}}}})
@@ -1,5 +1,7 @@
(ns ^{:author "Chas Emerick"}
- clojure.tools.nrepl.middleware.load-file)
+ clojure.tools.nrepl.middleware.load-file
+ (:require [clojure.tools.nrepl.middleware.interruptible-eval :as eval])
+ (:use [clojure.tools.nrepl.middleware :as middleware :only (set-descriptor!)]))
(defn load-file-code
"Given the contents of a file, its _source-path-relative_ path,
@@ -25,3 +27,15 @@
(h (assoc msg
:op "eval"
:code (load-file-code file file-path file-name))))))
+
+(set-descriptor! #'wrap-load-file
+ {:handles {"load-file"
+ {:doc "Loads a body of code, using supplied path and filename info to set source file and line number metadata. Delegates to underlying \"eval\" middleware/handler."
+ :requires {"file" "Full contents of a file of code."}
+ :optional {"file-path" "Source-path-relative path of the source file, e.g. clojure/java/io.clj"
+ "file-name" "Name of source file, e.g. io.clj"}
+ :returns (-> (meta #'eval/interruptible-eval)
+ ::middleware/descriptor
+ :handles
+ (get "eval")
+ :returns)}}})
@@ -3,7 +3,8 @@
:author "Chas Emerick"}
clojure.tools.nrepl.middleware.session
(:use [clojure.tools.nrepl.misc :only (uuid response-for returning log)]
- [clojure.tools.nrepl.middleware.interruptible-eval :only (*msg*)])
+ [clojure.tools.nrepl.middleware.interruptible-eval :only (*msg*)]
+ [clojure.tools.nrepl.middleware :only (set-descriptor!)])
(:require (clojure main test)
[clojure.tools.nrepl.transport :as t])
(:import clojure.tools.nrepl.transport.Transport
@@ -163,6 +164,23 @@
:sessions (or (keys @sessions) [])))
(h msg)))))))
+(set-descriptor! #'session
+ {:handles {"close"
+ {:doc "Closes the specified session."
+ :requires {"session" "The ID of the session to be closed."}
+ :optional {}
+ :returns {}}
+ "ls-sessions"
+ {:doc "Lists the IDs of all active sessions."
+ :requires {}
+ :optional {}
+ :returns {"sessions" "A list of all available session IDs."}}
+ "clone"
+ {:doc "Clones the current session, returning the ID of the newly-created session."
+ :requires {}
+ :optional {"session" "The ID of the session to be cloned; if not provided, a new session with default bindings is created, and mapped to the returned session ID."}
+ :returns {"new-session" "The ID of the new session."}}}})
+
(defn add-stdin
"stdin middleware. Returns a handler that supports a \"stdin\" :op-eration, which
adds content provided in a :stdin slot to the session's *in* Reader. Delegates to
@@ -183,3 +201,10 @@
(t/send transport (response-for msg :status :done)))
:else
(h msg))))
+
+(set-descriptor! #'add-stdin
+ {:handles {"stdin"
+ {:doc "Add content from the value of \"stdin\" to *in* in the current session."
+ :requires {"stdin" "Content to add to *in*."}
+ :optional {}
+ :returns {"status" "A status of \"need-input\" will be sent if a session's *in* requires content in order to satisfy an attempted read operation."}}}})
@@ -51,6 +51,7 @@
readable representations of evaluated expressions via `pr`."
[]
(-> unknown-op
+ clojure.tools.nrepl.middleware/wrap-describe
clojure.tools.nrepl.middleware.interruptible-eval/interruptible-eval
clojure.tools.nrepl.middleware.load-file/wrap-load-file
clojure.tools.nrepl.middleware.pr-values/pr-values
@@ -0,0 +1,29 @@
+(ns ^{:author "Chas Emerick"}
+ clojure.tools.nrepl.describe-test
+ (:use [clojure.tools.nrepl-test :only (def-repl-test repl-server-fixture)]
+ clojure.test)
+ (:require [clojure.tools.nrepl :as nrepl]
+ [clojure.tools.nrepl.middleware :as middleware]))
+
+(use-fixtures :once repl-server-fixture)
+
+(def ^{:private true} op-names
+ #{:load-file :ls-sessions :interrupt :stdin
+ :describe :eval :close :clone})
+
+(def-repl-test simple-describe
+ (let [{{:keys [nrepl clojure]} :versions
+ ops :ops} (nrepl/combine-responses
+ (nrepl/message timeout-client {:op "describe"}))]
+ (testing "versions"
+ (is (= (#'middleware/safe-version clojure.tools.nrepl/version) nrepl))
+ (is (= (#'middleware/safe-version *clojure-version*) clojure)))
+
+ (is (= op-names (set (keys ops))))
+ (is (every? empty? (map val ops)))))
+
+(def-repl-test verbose-describe
+ (let [{:keys [ops]} (nrepl/combine-responses
+ (nrepl/message timeout-client {:op "describe" :verbose? "true"}))]
+ (is (= op-names (set (keys ops))))
+ (is (every? seq (map (comp :doc val) ops)))))
@@ -2,7 +2,7 @@
clojure.tools.nrepl.load-file-test
(:import (java.io File))
(:use [clojure.tools.nrepl-test :only (def-repl-test repl-server-fixture)]
- clojure.test)
+ clojure.test)
(:require [clojure.tools.nrepl :as nrepl]))
(def project-base-dir (File. (System/getProperty "nrepl.basedir" ".")))

0 comments on commit 96b58c0

Please sign in to comment.