@cindex JSONRPC application interfaces
@enumerate
-@item A user interface for building JSONRPC applications
+@item An API for building JSONRPC applications
@findex :request-dispatcher
@findex :notification-dispatcher
@findex jsonrpc-notify
@findex jsonrpc-request
@findex jsonrpc-async-request
-In this scenario, the JSONRPC application selects a concrete subclass
-of @code{jsonrpc-connection}, and proceeds to create objects of that
-subclass using @code{make-instance}. To initiate a contact to the
-remote endpoint, the JSONRPC application passes this object to the
-functions @code{jsonrpc-notify}, @code{jsonrpc-request}, and/or
-@code{jsonrpc-async-request}. For handling remotely initiated
-contacts, which generally come in asynchronously, the instantiation
-should include @code{:request-dispatcher} and
-@code{:notification-dispatcher} initargs, which are both functions of
-3 arguments: the connection object; a symbol naming the JSONRPC method
-invoked remotely; and a JSONRPC @code{params} object.
+In this scenario, a new aspiring JSONRPC-based application selects a
+concrete subclass of @code{jsonrpc-connection} that provides the
+transport for the JSONRPC messages to be exchanged between endpoints.
+
+The application creates objects of that subclass using
+@code{make-instance}. To initiate a contact to a remote endpoint, the
+application passes this object to the functions such as
+@code{jsonrpc-notify}, @code{jsonrpc-request}, or
+@code{jsonrpc-async-request}.
+
+For handling remotely initiated contacts, which generally come in
+asynchronously, the @code{make-instance} instantiation should
+initialize it the @code{:request-dispatcher} and
+@code{:notification-dispatcher} EIEIO keyword arguments. These are
+both functions of 3 arguments: the connection object; a symbol naming
+the JSONRPC method invoked remotely; and a JSONRPC @code{params}
+object.
@findex jsonrpc-error
The function passed as @code{:request-dispatcher} is responsible for
handling the remote endpoint's requests, which expect a reply from the
-local endpoint (in this case, the program you're building). Inside
-that function, you may either return locally (a normal return) or
-non-locally (an error return). A local return value must be a Lisp
-object that can be serialized as JSON (@pxref{Parsing JSON}). This
-determines a success response, and the object is forwarded to the
-server as the JSONRPC @code{result} object. A non-local return,
-achieved by calling the function @code{jsonrpc-error}, causes an error
-response to be sent to the server. The details of the accompanying
-JSONRPC @code{error} are filled out with whatever was passed to
+local endpoint (in this case, the application you're building).
+Inside that function, you may either return locally (a regular return)
+or non-locally (throw an error). Both exits from the request
+dispatcher cause a reply to the remote endpoint's request to be sent
+through the transport.
+
+A regular return determines a success response, and the return value
+must be a Lisp object that can be serialized as JSON (@pxref{Parsing
+JSON}). The result is forwarded to the server as the JSONRPC
+@code{result} object. A non-local return, achieved by calling the
+function @code{jsonrpc-error}, causes an error response to be sent to
+the server. The details of the accompanying JSONRPC @code{error}
+object are filled out with whatever was passed to
@code{jsonrpc-error}. A non-local return triggered by an unexpected
error of any other type also causes an error response to be sent
(unless you have set @code{debug-on-error}, in which case this calls
the Lisp debugger, @pxref{Error Debugging}).
-@item A inheritance interface for building JSONRPC transport implementations
-
-In this scenario, @code{jsonrpc-connection} is subclassed to implement
+@findex jsonrpc-convert-to-endpoint
+@findex jsonrpc-convert-from-endpoint
+It's possible to use the @code{jsonrpc} library to build applications
+based on transport protocols that can be described as
+``quasi-JSONRPC''. These are similar, but not quite identical to
+JSONRPC, such as the @uref{https://www.jsonrpc.org/, DAP (Debug
+Adapter Protocol)}. These protocols also define request, response and
+notification messages but the format is not quite the same as JSONRPC.
+The generic functions @code{jsonrpc-convert-to-endpoint} and
+@code{jsonrpc-convert-from-endpoint} can be customized for converting
+between the internal representation of JSONRPC and whatever the
+endpoint accepts (@pxref{Generic Functions}).
+
+@item An API for building JSONRPC transports
+
+In this scenario, @code{jsonrpc-connection} is sub-classed to implement
a different underlying transport strategy (for details on how to
subclass, see @ref{Inheritance,Inheritance,,eieio}.). Users of the
application-building interface can then instantiate objects of this
concrete class (using the @code{make-instance} function) and connect
-to JSONRPC endpoints using that strategy.
+to JSONRPC endpoints using that strategy. See @ref{Process-based
+JSONRPC connections} for a built-in transport implementation.
This API has mandatory and optional parts.
@findex jsonrpc-connection-send
To allow its users to initiate JSONRPC contacts (notifications or
-requests) or reply to endpoint requests, the subclass must have an
-implementation of the @code{jsonrpc-connection-send} method.
+requests) or reply to endpoint requests, the new transport
+implementation must equip the @code{jsonrpc-connection-send} generic
+function with a specialization for the the new subclass
+(@pxref{Generic Functions}). This generic function is called
+automatically by primitives such as @code{jsonrpc-request} and
+@code{jsonrpc-notify}. The specialization should ensure that the
+message described in the argument list is sent through whatever
+underlying communication mechanism (a.k.a.@: ``wire'') is used by the
+new transport to talk to endpoints. This ``wire'' may be a network
+socket, a serial interface, an HTTP connection, etc.
@findex jsonrpc-connection-receive
Likewise, for handling the three types of remote contacts (requests,
notifications, and responses to local requests), the transport
implementation must arrange for the function
-@code{jsonrpc-connection-receive} to be called after noticing a new
-JSONRPC message on the wire (whatever that "wire" may be).
+@code{jsonrpc-connection-receive} to be called from Elisp after
+noticing some data on the ``wire'' that can be used to craft a JSONRPC
+(or quasi-JSONRPC) message.
@findex jsonrpc-shutdown
@findex jsonrpc-running-p
Finally, and optionally, the @code{jsonrpc-connection} subclass should
-implement the @code{jsonrpc-shutdown} and @code{jsonrpc-running-p}
-methods if these concepts apply to the transport. If they do, then
-any system resources (e.g.@: processes, timers, etc.) used to listen for
-messages on the wire should be released in @code{jsonrpc-shutdown},
-i.e.@: they should only be needed while @code{jsonrpc-running-p} is
-non-@code{nil}.
+add specializations to the @code{jsonrpc-shutdown} and
+@code{jsonrpc-running-p} generic functions if these concepts apply to
+the transport. The specialization of @code{jsonrpc-shutdown} should
+ensure the release of any system resources (e.g.@: processes, timers,
+etc.) used to listen for messages on the wire. The specialization of
+@code{jsonrpc-running-p} should tell if these resources are still
+active or have already been released (via @code{jsonrpc-shutdown} or
+otherwise).
@end enumerate
;; Author: João Távora <joaotavora@gmail.com>
;; Keywords: processes, languages, extensions
-;; Version: 1.0.18
+;; Version: 1.0.19
;; Package-Requires: ((emacs "25.2"))
;; This is a GNU ELPA :core package. Avoid functionality that is not
(:method (_s _what) ;; by default all connections are ready
t))
+;;; API optional
+(cl-defgeneric jsonrpc-convert-to-endpoint (connection message subtype)
+ "Convert MESSAGE to JSONRPCesque message accepted by endpoint.
+MESSAGE is a plist, jsonrpc.el's internal representation of a
+JSONRPC message. SUBTYPE is one of `request', `reply' or
+`notification'.
+
+Return a plist to be serialized to JSON with `json-serialize' and
+transmitted to endpoint."
+ ;; TODO: describe representations and serialization in manual and
+ ;; link here.
+ (:method (_s message subtype)
+ `(:jsonrpc "2.0"
+ ,@(if (eq subtype 'reply)
+ ;; true JSONRPC doesn't have `method'
+ ;; fields in responses.
+ (cl-loop for (k v) on message by #'cddr
+ unless (eq k :method)
+ collect k and collect v)
+ message))))
+
+;;; API optional
+(cl-defgeneric jsonrpc-convert-from-endpoint (connection remote-message)
+ "Convert JSONRPC-esque REMOTE-MESSAGE to a plist.
+REMOTE-MESSAGE is a plist read with `json-parse'.
+
+Return a plist of jsonrpc.el's internal representation of a
+JSONRPC message."
+ ;; TODO: describe representations and serialization in manual and
+ ;; link here.
+ (:method (_s remote-message) remote-message))
+
\f
;;; Convenience
;;;
This function will destructure MESSAGE and call the appropriate
dispatcher in CONNECTION."
(cl-destructuring-bind (&key method id error params result _jsonrpc)
- message
+ (jsonrpc-convert-from-endpoint connection message)
+ (jsonrpc--log-event connection message 'server
+ (cond ((and method id) 'request)
+ (method 'notification)
+ (id 'reply)))
(let (continuations)
- (jsonrpc--log-event connection message 'server)
(setf (jsonrpc-last-error connection) error)
(cond
(;; A remote request
"Internal error")))))
(error
'(:error (:code -32603 :message "Internal error"))))))
- (apply #'jsonrpc--reply connection id reply)))
+ (apply #'jsonrpc--reply connection id method reply)))
(;; A remote notification
method
(funcall (jsonrpc--notification-dispatcher connection)
(cl-defmethod jsonrpc-connection-send ((connection jsonrpc-process-connection)
&rest args
&key
- _id
+ id
method
_params
- _result
- _error
+ (_result nil result-supplied-p)
+ error
_partial)
"Send MESSAGE, a JSON object, to CONNECTION."
(when method
((symbolp method) (symbol-name method))
((stringp method) method)
(t (error "[jsonrpc] invalid method %s" method)))))
- (let* ( (message `(:jsonrpc "2.0" ,@args))
- (json (jsonrpc--json-encode message))
- (headers
- `(("Content-Length" . ,(format "%d" (string-bytes json)))
- ;; ("Content-Type" . "application/vscode-jsonrpc; charset=utf-8")
- )))
+ (let* ((subtype (cond ((or result-supplied-p error) 'reply)
+ (id 'request)
+ (method 'notification)))
+ (converted (jsonrpc-convert-to-endpoint connection args subtype))
+ (json (jsonrpc--json-encode converted))
+ (headers
+ `(("Content-Length" . ,(format "%d" (string-bytes json)))
+ ;; ("Content-Type" . "application/vscode-jsonrpc; charset=utf-8")
+ )))
(process-send-string
(jsonrpc--process connection)
(cl-loop for (header . value) in headers
concat (concat header ": " value "\r\n") into header-section
finally return (format "%s\r\n%s" header-section json)))
- (jsonrpc--log-event connection message 'client)))
+ (jsonrpc--log-event connection converted 'client subtype)))
(defun jsonrpc-process-type (conn)
"Return the `process-type' of JSONRPC connection CONN."
"Encode OBJECT into a JSON string.")
(cl-defun jsonrpc--reply
- (connection id &key (result nil result-supplied-p) (error nil error-supplied-p))
+ (connection id method &key (result nil result-supplied-p) (error nil error-supplied-p))
"Reply to CONNECTION's request ID with RESULT or ERROR."
(apply #'jsonrpc-connection-send connection
`(:id ,id
,@(and result-supplied-p `(:result ,result))
- ,@(and error-supplied-p `(:error ,error)))))
+ ,@(and error-supplied-p `(:error ,error))
+ :method ,method)))
(defun jsonrpc--call-deferred (connection)
"Call CONNECTION's deferred actions, who may again defer themselves."
(apply #'format format args)
:warning)))
-(defun jsonrpc--log-event (connection message &optional type)
+(defun jsonrpc--log-event (connection message &optional origin subtype)
"Log a JSONRPC-related event.
CONNECTION is the current connection. MESSAGE is a JSON-like
-plist. TYPE is a symbol saying if this is a client or server
-originated."
+plist. ORIGIN is a symbol saying where event originated.
+SUBTYPE tells more about the event."
(let ((max (jsonrpc--events-buffer-scrollback-size connection)))
(when (or (null max) (cl-plusp max))
(with-current-buffer (jsonrpc-events-buffer connection)
- (cl-destructuring-bind (&key method id error &allow-other-keys) message
+ (cl-destructuring-bind (&key _method id error &allow-other-keys) message
(let* ((inhibit-read-only t)
- (subtype (cond ((and method id) 'request)
- (method 'notification)
- (id 'reply)
- (t 'message)))
(type
- (concat (format "%s" (or type 'internal))
- (if type
- (format "-%s" subtype)))))
+ (concat (format "%s" (or origin 'internal))
+ (if origin (format "-%s" (or subtype 'message))))))
(goto-char (point-max))
(prog1
(let ((msg (format "[%s]%s%s %s:\n%s"
client-replies))
(advice-add
#'jsonrpc--log-event :before
- (lambda (_proc message &optional type)
- (cl-destructuring-bind (&key method id _error &allow-other-keys)
- message
- (let ((req-p (and method id))
- (notif-p method)
- (reply-p id))
- (cond
- ((eq type 'server)
- (cond (req-p ,(when server-requests
- `(push message ,server-requests)))
- (notif-p ,(when server-notifications
- `(push message ,server-notifications)))
- (reply-p ,(when server-replies
- `(push message ,server-replies)))))
- ((eq type 'client)
- (cond (req-p ,(when client-requests
- `(push message ,client-requests)))
- (notif-p ,(when client-notifications
- `(push message ,client-notifications)))
- (reply-p ,(when client-replies
- `(push message ,client-replies)))))))))
+ (lambda (_proc message &optional origin subtype)
+ (let ((req-p (eq subtype 'request))
+ (notif-p (eq subtype 'notification))
+ (reply-p (eql subtype 'reply)))
+ (cond
+ ((eq origin 'server)
+ (cond (req-p ,(when server-requests
+ `(push message ,server-requests)))
+ (notif-p ,(when server-notifications
+ `(push message ,server-notifications)))
+ (reply-p ,(when server-replies
+ `(push message ,server-replies)))))
+ ((eq origin 'client)
+ (cond (req-p ,(when client-requests
+ `(push message ,client-requests)))
+ (notif-p ,(when client-notifications
+ `(push message ,client-notifications)))
+ (reply-p ,(when client-replies
+ `(push message ,client-replies))))))))
'((name . ,log-event-ad-sym)))
,@body)
(advice-remove #'jsonrpc--log-event ',log-event-ad-sym))))