« ^ »

EmacsのrestclientでWeb APIを呼び出す仕組みをパッと見で分かるようにする

所要時間: 約 4分

ローカルで動作する個人的なツールやシステム間連携など、Web APIを呼び出して何かを行う処理を実装する事はよくある。Web APIを呼び出す処理の実装では、通信的な問題、意図しない形式や状態のリクエストを送信した、又は意図しない形式や状態のレスポンスを受信したといった問題が発生する。

そういった問題の解消のため、リクエストを送信し、内容に間違いがないか、状態は正しいか、期待している形式のレスポンスか等を確認していく。しかし、実際のリクエストを送信しているコードは、実際に呼び出すためには事前準備が必要で手間だったり、ラップされすぎて該当のコードを見付ける事が難しかったり、意味を読み解くのが難しかったりする。ただHTTPリクエストを送信し、レスポンスを確認したいだけなのに。

リクエストの組立、通信、レスポンスの解析などがごちゃまぜになっているような処理は、実際にどんなリクエストを送信しレスポンスを受け取っているのかを調べる事すら難しく、その結果 mitmproxy に一度通信を中継したり、tcpdumpやwiresharkなどの盗聴ツールで中身を確認するといった事もできなくはない。ただ、ちょっと大変過ぎないだろうか。

EmacsにはHTTP形式と似たような形式でリクエストを記述するための機構として、 http.elrestclient.el がある。これらはBabelと組み合せてドキュメント内に記述する事もできるため、Web APIの調査には重宝するのだが、システムとして組み込もうとすると使いにくくなる。その理由の1つは受信したレスポンスを出力するバッファがポップアップされる事だ。Emacsのバックグラウンドジョブとしてタイマーを使って呼び出しているとして、リクエストを送信するたびバッファがポップアップされたらげんなりする。そこで、今回は restclient.el を使ってリクエストを送信するが、レスポンスバッファをポップアップさせない方法を探る事にした。

送信するリクエストは以下の test.http とする。

:ORIGIN = http://localhost:8000

GET :ORIGIN

ポップアップの処理は restclient-http-handle-response という関数の最後に呼出される display-bufferswitch-to-buffer-other-window によって行われる。

実装を見てもらえれば分かるが、この実装ではそれらの関数を呼び出さないようにする事は難しそうだった。そこで、それらをコメントアウントした新しい関数 restclient-http-handle-response-custom を定義した。

(require 'restclient)

(defun restclient-http-handle-response-custom (status method url bufname raw stay-in-window)
  "Switch to the buffer returned by `url-retreive'.
The buffer contains the raw HTTP response sent by the server."
  (setq restclient-within-call nil)
  (setq restclient-request-time-end (current-time))
  (if (= (point-min) (point-max))
      (signal (car (plist-get status :error)) (cdr (plist-get status :error)))
    (when (buffer-live-p (current-buffer))
      (with-current-buffer (restclient-decode-response
                            (current-buffer)
                            bufname
                            restclient-same-buffer-response)
        (run-hooks 'restclient-response-received-hook)
        (unless raw
          (restclient-prettify-response method url))
        (buffer-enable-undo)
	(restclient-response-mode)
        (run-hooks 'restclient-response-loaded-hook)
        ;; (if stay-in-window
        ;;     (display-buffer (current-buffer) t)
        ;;   (switch-to-buffer-other-window (current-buffer)))))))
	))))
restclient-http-handle-response-custom

この新たに定義した関数を restclient-http-handle-response シンボルに束縛し、引く事ができるようにする。シンボルの関数スロットを書き換えるには fset を使用する。この fsetlet のようにそのS式が終了したら元に戻るというものではなく、 setq のように書き変わったままになってしまう。それでは元の restclient.el の挙動を使用できなくなり都合が悪い。そこで unwind_protect を使い、一時的にシンボルの関数スロットを書き換え、処理終了後に元に戻す事にする。 に置き換える。シンボル表の関数せる

(require 'restclient)

(defun restclient-http-handle-response-custom (status method url bufname raw stay-in-window)
  "Switch to the buffer returned by `url-retreive'.
The buffer contains the raw HTTP response sent by the server."
  (setq restclient-within-call nil)
  (setq restclient-request-time-end (current-time))
  (if (= (point-min) (point-max))
      (signal (car (plist-get status :error)) (cdr (plist-get status :error)))
    (when (buffer-live-p (current-buffer))
      (with-current-buffer (restclient-decode-response
                            (current-buffer)
                            bufname
                            restclient-same-buffer-response)
        (run-hooks 'restclient-response-received-hook)
        (unless raw
          (restclient-prettify-response method url))
        (buffer-enable-undo)
	(restclient-response-mode)
        (run-hooks 'restclient-response-loaded-hook)
        ;; (if stay-in-window
        ;;     (display-buffer (current-buffer) t)
        ;;   (switch-to-buffer-other-window (current-buffer)))))))
	))))

(let ((original-fn (symbol-function #'restclient-http-handle-response)))
  (unwind-protect
      (progn
	(fset #'restclient-http-handle-response
	      (symbol-function #'restclient-http-handle-response-custom))

	;; 呼び出し処理はここに記述する
	(with-current-buffer (find-file-noselect "test.http")
	  (restclient-http-send-current-stay-in-window))
	)
    (fset #'restclient-http-handle-response original-fn)))

かなり雑な方法ではあるが、これでレスポンスバッファはポップアップされなくなった。この実装を元にrestclientと似たようなI/Fを提供するようにパッケージにした。

;;; restclient-custom.el --- Restclient customize.

;; Copyright (C) 2023 TakesxiSximada

;; Author: TakesxiSximada
;; Maintainer: TakesxiSximada
;; Version: 1.0
;; Package-Version: 20231105.0000
;; Package-Requires: ((emacs "29.1") (restclient))
;; Date: 2023-11-05

;; This file is part of soundplay.el.

;;; License:

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

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;; Affero General Public License for more details.

;; You should have received a copy of the GNU Affero General Public
;; License along with this program.  If not, see
;; <https://www.gnu.org/licenses/>.

;;; Commentary:

;; KeePassXC Emacs Integration.

;;; Code:
(require 'restclient)

;; Original function is `restclient-http-handle-response'.
(defun restclient-http-handle-response-custom (status method url bufname raw stay-in-window)
  "Switch to the buffer returned by `url-retreive'.
The buffer contains the raw HTTP response sent by the server."
  (setq restclient-within-call nil)
  (setq restclient-request-time-end (current-time))
  (if (= (point-min) (point-max))
      (signal (car (plist-get status :error)) (cdr (plist-get status :error)))
    (when (buffer-live-p (current-buffer))
      (with-current-buffer (restclient-decode-response
                            (current-buffer)
                            bufname
                            restclient-same-buffer-response)
        (run-hooks 'restclient-response-received-hook)
        (unless raw
          (restclient-prettify-response method url))
        (buffer-enable-undo)
	(restclient-response-mode)
        (run-hooks 'restclient-response-loaded-hook)
	;; ----- Monkey Paching -------------------------------------
        ;; (if stay-in-window
        ;;     (display-buffer (current-buffer) t)
        ;;   (switch-to-buffer-other-window (current-buffer)))))))
	;; ----------------------------------------------------------
	))))

;;;###autoload
(defun restclient-http-send-current-no-popup ()
  "Send requests without popup."
  (interactive)
  (let ((original-fn (symbol-function #'restclient-http-handle-response)))
    (unwind-protect
	(progn
	  (fset #'restclient-http-handle-response
		(symbol-function #'restclient-http-handle-response-custom))
	  (restclient-http-send-current-stay-in-window)) ;; 呼び出し処理はここに記述する
      (fset #'restclient-http-handle-response original-fn))))

(provide 'restclient-custom)
;; restclient-custom.el ends here