forked from pashky/restclient.el
-
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathrestclient.el
More file actions
1328 lines (1164 loc) · 56 KB
/
restclient.el
File metadata and controls
1328 lines (1164 loc) · 56 KB
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; restclient.el --- An interactive HTTP client for Emacs -*- lexical-binding: t; -*-
;;
;; Public domain.
;; Author: Pavel Kurnosov <pashky@gmail.com>
;; Maintainer: Peder O. Klingenberg <peder@klingenberg.no>
;; Created: 01 Apr 2012
;; Keywords: http comm tools
;; URL: https://github.com/emacsorphanage/restclient
;; Package-Requires: ((emacs "26.1") (compat "30.1.0.0"))
;; Version: 1.0
;; 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)
(require 'view)
(require 'compat)
(eval-when-compile (require 'subr-x))
(eval-when-compile
(if (version< emacs-version "26")
(require 'cl)
(require 'cl-lib)))
(eval-when-compile
(unless (functionp 'hash-table-contains-p)
(let ((missing (make-symbol "missing")))
(defsubst hash-table-contains-p (key table)
"Return non-nil if TABLE has an element with KEY."
(declare (side-effect-free t))
(not (eq (gethash key table missing) missing))))))
(defgroup restclient nil
"An interactive HTTP client for Emacs."
:group 'tools)
(defcustom restclient-log-request t
"Log restclient requests to *Messages*."
:group 'restclient
:type 'boolean)
(defcustom restclient-same-buffer-response t
"Re-use same buffer for responses or create a new one each time."
:group 'restclient
:type 'boolean)
(defcustom restclient-same-buffer-response-name "*HTTP Response*"
"Name for response buffer."
:group 'restclient
:type 'string)
(defcustom restclient-response-size-threshold 100000
"Size of the response restclient can display without performance impact."
:group 'restclient
:type 'integer)
(defvar restclient-threshold-multiplier 10
"In how many times size-threshold should be exceed to use fundamental mode.")
(defcustom restclient-info-buffer-name "*Restclient Info*"
"Name for info buffer."
:group 'restclient
:type 'string)
(defcustom restclient-inhibit-cookies nil
"Inhibit restclient from sending cookies implicitly."
:group 'restclient
:type 'boolean)
(defcustom 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 'restclient
:type '(alist :key-type string :value-type symbol))
(defcustom restclient-response-body-only nil
"When parsing response, only return its body."
:group 'restclient
:type 'boolean)
(defcustom 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 'restclient
:type 'integer)
(defcustom restclient-user-agent nil
"User Agent used in the requests.
Passed to `url-user-agent'. See that variable for valid values.
Default is nil, to allow requests to set User-Agent as a header."
:group 'restclient
:type '(choice
(string :tag "A static User-Agent string")
(function :tag "Call a function to get the User-Agent string")
(const :tag "No User-Agent at all" :value nil)
(const :tag "An string auto-generated according to `url-privacy-level'"
:value default)))
(defcustom restclient-query-use-continuation-lines nil
"Whether to allow request parameters to span multiple lines.
Default is nil, query parameters must be part of the single line URL in the
request, as the HTTP requires. If non-nil, continuation lines must directly
follow the initial request line, indented by whitespace.
The value of this parameter also determines how the continuation lines
are interpreted. Valid values are:
* nil - Do not allow continuation lines (default).
* `literal' - Append each continuation line to the query literally.
* `smart' - Each continuation line is interpreted as a key/value pair,
separated by =. Both keys and values are passed through
`url-hexify-string' before being appended to the query.
Separators between parameters are added automatically."
:group 'restclient
:type '(choice
(const :tag "Do not allow continuation lines.")
(const :tag "Append continuation lines literally."
:value literal)
(const :tag "Continuation liens are key/value pairs."
:value smart)))
(defcustom restclient-follow-redirects t
"Whether restclient follows redirects.
If t, the default, restclient will follow up to `url-max-redirections'
links in the redirection chain before giving up. If nil, no
redirections will be followed. If an integer, `url-max-redirections'
will be temporarily set to that number."
:group 'restclient
:type '(choice
(const :tag "Follow redirects" :value t)
(const :tag "Do not follow redirects" :value nil)
(integer :tag "Follow this many redirects")))
(defcustom restclient-results-in-view-mode t
"Determines if the response buffer should be put in view-mode or left
editable."
:group 'restclient
:type 'boolean)
(defcustom restclient-enable-eval nil
"Whether eval features are enabled.
Do not set to non-nil when working with untrusted documents."
:group 'restclient
:type 'boolean)
(defcustom restclient-strip-http-version nil
"Whether or not to strip \"HTTP/x.y\" from request lines.
`url.el' will automatically append a HTTP version clause,
but many .http files specify these in the request lines.
If this variable is non-nil, restclient will strip the clause
from the end of the request before passing it to `url.el'."
:group 'restclient
:type 'boolean)
(defgroup restclient-faces nil
"Faces used in Restclient Mode."
:group 'restclient
:group 'faces)
(defface restclient-variable-name-face
'((t (:inherit font-lock-preprocessor-face)))
"Face for variable name."
:group 'restclient-faces)
(defface restclient-variable-string-face
'((t (:inherit font-lock-string-face)))
"Face for variable value (string)."
:group 'restclient-faces)
(defface restclient-variable-elisp-face
'((t (:inherit font-lock-function-name-face)))
"Face for variable value (Emacs Lisp)."
:group 'restclient-faces)
(defface restclient-variable-multiline-face
'((t (:inherit font-lock-doc-face)))
"Face for multi-line variable value marker."
:group 'restclient-faces)
(defface restclient-variable-usage-face
'((t (:inherit 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 'restclient-faces)
(defface restclient-method-face
'((t (:inherit font-lock-keyword-face)))
"Face for HTTP method."
:group 'restclient-faces)
(defface restclient-url-face
'((t (:inherit font-lock-function-name-face)))
"Face for variable value (Emacs Lisp)."
:group 'restclient-faces)
(defface restclient-file-upload-face
'((t (:inherit restclient-variable-multiline-face)))
"Face for highlighting upload file paths."
:group 'restclient-faces)
(defface restclient-header-name-face
'((t (:inherit font-lock-variable-name-face)))
"Face for HTTP header name."
:group 'restclient-faces)
(defface restclient-header-value-face
'((t (:inherit font-lock-string-face)))
"Face for HTTP header value."
:group 'restclient-faces)
(defface restclient-request-hook-face
'((t (:inherit font-lock-preprocessor-face)))
"Face for single request hook indicator."
:group 'restclient-faces)
(defface restclient-request-hook-name-face
'((t (:inherit font-lock-function-name-face)))
"Face for single request hook type names."
:group 'restclient-faces)
(defface restclient-request-hook-args-face
'((t (:inherit font-lock-string-face)))
"Face for single request hook type arguments."
:group 'restclient-faces)
(defvar restclient-within-call nil)
(defvar restclient-request-time-start nil)
(defvar restclient-request-time-end nil)
(defvar 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 restclient-var-defaults nil
"An alist of fallback values for vars not defined elsewhere.")
(defvar restclient-current-env-file nil
"The file containing current environment definitions.")
(defvar restclient-current-env-name nil
"Current environment name, defined in `restclient-current-env-file'.")
(defvar restclient-result-handlers '()
"A registry of available completion hooks.
Stored as an alist of name -> (hook-creation-func . description)")
(defvar restclient-curr-request-functions nil
"A list of functions to run once when the next request is loaded.")
(defvar restclient-response-loaded-hook nil
"Hook run after response buffer is formatted.")
(defvar restclient-http-do-hook nil
"Hook to run before making request.")
(defvar restclient-response-received-hook nil
"Hook run after data is loaded into response buffer.")
(defvar restclient-current-request-marker (make-marker)
"Marker keeping track of the last executed request.")
(defvar-local restclient--header-start-position nil
"Position in the buffer where headers start.")
(defconst restclient-comment-separator "#")
(defconst restclient-comment-start-regexp (concat "^" restclient-comment-separator))
(defconst restclient-comment-not-regexp (concat "^[^" restclient-comment-separator "]"))
(defconst restclient-empty-line-regexp "^\\s-*$")
(defconst restclient-method-url-regexp
"^[[:blank:]]*\\(GET\\|POST\\|DELETE\\|PUT\\|HEAD\\|OPTIONS\\|PATCH\\|PROPFIND\\) \\(.*\\)$")
(defconst restclient-url-continuation-line-regexp
"^[[:blank:]]+\\(.*\\)$")
(defconst restclient-url-continuation-key-value-regexp
"\\([^= ]+\\)\\s-*=\\s-*\\(.*\\)$")
(defconst restclient-url-version-regexp "[[:blank:]]+HTTP/[0-9]+\\.[0-9]+$")
(defconst restclient-method-body-prohibited-regexp
"^GET\\|HEAD$")
(defconst restclient-header-regexp
"^\\([^](),/:;@[\\{}= \t]+\\): \\(.*\\)$")
(defconst restclient-use-var-regexp
"^:\\([^: \n]+\\)\\|{{\\([^} \n]+\\)}}$")
(defconst restclient-var-regexp
(concat "^\\(?::\\([^:= ]+\\)\\|@\\([^:= ]+\\)\\)[ \t]*\\(:?\\)=[ \t]*\\(<<[ \t]*\n\\(\\(.*\n\\)*?\\)" restclient-comment-separator "\\|\\([^<].*\\)$\\)"))
(defconst restclient-svar-regexp
"^\\(:[^:= ]+\\)[ \t]*=[ \t]*\\(.+?\\)$")
(defconst restclient-evar-regexp
"^\\(:[^: ]+\\)[ \t]*:=[ \t]*\\(.+?\\)$")
(defconst restclient-mvar-regexp
"^\\(:[^: ]+\\)[ \t]*:?=[ \t]*\\(<<\\)[ \t]*$")
(defconst restclient-file-regexp
"^<[ \t]*\\([^<>\n\r]+\\)[ \t]*$")
(defconst restclient-content-type-regexp
"^Content-[Tt]ype: \\(\\w+\\)/\\(?:[^\\+\r\n]*\\+\\)*\\([^;\r\n]+\\)")
(defconst 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.
(define-advice url-http-handle-authentication (:around (orig &rest args) restclient-disable-auth)
"Disable interactive request for username/password."
(if restclient-within-call
t ;; Means authorization failed.
(apply orig args)))
(define-advice url-cache-extract (:around (orig &rest args) restclient-disable-cache)
"Disable cache."
(unless restclient-within-call
(apply orig args)))
(defvar restclient--globals-stack
(make-hash-table))
(defmacro restclient--push-global-var (var new-val)
"Save current value of VAR, and set VAR to NEW-VAL.
Workaround for Emacs bug#61916"
`(progn
(push ,var (gethash ',var restclient--globals-stack ()))
(setq-default ,var ,new-val)))
(defmacro restclient--pop-global-var (var)
"Restore old global value of VAR, if any."
`(when (and (hash-table-contains-p ',var restclient--globals-stack)
(< 0 (length (gethash ',var restclient--globals-stack))))
(setq-default ,var (pop (gethash ',var restclient--globals-stack)))))
(defun restclient-http-do (method url headers entity &rest handle-args)
"Send ENTITY and HEADERS to URL as a METHOD request."
(if 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 (if (string-match restclient-method-body-prohibited-regexp method)
nil
(encode-coding-string entity 'utf-8))))
(restclient--push-global-var url-mime-charset-string (url-mime-charset-string))
(restclient--push-global-var url-mime-language-string nil)
(restclient--push-global-var url-mime-encoding-string nil)
(restclient--push-global-var url-mime-accept-string nil)
(restclient--push-global-var url-user-agent restclient-user-agent)
(restclient--push-global-var url-max-redirections
(cond
((not restclient-follow-redirects)
0)
((integerp restclient-follow-redirects)
restclient-follow-redirects)
(t
url-max-redirections)))
(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 restclient-within-call t)
(setq restclient-request-time-start (current-time))
(run-hooks 'restclient-http-do-hook)
(url-retrieve url 'restclient-http-handle-response
(append (list method url (if restclient-same-buffer-response
restclient-same-buffer-response-name
(format "*HTTP %s %s*" method url)))
handle-args)
nil restclient-inhibit-cookies)))
(defun restclient--preferred-mode (content-type)
"Look up the user's preferred mode for handling content of type CONTENT-TYPE.
The user defines their preferences in `restclient-content-type-modes'."
(cdr (assoc-string content-type restclient-content-type-modes t)))
(defun restclient-prettify-response (method url status)
"Format the result of the API call in a pleasing way.
METHOD, URL and STATUS are displayed along with the response headers."
(save-excursion
(let ((start (point))
(guessed-mode)
(end-of-headers))
(while (and (not (looking-at restclient-empty-line-regexp))
(eq (progn
(when (looking-at restclient-content-type-regexp)
(setq guessed-mode
(restclient--preferred-mode (concat
(match-string-no-properties 1)
"/"
(match-string-no-properties 2)))))
(forward-line))
0)))
(setq end-of-headers (point))
(while (and (looking-at restclient-empty-line-regexp)
(eq (forward-line) 0)))
(unless guessed-mode
(setq guessed-mode
(or (assoc-default nil
;; magic mode matches
`(("<\\?xml " . ,(restclient--preferred-mode "application/xml"))
("{\\s-*\"" . ,(restclient--preferred-mode "application/json")))
(lambda (re _dummy)
(looking-at re)))
(restclient--preferred-mode "application/json"))))
(let ((headers (buffer-substring-no-properties start end-of-headers)))
(when guessed-mode
(delete-region start (point))
(unless (eq guessed-mode (restclient--preferred-mode "image/png"))
(cond ((and restclient-response-size-threshold
(> (buffer-size) (* restclient-response-size-threshold
restclient-threshold-multiplier)))
(fundamental-mode)
(setq comment-start (let ((guessed-mode guessed-mode))
(with-temp-buffer
(apply guessed-mode '())
comment-start)))
(message
"Response is too huge, using fundamental-mode to display it!"))
((and restclient-response-size-threshold
(> (buffer-size) restclient-response-size-threshold))
(delay-mode-hooks (apply guessed-mode '()))
(message
"Response is too big, using bare %s to display it!" guessed-mode))
(t
(apply guessed-mode '())))
(if (fboundp 'font-lock-flush)
(font-lock-flush)
(with-no-warnings
(font-lock-fontify-buffer))))
(cond
((eq guessed-mode (restclient--preferred-mode "application/xml"))
(goto-char (point-min))
(while (search-forward-regexp "\>[ \\t]*\<" nil t)
(backward-char) (insert "\n"))
(indent-region (point-min) (point-max)))
((eq guessed-mode (restclient--preferred-mode "image/png"))
(let* ((img (buffer-string)))
(delete-region (point-min) (point-max))
(fundamental-mode)
(insert-image (create-image img nil t))))
((eq guessed-mode (restclient--preferred-mode "application/json"))
(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 minimized
;; JSON. We don't need that here.
(json-pretty-print-max-secs 0))
(ignore-errors (json-pretty-print-buffer)))
(restclient-prettify-json-unicode)))
(goto-char (point-max))
(or (eq (point) (point-min)) (insert "\n"))
(unless restclient-response-body-only
(let ((hstart (point)))
(setq restclient--header-start-position hstart)
(insert method " " url "\n")
(cl-loop for (data event) on (reverse status) by #'cddr
when (eq event :redirect)
do (insert "Redirect: " data "\n"))
(insert headers)
(insert (format "Request duration: %fs\n" (float-time (time-subtract restclient-request-time-end restclient-request-time-start))))
(unless (member guessed-mode (list (restclient--preferred-mode "image/png")
(restclient--preferred-mode "text/plain")))
(comment-region hstart (point))))))))))
(defun restclient-prettify-json-unicode ()
"Convert hex representations of unicode to characters."
(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 restclient-http-handle-response (status method url bufname raw stay-in-window suppress-response-buffer)
"Switch to the buffer returned by `url-retrieve'.
The buffer contains the raw HTTP response sent by the server.
STATUS: http status of the response.
METHOD: http method of the request.
URL: url of the request.
BUFNAME; the name of the buffer in which to display results
RAW: if non-nil, the raw response will be displayed, instead of a pretty-printed
version.
STAY-IN-WINDOW: if non-nil, do not switch to the output buffer, only show it.
SUPPRESS-RESPONSE-BUFFER: do not show the reponse at all."
(setq restclient-within-call nil)
(setq restclient-request-time-end (current-time))
(restclient--pop-global-var url-mime-charset-string)
(restclient--pop-global-var url-mime-language-string)
(restclient--pop-global-var url-mime-encoding-string)
(restclient--pop-global-var url-mime-accept-string)
(restclient--pop-global-var url-user-agent)
(restclient--pop-global-var url-max-redirections)
(if (= (point-min) (point-max))
(let ((error-status (plist-get status :error)))
(if error-status
(error (format "%s: %s" (car error-status) (cdr error-status)))
(error "Empty response from server")))
(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 status))
(buffer-enable-undo)
(when restclient-results-in-view-mode
(view-mode-enter))
(restclient--setup-response-buffer-map)
(run-hooks 'restclient-response-loaded-hook)
(unless suppress-response-buffer
(if stay-in-window
(display-buffer (current-buffer) t)
(switch-to-buffer-other-window (current-buffer)))))
(message "")))) ;; Request complete, remove the "Contacting host"-message from url-http
(defun restclient-decode-response (raw-http-response-buffer target-buffer-name same-name)
"Decode the HTTP response.
Use the charset (encoding) specified in the Content-Type header. If no
charset is specified, default to UTF-8.
RAW-HTTP-RESPONSE-BUFFER: the buffer where the URL library has deposited the
reponse.
TARGET-BUFFER-NAME: the name of the buffer into which we will place the decoded
result.
SAME-NAME: if non-nil, reuse the target buffer if it exists, otherwise generate
a fresh buffer."
(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?
;; Don't 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
(view-mode-exit t)
(setq buffer-file-coding-system encoding)
(setq restclient--header-start-position (point-min))
(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 restclient-current-min ()
"Return the position of the start of the current request."
(save-excursion
(beginning-of-line)
(if (looking-at restclient-comment-start-regexp)
(if (re-search-forward restclient-comment-not-regexp (point-max) t)
(line-beginning-position) (point-max))
(if (re-search-backward restclient-comment-start-regexp (point-min) t)
(line-beginning-position 2)
(point-min)))))
(defun restclient-current-max ()
"Return the position of the end of the current request."
(save-excursion
(if (re-search-forward restclient-comment-start-regexp (point-max) t)
(max (- (line-beginning-position) 1) 1)
(progn (goto-char (point-max))
(if (looking-at "^$") (- (point) 1) (point))))))
(defun restclient-replace-all-in-string (replacements string)
"Replace variables in STRING.
REPLACEMENTS is an alist containing the current variable values.
Return a string with variables replaced with their values, possibly recursively."
(if replacements
(let ((current string)
(prev nil)
(regexp (regexp-opt (append
(mapcar #'(lambda (r)
(format ":%s" (car r)))
replacements)
(mapcar #'(lambda (r)
(format "{{%s}}" (car r)))
replacements))))
(pass restclient-vars-max-passes)
(continue t))
(while (and continue (> pass 0))
(setq pass (- pass 1))
(setq prev current)
(setq current (replace-regexp-in-string
regexp
(lambda (key)
(setq key (restclient-sanitize-var-name key))
(cdr (assoc key replacements)))
current t t))
(setq continue (not (equal prev current))))
current)
string))
(defun restclient-replace-all-in-header (replacements header)
"Calls `restclient-replace-all-in-string' on a header value.
REPLACEMENTS is an alist containing the current variable values.
HEADER is an alist element (<header-name> . <value>).
Returns a new alist elements with the same header name, and a variable-expanded
value."
(cons (car header)
(restclient-replace-all-in-string replacements (cdr header))))
(defun restclient-chop (text)
"Remove newline at the end of TEXT, if any."
(if text (replace-regexp-in-string "\n$" "" text) nil))
(defun restclient-set-env (env-file env-name)
"Define variables for the current environment.
ENV-FILE is a json file as defined in
https://learn.microsoft.com/en-us/aspnet/core/test/http-files?view=aspnetcore-9.0#environment-files.
Alternatively, a VS Code settings file with the environments defined under the
key rest-client.environmentVariables is acceptable.
ENV-NAME is the name of a specific environment defined in ENV-FILE.
The special environment name `$shared' will always load in addition to the
requested env, with lower priority."
(interactive (let* ((default-dir (when restclient-current-env-file
(file-name-directory restclient-current-env-file)))
(default-file (when restclient-current-env-file
(file-name-nondirectory restclient-current-env-file)))
(filename
(read-file-name "Environment file name: "
default-dir default-file t))
(envs (mapcar #'car (restclient-parse-env-file filename))))
(list filename (completing-read "Environment name: " envs nil t))))
(setq restclient-current-env-file env-file)
(setq restclient-current-env-name env-name)
(restclient-reload-current-env))
(defun restclient-parse-env-file (filename)
"Read environments from FILENAME.
Environments contain sets of variable definitions. A file can contain multiple
environment definitions."
(let* ((json-key-type 'string)
(envs (json-read-file filename)))
(when (assoc "rest-client.environmentVariables" envs)
(setq envs (cdr (assoc "rest-client.environmentVariables" envs))))
envs))
(defun restclient-reload-current-env ()
"Refresh variable definitions from current environment definition."
(interactive)
(when (and restclient-current-env-file restclient-current-env-name)
(let* ((envs (restclient-parse-env-file restclient-current-env-file))
(shared-name "$shared")
(nonshared (when (not (equal restclient-current-env-name shared-name))
(cdr (assoc restclient-current-env-name envs)))))
(setq restclient-var-defaults
(append nonshared
(cdr (assoc shared-name envs)))))
(message "Environment \"%s\" loaded" restclient-current-env-name)))
(defun restclient-find-vars-before-point ()
"Determine which variables are valid at the current position."
(let ((vars nil)
(bound (point)))
(save-match-data
(save-excursion
(goto-char (point-min))
(while (search-forward-regexp restclient-var-regexp bound t)
(let ((name (or (match-string-no-properties 1)
(match-string-no-properties 2)))
(should-eval (> (length (match-string 3)) 0))
(value (or (restclient-chop (match-string-no-properties 5)) (match-string-no-properties 4))))
(setq value (cond
((not should-eval)
value)
((not restclient-enable-eval)
(warn "Evaluation disabled in current buffer: `restclient-enable-eval' is nil")
value)
(t
(restclient-eval-var value))))
(push (cons name value) vars)))
(append restclient-var-overrides vars restclient-var-defaults)))))
(defun restclient-eval-var (string)
"Evaluate the Lisp code contained in STRING.
The result of the evaluation is returned as a string."
(with-output-to-string (princ (eval (read string)))))
(defun restclient-make-header (&optional string)
"Create an alist element from STRING.
Match data must be set by caller."
(cons (match-string-no-properties 1 string)
(match-string-no-properties 2 string)))
(defun restclient-parse-headers (string)
"Create a header alist from STRING."
(let ((start 0)
(headers '()))
(while (string-match restclient-header-regexp string start)
(setq headers (cons (restclient-make-header string) headers)
start (match-end 0)))
headers))
(defun restclient-get-response-headers ()
"Returns alist of current response headers."
(let* ((start restclient--header-start-position)
(headers-end (+ 1 (or (string-match "\n\n" (buffer-substring-no-properties start (point-max)))
(buffer-size))))
(headers-commented-p (and (< 1 start) ;; Catches raw response buffers
(not (member major-mode (list (restclient--preferred-mode "image/png")
(restclient--preferred-mode "text/plain"))))))
(headers-string (buffer-substring-no-properties start headers-end)))
(when headers-commented-p
;; Temporarily uncomment to extract string
(uncomment-region start headers-end)
(setq headers-end (+ 1 (or (string-match "\n\n" (buffer-substring-no-properties start (point-max)))
(buffer-size))))
(setq headers-string (buffer-substring-no-properties start headers-end))
(comment-region start headers-end))
(restclient-parse-headers headers-string)))
(defun restclient-set-var-from-header (var header)
"Record a dynamic variable VAR from response headers.
HEADER is the name of the header to look up in the response."
(restclient-set-var var (cdr (assoc header (restclient-get-response-headers)))))
(defun restclient-read-file (path)
"Return the contents of the file PATH as a string."
(with-temp-buffer
(insert-file-contents path)
(buffer-string)))
(defun restclient-replace-path-with-contents (entity)
"Include file contents in request.
ENTITY is the request body, Possibly with `< /file/path' embeded, which
will be replaced with the contents of `/file/path' if it exists. If
`/file/path' does not exist, the construct may be an XML tag or other
data, not a file embedding, and will not be replaced."
(replace-regexp-in-string
restclient-file-regexp
(lambda (match)
(let ((filename (match-string 1 match)))
(if (file-exists-p filename)
(restclient-read-file filename)
match)))
entity t t))
(defun restclient-parse-body (entity vars)
"Prepare a request body for sending.
Replace variables with their values, and include file contents.
ENTITY is the body of the request (a string), VARS is an alist of currently
defined variables."
(restclient-replace-path-with-contents (restclient-replace-all-in-string vars entity)))
(defun restclient-parse-hook (cb-type args-offset args)
"Parse a hook definition.
CB-TYPE is the callback type, must be a previously registered type of handler.
ARGS-OFFSET is the position in the buffer where the arguments to / contents of
the hook begin. ARGS is the string from after the CB-TYPE to the end of the
line.
Registered callback handlers will typically use only one of ARGS or ARGS-OFFSET."
(if-let* ((handler (assoc cb-type restclient-result-handlers)))
(funcall (cadr handler) args args-offset)
`(lambda ()
(message "Unknown restclient hook type %s" ,cb-type))))
(defun restclient-register-result-func (name creation-func description)
"Register a new callback type.
NAME: The name of the callback type.
CREATION-FUNC: A function that interprets the rest of the hook definition and
returns a function that will be called in the context of the result buffer.
DESCRIPTION: Descriptive text."
(let ((new-cell (cons name (cons creation-func description))))
(setq restclient-result-handlers (cons new-cell restclient-result-handlers))))
(defun restclient-sanitize-var-name (var-name)
"Return the name of a variable, without decorations like `:' or `{{}}'.
VAR-NAME: a variable with or without decorations."
(save-match-data
(cond
((string-match restclient-use-var-regexp var-name)
(setq var-name (or (match-string 1 var-name) (match-string 2 var-name))))
((string-match "^@\\([^@ \n]+\\)$" var-name)
(setq var-name (match-string 1 var-name)))))
var-name)
(defun restclient-remove-var (var-name)
"Remove VAR-NAME from the list of dynamic variables."
(let ((var-name (restclient-sanitize-var-name var-name)))
(setq restclient-var-overrides (compat-call assoc-delete-all var-name restclient-var-overrides))))
(defun restclient-set-var (var-name value)
"Set VAR-NAME to VALUE for any subsequent requests."
(let ((var-name (restclient-sanitize-var-name var-name)))
(restclient-remove-var var-name)
(setq restclient-var-overrides (cons (cons var-name value) restclient-var-overrides))))
(defun restclient-get-var-at-point (var-name buffer-name buffer-pos)
"Look up the value of VAR-NAME in the current context.
Context is defined by environment, dynamically set variables, and variables
defined in BUFFER-NAME prior to BUFFER-POS."
;(message (format "getting var %s from %s at %s" var-name buffer-name buffer-pos))
(let* ((var-name (restclient-sanitize-var-name var-name))
(vars-at-point (save-excursion
(switch-to-buffer buffer-name)
(goto-char buffer-pos)
;; if we're called from a restclient buffer we need to lookup vars before the current hook or evar
;; outside a restclient buffer only globals are available so moving the point wont matter
(re-search-backward "^:\\|->" (point-min) t)
(restclient-find-vars-before-point))))
(restclient-replace-all-in-string vars-at-point (cdr (assoc var-name vars-at-point)))))
(defmacro restclient-get-var (var-name)
"Get the value of VAR-NAME in the current buffer."
(let ((buf-name (buffer-name (current-buffer)))
(buf-point (point)))
`(restclient-get-var-at-point ,var-name ,buf-name ,buf-point)))
(defun restclient-single-request-function ()
"Execute the callbacks/hooks defined for the current request."
(dolist (f restclient-curr-request-functions)
(save-excursion
(ignore-errors
(funcall f))))
(setq restclient-curr-request-functions nil)
(remove-hook 'restclient-response-loaded-hook 'restclient-single-request-function))
(defun restclient--parse-continuation-line (vars line separator)
"Read a key/value pair from LINE.
Return a %-encoded line preceeded by SEPARATOR.
Restclient variables in are expanded in keys and values separately,
using definitions passed in VARS."
(if (string-match restclient-url-continuation-key-value-regexp line)
(let ((key (match-string-no-properties 1 line))
(val (match-string-no-properties 2 line)))
(concat separator
(url-hexify-string (restclient-replace-all-in-string vars key))
"="
(url-hexify-string (restclient-replace-all-in-string vars val))))
(error "Line is not a valid key/value pair")))
(defun restclient-http-parse-current-and-do (func &rest args)
"Execute FUNC with the current request.
FUNC will receive the http method, url, headers and body of the request around
point as arguments, with ARGS included as the final argument."
(set-marker restclient-current-request-marker (point))
(save-excursion
(goto-char (restclient-current-min))
(when (re-search-forward restclient-method-url-regexp (point-max) t)
(let* ((vars (restclient-find-vars-before-point))
(method (match-string-no-properties 1))
(url (restclient-replace-all-in-string
vars (string-trim (match-string-no-properties 2))))
(q-param-separator (if (memq ?? (string-to-list url))
"&"
"?"))
(headers '()))
(forward-line)
(while (and restclient-query-use-continuation-lines
(looking-at restclient-url-continuation-line-regexp))
(let ((line (match-string-no-properties 1)))
(setq url
(concat url
(cond
((eq 'literal restclient-query-use-continuation-lines)
(restclient-replace-all-in-string vars (string-trim line)))
((eq 'smart restclient-query-use-continuation-lines)
(restclient--parse-continuation-line vars line q-param-separator))
(t
(error "Unknown value for `restclient-query-use-continuation-lines': %s"
restclient-query-use-continuation-lines))))))
(setq q-param-separator "&")
(forward-line))
(when restclient-strip-http-version
(setq url (replace-regexp-in-string restclient-url-version-regexp "" url)))
(while (cond
((looking-at restclient-response-hook-regexp)
(when-let* ((hook-function (restclient-parse-hook (match-string-no-properties 2)
(match-end 2)
(match-string-no-properties 3))))
(push hook-function restclient-curr-request-functions)))
((and (looking-at restclient-header-regexp) (not (looking-at restclient-empty-line-regexp)))
(setq headers (cons (restclient-replace-all-in-header vars (restclient-make-header)) headers)))
((looking-at restclient-use-var-regexp)
(setq headers (append headers (restclient-parse-headers (restclient-replace-all-in-string vars (match-string 0)))))))
(forward-line))
(when (looking-at restclient-empty-line-regexp)
(forward-line))
(when restclient-curr-request-functions
(add-hook 'restclient-response-loaded-hook 'restclient-single-request-function))
(let* ((cmax (restclient-current-max))
(entity (restclient-parse-body (buffer-substring (min (point) cmax) cmax) vars)))
(apply func method url headers entity args))))))
(defun restclient-copy-curl-command ()
"Formats the request as a curl command and copies the command to the clipboard."
(interactive)
(restclient-http-parse-current-and-do
'(lambda (method url headers entity)
(let ((include-arg (if restclient-response-body-only
""
"-i"))
(header-args
(mapconcat (lambda (header)
(format "-H \"%s: %s\" " (car header) (cdr header)))
headers))
(method-arg (format "-X %s" method))
(url-arg (format "\"%s\"" url))
(body-arg (if (< 0 (length entity))
(format "-d '%s'"
(replace-regexp-in-string "'" "'\\\\''" entity))
"")))
(kill-new (format "curl %s %s %s %s %s"
include-arg
header-args
method-arg
url-arg
body-arg)))
(message "curl command copied to clipboard."))))
(declare-function edit-indirect-region "ext:edit-indirect")
(defun restclient-edit-indirect-guess-mode (_parent-buffer _beg _end)
"Alternative to `edit-indirect-default-guess-mode'.
Customize `edit-indirect-guess-mode-function' to name this function
to get JSON request bodies editable in your preferred json mode."
;; FIXME: Ideally, this should check the content-type of the
;; current request. Could use restclient-http-parse-current-and-do,
;; but would need to stop request hook setup.
(save-excursion
(goto-char (point-min))
(if (looking-at "[[{]" t)
(funcall (restclient--preferred-mode "application/json"))
(normal-mode))))
(defun restclient-indirect-edit ()
"Use `edit-indirect-region' to edit the request body in a separate buffer."
(interactive)
(if (not (fboundp 'edit-indirect-region))
(message "edit-indirect is not installed")
(save-excursion
(goto-char (restclient-current-min))