-
Notifications
You must be signed in to change notification settings - Fork 20
/
verb.el
2759 lines (2451 loc) · 110 KB
/
verb.el
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
;;; verb.el --- Organize and send HTTP requests -*- lexical-binding: t -*-
;; Copyright (C) 2023 Federico Tedin
;; Author: Federico Tedin <federicotedin@gmail.com>
;; Maintainer: Federico Tedin <federicotedin@gmail.com>
;; Homepage: https://github.com/federicotdn/verb
;; Keywords: tools
;; Package-Version: 2.16.0
;; Package-Requires: ((emacs "26.3"))
;; This file is NOT part of GNU Emacs.
;; verb 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, or (at your option)
;; any later version.
;;
;; verb 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 General Public
;; License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with verb. If not, see http://www.gnu.org/licenses.
;;; Commentary:
;; Verb is a package that allows you to organize and send HTTP
;; requests from Emacs. See the project's README.md file for more
;; details.
;;; Code:
(require 'org)
(require 'org-element)
(require 'ob)
(require 'eieio)
(require 'subr-x)
(require 'url)
(require 'mm-util)
(require 'json)
(require 'js)
(require 'seq)
(require 'verb-util)
(defgroup verb nil
"An HTTP client for Emacs that extends Org mode."
:prefix "verb-"
:group 'tools)
(defcustom verb-default-response-charset "utf-8"
"Default charset to use when reading HTTP responses.
This variable is only used when the charset isn't specified in the
\"Content-Type\" header value (\"charset=utf-8\")."
:type 'string)
(defcustom verb-default-request-charset "utf-8"
"Charset to add to \"Content-Type\" headers in HTTP requests.
This variable is only used when the charset isn't specified in the
header value (\"charset=utf-8\")."
:type 'string)
(defcustom verb-content-type-handlers
'(;; Text handlers
("text/html" html-mode)
("\\(application\\|text\\)/xml" xml-mode)
("application/xhtml\\+xml" xml-mode)
("application/\\(json\\|vscode-jsonrpc\\)" verb-handler-json)
("application/javascript" js-mode)
("application/css" css-mode)
("text/plain" text-mode)
;; Binary handlers
("application/pdf" doc-view-mode t)
("image/png" image-mode t)
("image/svg\\+xml" image-mode t)
("image/x-windows-bmp" image-mode t)
("image/gif" image-mode t)
("image/jpe?g" image-mode t))
"List of content type handlers.
Handlers are functions to be called without any arguments. There are
two types of handlers: text and binary.
Text handlers are called after the text contents of the response have
been decoded into a multibyte buffer (with that buffer as the current
buffer).
Binary handlers, on the other hand, are called after the binary
contents of the response have been inserted into a unibyte buffer
\(with that buffer as the current buffer).
Both handler types should prepare the contents of the response buffer,
so that the user can then access or modify the information received in
a convenient way.
Entries of the alist must have the form (CONTENT-TYPE HANDLER BIN?).
CONTENT-TYPE must be a regexp which can match any number of valid
content types, or a string containing a content type. HANDLER must be
a function that takes no arguments. BIN?, if present, must be t, in
order to indicate that this handler is binary instead of text.
To choose a handler, Verb will try to match the received content type
with each CONTENT-TYPE in the alist (sequentially) using
`string-match-p'. The handler for the first CONTENT-TYPE to match
will be used."
:type '(repeat (list regexp function
(choice (const :tag "Binary" t)
(const :tag "Text" nil)))))
(defcustom verb-default-content-type-handler '(fundamental-mode)
"Default content type handler.
This handler is used when no appropriate handler was found in
`verb-content-type-handlers'."
:type '(list function (choice (const :tag "Binary" t)
(const :tag "Text" nil))))
(defcustom verb-export-functions
'(("verb" ?v verb--export-to-verb)
("curl" ?c verb--export-to-curl)
("eww" ?e verb--export-to-eww)
("websocat" ?w verb--export-to-websocat))
"Alist of request specification export functions.
Each element should have the form (NAME . FN), where NAME should be a
user-friendly name for this function, and FN should be the function
itself. FN should take a `verb-request-spec' object as its only
argument."
:type '(repeat (list string character function)))
(defcustom verb-auto-kill-response-buffers nil
"Whether to kill existing response buffers before sending a request.
Set this variable to t if you wish to have all old response buffers (named
*HTTP Response*) automatically killed when sending a new HTTP
request.
Set this variable to an integer number if you wish to have all old response
buffers killed, except the N most recent ones.
Set this variable to nil if you do not wish to have any old response buffers
killed before sending a request."
:type '(choice (const :tag "Never" nil)
(integer :tag "Kill all but keep N most recent")
(const :tag "Kill all" t)))
(defcustom verb-inhibit-cookies nil
"If non-nil, do not send or receive cookies when sending requests."
:type 'boolean)
(defcustom verb-advice-url t
"Whether to advice url.el functions or not.
If non-nil, the following url.el functions will be advised in order to
make Verb more flexible and user-friendly:
- `url-http-user-agent-string': Advised to allow the user to set their
own \"User-Agent\" headers.
- `url-http-handle-authentication': Advised to disable annoying user
prompt on 401 responses.
Note that the functions will be advised only during the duration of
the HTTP requests made."
:type 'boolean)
(defcustom verb-auto-show-headers-buffer nil
"Automatically show headers buffer after receiving an HTTP response.
Value nil means never show the headers buffer.
Value `when-empty' means automatically show the headers buffer only
when the response's body size is 0.
Any other value means always show the headers buffer."
:type '(choice (const :tag "Never" nil)
(const :tag "When empty" when-empty)
(const :tag "Always" t)))
(defcustom verb-show-timeout-warning 10.0
"Reasonable max duration (s) for an HTTP request.
Indicates the number of seconds to wait after an HTTP request is sent
to warn the user about a possible network timeout. When set to nil,
don't show any warnings."
:type '(choice (float :tag "Time in seconds")
(const :tag "Off" nil)))
(defcustom verb-babel-timeout 10.0
"Timeout (s) for HTTP requests made from a Babel source blocks.
Note that Emacs will be blocked while the response hasn't been
received."
:type 'float)
(defcustom verb-code-tag-delimiters '("{{" . "}}")
"Lisp code tag delimiters for HTTP request specifications.
The delimiters (left and right) are used to specify, evaluate and then
substitute Lisp code tags inside HTTP request specifications.
If different parts of your HTTP request specifications need to include
literal values identical to one or both of the delimiters, it is
recommended you change them to something else through this setting."
:type '(cons string string))
(defcustom verb-url-retrieve-function #'url-retrieve
"Function to use in order to send HTTP requests.
For more information on `url-retrieve' and `url-queue-retrieve', see
info node `(url)Retrieving URLs'."
:type '(choice (function-item
:tag "url-retrieve from url.el"
url-retrieve)
(function-item
:tag "url-queue-retrieve from url-queue.el"
url-queue-retrieve)))
(make-obsolete-variable 'verb-url-retrieve-function
"this feature is no longer supported."
"2024-04-02")
(defcustom verb-json-max-pretty-print-size (* 1 1024 1024)
"Max JSON file size (bytes) to automatically prettify when received.
If nil, never prettify JSON files automatically. This variable only applies
if `verb-handler-json' is being used to handle JSON responses."
:type '(choice (integer :tag "Max bytes")
(const :tag "Off" nil)))
(defcustom verb-json-use-mode #'js-mode
"Mode to enable in response buffers containing JSON data.
This variable only applies if `verb-handler-json' is being used to
handle JSON responses."
:type 'function)
(defcustom verb-post-response-hook nil
"Hook run after receiving an HTTP response.
The hook is run with the response body buffer as the current buffer.
The appropriate major mode will have already been activated, and
`verb-response-body-mode' as well. The buffer will contain the
response's decoded contents. The buffer-local `verb-http-response'
variable will be set to the corresponding class `verb-response'
object."
:type 'hook)
(defcustom verb-tag "verb"
"Tag used to mark headings that contain HTTP request specs.
Headings that do not contain this tag will be ignored when building
requests from heading hierarchies.
If set to t, consider all headings to contain HTTP request specs.
You can set this variable file-locally to use different tags on
different files, like so:
# -*- verb-tag: \"foo\" -*-
Note that if a heading has a tag, then all its subheadings inherit
that tag as well. This can be changed via the
`org-use-tag-inheritance' variable."
:type '(choice (string :tag "verb")
(const :tag "All" t)))
(defcustom verb-trim-body-end nil
"When set to a regexp, use it to trim request body endings.
If set to nil, read request bodies as they appear on the buffer. In
that case, if any whitespace is present between the body end and the
next heading (or buffer end), it will be included in the body."
:type '(choice (regexp :tag "Custom regexp")
(const :tag "All whitespace" "[ \t\n\r]+")
(const :tag "Disable" nil)))
(defcustom verb-base-headers nil
"Set of HTTP headers used as base when reading request specs.
These headers will be included by default in requests, but still may
be overridden by re-specifying them somewhere in the headings
hierarchy."
:type '(alist :key-type string :value-type string))
(defcustom verb-enable-elisp-completion t
"When set to a non-nil value, enable Lisp completion in code tags.
Completion is handled by the `verb-elisp-completion-at-point'
function.
Note the the point must be between the two code tag delimiters
\(e.g. \"{{\" and \"}}\") for the completion function to work."
:type 'boolean)
(defcustom verb-enable-var-preview t
"When set to a non-nil value, enable preview of Verb variables.
A preview of the value of a Verb variable will be shown in the
minibuffer, when the point is moved over a code tag containing only
a call to `verb-var'."
:type 'boolean)
(defcustom verb-enable-log t
"When non-nil, log different events in the *Verb Log* buffer."
:group :verb
:type 'boolean)
(defcustom verb-suppress-load-unsecure-prelude-warning nil
"When set to a non-nil, suppress warning about loading Emacs Lisp Preludes.
Loading Emacs Lisp (.el) configuration files as a prelude is
potentially unsafe, so if this setting is nil a warning prompt is
shown asking user to allow it to be loaded and evaluated. If non-nil,
no warning will be shown when loading Emacs Lisp external files."
:type 'boolean)
(defface verb-http-keyword '((t :inherit font-lock-constant-face
:weight bold))
"Face for highlighting HTTP methods.")
(defface verb-header '((t :inherit font-lock-constant-face))
"Face for highlighting HTTP headers.")
(defface verb-code-tag '((t :inherit italic))
"Face for highlighting Lisp code tags.")
(defface verb-json-key '((t :inherit font-lock-doc-face))
"Face for highlighting JSON keys.")
(defconst verb--http-methods '("GET" "POST" "DELETE" "PUT"
"OPTIONS" "HEAD" "PATCH"
"TRACE" "CONNECT")
"List of valid HTTP methods.")
(defconst verb--bodyless-http-methods '("GET" "HEAD" "DELETE" "TRACE"
"OPTIONS" "CONNECT")
"List of HTTP methods which usually don't include bodies.")
(defconst verb--url-pre-defined-headers '("MIME-Version"
"Connection"
"Host"
"Accept-Encoding"
"Extension"
"Content-Length")
"List of HTTP headers which are automatically added by url.el.
The values of these headers can't be easily modified by Verb, so a
warning will be shown to the user if they set any of them (as they
will appear duplicated in the request).")
(defconst verb--template-keyword "TEMPLATE"
"Keyword to use when defining request templates.
Request templates are defined without HTTP methods, paths or hosts.")
(defconst verb--http-header-regexp "^\\s-*\\([[:alnum:]_-]+:\\).*$"
"Regexp for font locking HTTP headers.")
(defconst verb--metadata-prefix "verb-"
"Prefix for Verb metadata keys in heading properties.
Matching is case insensitive.")
(defconst verb-version "2.16.0"
"Verb package version.")
(defconst verb--multipart-boundary-alphabet
(concat "abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789")
"Valid characters for multipart form boundaries.")
(defconst verb--multipart-boundary-length 64
"Number of characters per multipart form boundary.")
(defvar-local verb-http-response nil
"HTTP response for this response buffer (`verb-response' object).
The decoded body contents of the response are included in the buffer
itself.
In a response buffer, before the response has been received, this
variable will be set to t.")
(put 'verb-http-response 'permanent-local t)
(defvar-local verb--response-headers-buffer nil
"Buffer currently showing the HTTP response's headers.
This variable is only set on buffers showing HTTP response bodies.")
(defvar-local verb-kill-this-buffer nil
"If non-nil, kill this buffer after readings its contents.
When Verb evaluates Lisp code tags, a tag may produce a buffer as a
result. If the buffer-local value of this variable is non-nil for
that buffer, Verb will kill it after it has finished reading its
contents.")
(defvar-local verb--multipart-boundary nil
"Current multipart form boundary available for use in specs.")
(defvar verb-last nil
"Stores the last received HTTP response (`verb-response' object).
This variable is shared across any buffers using Verb mode. Consider
using this variable inside code tags if you wish to use results from
previous requests on new requests.")
(defvar verb--stored-responses nil
"Alist of stored HTTP responses.
Responses are stored only when the corresponding HTTP request contains
a nonempty \"Verb-Store\" metadata field. The response will be stored
here under its value.")
(defvar-local verb--vars nil
"List of values set with `verb-var', with their corresponding names.")
(defvar verb--set-var-hist nil
"Input history for `verb-set-var'.")
(defvar verb--requests-count 0
"Number of HTTP requests sent in the past.")
(defvar-local verb--response-number nil
"The number of this particular HTTP response buffer.")
(put 'verb--response-number 'permanent-local t)
(defvar verb--inhibit-code-tags-evaluation nil
"When non-nil, do not evaluate code tags in requests specs.
This variable is used mostly to parse and then copy request specs to
other buffers without actually expanding the embedded code tags.")
(defvar verb--elisp-completion-buffer nil
"Auxiliary buffer for performing completion for Lisp code.")
;;;###autoload
(defvar verb-command-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-s") #'verb-send-request-on-point-other-window)
(define-key map (kbd "C-r") #'verb-send-request-on-point-other-window-stay)
(define-key map (kbd "C-<return>") #'verb-send-request-on-point-no-window)
(define-key map (kbd "C-f") #'verb-send-request-on-point)
(define-key map (kbd "C-k") #'verb-kill-all-response-buffers)
(define-key map (kbd "C-e") #'verb-export-request-on-point)
(define-key map (kbd "C-v") #'verb-set-var)
(define-key map (kbd "C-x") #'verb-show-vars)
map)
"Keymap for `verb-mode' commands.
Bind this to an easy-to-reach key in Org mode in order to use Verb
comfortably. All commands listed in this keymap automatically enable
`verb-mode' in the current buffer when used.")
(defun verb--setup-font-lock-keywords (&optional remove)
"Configure font lock keywords for `verb-mode'.
If REMOVE is nil, add the necessary keywords to
`font-lock-keywords'. Otherwise, remove them."
(funcall
(if remove #'font-lock-remove-keywords #'font-lock-add-keywords)
nil
`(;; GET
(,(concat "^\\s-*\\(" (verb--http-methods-regexp) "\\)$")
(1 'verb-http-keyword))
;; GET www.example.com
(,(concat "^\\s-*\\(" (verb--http-methods-regexp) "\\)\\s-+.+$")
(1 'verb-http-keyword))
;; Content-type: application/json
(,verb--http-header-regexp
(1 'verb-header))
;; "something": 123
("\\s-\\(\"[[:graph:]]+?\"\\)\\s-*:."
(1 'verb-json-key))
;; {{(format "%s" "Lisp code tag")}}
(,(concat (car verb-code-tag-delimiters)
".*?"
(cdr verb-code-tag-delimiters))
(0 'verb-code-tag))))
(font-lock-flush))
(defvar verb-mode-map
(let ((map (make-sparse-keymap)))
(easy-menu-define verb-mode-menu map
"Menu for Verb mode"
'("Verb"
["Send request on selected window" verb-send-request-on-point]
["Send request on other window & switch"
verb-send-request-on-point-other-window]
["Send request on other window"
verb-send-request-on-point-other-window-stay]
["Send request without showing response"
verb-send-request-on-point-no-window]
"--"
["Kill response buffers" verb-kill-all-response-buffers]
"--"
["Set variable value" verb-set-var]
["Show all variable values" verb-show-vars]
["Unset all variables" verb-unset-vars]
"--"
["Export request to curl" verb-export-request-on-point-curl]
["Export request to Verb" verb-export-request-on-point-verb]
["Export request to EWW" verb-export-request-on-point-eww]
["Export request to websocat" verb-export-request-on-point-websocat]
"--"
["Customize Verb" verb-customize-group]
["Show log" verb-util-show-log]))
map)
"Keymap for `verb-mode'.")
;;;###autoload
(define-minor-mode verb-mode
"Minor mode for organizing and making HTTP requests from Emacs.
This mode acts as an extension to Org mode. Make sure you enable it
on buffers using Org as their major mode.
See the documentation in URL `https://github.com/federicotdn/verb' for
more details on how to use it."
:lighter " Verb"
:group 'verb
(if verb-mode
;; Enable verb-mode.
(progn
(verb--setup-font-lock-keywords)
(when verb-enable-elisp-completion
(add-hook 'completion-at-point-functions
#'verb-elisp-completion-at-point
nil 'local))
(add-hook 'post-command-hook #'verb--var-preview nil t)
(when (buffer-file-name)
(verb-util--log nil 'I
"Verb mode enabled in buffer: %s"
(buffer-name))
(verb-util--log nil 'I "Verb version: %s" verb-version)
(verb-util--log nil 'I "Org version: %s, GNU Emacs version: %s"
(org-version)
emacs-version)))
;; Disable verb-mode.
(verb--setup-font-lock-keywords t)
(remove-hook 'completion-at-point-functions
#'verb-elisp-completion-at-point
'local)))
(defvar verb-response-headers-mode-map
(let ((map (make-sparse-keymap)))
(easy-menu-define verb-mode-menu map
"Menu for Verb response headers mode"
'("Verb"
["Hide response headers" verb-kill-buffer-and-window]))
(define-key map (kbd "q") 'verb-kill-buffer-and-window)
map)
"Keymap for `verb-response-headers-mode'.")
(define-derived-mode verb-response-headers-mode special-mode "Verb[Headers]"
"Major mode for displaying an HTTP response's headers."
(font-lock-add-keywords
nil `(;; Key: Value
(,verb--http-header-regexp
(1 'verb-header)))))
(defun verb--http-method-p (m)
"Return non-nil if M is a valid HTTP method."
(member m verb--http-methods))
(defun verb--alist-p (l)
"Return non-nil if L is an alist."
(when (consp l)
(catch 'end
(dolist (elem l)
(unless (consp elem)
(throw 'end nil)))
t)))
(defun verb--http-headers-p (h)
"Return non-nil if H is an alist of (KEY . VALUE) elements.
KEY and VALUE must be strings. KEY must not be the empty string."
(when (consp h)
(catch 'end
(dolist (elem h)
(unless (and (consp elem)
(stringp (car elem))
(stringp (cdr elem))
(not (string-empty-p (car elem))))
(throw 'end nil)))
t)))
(cl-deftype verb--http-method-type ()
'(or null (satisfies verb--http-method-p)))
(cl-deftype verb--alist-type ()
'(or null (satisfies verb--alist-p)))
(cl-deftype verb--http-headers-type ()
'(or null (satisfies verb--http-headers-p)))
(defclass verb-request-spec ()
((method :initarg :method
:initform nil
:type verb--http-method-type
:documentation "HTTP method.")
(url :initarg :url
:initform nil
:type (or null url)
:documentation "Request URL.")
(headers :initarg :headers
:initform ()
:type verb--http-headers-type
:documentation "HTTP headers.")
(body :initarg :body
:initform nil
:type (or null string)
:documentation "Request body.")
(metadata :initarg :metadata
:initform nil
:type verb--alist-type
:documentation "User-defined request metadata."))
"Represents an HTTP request to be made.")
(defclass verb-response ()
((request :initarg :request
:type verb-request-spec
:documentation "Corresponding request.")
(headers :initarg :headers
:type verb--http-headers-type
:documentation "Response headers.")
(status :initarg :status
:type (or null string)
:documentation "Response's first line.")
(duration :initarg :duration
:type float
:documentation
"Time taken for response to be received, in seconds.")
(body :initarg :body
:initform nil
:type (or null string)
:documentation "Response body.")
(body-bytes :initarg :body-bytes
:initform 0
:type integer
:documentation
"Number of bytes in response body."))
"Represents an HTTP response to a request.")
(defvar verb-response-body-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-r C-r") #'verb-toggle-show-headers)
(define-key map (kbd "C-c C-r C-k") #'verb-kill-response-buffer-and-window)
(define-key map (kbd "C-c C-r C-f") #'verb-re-send-request)
(define-key map (kbd "C-c C-r C-w") #'verb-re-send-request-eww)
(define-key map (kbd "C-c C-r C-s") #'verb-show-request)
(easy-menu-define verb-response-body-mode-menu map
"Menu for Verb response body mode"
'("Verb[Body]"
["Toggle show response headers" verb-toggle-show-headers]
["Kill buffer and window" verb-kill-response-buffer-and-window]
"--"
["Re-send request" verb-re-send-request]
["Re-send request with EWW" verb-re-send-request-eww]
["Show corresponding request" verb-show-request]))
map)
"Keymap for `verb-response-body-mode'.")
(define-minor-mode verb-response-body-mode
"Minor mode for displaying an HTTP response's body."
:lighter " Verb[Body]"
:group 'verb
(if verb-response-body-mode
(progn
(setq header-line-format
(verb--response-header-line-string verb-http-response))
(when verb-auto-show-headers-buffer
(if (eq verb-auto-show-headers-buffer 'when-empty)
(when (zerop (oref verb-http-response body-bytes))
(verb-toggle-show-headers))
(verb-toggle-show-headers))))
(setq header-line-format nil)))
(defun verb-customize-group ()
"Show the Customize menu buffer for the Verb package group."
(interactive)
(customize-group "verb"))
(defun verb--var-preview ()
"Preview the value of a Verb variable in the minibuffer."
(when-let* (((not (current-message)))
(verb-enable-var-preview)
((not (string= (buffer-substring (line-beginning-position)
(1+ (line-beginning-position)))
"#")))
;; Get the contents inside the code tag: {{<content>}}.
(beg (save-excursion
(when (search-backward (car verb-code-tag-delimiters)
(line-beginning-position) t)
(match-end 0))))
(end (save-excursion
(when (search-forward (cdr verb-code-tag-delimiters)
(line-end-position) t)
(match-beginning 0))))
(code (buffer-substring-no-properties beg end))
(form (car (ignore-errors (read-from-string code))))
((eq (car form) 'verb-var))
(var (cadr form))
(val (assq var verb--vars)))
(let ((message-log-max nil))
(message "Current value for %s: %s" (car val) (cdr val)))))
(defun verb-elisp-completion-at-point ()
"Completion at point function for Lisp code tags."
(when-let (;; Get the contents inside the code tag: {{<content>}}.
(beg (save-excursion
(when (search-backward (car verb-code-tag-delimiters)
(line-beginning-position) t)
(match-end 0))))
(end (save-excursion
(when (search-forward (cdr verb-code-tag-delimiters)
(line-end-position) t)
(match-beginning 0)))))
;; Set up the buffer where we'll run `elisp-completion-at-point'.
(unless verb--elisp-completion-buffer
(setq verb--elisp-completion-buffer
(get-buffer-create " *verb-elisp-completion*")))
;; Copy the contents of the code tag to the empty buffer, run
;; completion there.
(let* ((code (buffer-substring-no-properties beg end))
(point-offset (1+ (- (point) beg)))
(completions (with-current-buffer verb--elisp-completion-buffer
(erase-buffer)
(insert code)
(goto-char point-offset)
(elisp-completion-at-point))))
;; The beginning/end positions will belong to the other buffer, add
;; `beg' so that they make sense on the original one.
(when completions
(append (list (+ (nth 0 completions) beg -1)
(+ (nth 1 completions) beg -1))
(cddr completions))))))
(defun verb--ensure-org-mode ()
"Ensure `org-mode' is enabled in the current buffer."
(unless (derived-mode-p 'org-mode)
(org-mode)))
(defun verb--ensure-verb-mode ()
"Ensure `verb-mode' is enabled in the current buffer."
(unless verb-mode
(verb-mode)))
(defun verb-headers-get (headers name)
"Return value for HTTP header under NAME in HEADERS.
HEADERS must be an alist of (KEY . VALUE) elements. NAME and KEY will
be compared ignoring case. If no value is present under NAME, signal
an error."
(if-let ((val (assoc-string name headers t)))
(cdr val)
(user-error "HTTP header has no value for \"%s\"" name)))
(defalias 'verb-shell #'shell-command-to-string
"Alias to `shell-command-to-string'.")
(defun verb-unix-epoch ()
"Return the current time as an integer number of seconds since the epoch."
(floor (time-to-seconds)))
(defun verb-json-get (text &rest path)
"Interpret TEXT as a JSON object and return value under PATH.
The outermost JSON element in TEXT must be an object.
PATH must be a list of strings, symbols (which will be converted to
strings), or integers. The PATH list will be traversed from beginning
to end, using each item to access a sub-value in the current JSON
element (and setting the current JSON element to that new value).
This is supposed to work in a similar way JSONPath does, more info at
URL `https://goessner.net/articles/JsonPath/'.
For example, for the following TEXT:
{
\"test\": {
\"foo\": [\"apples\", \"oranges\"]
}
}
Using PATH (\"test\" \"foo\" 1) will yield \"oranges\"."
(unless path
(user-error "%s" "No path specified for JSON value"))
(let ((obj (json-read-from-string text)))
(dolist (key path)
(setq
obj
(cond
(;; Path element is a string (or symbol).
(or (stringp key) (symbolp key))
(when (symbolp key)
(setq key (symbol-name key)))
;; Obj may be an alist, plist or hash-table.
(pcase json-object-type
('alist
(cdr (assoc-string key obj)))
('plist
(plist-get obj (intern (concat ":" key))))
('hash-table
(gethash key obj))
(_
(user-error "%s" "Unknown value for `json-object-type'"))))
(;; Path element is an integer.
(integerp key)
;; Handle negative indexes by adding the negative index to the size of
;; the sequence, and using that as the new index.
(when (and (< key 0) (seqp obj))
(setq key (+ key (length obj))))
;; Obj may be a list or a vector.
(pcase json-array-type
('list
(nth key obj))
('vector
(aref obj key))
(_
(user-error "%s" "Unknown value for `json-array-type'"))))
(;; Invalid key type.
t
(user-error "Invalid key: %s" key)))))
obj))
(defun verb--buffer-string-no-properties ()
"Return the contents of the current buffer as a string.
Do not include text properties."
(buffer-substring-no-properties (point-min) (point-max)))
(defun verb--back-to-heading ()
"Move to the previous heading.
Or, move to beginning of this line if it's a heading. If there are no
headings, move to the beginning of buffer. Return t if a heading was
found."
(if (ignore-errors (org-back-to-heading t))
t
(goto-char (point-min))
nil))
(defun verb--up-heading ()
"Move to the parent heading, if there is one.
Return t if there was a heading to move towards to and nil otherwise."
(let ((p (point)))
(ignore-errors
(org-up-heading-all 1)
(not (= p (point))))))
(defun verb--heading-tags ()
"Return all (inherited) tags from current heading."
(verb--back-to-heading)
(when-let ((tags (org-entry-get (point) "ALLTAGS")))
(seq-filter (lambda (s) (or org-use-tag-inheritance
(not (get-text-property 0 'inherited s))))
(split-string tags ":" t))))
(defun verb--heading-properties (prefix)
"Return alist of current heading properties starting with PREFIX.
Respects `org-use-property-inheritance'. Matching is case-insensitive."
(verb--back-to-heading)
(thread-last
;; 1) Get all doc properties and filter them by prefix. This will push the
;; property already `upcase''d, but only if there is no `string=' the
;; `upcase''d value.
(seq-reduce (lambda (properties property)
(if (string-prefix-p prefix property t)
(cl-pushnew (upcase property) properties :test #'string=)
properties))
(org-buffer-property-keys)
'())
;; 2) Get the value for each of those properties and return an alist
;; Note: this will respect the value of `org-use-property-inheritance'.
(mapcar (lambda (key) (cons key (org-entry-get (point) key 'selective))))
;; 3) Discard all (key . nil) elements in the list.
(seq-filter #'cdr)))
(defun verb--heading-contents (&optional point)
"Return text under the current heading, with some conditions.
If one or more Babel source blocks are present in the text, and the
point is located inside one of them, return the content of that source
block. Otherwise, simply return the all the text content under the
current heading.
Additionally, assume point was at position POINT before it was moved
to the heading.
If not on a heading, signal an error."
(unless (org-at-heading-p)
(user-error "%s" "Can't get heading text contents: not at a heading"))
(let ((start (save-excursion
(end-of-line)
(unless (eobp) (forward-char))
(point)))
(end (save-excursion
(goto-char (org-entry-end-position))
(when (and (org-at-heading-p)
(not (eobp)))
(backward-char))
(point))))
(when (< end start) (setq end start))
(verb--maybe-extract-babel-src-block point start end)))
(defun verb--maybe-extract-babel-src-block (point start end)
"Return the text between START and END, with some exceptions.
If there are one or more Babel source blocks within the text, and the
position POINT lies within one of these blocks, return that block's
text contents.
If POINT is nil, set it to START. Also, clamp POINT between START and
END."
(unless point (setq point start))
(when (< point start) (setq point start))
(when (< end point) (setq point end))
(save-excursion
(save-match-data
(goto-char point)
(let ((case-fold-search t)
block-start)
(when (re-search-backward "#\\+begin_src\\s-+verb" start t)
;; Found the start.
(end-of-line)
(forward-char)
(setq block-start (point))
(goto-char point)
(when (re-search-forward "#\\+end_src" end t)
;; Found the end.
(beginning-of-line)
(backward-char)
(setq start block-start)
(setq end (point))))
(buffer-substring-no-properties start end)))))
(defun verb--request-spec-from-heading (point)
"Return a request spec from the current heading's text contents.
If a heading is found, get its contents using
`verb--heading-contents'. From that result, try to parse a request
specification. Return nil if the heading has no text contents, if
contains only comments, or if the heading does not have the tag
`verb-tag'.
Additionally, assume point was at position POINT before it was moved
to the heading.
If not on a heading, signal an error."
(unless (org-at-heading-p)
(user-error "%s" "Can't read request spec: not at a heading"))
(when (or (member verb-tag (verb--heading-tags))
(eq verb-tag t))
(let ((text (verb--heading-contents point))
(metadata (verb--heading-properties verb--metadata-prefix)))
(unless (string-empty-p text)
(condition-case nil
(verb-request-spec-from-string text metadata)
(verb-empty-spec nil))))))
(defun verb--request-spec-from-babel-src-block (pos body vars)
"Return a request spec generated from a Babel source block.
BODY should contain the body of the source block. POS should be a
position of the buffer that lies inside the source block. VARS should
be an alist of argument names and values that should be temporarily
added to the values available through `verb-var'.
Note that the entire buffer is considered when generating the request
spec, not only the section contained by the source block.
This function is called from ob-verb.el (`org-babel-execute:verb')."
(verb-load-prelude-files-from-hierarchy)
(save-excursion
(goto-char pos)
(let* ((verb--vars (append vars verb--vars))
(metadata (verb--heading-properties verb--metadata-prefix))
(rs (verb-request-spec-from-string body metadata)))
;; Go up one level first, if possible. Do this to avoid
;; re-reading the request in the current level (contained in the
;; source block). If no more levels exist, skip the call to
;; `verb--request-spec-from-hierarchy'.
(if (verb--up-heading)
;; Continue reading requests from the headings
;; hierarchy. Pre-include the one we read from the source block
;; at the end of the list.
(verb--request-spec-from-hierarchy rs)
(verb--request-spec-post-process rs)))))
(defun verb--object-of-class-p (obj class)
"Return non-nil if OBJ is an instance of CLASS.
CLASS must be an EIEIO class."
(ignore-errors
(object-of-class-p obj class)))
(defun verb--try-read-fn-form (form)
"Try `read'ing FORM and throw error if failed."
(condition-case _err (read form)
(end-of-file (user-error "`%s' is a malformed expression" form))))
(defun verb--request-spec-metadata-get (rs key)
"Get the metadata value under KEY for request spec RS.
If no value is found under KEY, or if the value associated is the
empty string, return nil. KEY must NOT have the prefix
`verb--metadata-prefix' included."
(thread-first
(concat verb--metadata-prefix key)
(assoc-string (oref rs metadata) t)
cdr
verb-util--nonempty-string))
(defun verb--request-spec-post-process (rs)
"Validate and prepare request spec RS to be used.
The following checks/preparations are run:
1) Check if `verb-base-headers' needs to be applied.
2) Apply request mapping function, if one was specified.
3) Run validations with `verb-request-spec-validate'.
After that, return RS."
;; Use `verb-base-headers' if necessary.
(when verb-base-headers
(setq rs (verb-request-spec-override
(verb-request-spec :headers verb-base-headers
:url (oref rs url))
rs)))
;; Apply the request mapping function, if present.
(when-let ((form (verb--request-spec-metadata-get rs "map-request"))
(fn (verb--try-read-fn-form form)))
(if (functionp fn)
(setq rs (funcall fn rs))
(user-error "`%s' is not a valid function" fn))
(unless (verb--object-of-class-p rs 'verb-request-spec)
(user-error (concat "Request mapping function `%s' must return a "
"`verb-request-spec' value")
fn)))
;; Validate and return.
(verb-request-spec-validate rs))
(defun verb--request-spec-from-hierarchy (&rest specs)
"Return a request spec generated from the headings hierarchy.
To do this, use `verb--request-spec-from-heading' for the current
heading, for that heading's parent, and so on until the root of the
hierarchy is reached.
Once all the request specs have been collected, override them in
inverse order according to the rules described in
`verb-request-spec-override'. After that, override that result with
all the request specs in SPECS, in the order they were passed in."
;; Load all prelude verb-var's before rest of the spec to be complete, unless
;; specs already exists which means called from ob-verb block and loaded.
(unless specs