|
|
@ -1,791 +0,0 @@ |
|
|
|
;;; custom/leo-restclient.el -*- lexical-binding: t; -*- |
|
|
|
|
|
|
|
;;; leo/restclient.el --- An interactive HTTP client for Emacs |
|
|
|
;; |
|
|
|
;; Public domain. |
|
|
|
|
|
|
|
;; Author: Pavel Kurnosov <pashky@gmail.com> |
|
|
|
;; Maintainer: Pavel Kurnosov <pashky@gmail.com> |
|
|
|
;; Created: 01 Apr 2012 |
|
|
|
;; Keywords: http |
|
|
|
|
|
|
|
;; This file is not part of GNU Emacs. |
|
|
|
;; This file is public domain software. Do what you want. |
|
|
|
|
|
|
|
;;; Commentary: |
|
|
|
;; |
|
|
|
;; This is a tool to manually explore and test HTTP REST |
|
|
|
;; webservices. Runs queries from a plain-text query sheet, displays |
|
|
|
;; results as a pretty-printed XML, JSON and even images. |
|
|
|
|
|
|
|
;;; Code: |
|
|
|
;; |
|
|
|
(require 'url) |
|
|
|
(require 'json) |
|
|
|
(require 'outline) |
|
|
|
(eval-when-compile (require 'subr-x)) |
|
|
|
(eval-when-compile (require 'cl)) |
|
|
|
|
|
|
|
(defgroup leo/restclient nil |
|
|
|
"An interactive HTTP client for Emacs." |
|
|
|
:group 'tools) |
|
|
|
|
|
|
|
(defcustom leo/restclient-log-request t |
|
|
|
"Log leo/restclient requests to *Messages*." |
|
|
|
:group 'leo/restclient |
|
|
|
:type 'boolean) |
|
|
|
|
|
|
|
(defcustom leo/restclient-same-buffer-response t |
|
|
|
"Re-use same buffer for responses or create a new one each time." |
|
|
|
:group 'leo/restclient |
|
|
|
:type 'boolean) |
|
|
|
|
|
|
|
(defcustom leo/restclient-same-buffer-response-name "*HTTP Response*" |
|
|
|
"Name for response buffer." |
|
|
|
:group 'leo/restclient |
|
|
|
:type 'string) |
|
|
|
|
|
|
|
(defcustom leo/restclient-info-buffer-name "*Leo/Restclient Info*" |
|
|
|
"Name for info buffer." |
|
|
|
:group 'leo/restclient |
|
|
|
:type 'string) |
|
|
|
|
|
|
|
(defcustom leo/restclient-inhibit-cookies nil |
|
|
|
"Inhibit leo/restclient from sending cookies implicitly." |
|
|
|
:group 'leo/restclient |
|
|
|
:type 'boolean) |
|
|
|
|
|
|
|
(defcustom leo/restclient-content-type-modes '(("text/xml" . xml-mode) |
|
|
|
("text/plain" . text-mode) |
|
|
|
("application/xml" . xml-mode) |
|
|
|
("application/json" . js-mode) |
|
|
|
("image/png" . image-mode) |
|
|
|
("image/jpeg" . image-mode) |
|
|
|
("image/jpg" . image-mode) |
|
|
|
("image/gif" . image-mode) |
|
|
|
("text/html" . html-mode)) |
|
|
|
"An association list mapping content types to buffer modes" |
|
|
|
:group 'leo/restclient |
|
|
|
:type '(alist :key-type string :value-type symbol)) |
|
|
|
|
|
|
|
(defgroup leo/restclient-faces nil |
|
|
|
"Faces used in Leo/Restclient Mode" |
|
|
|
:group 'leo/restclient |
|
|
|
:group 'faces) |
|
|
|
|
|
|
|
(defface leo/restclient-variable-name-face |
|
|
|
'((t (:inherit font-lock-preprocessor-face))) |
|
|
|
"Face for variable name." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
(defface leo/restclient-variable-string-face |
|
|
|
'((t (:inherit font-lock-string-face))) |
|
|
|
"Face for variable value (string)." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
(defface leo/restclient-variable-elisp-face |
|
|
|
'((t (:inherit font-lock-function-name-face))) |
|
|
|
"Face for variable value (Emacs lisp)." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
(defface leo/restclient-variable-multiline-face |
|
|
|
'((t (:inherit font-lock-doc-face))) |
|
|
|
"Face for multi-line variable value marker." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
(defface leo/restclient-variable-usage-face |
|
|
|
'((t (:inherit leo/restclient-variable-name-face))) |
|
|
|
"Face for variable usage (only used when headers/body is represented as a single variable, not highlighted when variable appears in the middle of other text)." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
(defface leo/restclient-method-face |
|
|
|
'((t (:inherit font-lock-keyword-face))) |
|
|
|
"Face for HTTP method." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
(defface leo/restclient-url-face |
|
|
|
'((t (:inherit font-lock-function-name-face))) |
|
|
|
"Face for variable value (Emacs lisp)." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
(defface leo/restclient-file-upload-face |
|
|
|
'((t (:inherit leo/restclient-variable-multiline-face))) |
|
|
|
"Face for highlighting upload file paths." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
(defface leo/restclient-header-name-face |
|
|
|
'((t (:inherit font-lock-variable-name-face))) |
|
|
|
"Face for HTTP header name." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
(defface leo/restclient-header-value-face |
|
|
|
'((t (:inherit font-lock-string-face))) |
|
|
|
"Face for HTTP header value." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
(defface leo/restclient-request-hook-face |
|
|
|
'((t (:inherit font-lock-preprocessor-face))) |
|
|
|
"Face for single request hook indicator." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
(defface leo/restclient-request-hook-name-face |
|
|
|
'((t (:inherit font-lock-function-name-face))) |
|
|
|
"Face for single request hook type names." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
(defface leo/restclient-request-hook-args-face |
|
|
|
'((t (:inherit font-lock-string-face))) |
|
|
|
"Face for single request hook type arguments." |
|
|
|
:group 'leo/restclient-faces) |
|
|
|
|
|
|
|
|
|
|
|
(defvar leo/restclient-within-call nil) |
|
|
|
|
|
|
|
(defvar leo/restclient-request-time-start nil) |
|
|
|
(defvar leo/restclient-request-time-end nil) |
|
|
|
|
|
|
|
(defvar leo/restclient-var-overrides nil |
|
|
|
"An alist of vars that will override any set in the file, |
|
|
|
also where dynamic vars set on callbacks are stored.") |
|
|
|
|
|
|
|
(defvar leo/restclient-result-handlers '() |
|
|
|
"A registry of available completion hooks. |
|
|
|
Stored as an alist of name -> (hook-creation-func . description)") |
|
|
|
|
|
|
|
(defvar leo/restclient-curr-request-functions nil |
|
|
|
"A list of functions to run once when the next request is loaded") |
|
|
|
|
|
|
|
(defvar leo/restclient-response-loaded-hook nil |
|
|
|
"Hook run after response buffer is formatted.") |
|
|
|
|
|
|
|
(defvar leo/restclient-http-do-hook nil |
|
|
|
"Hook to run before making request.") |
|
|
|
|
|
|
|
(defvar leo/restclient-response-received-hook nil |
|
|
|
"Hook run after data is loaded into response buffer.") |
|
|
|
|
|
|
|
(defcustom leo/restclient-vars-max-passes 10 |
|
|
|
"Maximum number of recursive variable references. This is to prevent hanging if two variables reference each other directly or indirectly." |
|
|
|
:group 'leo/restclient |
|
|
|
:type 'integer) |
|
|
|
|
|
|
|
(defconst leo/restclient-comment-separator "#") |
|
|
|
(defconst leo/restclient-comment-start-regexp (concat "^" leo/restclient-comment-separator)) |
|
|
|
(defconst leo/restclient-comment-not-regexp (concat "^[^" leo/restclient-comment-separator "]")) |
|
|
|
(defconst leo/restclient-empty-line-regexp "^\\s-*$") |
|
|
|
|
|
|
|
(defconst leo/restclient-method-url-regexp |
|
|
|
"^\\(GET\\|POST\\|DELETE\\|PUT\\|HEAD\\|OPTIONS\\|PATCH\\) \\(.*\\)$") |
|
|
|
|
|
|
|
(defconst leo/restclient-header-regexp |
|
|
|
"^\\([^](),/:;@[\\{}= \t]+\\): \\(.*\\)$") |
|
|
|
|
|
|
|
(defconst leo/restclient-use-var-regexp |
|
|
|
"^\\(:[^: \n]+\\)$") |
|
|
|
|
|
|
|
(defconst leo/restclient-var-regexp |
|
|
|
(concat "^\\(:[^:= ]+\\)[ \t]*\\(:?\\)=[ \t]*\\(<<[ \t]*\n\\(\\(.*\n\\)*?\\)" leo/restclient-comment-separator "\\|\\([^<].*\\)$\\)")) |
|
|
|
|
|
|
|
(defconst leo/restclient-svar-regexp |
|
|
|
"^\\(:[^:= ]+\\)[ \t]*=[ \t]*\\(.+?\\)$") |
|
|
|
|
|
|
|
(defconst leo/restclient-evar-regexp |
|
|
|
"^\\(:[^: ]+\\)[ \t]*:=[ \t]*\\(.+?\\)$") |
|
|
|
|
|
|
|
(defconst leo/restclient-mvar-regexp |
|
|
|
"^\\(:[^: ]+\\)[ \t]*:?=[ \t]*\\(<<\\)[ \t]*$") |
|
|
|
|
|
|
|
(defconst leo/restclient-file-regexp |
|
|
|
"^<[ \t]*\\([^<>\n\r]+\\)[ \t]*$") |
|
|
|
|
|
|
|
(defconst leo/restclient-content-type-regexp |
|
|
|
"^Content-[Tt]ype: \\(\\w+\\)/\\(?:[^\\+\r\n]*\\+\\)*\\([^;\r\n]+\\)") |
|
|
|
|
|
|
|
(defconst leo/restclient-response-hook-regexp |
|
|
|
"^\\(->\\) \\([^[:space:]]+\\) +\\(.*\\)$") |
|
|
|
|
|
|
|
;; The following disables the interactive request for user name and |
|
|
|
;; password should an API call encounter a permission-denied response. |
|
|
|
;; This API is meant to be usable without constant asking for username |
|
|
|
;; and password. |
|
|
|
(defadvice url-http-handle-authentication (around leo/restclient-fix) |
|
|
|
(if leo/restclient-within-call |
|
|
|
(setq ad-return-value t) |
|
|
|
ad-do-it)) |
|
|
|
(ad-activate 'url-http-handle-authentication) |
|
|
|
|
|
|
|
(defadvice url-cache-extract (around leo/restclient-fix-2) |
|
|
|
(unless leo/restclient-within-call |
|
|
|
ad-do-it)) |
|
|
|
(ad-activate 'url-cache-extract) |
|
|
|
|
|
|
|
(defadvice url-http-user-agent-string (around leo/restclient-fix-3) |
|
|
|
(if leo/restclient-within-call |
|
|
|
(setq ad-return-value nil) |
|
|
|
ad-do-it)) |
|
|
|
(ad-activate 'url-http-user-agent-string) |
|
|
|
|
|
|
|
(defun leo/restclient-http-do (method url headers entity &rest handle-args) |
|
|
|
"Send ENTITY and HEADERS to URL as a METHOD request." |
|
|
|
(if leo/restclient-log-request |
|
|
|
(message "HTTP %s %s Headers:[%s] Body:[%s]" method url headers entity)) |
|
|
|
(let ((url-request-method (encode-coding-string method 'us-ascii)) |
|
|
|
(url-request-extra-headers '()) |
|
|
|
(url-request-data (encode-coding-string entity 'utf-8)) |
|
|
|
(url-mime-charset-string (url-mime-charset-string)) |
|
|
|
(url-mime-language-string nil) |
|
|
|
(url-mime-encoding-string nil) |
|
|
|
(url-mime-accept-string nil) |
|
|
|
(url-personal-mail-address nil)) |
|
|
|
|
|
|
|
(dolist (header headers) |
|
|
|
(let* ((mapped (assoc-string (downcase (car header)) |
|
|
|
'(("from" . url-personal-mail-address) |
|
|
|
("accept-encoding" . url-mime-encoding-string) |
|
|
|
("accept-charset" . url-mime-charset-string) |
|
|
|
("accept-language" . url-mime-language-string) |
|
|
|
("accept" . url-mime-accept-string))))) |
|
|
|
|
|
|
|
(if mapped |
|
|
|
(set (cdr mapped) (encode-coding-string (cdr header) 'us-ascii)) |
|
|
|
(let* ((hkey (encode-coding-string (car header) 'us-ascii)) |
|
|
|
(hvalue (encode-coding-string (cdr header) 'us-ascii))) |
|
|
|
(setq url-request-extra-headers (cons (cons hkey hvalue) url-request-extra-headers)))))) |
|
|
|
|
|
|
|
(setq leo/restclient-within-call t) |
|
|
|
(setq leo/restclient-request-time-start (current-time)) |
|
|
|
(run-hooks 'leo/restclient-http-do-hook) |
|
|
|
(url-retrieve url 'leo/restclient-http-handle-response |
|
|
|
(append (list method url (if leo/restclient-same-buffer-response |
|
|
|
leo/restclient-same-buffer-response-name |
|
|
|
(format "*HTTP %s %s*" method url))) handle-args) nil leo/restclient-inhibit-cookies))) |
|
|
|
|
|
|
|
(defun leo/restclient-prettify-response (method url) |
|
|
|
(save-excursion |
|
|
|
(let ((start (point)) (guessed-mode) (end-of-headers)) |
|
|
|
(while (and (not (looking-at leo/restclient-empty-line-regexp)) |
|
|
|
(eq (progn |
|
|
|
(when (looking-at leo/restclient-content-type-regexp) |
|
|
|
(setq guessed-mode |
|
|
|
(cdr (assoc-string (concat |
|
|
|
(match-string-no-properties 1) |
|
|
|
"/" |
|
|
|
(match-string-no-properties 2)) |
|
|
|
leo/restclient-content-type-modes |
|
|
|
t)))) |
|
|
|
(forward-line)) 0))) |
|
|
|
(setq end-of-headers (point)) |
|
|
|
(while (and (looking-at leo/restclient-empty-line-regexp) |
|
|
|
(eq (forward-line) 0))) |
|
|
|
(unless guessed-mode |
|
|
|
(setq guessed-mode |
|
|
|
(or (assoc-default nil |
|
|
|
;; magic mode matches |
|
|
|
'(("<\\?xml " . xml-mode) |
|
|
|
("{\\s-*\"" . js-mode)) |
|
|
|
(lambda (re _dummy) |
|
|
|
(looking-at re))) 'js-mode))) |
|
|
|
(let ((headers (buffer-substring-no-properties start end-of-headers))) |
|
|
|
(when guessed-mode |
|
|
|
(delete-region start (point)) |
|
|
|
(unless (eq guessed-mode 'image-mode) |
|
|
|
(apply guessed-mode '()) |
|
|
|
(if (fboundp 'font-lock-flush) |
|
|
|
(font-lock-flush) |
|
|
|
(with-no-warnings |
|
|
|
(font-lock-fontify-buffer)))) |
|
|
|
|
|
|
|
(cond |
|
|
|
((eq guessed-mode 'xml-mode) |
|
|
|
(goto-char (point-min)) |
|
|
|
(while (search-forward-regexp "\>[ \\t]*\<" nil t) |
|
|
|
(backward-char) (insert "\n")) |
|
|
|
(indent-region (point-min) (point-max))) |
|
|
|
|
|
|
|
((eq guessed-mode 'image-mode) |
|
|
|
(let* ((img (buffer-string))) |
|
|
|
(delete-region (point-min) (point-max)) |
|
|
|
(fundamental-mode) |
|
|
|
(insert-image (create-image img nil t)))) |
|
|
|
|
|
|
|
((eq guessed-mode 'js-mode) |
|
|
|
(let ((json-special-chars (remq (assoc ?/ json-special-chars) json-special-chars)) |
|
|
|
;; Emacs 27 json.el uses `replace-buffer-contents' for |
|
|
|
;; pretty-printing which is great because it keeps point and |
|
|
|
;; markers intact but can be very slow with huge minimalized |
|
|
|
;; JSON. We don't need that here. |
|
|
|
(json-pretty-print-max-secs 0)) |
|
|
|
(ignore-errors (json-pretty-print-buffer))) |
|
|
|
(leo/restclient-prettify-json-unicode))) |
|
|
|
|
|
|
|
(goto-char (point-max)) |
|
|
|
(or (eq (point) (point-min)) (insert "\n")) |
|
|
|
(let ((hstart (point))) |
|
|
|
(insert method " " url "\n" headers) |
|
|
|
(insert (format "Request duration: %fs\n" (float-time (time-subtract leo/restclient-request-time-end leo/restclient-request-time-start)))) |
|
|
|
(unless (member guessed-mode '(image-mode text-mode)) |
|
|
|
(comment-region hstart (point))))))))) |
|
|
|
|
|
|
|
(defun leo/restclient-prettify-json-unicode () |
|
|
|
(save-excursion |
|
|
|
(goto-char (point-min)) |
|
|
|
(while (re-search-forward "\\\\[Uu]\\([0-9a-fA-F]\\{4\\}\\)" nil t) |
|
|
|
(replace-match (char-to-string (decode-char 'ucs (string-to-number (match-string 1) 16))) t nil)))) |
|
|
|
|
|
|
|
(defun leo/restclient-http-handle-response (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 leo/restclient-within-call nil) |
|
|
|
(setq leo/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 (leo/restclient-decode-response |
|
|
|
(current-buffer) |
|
|
|
bufname |
|
|
|
leo/restclient-same-buffer-response) |
|
|
|
(run-hooks 'leo/restclient-response-received-hook) |
|
|
|
(unless raw |
|
|
|
(leo/restclient-prettify-response method url)) |
|
|
|
(buffer-enable-undo) |
|
|
|
(leo/restclient-response-mode) |
|
|
|
(run-hooks 'leo/restclient-response-loaded-hook) |
|
|
|
(if stay-in-window |
|
|
|
(display-buffer (current-buffer) t) |
|
|
|
(switch-to-buffer-other-window (current-buffer))))))) |
|
|
|
|
|
|
|
(defun leo/restclient-decode-response (raw-http-response-buffer target-buffer-name same-name) |
|
|
|
"Decode the HTTP response using the charset (encoding) specified in the Content-Type header. If no charset is specified, default to UTF-8." |
|
|
|
(let* ((charset-regexp "^Content-Type.*charset=\\([-A-Za-z0-9]+\\)") |
|
|
|
(image? (save-excursion |
|
|
|
(search-forward-regexp "^Content-Type.*[Ii]mage" nil t))) |
|
|
|
(encoding (if (save-excursion |
|
|
|
(search-forward-regexp charset-regexp nil t)) |
|
|
|
(intern (downcase (match-string 1))) |
|
|
|
'utf-8))) |
|
|
|
(if image? |
|
|
|
;; Dont' attempt to decode. Instead, just switch to the raw HTTP response buffer and |
|
|
|
;; rename it to target-buffer-name. |
|
|
|
(with-current-buffer raw-http-response-buffer |
|
|
|
;; We have to kill the target buffer if it exists, or `rename-buffer' |
|
|
|
;; will raise an error. |
|
|
|
(when (get-buffer target-buffer-name) |
|
|
|
(kill-buffer target-buffer-name)) |
|
|
|
(rename-buffer target-buffer-name) |
|
|
|
raw-http-response-buffer) |
|
|
|
;; Else, switch to the new, empty buffer that will contain the decoded HTTP |
|
|
|
;; response. Set its encoding, copy the content from the unencoded |
|
|
|
;; HTTP response buffer and decode. |
|
|
|
(let ((decoded-http-response-buffer |
|
|
|
(get-buffer-create |
|
|
|
(if same-name target-buffer-name (generate-new-buffer-name target-buffer-name))))) |
|
|
|
(with-current-buffer decoded-http-response-buffer |
|
|
|
(setq buffer-file-coding-system encoding) |
|
|
|
(save-excursion |
|
|
|
(erase-buffer) |
|
|
|
(insert-buffer-substring raw-http-response-buffer)) |
|
|
|
(kill-buffer raw-http-response-buffer) |
|
|
|
(condition-case nil |
|
|
|
(decode-coding-region (point-min) (point-max) encoding) |
|
|
|
(error |
|
|
|
(message (concat "Error when trying to decode http response with encoding: " |
|
|
|
(symbol-name encoding))))) |
|
|
|
decoded-http-response-buffer))))) |
|
|
|
|
|
|
|
(defun leo/restclient-current-min () |
|
|
|
(save-excursion |
|
|
|
(beginning-of-line) |
|
|
|
(if (looking-at leo/restclient-comment-start-regexp) |
|
|
|
(if (re-search-forward leo/restclient-comment-not-regexp (point-max) t) |
|
|
|
(point-at-bol) (point-max)) |
|
|
|
(if (re-search-backward leo/restclient-comment-start-regexp (point-min) t) |
|
|
|
(point-at-bol 2) |
|
|
|
(point-min))))) |
|
|
|
|
|
|
|
(defun leo/restclient-current-max () |
|
|
|
(save-excursion |
|
|
|
(if (re-search-forward leo/restclient-comment-start-regexp (point-max) t) |
|
|
|
(max (- (point-at-bol) 1) 1) |
|
|
|
(progn (goto-char (point-max)) |
|
|
|
(if (looking-at "^$") (- (point) 1) (point)))))) |
|
|
|
|
|
|
|
(defun leo/restclient-replace-all-in-string (replacements string) |
|
|
|
(if replacements |
|
|
|
(let ((current string) |
|
|
|
(pass leo/restclient-vars-max-passes) |
|
|
|
(continue t)) |
|
|
|
(while (and continue (> pass 0)) |
|
|
|
(setq pass (- pass 1)) |
|
|
|
(setq current (replace-regexp-in-string (regexp-opt (mapcar 'car replacements)) |
|
|
|
(lambda (key) |
|
|
|
(setq continue t) |
|
|
|
(cdr (assoc key replacements))) |
|
|
|
current t t))) |
|
|
|
current) |
|
|
|
string)) |
|
|
|
|
|
|
|
(defun leo/restclient-replace-all-in-header (replacements header) |
|
|
|
(cons (car header) |
|
|
|
(leo/restclient-replace-all-in-string replacements (cdr header)))) |
|
|
|
|
|
|
|
(defun leo/restclient-chop (text) |
|
|
|
(if text (replace-regexp-in-string "\n$" "" text) nil)) |
|
|
|
|
|
|
|
(defun leo/restclient-find-vars-before-point () |
|
|
|
(let ((vars nil) |
|
|
|
(bound (point))) |
|
|
|
(save-excursion |
|
|
|
(goto-char (point-min)) |
|
|
|
(while (search-forward-regexp leo/restclient-var-regexp bound t) |
|
|
|
(let ((name (match-string-no-properties 1)) |
|
|
|
(should-eval (> (length (match-string 2)) 0)) |
|
|
|
(value (or (leo/restclient-chop (match-string-no-properties 4)) (match-string-no-properties 3)))) |
|
|
|
(setq vars (cons (cons name (if should-eval (leo/restclient-eval-var value) value)) vars)))) |
|
|
|
(append leo/restclient-var-overrides vars)))) |
|
|
|
|
|
|
|
(defun leo/restclient-eval-var (string) |
|
|
|
(with-output-to-string (princ (eval (read string))))) |
|
|
|
|
|
|
|
(defun leo/restclient-make-header (&optional string) |
|
|
|
(cons (match-string-no-properties 1 string) |
|
|
|
(match-string-no-properties 2 string))) |
|
|
|
|
|
|
|
(defun leo/restclient-parse-headers (string) |
|
|
|
(let ((start 0) |
|
|
|
(headers '())) |
|
|
|
(while (string-match leo/restclient-header-regexp string start) |
|
|
|
(setq headers (cons (leo/restclient-make-header string) headers) |
|
|
|
start (match-end 0))) |
|
|
|
headers)) |
|
|
|
|
|
|
|
(defun leo/restclient-read-file (path) |
|
|
|
(with-temp-buffer |
|
|
|
(insert-file-contents path) |
|
|
|
(buffer-string))) |
|
|
|
|
|
|
|
(defun leo/restclient-parse-body (entity vars) |
|
|
|
(if (= 0 (or (string-match leo/restclient-file-regexp entity) 1)) |
|
|
|
(leo/restclient-read-file (match-string 1 entity)) |
|
|
|
(leo/restclient-replace-all-in-string vars entity))) |
|
|
|
|
|
|
|
(defun leo/restclient-parse-hook (cb-type args-offset args) |
|
|
|
(if-let ((handler (assoc cb-type leo/restclient-result-handlers))) |
|
|
|
(funcall (cadr handler) args args-offset) |
|
|
|
`(lambda () |
|
|
|
(message "Unknown leo/restclient hook type %s" ,cb-type)))) |
|
|
|
|
|
|
|
(defun leo/restclient-register-result-func (name creation-func description) |
|
|
|
(let ((new-cell (cons name (cons creation-func description)))) |
|
|
|
(setq leo/restclient-result-handlers (cons new-cell leo/restclient-result-handlers)))) |
|
|
|
|
|
|
|
(defun leo/restclient-remove-var (var-name) |
|
|
|
(setq leo/restclient-var-overrides (assoc-delete-all var-name leo/restclient-var-overrides))) |
|
|
|
|
|
|
|
(defun leo/restclient-set-var (var-name value) |
|
|
|
(leo/restclient-remove-var var-name) |
|
|
|
(setq leo/restclient-var-overrides (cons (cons var-name value) leo/restclient-var-overrides))) |
|
|
|
|
|
|
|
(defun leo/restclient-get-var-at-point (var-name buffer-name buffer-pos) |
|
|
|
(message (format "getting var %s form %s at %s" var-name buffer-name buffer-pos)) |
|
|
|
(let* ((vars-at-point (save-excursion |
|
|
|
(switch-to-buffer buffer-name) |
|
|
|
(goto-char buffer-pos) |
|
|
|
;; if we're called from a leo/restclient buffer we need to lookup vars before the current hook or evar |
|
|
|
;; outside a leo/restclient buffer only globals are available so moving the point wont matter |
|
|
|
(re-search-backward "^:\\|->" (point-min) t) |
|
|
|
(leo/restclient-find-vars-before-point)))) |
|
|
|
(leo/restclient-replace-all-in-string vars-at-point (cdr (assoc var-name vars-at-point))))) |
|
|
|
|
|
|
|
(defmacro leo/restclient-get-var (var-name) |
|
|
|
(lexical-let ((buf-name (buffer-name (current-buffer))) |
|
|
|
(buf-point (point))) |
|
|
|
`(leo/restclient-get-var-at-point ,var-name ,buf-name ,buf-point))) |
|
|
|
|
|
|
|
(defun leo/restclient-single-request-function () |
|
|
|
(dolist (f leo/restclient-curr-request-functions) |
|
|
|
(ignore-errors |
|
|
|
(funcall f))) |
|
|
|
(setq leo/restclient-curr-request-functions nil) |
|
|
|
(remove-hook 'leo/restclient-response-loaded-hook 'leo/restclient-single-request-function)) |
|
|
|
|
|
|
|
|
|
|
|
(defun leo/restclient-http-parse-current-and-do (func &rest args) |
|
|
|
(save-excursion |
|
|
|
(goto-char (leo/restclient-current-min)) |
|
|
|
(when (re-search-forward leo/restclient-method-url-regexp (point-max) t) |
|
|
|
(let ((method (match-string-no-properties 1)) |
|
|
|
(url (match-string-no-properties 2)) |
|
|
|
(vars (leo/restclient-find-vars-before-point)) |
|
|
|
(headers '())) |
|
|
|
(forward-line) |
|
|
|
(while (cond |
|
|
|
((looking-at leo/restclient-response-hook-regexp) |
|
|
|
(when-let (hook-function (leo/restclient-parse-hook (match-string-no-properties 2) |
|
|
|
(match-end 2) |
|
|
|
(match-string-no-properties 3))) |
|
|
|
(push hook-function leo/restclient-curr-request-functions))) |
|
|
|
((and (looking-at leo/restclient-header-regexp) (not (looking-at leo/restclient-empty-line-regexp))) |
|
|
|
(setq headers (cons (leo/restclient-replace-all-in-header vars (leo/restclient-make-header)) headers))) |
|
|
|
((looking-at leo/restclient-use-var-regexp) |
|
|
|
(setq headers (append headers (leo/restclient-parse-headers (leo/restclient-replace-all-in-string vars (match-string 1))))))) |
|
|
|
(forward-line)) |
|
|
|
(when (looking-at leo/restclient-empty-line-regexp) |
|
|
|
(forward-line)) |
|
|
|
(when leo/restclient-curr-request-functions |
|
|
|
(add-hook 'leo/restclient-response-loaded-hook 'leo/restclient-single-request-function)) |
|
|
|
(let* ((cmax (leo/restclient-current-max)) |
|
|
|
(entity (leo/restclient-parse-body (buffer-substring (min (point) cmax) cmax) vars)) |
|
|
|
(url (leo/restclient-replace-all-in-string vars url))) |
|
|
|
(apply func method url headers entity args)))))) |
|
|
|
|
|
|
|
(defun leo/restclient-copy-curl-command () |
|
|
|
"Formats the request as a curl command and copies the command to the clipboard." |
|
|
|
(interactive) |
|
|
|
(leo/restclient-http-parse-current-and-do |
|
|
|
'(lambda (method url headers entity) |
|
|
|
(let ((header-args |
|
|
|
(apply 'append |
|
|
|
(mapcar (lambda (header) |
|
|
|
(list "-H" (format "%s: %s" (car header) (cdr header)))) |
|
|
|
headers)))) |
|
|
|
(kill-new (concat "curl " |
|
|
|
(mapconcat 'shell-quote-argument |
|
|
|
(append '("-i") |
|
|
|
header-args |
|
|
|
(list (concat "-X" method)) |
|
|
|
(list url) |
|
|
|
(when (> (string-width entity) 0) |
|
|
|
(list "-d" entity))) |
|
|
|
" ")))) |
|
|
|
(message "curl command copied to clipboard.")))) |
|
|
|
|
|
|
|
|
|
|
|
(defun leo/restclient-elisp-result-function (args offset) |
|
|
|
(goto-char offset) |
|
|
|
(lexical-let ((form (macroexpand-all (read (current-buffer))))) |
|
|
|
(lambda () |
|
|
|
(eval form)))) |
|
|
|
|
|
|
|
(leo/restclient-register-result-func |
|
|
|
"run-hook" #'leo/restclient-elisp-result-function |
|
|
|
"Call the provided (possibly multi-line) elisp when the result |
|
|
|
buffer is formatted. Equivalent to a leo/restclient-response-loaded-hook |
|
|
|
that only runs for this request. |
|
|
|
eg. -> on-response (message \"my hook called\")" ) |
|
|
|
|
|
|
|
;;;###autoload |
|
|
|
(defun leo/restclient-http-send-current (&optional raw stay-in-window) |
|
|
|
"Sends current request. |
|
|
|
Optional argument RAW don't reformat response if t. |
|
|
|
Optional argument STAY-IN-WINDOW do not move focus to response buffer if t." |
|
|
|
(interactive) |
|
|
|
(leo/restclient-http-parse-current-and-do 'leo/restclient-http-do raw stay-in-window)) |
|
|
|
|
|
|
|
;;;###autoload |
|
|
|
(defun leo/restclient-http-send-current-raw () |
|
|
|
"Sends current request and get raw result (no reformatting or syntax highlight of XML, JSON or images)." |
|
|
|
(interactive) |
|
|
|
(leo/restclient-http-send-current t)) |
|
|
|
|
|
|
|
;;;###autoload |
|
|
|
(defun leo/restclient-http-send-current-stay-in-window () |
|
|
|
"Send current request and keep focus in request window." |
|
|
|
(interactive) |
|
|
|
(leo/restclient-http-send-current nil t)) |
|
|
|
|
|
|
|
(defun leo/restclient-jump-next () |
|
|
|
"Jump to next request in buffer." |
|
|
|
(interactive) |
|
|
|
(let ((last-min nil)) |
|
|
|
(while (not (eq last-min (goto-char (leo/restclient-current-min)))) |
|
|
|
(goto-char (leo/restclient-current-min)) |
|
|
|
(setq last-min (point)))) |
|
|
|
(goto-char (+ (leo/restclient-current-max) 1)) |
|
|
|
(goto-char (leo/restclient-current-min))) |
|
|
|
|
|
|
|
(defun leo/restclient-jump-prev () |
|
|
|
"Jump to previous request in buffer." |
|
|
|
(interactive) |
|
|
|
(let* ((current-min (leo/restclient-current-min)) |
|
|
|
(end-of-entity |
|
|
|
(save-excursion |
|
|
|
(progn (goto-char (leo/restclient-current-min)) |
|
|
|
(while (and (or (looking-at "^\s*\\(#.*\\)?$") |
|
|
|
(eq (point) current-min)) |
|
|
|
(not (eq (point) (point-min)))) |
|
|
|
(forward-line -1) |
|
|
|
(beginning-of-line)) |
|
|
|
(point))))) |
|
|
|
(unless (eq (point-min) end-of-entity) |
|
|
|
(goto-char end-of-entity) |
|
|
|
(goto-char (leo/restclient-current-min))))) |
|
|
|
|
|
|
|
(defun leo/restclient-mark-current () |
|
|
|
"Mark current request." |
|
|
|
(interactive) |
|
|
|
(goto-char (leo/restclient-current-min)) |
|
|
|
(set-mark-command nil) |
|
|
|
(goto-char (leo/restclient-current-max)) |
|
|
|
(backward-char 1) |
|
|
|
(setq deactivate-mark nil)) |
|
|
|
|
|
|
|
(defun leo/restclient-show-info () |
|
|
|
;; leo/restclient-info-buffer-name |
|
|
|
(interactive) |
|
|
|
(let ((vars-at-point (leo/restclient-find-vars-before-point))) |
|
|
|
(cl-labels ((non-overidden-vars-at-point () |
|
|
|
(seq-filter (lambda (v) |
|
|
|
(null (assoc (car v) leo/restclient-var-overrides))) |
|
|
|
vars-at-point)) |
|
|
|
(sanitize-value-cell (var-value) |
|
|
|
(replace-regexp-in-string "\n" "|\n| |" |
|
|
|
(replace-regexp-in-string "\|" "\\\\vert{}" |
|
|
|
(leo/restclient-replace-all-in-string vars-at-point var-value)))) |
|
|
|
(var-row (var-name var-value) |
|
|
|
(insert "|" var-name "|" (sanitize-value-cell var-value) "|\n")) |
|
|
|
(var-table (table-name) |
|
|
|
(insert (format "* %s \n|--|\n|Name|Value|\n|---|\n" table-name))) |
|
|
|
(var-table-footer () |
|
|
|
(insert "|--|\n\n"))) |
|
|
|
|
|
|
|
(with-current-buffer (get-buffer-create leo/restclient-info-buffer-name) |
|
|
|
;; insert our info |
|
|
|
(erase-buffer) |
|
|
|
|
|
|
|
(insert "\Leo/Restclient Info\ \n\n") |
|
|
|
|
|
|
|
(var-table "Dynamic Variables") |
|
|
|
(dolist (dv leo/restclient-var-overrides) |
|
|
|
(var-row (car dv) (cdr dv))) |
|
|
|
(var-table-footer) |
|
|
|
|
|
|
|
;; (insert ":Info:\n Dynamic vars defined by request hooks or with calls to leo/restclient-set-var\n:END:") |
|
|
|
|
|
|
|
(var-table "Vars at current position") |
|
|
|
(dolist (dv (non-overidden-vars-at-point)) |
|
|
|
(var-row (car dv) (cdr dv))) |
|
|
|
(var-table-footer) |
|
|
|
|
|
|
|
|
|
|
|
;; registered callbacks |
|
|
|
(var-table "Registered request hook types") |
|
|
|
(dolist (handler-name (delete-dups (mapcar 'car leo/restclient-result-handlers))) |
|
|
|
(var-row handler-name (cddr (assoc handler-name leo/restclient-result-handlers)))) |
|
|
|
(var-table-footer) |
|
|
|
|
|
|
|
(insert "\n\n'q' to exit\n") |
|
|
|
(org-mode) |
|
|
|
(org-toggle-pretty-entities) |
|
|
|
(org-table-iterate-buffer-tables) |
|
|
|
(outline-show-all) |
|
|
|
(leo/restclient-response-mode) |
|
|
|
(goto-char (point-min)))) |
|
|
|
(switch-to-buffer-other-window leo/restclient-info-buffer-name))) |
|
|
|
|
|
|
|
(defun leo/restclient-narrow-to-current () |
|
|
|
"Narrow to region of current request" |
|
|
|
(interactive) |
|
|
|
(narrow-to-region (leo/restclient-current-min) (leo/restclient-current-max))) |
|
|
|
|
|
|
|
(defun leo/restclient-toggle-body-visibility () |
|
|
|
(interactive) |
|
|
|
;; If we are not on the HTTP call line, don't do anything |
|
|
|
(let ((at-header (save-excursion |
|
|
|
(beginning-of-line) |
|
|
|
(looking-at leo/restclient-method-url-regexp)))) |
|
|
|
(when at-header |
|
|
|
(save-excursion |
|
|
|
(end-of-line) |
|
|
|
;; If the overlays at this point have 'invisible set, toggling |
|
|
|
;; must make the region visible. Else it must hide the region |
|
|
|
|
|
|
|
;; This part of code is from org-hide-block-toggle method of |
|
|
|
;; Org mode |
|
|
|
(let ((overlays (overlays-at (point)))) |
|
|
|
(if (memq t (mapcar |
|
|
|
(lambda (o) |
|
|
|
(eq (overlay-get o 'invisible) 'outline)) |
|
|
|
overlays)) |
|
|
|
(outline-flag-region (point) (leo/restclient-current-max) nil) |
|
|
|
(outline-flag-region (point) (leo/restclient-current-max) t)))) t))) |
|
|
|
|
|
|
|
(defun leo/restclient-toggle-body-visibility-or-indent () |
|
|
|
(interactive) |
|
|
|
(unless (leo/restclient-toggle-body-visibility) |
|
|
|
(indent-for-tab-command))) |
|
|
|
|
|
|
|
(defconst leo/restclient-mode-keywords |
|
|
|
(list (list leo/restclient-method-url-regexp '(1 'leo/restclient-method-face) '(2 'leo/restclient-url-face)) |
|
|
|
(list leo/restclient-svar-regexp '(1 'leo/restclient-variable-name-face) '(2 'leo/restclient-variable-string-face)) |
|
|
|
(list leo/restclient-evar-regexp '(1 'leo/restclient-variable-name-face) '(2 'leo/restclient-variable-elisp-face t)) |
|
|
|
(list leo/restclient-mvar-regexp '(1 'leo/restclient-variable-name-face) '(2 'leo/restclient-variable-multiline-face t)) |
|
|
|
(list leo/restclient-use-var-regexp '(1 'leo/restclient-variable-usage-face)) |
|
|
|
(list leo/restclient-file-regexp '(0 'leo/restclient-file-upload-face)) |
|
|
|
(list leo/restclient-header-regexp '(1 'leo/restclient-header-name-face t) '(2 'leo/restclient-header-value-face t)) |
|
|
|
(list leo/restclient-response-hook-regexp '(1 ' leo/restclient-request-hook-face t) |
|
|
|
'(2 'leo/restclient-request-hook-name-face t) |
|
|
|
'(3 'leo/restclient-request-hook-args-face t)))) |
|
|
|
|
|
|
|
(defconst leo/restclient-mode-syntax-table |
|
|
|
(let ((table (make-syntax-table))) |
|
|
|
(modify-syntax-entry ?\# "<" table) |
|
|
|
(modify-syntax-entry ?\n ">#" table) |
|
|
|
table)) |
|
|
|
|
|
|
|
(defvar leo/restclient-mode-map |
|
|
|
(let ((map (make-sparse-keymap))) |
|
|
|
(define-key map (kbd "C-c C-c") 'leo/restclient-http-send-current) |
|
|
|
(define-key map (kbd "C-c C-r") 'leo/restclient-http-send-current-raw) |
|
|
|
(define-key map (kbd "C-c C-v") 'leo/restclient-http-send-current-stay-in-window) |
|
|
|
(define-key map (kbd "C-c C-n") 'leo/restclient-jump-next) |
|
|
|
(define-key map (kbd "C-c C-p") 'leo/restclient-jump-prev) |
|
|
|
(define-key map (kbd "C-c C-.") 'leo/restclient-mark-current) |
|
|
|
(define-key map (kbd "C-c C-u") 'leo/restclient-copy-curl-command) |
|
|
|
(define-key map (kbd "C-c n n") 'leo/restclient-narrow-to-current) |
|
|
|
(define-key map (kbd "C-c C-i") 'leo/restclient-show-info) |
|
|
|
map) |
|
|
|
"Keymap for leo/restclient-mode.") |
|
|
|
|
|
|
|
(define-minor-mode leo/restclient-outline-mode |
|
|
|
"Minor mode to allow show/hide of request bodies by TAB." |
|
|
|
:init-value nil |
|
|
|
:lighter nil |
|
|
|
:keymap '(("\t" . leo/restclient-toggle-body-visibility-or-indent) |
|
|
|
("\C-c\C-a" . leo/restclient-toggle-body-visibility-or-indent)) |
|
|
|
:group 'leo/restclient) |
|
|
|
|
|
|
|
(define-minor-mode leo/restclient-response-mode |
|
|
|
"Minor mode to allow additional keybindings in leo/restclient response buffer." |
|
|
|
:init-value nil |
|
|
|
:lighter nil |
|
|
|
:keymap '(("q" . (lambda () |
|
|
|
(interactive) |
|
|
|
(quit-window (get-buffer-window (current-buffer)))))) |
|
|
|
:group 'leo/restclient) |
|
|
|
|
|
|
|
;;;###autoload |
|
|
|
(define-derived-mode leo/restclient-mode fundamental-mode "REST Client" |
|
|
|
"Turn on leo/restclient mode." |
|
|
|
(set (make-local-variable 'comment-start) "# ") |
|
|
|
(set (make-local-variable 'comment-start-skip) "# *") |
|
|
|
(set (make-local-variable 'comment-column) 48) |
|
|
|
|
|
|
|
(set (make-local-variable 'font-lock-defaults) '(leo/restclient-mode-keywords)) |
|
|
|
;; We use outline-mode's method outline-flag-region to hide/show the |
|
|
|
;; body. As a part of it, it sets 'invisibility text property to |
|
|
|
;; 'outline. To get ellipsis, we need 'outline to be in |
|
|
|
;; buffer-invisibility-spec |
|
|
|
(add-to-invisibility-spec '(outline . t))) |
|
|
|
|
|
|
|
(add-hook 'leo/restclient-mode-hook 'leo/restclient-outline-mode) |
|
|
|
|
|
|
|
(provide 'leo/restclient) |
|
|
|
|
|
|
|
;; (eval-after-load 'helm |
|
|
|
;; '(ignore-errors (require 'leo/restclient-helm))) |
|
|
|
|
|
|
|
;; (eval-after-load 'jq-mode |
|
|
|
;; '(ignore-errors (require 'leo/restclient-jq))) |
|
|
|
|
|
|
|
(provide 'leo/restclient) |
|
|
|
;;; leo/restclient.el ends here |