-
Notifications
You must be signed in to change notification settings - Fork 0
/
reel.el
176 lines (147 loc) · 6.11 KB
/
reel.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
;;; reel.el --- Rust-based HTTP library for Emacs Lisp. -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2024 Ryan Faulhaber
;;
;; Author: Ryan Faulhaber <ryf@sent.as>
;; Maintainer: Ryan Faulhaber <ryf@sent.as>
;; Created: January 25, 2023
;; Modified: October 04, 2023
;; Version: 0.1.0
;; Keywords: lisp
;; Homepage: https://github.com/rfaulhaber/reel
;; Package-Requires: ((emacs "27.1"))
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; Rust-based HTTP library for Emacs Lisp.
;;
;;; Code:
(require 'cl-lib)
(require 'eieio)
(unless (functionp 'module-load)
(error "Dynamic module feature not available, please compile Emacs --with-modules option turned on"))
(eval-and-compile
(defvar reel-dyn--version)
(let* ((ext (pcase system-type
('gnu/linux "so")
('darwin "dylib")
('windows-nt "dll")))
(path (format "%s.%s" (if (eq system-type 'windows-nt) "reel" "libreel") ext)))
;; we load the rust output depending on the context
;; if the module exists as a sibling to this file, we immediately load that
(if (file-exists-p (expand-file-name path))
(module-load (expand-file-name path))
;; by default, we check the load path. this is how we get eask install to work
(load path))
(unless (or reel-dyn--version (featurep 'reel-dyn))
(error "Dynamic module was not loaded correctly"))))
(require 'reel-dyn)
(defconst reel-http-methods '(GET HEAD POST PUT DELETE CONNECT OPTIONS TRACE PATCH)
"Valid HTTP methods.")
(defconst reel-default-user-agent (format "reel (GNU Emacs %s %s)/%s" emacs-version system-type reel-dyn--version)
"Default user-agent header.")
(cl-defstruct (reel-request
(:constructor reel-make-request)
(:copier nil))
"An HTTP request."
(url nil :read-only t)
(method nil :read-only t)
(headers nil :read-only t)
(body nil :read-only t))
(cl-defstruct (reel-response
(:constructor reel-make-response)
(:copier nil))
"An HTTP response."
(status nil :read-only t)
(headers nil :read-only t)
(body nil :read-only t))
(cl-defstruct (reel-client
(:constructor reel--make-client)
(:copier nil))
"A reusable reel client."
(ptr nil :read-only t))
(define-error 'reel-invalid-body-type "the :body keyword of a reel request must be a string or an alist")
(define-error 'reel-invalid-method "invalid HTTP method passed to :method")
;;;###autoload
(cl-defun reel (url-or-request &key method headers body)
"Make an HTTP request with URL-OR-REQUEST.
The key arguments will adjust the behavior of the request.
URL-OR-REQUEST can be either a string URL or an instance of a reel-request.
METHOD is a string and one of: GET HEAD POST PUT DELETE CONNECT OPTIONS TRACE
PATCH
HEADERS is an alist of header/value pairs. E.g. `\'((\"Content-Type\" .
\"application/json\"))'. Keys and values must be strings.
BODY is either the string representation of the request body or an alist of
key-value pairs for form submission.
`reel' is synchronous."
(let ((client (reel-dyn-make-client)))
(if (reel-request-p url-or-request)
(with-slots (url method headers body) url-or-request
(reel--make-request client url method headers body))
(reel--make-request client url-or-request method headers body))))
(defun reel-make-client ()
"Returns an instance of a reel client."
(reel--make-client
:ptr (reel-dyn-make-client)))
(cl-defun reel-make-client-request (client &key url method headers body)
"Makes a request using a reel CLIENT."
(reel--make-request client url method headers body))
(defun reel-url-search-parameters (parameters)
"Given an alist PARAMETERS, converts the alist to a search query string."
(when (not (proper-list-p parameters))
(user-error "PARAMETERS must be a proper alist"))
(if (null parameters)
""
(concat "?"
(string-join
(seq-map (lambda (param)
(concat (car param) "=" (cdr param)))
parameters)
"&"))))
(defun reel-valid-body-p (body)
"Returns non-nil if BODY is valid for the `reel' function :body keyword."
(or (stringp body)
(proper-list-p body)))
(defun reel-valid-method-p (method)
"Returns non-nil if METHOD is a valid HTTP method."
(let ((canonical-method (if (stringp method)
(intern (upcase method))
method)))
(member canonical-method reel-http-methods)))
(defun reel--make-request (client url method headers body)
"Makes a request via reel-dyn using defaults."
(when (and (not (null body))
(not (reel-valid-body-p body)))
(signal 'reel-invalid-body-type `(reel-valid-body-p ,(type-of body))))
(when (and (not (null method))
(not (reel-valid-method-p method)))
(signal 'reel-invalid-method `(reel-valid-method-p ,method)))
(let* ((client (if (reel-client-p client) (reel-client-ptr client) client))
(method (or method "GET"))
(default-headers (if (assoc "user-agent" headers) headers (push `("user-agent" . ,reel-default-user-agent) headers)))
(headers (reel--make-header-map default-headers)))
(reel--build-response
(reel-dyn-make-client-request client url method headers body))))
(defun reel--make-header-map (headers)
"Given an alist of HEADERS, converts them into a header map."
(let ((header-map (reel-dyn-make-header-map)))
(mapc (lambda (header)
(reel-dyn-insert-header header-map (car header) (cdr header)))
headers)
header-map))
(defun reel--build-response (resp-pointer)
"Builds a reel-response struct out of RESP-POINTER."
(reel-make-response
:status (reel-dyn-get-response-status resp-pointer)
:headers (reel--get-headers (reel-dyn-get-response-headers resp-pointer))
:body (reel-dyn-get-response-body resp-pointer)))
(defun reel--get-headers (headers)
"Pulls header keys and values out of HEADERS and into an alist."
(let ((keys (reel-dyn-get-header-keys headers)))
(mapcar (lambda (key)
(cons key (reel-dyn-get-header headers key)))
keys)))
(provide 'reel)
;;; reel.el ends here