summaryrefslogtreecommitdiff
path: root/tramp-rpc/lisp/tramp-rpc-protocol.el
blob: 59f60f1863fef9a77d43c369ed9ac66dc4e73354 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
;;; tramp-rpc-protocol.el --- JSON-RPC protocol for TRAMP-RPC -*- lexical-binding: t; -*-

;; Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>

;; Author: Arthur Heymans <arthur@aheymans.xyz>
;; Keywords: comm, processes
;; Package-Requires: ((emacs "30.1"))

;; This file is part of tramp-rpc.

;; tramp-rpc is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;;; Commentary:

;; This file provides the JSON-RPC 2.0 protocol implementation for
;; communicating with the tramp-rpc-server binary.

;;; Code:

(require 'json)

(defvar tramp-rpc-protocol--request-id 0
  "Counter for generating unique request IDs.")

(defun tramp-rpc-protocol--next-id ()
  "Generate the next request ID."
  (cl-incf tramp-rpc-protocol--request-id))

(defun tramp-rpc-protocol-encode-request (method params)
  "Encode a JSON-RPC 2.0 request for METHOD with PARAMS.
Returns a JSON string."
  (let ((request `((jsonrpc . "2.0")
                   (id . ,(tramp-rpc-protocol--next-id))
                   (method . ,method)
                   (params . ,params))))
    (json-encode request)))

(defun tramp-rpc-protocol-encode-request-with-id (method params)
  "Encode a JSON-RPC 2.0 request for METHOD with PARAMS.
Returns a cons cell (ID . JSON-STRING) for pipelining support."
  (let* ((id (tramp-rpc-protocol--next-id))
         (request `((jsonrpc . "2.0")
                    (id . ,id)
                    (method . ,method)
                    (params . ,params))))
    (cons id (json-encode request))))

(defun tramp-rpc-protocol-decode-response (json-string)
  "Decode a JSON-RPC 2.0 response from JSON-STRING.
Returns a plist with :id, :result, and :error keys."
  (let* ((response (json-parse-string json-string
                                      :object-type 'alist
                                      :false-object nil
                                      :null-object nil))
         (id (alist-get 'id response))
         (result (alist-get 'result response))
         (error-obj (alist-get 'error response)))
    (list :id id
          :result result
          :error (when error-obj
                   (list :code (alist-get 'code error-obj)
                         :message (alist-get 'message error-obj)
                         :data (alist-get 'data error-obj))))))

(defun tramp-rpc-protocol-error-p (response)
  "Return non-nil if RESPONSE contains an error."
  (plist-get response :error))

(defun tramp-rpc-protocol-error-message (response)
  "Extract the error message from RESPONSE."
  (plist-get (plist-get response :error) :message))

(defun tramp-rpc-protocol-error-code (response)
  "Extract the error code from RESPONSE."
  (plist-get (plist-get response :error) :code))

;; Error codes
(defconst tramp-rpc-protocol-error-parse -32700)
(defconst tramp-rpc-protocol-error-invalid-request -32600)
(defconst tramp-rpc-protocol-error-method-not-found -32601)
(defconst tramp-rpc-protocol-error-invalid-params -32602)
(defconst tramp-rpc-protocol-error-internal -32603)
(defconst tramp-rpc-protocol-error-file-not-found -32001)
(defconst tramp-rpc-protocol-error-permission-denied -32002)
(defconst tramp-rpc-protocol-error-io -32003)
(defconst tramp-rpc-protocol-error-process -32004)

;; ============================================================================
;; Batch request support
;; ============================================================================

(defun tramp-rpc-protocol-encode-batch-request (requests)
  "Encode a batch request containing multiple REQUESTS.
REQUESTS is a list of (METHOD . PARAMS) cons cells.
Returns a JSON string for a single RPC call to the batch method."
  (let ((batch-requests
         (mapcar (lambda (req)
                   `((method . ,(car req))
                     (params . ,(cdr req))))
                 requests)))
    (tramp-rpc-protocol-encode-request
     "batch"
     `((requests . ,(vconcat batch-requests))))))

(defun tramp-rpc-protocol-encode-batch-request-with-id (requests)
  "Encode a batch request containing multiple REQUESTS.
REQUESTS is a list of (METHOD . PARAMS) cons cells.
Returns a cons cell (ID . JSON-STRING) for ID tracking."
  (let ((batch-requests
         (mapcar (lambda (req)
                   `((method . ,(car req))
                     (params . ,(cdr req))))
                 requests)))
    (tramp-rpc-protocol-encode-request-with-id
     "batch"
     `((requests . ,(vconcat batch-requests))))))

(defun tramp-rpc-protocol-decode-batch-response (response)
  "Decode a batch response into a list of individual results.
RESPONSE is the decoded response plist from
`tramp-rpc-protocol-decode-response'.
Returns a list where each element is either:
  - The result value (if successful)
  - A plist (:error CODE :message MSG) if that sub-request failed."
  (let ((results-array (alist-get 'results (plist-get response :result))))
    (mapcar (lambda (result-obj)
              (if-let ((error-obj (alist-get 'error result-obj)))
                  (list :error (alist-get 'code error-obj)
                        :message (alist-get 'message error-obj))
                (alist-get 'result result-obj)))
            results-array)))

(provide 'tramp-rpc-protocol)
;;; tramp-rpc-protocol.el ends here