jcaw / json-rpc-server.el

πŸ”Œ Server-side implementation of the JSON-RPC 2.0 protocol for Emacs.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

json-rpc-server logo

JSON-RPC-Server

Server-side implementation of the JSON-RPC 2.0 protocol for Emacs.

πŸ”Œ


This is a full implementation of the JSON-RPC 2.0 protocol for Emacs. You pass in a JSON string, Emacs executes the specified function(s), and a JSON-RPC 2.0 response is returned.

This package is designed sit underneath a transport layer. No transport logic is included. The transport layer is responsible for communicating with external clients. Since JSON-RPC provides no inbuilt mechanism for authenticating requests, the transport layer should also handle authentication.

The default transport layer is Porthole. It uses the HTTP protocol.


Table of Contents

How it Works

json-rpc-server-handle is the main entry point into the package (functions in this package are prefixed with json-rpc-server-). json-rpc-server-handle takes a JSON-RPC 2.0 request string, and a list of functions that are allowed to be called remotely.

;; This will decode a JSON-RPC 2.0 request, execute it, and return the JSON-RPC 2.0 response.
(json-rpc-server-handle string-encoded-json-rpc-request
             list-of-legal-functions)

If successful, the result will be a string containing the JSON-RPC 2.0 result response. If an error occurs, it will contain a JSON-RPC 2.0 error response. Errors will be captured and encoded into strings - they won't be raised above the handler (unless you are debugging).

Only functions you have specifically exposed can be called via RPC. You must pass a list of functions to json-rpc-server-handle so it knows which functions it's allowed to execute, and which it is not.

Examples

Example: Calling a Method

Encode a request according to the JSON-RPC 2.0 protocol. method should be the method name, as a string.

Here's an example request:

{
    "jsonrpc": "2.0",
    "method": "+",
    "params": [1, 2, 3],
    "id": 29492
}

Let's encode this into a string and pass it to json-rpc-server-handle:

(json-rpc-server-handle
 "{
    \"jsonrpc\": \"2.0\",
    \"method\": \"+\",
    \"params\": [1,2,3],
    \"id\": 29492
}"
 ;; We have to make sure the `+' function is exposed to RPC calls.
 '(+))

json-rpc-server will decode the request, then apply the function + to the list '(1 2 3). Here's what the result of json-rpc-server-handle will be:

"{\"jsonrpc\":\"2.0\",\"result\":6,\"id\":29492}"

Decoded:

{
    "jsonrpc": "2.0",
    "result": 6,
    "id": 29492
}

This string-encoded response can now be returned to the client.

(See the Symbols section for how to transfer symbols and keyword arguments.)

Example: Invalid Request

This time, let's try an invalid request.

{
    "params": [1, 2, 3],
    "id": 23092
}

This request is invalid because it has no "method". The call to json-rpc-server-handle:

(json-rpc-server-handle
 "{
    \"params\": [1, 2, 3],
    \"id\": 23092
}"
 '(+))

Here's what json-rpc-server-handle returns:

"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32600,\"message\":\"`method` was not provided.'\",\"data\":null},\"id\":23092}"

Decoded:

{
    "jsonrpc": "2.0",
    "error": {
        "code": -32600,
        "message": "`method` was not provided.",
        "data": null
    },
    "id": 23092
}

Note the "id" field. json-rpc-server-handle will do its best to extract an id from all requests, even invalid requests, so errors can be synced up to their respective requests.

Example: Malformed JSON

If there is a problem with the request (or another error occurs), json-rpc-server-handle will encode a JSON-RPC 2.0 error response. Here's an example.

Let's try some malformed JSON:

{Szx. dsd}

The call to json-rpc-server-handle:

(json-rpc-server-handle "{Szx. dsd}" '(+))

Here's what json-rpc-server-handle returns:

"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32700,\"message\":\"There was an error decoding the request's JSON.\",\"data\":{\"underlying-error\":{\"json-string-format\":[\"doesn't start with `\\\"'!\"]}}},\"id\":null}"

Decoded:

{
  "jsonrpc": "2.0",
  "error": {
    "code": -32700,
    "message": "There was an error decoding the request's JSON.",
    "data": {
      "underlying-error": {
        "type": "json-string-format",
        "data": ["doesn't start with `\"'!"]
      }
    }
  },
  "id": null
}

Note the "data" field. Some responses are triggered by an underlying error in the Elisp, which may contain more meaningful information about the error. When possible, that will be returned in the "underlying-error" field. If there is no underlying error, this field will not be present.

Example: Batch Requests

You can also execute multiple requests at once. This is useful if the client wants to minimize the number of requests they have to make to a slow transport layer. Each request will be executed, and a string response containing all the results will be returned. Unlike most JSON-RPC 2.0 protocols, batch requests are guaranteed to be executed in the same order they were received.

Let's make two requests - one to a valid function, one to an invalid one. We'll batch them.

[
  {
    "jsonrpc": "2.0",
    "method": "+",
    "params": [1, 2, 3],
    "id": 1
  },
  {
    "jsonrpc": "2.0",
    "method": "insert",
    "params": ["Some text to insert"],
    "id": 2
  }
]

The call to json-rpc-server-handle:

(json-rpc-server-handle
 "[
    {
        \"jsonrpc\": \"2.0\",
        \"method\": \"+\",
        \"params\": [1, 2, 3],
        \"id\": 1
    },
    {
        \"jsonrpc\": \"2.0\",
        \"method\": \"insert\",
        \"params\": [\"Some text to insert\"],
        \"id\": 2
    }
]"
 ;; Let's expose `+', but not `insert', to demonstrate a result and an error.
 '(+))

Here's what json-rpc-server-handle returns:

"[{\"jsonrpc\":\"2.0\",\"result\":6,\"id\":1},{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32601,\"message\":\"Function has not been exposed (it may or may not exist). Cannot execute.\",\"data\":null},\"id\":2}]"

Decoded:

[
  {
    "jsonrpc": "2.0",
    "result": 6,
    "id": 1
  },
  {
    "jsonrpc": "2.0",
    "error": {
      "code": -32601,
      "message": "Function has not been exposed (it may or may not exist). Cannot execute.",
      "data": null
    },
    "id": 2
  }
]

As you can see, one of the function calls executed successfully, another caused an error. The responses should be returned in the same order their requests were submitted, but they can also be synchronized based on their "id".

json-rpc-server-handle assumes that requests are atomic until proven otherwise. If your batch request is malformed, json-rpc-server-handle will probably not return a batch in response - it will respond with a single "malformed json" error.

Datatype Limitations

The structure of JSON limits the types of variables that can be transferred. JSON only contains six datatypes. Thus, functions exposed by this protocol must expect certain datatypes.

The datatypes are mapped as follows:

JSON Datatype Decodeded Elisp Datatype In JSON In Elisp
string string "string" "string"
quoted string symbol "'symbol", ":keyword" 'symbol, :keyword
number integer or float 21, 3.14 21, 3.14
boolean t or :json-false true, false t, :json-false
null nil null nil
object alist {"Key": "Value"} '(("Key" . "Value"))'
array list [1, 2, 4, 7] '(1 2 4 7)

Note that alist keys are decoded as strings.

You may notice that "quoted strings" are decoded differently to normal strings. See the Symbols section for a full explanation.

Other Types

Because of these type limitations, you cannot transfer vectors, plists, hash tables, cl-structs, etc.

There is no easy way around this. JSON-RPC provides simplicity, at the cost of flexibility. If you want to call a function that expects a different type, you must write an intermediary function that translates from the available ones and publish your intermediary instead.

Symbols

Symbols are important in Elisp. Luckily, by abusing the JSON-RPC syntax we can transfer symbols. Strings beginning with a single quote will be decoded into symbols. Strings that start with a single colon will be decoded into keywords.

For example:

  • The string "'a-symbol" becomes the symbol 'a-symbol.
  • The string ":a-keyword" becomes the symbol :a-keyword.
  • "'wrapped-string'" does not change, because it contains multiple quotes. It will stay a string.

Let's send a list:

["a string", "'a-symbol", ":a-keyword"]

That list will be decoded into:

'("a string" a-symbol :a-keyword)

Keyword Arguments

By default, JSON-RPC 2.0 requires that keyword arguments be passed as "objects" (you might know them as dictionaries - {"keyword": "value"}). This is not supported. In Elisp, you cannot reference positional arguments by name and they may be mixed with keyword arguments. Objects aren't compatible with that structure.

If you want to pass keyword arguments, you must encode them as a list:

{
    "params": ["positional-arg",
               ":keyword1", "value1", 
               ":keyword2", "value2"]
}

Example: Symbols and Keyword Arguments

Here's an example of a request containing symbols and a keyword argument. Let's say we want Emacs to flash the line after we scroll up, so we can keep track of the cursor.

In Elisp, we could do something like this:

;; Advise the `scroll-up' function to call `nav-flash-show' afterwards.
(advice-add 'scroll-up :after 'nav-flash-show)

Here's how to encode that in a JSON-RPC call:

{
    "jsonrpc": "2.0",
    "method": "advice-add",
    "params": ["'scroll-up" ":after" "'nav-flash-show"],
    "id": 29492,
}

This would be encoded into a string and passed to json-rpc-server-handle. It will decode a function call similar to the following:

(apply
 'advice-add
 '(switch-to-buffer
   :after
   save-current-buffer))

Expressed another way, this is equivalent to:

(advice-add 'switch-to-buffer :after 'save-current-buffer)

(Please note that this would be terrible way to flash the line in actual Emacs. Don't use it. You'd have to wait between each scroll press.)

Installation

The package itself is named json-rpc-server. It's easiest to install from MELPA. Make sure it's in your list of repositories, then:

M-x package-install RET json-rpc-server RET

Once installed, require it with:

(require 'json-rpc-server)

List of Transport Layers

If you want to actually make RPC calls to Emacs, you need to use a transport layer. Here's a list:

Project Protocol
Porthole HTTP

Have you written one? Open a pull request and I'll add it.

FAQ

  • Is it compatible with older versions of JSON-RPC?

    Yes. It should work fine with older JSON-RPC requests. However, they aren't officially supported and the response will still be JSON-RPC 2.0.

  • Does it support keyword arguments?

    Yes, but not in the standard format. You may not pass them as objects. Pass them as lists, just like in Elisp.

  • How can I send a [vector, hash table, etc]?

    You can't. You have to write an intermediate function that constructs these types from alists, strings, etc.

  • Does it support notifications?

    No. All requests block until a value is returned (or an error occurs). This could be implemented at the transport level, if desired.

  • Does it support batch requests?

    Yes. See the batch requests example.

  • Can I run multiple servers at once?

    json-rpc-server has a somewhat misleading name. It's not a server, it's a server-side implementation of the protocol. The transport layer can run as many servers as it likes.

  • Are you open to pull requests?

    Yes! Please pull against the develop branch.

About

πŸ”Œ Server-side implementation of the JSON-RPC 2.0 protocol for Emacs.

License:GNU General Public License v3.0


Languages

Language:Emacs Lisp 100.0%