[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/plz 6a21c7e809 12/81: Significant changes
From: |
ELPA Syncer |
Subject: |
[elpa] externals/plz 6a21c7e809 12/81: Significant changes |
Date: |
Wed, 11 May 2022 17:57:58 -0400 (EDT) |
branch: externals/plz
commit 6a21c7e809e3f23a983e1f50346f55008f79415f
Author: Adam Porter <adam@alphapapa.net>
Commit: Adam Porter <adam@alphapapa.net>
Significant changes
Thanks to Chris Wellons for his invaluable feedback.
---
notes.org | 72 ++++++++++++++++
plz.el | 246 +++++++++++++++++++++++++++++++++---------------------
tests/test-plz.el | 83 +++++++++++++++---
3 files changed, 293 insertions(+), 108 deletions(-)
diff --git a/notes.org b/notes.org
new file mode 100644
index 0000000000..8944270d80
--- /dev/null
+++ b/notes.org
@@ -0,0 +1,72 @@
+#+TITLE: plz Notes
+
+* API Design
+
+** Async
+
+Some sample cases that the API should make easy.
+
+*** Body as string
+
+#+BEGIN_SRC elisp
+ (plz-get url
+ :with 'body-string
+ :then (lambda (body-string)
+ (setf something body-string)))
+#+END_SRC
+
+*** Body as buffer
+
+#+BEGIN_SRC elisp
+ ;; Decodes body and narrows buffer to it.
+ (plz-get url
+ :with 'buffer
+ :then (lambda (buffer)
+ (with-current-buffer buffer
+ (setf text (buffer-substring (point-min) (point-max))))))
+#+END_SRC
+
+#+BEGIN_SRC elisp
+ ;; Narrows buffer to undecoded body, e.g. for binary files.
+ (plz-get url
+ :with 'buffer-undecoded ; `buffer-binary'?
+ :then (lambda (buffer)
+ (with-current-buffer buffer
+ (setf binary-content (buffer-substring (point-min)
(point-max))))))
+#+END_SRC
+
+**** Callback with point at body start
+:PROPERTIES:
+:ID: 1795462e-01bc-4f0b-97ab-3c1b2e75485c
+:END:
+
+Assuming that =plz= has already called =decode-coding-region=, this is
straightforward, but the caller shouldn't have to do this extra work.
+
+#+BEGIN_SRC elisp
+ (plz-get url
+ :then (lambda (buffer)
+ (buffer-substring (point) (point-max))))
+#+END_SRC
+
+*** Body parsed with function
+
+#+BEGIN_SRC elisp
+ ;; Narrows buffer to body, decodes it, calls callback with result of
`json-read'.
+ (plz-get url
+ :with #'json-read
+ :then (lambda (json)
+ (setf something (alist-get 'key json))))
+#+END_SRC
+
+#+BEGIN_SRC elisp
+ ;; Narrows buffer to body, decodes it, parses with
+ ;; `libxml-parse-html-region', calls callback with DOM.
+ (plz-get url
+ :with (lambda ()
+ (libxml-parse-html-region (point-min) (point-max) url))
+ :then (lambda (dom)
+ (with-current-buffer (generate-new-buffer "*plz-browse*")
+ (shr-insert-document dom))))
+#+END_SRC
+
+*** HTTP response with headers
diff --git a/plz.el b/plz.el
index 1af1de35de..d744e1b76b 100644
--- a/plz.el
+++ b/plz.el
@@ -141,10 +141,10 @@
;;;; Variables
-(defvar-local plz-error nil
+(defvar-local plz-else nil
"Callback function for errored completion of request in current curl process
buffer.")
-(defvar-local plz-success nil
+(defvar-local plz-then nil
"Callback function for successful completion of request in current curl
process buffer.")
;;;; Customization
@@ -172,39 +172,49 @@
;;;; Functions
-(cl-defun plz-get (url &key headers sync success error
- (connect-timeout plz-connect-timeout))
+(cl-defun plz-get (url &key headers as then
+ (connect-timeout plz-connect-timeout)
+ (decode t))
"Get HTTP URL with curl.
-If SYNC is non-nil, return the response object; otherwise, return
-the curl process object.
+
+FIXME: Docstring.
HEADERS may be an alist of extra headers to send with the
request. CONNECT-TIMEOUT may be a number of seconds to timeout
-the initial connection attempt.
-
-For asynchronous requests, SUCCESS and ERROR should be callback
-functions, called when the curl process finishes with a single
-argument: the `plz-response' object."
+the initial connection attempt."
+ (declare (indent defun))
(plz--request 'get url
- :sync sync
:headers headers
:connect-timeout connect-timeout
- :success success
- :error error))
+ :decode decode
+ :as as :then then))
+
+(cl-defun plz-get-sync (url &key headers as
+ (connect-timeout plz-connect-timeout)
+ (decode t))
+ "Get HTTP URL with curl synchronously.
-(cl-defun plz--request (_method url &key headers connect-timeout sync
- success error)
- "Return process or response for HTTP request to URL.
-If SYNC is non-nil, return the response object; otherwise, return
-the curl process object.
+FIXME: Docstring.
HEADERS may be an alist of extra headers to send with the
request. CONNECT-TIMEOUT may be a number of seconds to timeout
-the initial connection attempt.
+the initial connection attempt."
+ (declare (indent defun))
+ (plz--request-sync 'get url
+ :headers headers
+ :connect-timeout connect-timeout
+ :decode decode
+ :as as))
+
+(cl-defun plz--request (_method url &key headers connect-timeout
+ decode as then)
+ "Return curl process for HTTP request to URL.
+
+FIXME: Docstring.
-For asynchronous requests, SUCCESS and ERROR should be callback
-functions, called when the curl process finishes with a single
-argument: the `plz-response' object."
+HEADERS may be an alist of extra headers to send with the
+request. CONNECT-TIMEOUT may be a number of seconds to timeout
+the initial connection attempt."
;; Inspired by and copied from `elfeed-curl-retrieve'.
(let* ((coding-system-for-read 'binary)
(process-connection-type nil)
@@ -214,35 +224,74 @@ argument: the `plz-response' object."
(when connect-timeout
(list "--connect-timeout" (number-to-string
connect-timeout)))
(list url))))
- (pcase sync
- (`nil (plz-request--async curl-args :success success :error error))
- (_ (plz-request--sync curl-args :success success :error error)))))
-
-(cl-defun plz-request--async (curl-args &key success error)
- "Return process object for curl called with CURL-ARGS.
-SUCCESS and ERROR should be callback functions, called when the
-curl process finishes with a single argument: the `plz-response'
-object. Uses `make-process' to call curl asynchronously."
- (with-current-buffer (generate-new-buffer " *plz-request-curl*")
- (let ((process (make-process :name "plz-request-curl"
- :buffer (current-buffer)
- :command (append (list plz-curl-program)
curl-args)
- :connection-type 'pipe
- :sentinel #'plz--sentinel
- :stderr (current-buffer))))
- (setf plz-success success
- plz-error error)
- process)))
-
-(cl-defun plz-request--sync (curl-args &key success error)
- "Return HTTP response object for curl called with CURL-ARGS.
+ (with-current-buffer (generate-new-buffer " *plz-request-curl*")
+ (let ((process (make-process :name "plz-request-curl"
+ :buffer (current-buffer)
+ :command (append (list plz-curl-program)
curl-args)
+ :connection-type 'pipe
+ :sentinel #'plz--sentinel
+ :stderr (current-buffer)))
+ ;; The THEN function is called in the response buffer.
+ (then (pcase-exhaustive as
+ ('string (lambda ()
+ (let ((coding-system (or (plz--coding-system)
'utf-8)))
+ (plz--narrow-to-body)
+ (when decode
+ (decode-coding-region (point) (point-max)
coding-system))
+ (funcall then (buffer-string)))))
+ ('buffer (lambda ()
+ (funcall then (current-buffer))))
+ ('response (lambda ()
+ (funcall then (plz--response))))
+ ((pred functionp) (lambda ()
+ (let ((coding-system (or
(plz--coding-system) 'utf-8)))
+ (plz--narrow-to-body)
+ (when decode
+ (decode-coding-region (point)
(point-max) coding-system))
+ (funcall then (funcall as))))))))
+ (setf plz-then then)
+ process))))
+
+(cl-defun plz--request-sync (_method url &key headers connect-timeout
+ decode as)
+ "Return HTTP response for curl called with CURL-ARGS.
+FIXME: Docstring.
Uses `call-process' to call curl synchronously."
(with-current-buffer (generate-new-buffer " *plz-request-curl*")
- (let ((status (apply #'call-process plz-curl-program nil t nil
- curl-args))
- (plz-success #'identity))
+ (let* ((coding-system-for-read 'binary)
+ (process-connection-type nil)
+ (header-args (cl-loop for (key . value) in headers
+ collect (format "--header %s: %s" key value)))
+ (curl-args (append plz-curl-default-args header-args
+ (when connect-timeout
+ (list "--connect-timeout" (number-to-string
connect-timeout)))
+ (list url)))
+ (status (apply #'call-process plz-curl-program nil t nil
+ curl-args))
+ ;; THEn form copied from `plz--request'.
+ ;; TODO: DRY this. Maybe we could use a thread and a condition
variable, but...
+ (plz-then (pcase-exhaustive as
+ ('string (lambda ()
+ (let ((coding-system (or
(plz--coding-system) 'utf-8)))
+ (plz--narrow-to-body)
+ (when decode
+ (decode-coding-region (point)
(point-max) coding-system))
+ (buffer-string))))
+ ('response #'plz--response)
+ ((pred functionp) (lambda ()
+ (let ((coding-system (or
(plz--coding-system) 'utf-8)))
+ (plz--narrow-to-body)
+ (when decode
+ (decode-coding-region (point)
(point-max) coding-system))
+ (funcall as)))))))
(plz--sentinel (current-buffer) status))))
+(defun plz--narrow-to-body ()
+ "Narrow to body of HTTP response in current buffer."
+ (goto-char (point-min))
+ (re-search-forward "^\r\n" nil)
+ (narrow-to-region (point) (point-max)))
+
(defun plz--sentinel (process-or-buffer status)
"Process buffer of curl output in PROCESS-OR-BUFFER.
If PROCESS-OR-BUFFER if a process, uses its buffer; if a buffer,
@@ -256,13 +305,12 @@ node `(elisp) Sentinels'). Kills the buffer before
returning."
(with-current-buffer buffer
(pcase status
((or 0 "finished\n")
- ;; Request completed successfully: call success callback with
parsed response.
- (let ((response (plz--response buffer)))
- (funcall plz-success response)))
+ ;; Request completed successfully: call THEN.
+ (funcall plz-then))
+ ;; FIXME: Implement error callback handling.
((rx "exited abnormally with code " (group (1+ digit)))
;; Error: call error callback.
- ;; FIXME: Call with an error struct.
(warn "plz--sentinel: ERROR: %s" (buffer-string))
;; (let* ((code (string-to-number (match-string 1 status)))
;; (message (alist-get code plz-curl-errors)))
@@ -270,50 +318,56 @@ node `(elisp) Sentinels'). Kills the buffer before
returning."
)))
(kill-buffer buffer))))
-(defun plz--response (buffer)
- "Return response struct for HTTP response in BUFFER."
- (with-current-buffer buffer
- (save-excursion
- (goto-char (point-min))
- ;; Parse HTTP version and status code.
- (looking-at (rx "HTTP/" (group (1+ (or digit "."))) (1+ blank)
- (group (1+ digit))))
- (let* ((http-version (string-to-number (match-string 1)))
- (status-code (string-to-number (match-string 2)))
- (headers (plz--headers buffer))
- (coding-system (or (when-let* ((it (alist-get "Content-Type"
headers nil nil #'string=)))
- (coding-system-from-name it))
- 'utf-8))
- (body (plz--decode-body buffer coding-system)))
- (make-plz-response
- :version http-version
- :status status-code
- :headers headers
- :body body)))))
-
-(defun plz--headers (buffer)
- "Return headers alist for HTTP response in BUFFER."
- (with-current-buffer buffer
- (save-excursion
- (goto-char (point-min))
- (forward-line 1)
- (let ((limit (save-excursion
- (re-search-forward "^\r\n" nil)
- (point))))
- (cl-loop while (re-search-forward (rx bol (group (1+ (not (in ":"))))
":" (1+ blank)
- (group (1+ (not (in "\r\n")))))
- limit t)
- collect (cons (match-string 1) (match-string 2)))))))
-
-(defun plz--decode-body (buffer coding-system)
- "Return decoded body for HTTP response in BUFFER.
-Decodes with `decode-coding-region' according to CODING-SYSTEM."
- (with-current-buffer buffer
- (save-excursion
- (goto-char (point-min))
- ;; Skip headers.
- (re-search-forward "^\r\n" nil)
- (decode-coding-region (point) (point-max) coding-system t))))
+(defun plz--response ()
+ "Return response struct for HTTP response in current buffer."
+ (save-excursion
+ (goto-char (point-min))
+ ;; Parse HTTP version and status code.
+ (looking-at (rx "HTTP/" (group (1+ (or digit "."))) (1+ blank)
+ (group (1+ digit))))
+ (let* ((http-version (string-to-number (match-string 1)))
+ (status-code (string-to-number (match-string 2)))
+ (headers (plz--headers))
+ (coding-system (or (plz--coding-system headers) 'utf-8)))
+ (plz--narrow-to-body)
+ (decode-coding-region (point) (point-max) coding-system)
+ (make-plz-response
+ :version http-version
+ :status status-code
+ :headers headers
+ :body (buffer-string)))))
+
+(defun plz--coding-system (&optional headers)
+ "Return coding system for HTTP response in current buffer.
+HEADERS may optionally be an alist of parsed HTTP headers to
+refer to rather than the current buffer's unparsed headers."
+ (let* ((headers (or headers (plz--headers)))
+ (content-type (alist-get "Content-Type" headers nil nil #'string=)))
+ (when content-type
+ (coding-system-from-name content-type))))
+
+(defun plz--headers ()
+ "Return headers alist for HTTP response in current buffer"
+ (save-excursion
+ (goto-char (point-min))
+ (forward-line 1)
+ (let ((limit (save-excursion
+ (re-search-forward "^\r\n" nil)
+ (point))))
+ (cl-loop while (re-search-forward (rx bol (group (1+ (not (in ":"))))
":" (1+ blank)
+ (group (1+ (not (in "\r\n")))))
+ limit t)
+ collect (cons (match-string 1) (match-string 2))))))
+
+(defun plz--decode-body (coding-system)
+ "Decode body for HTTP response in current buffer.
+Return length of decoded text. Decodes with
+`decode-coding-region' according to CODING-SYSTEM."
+ (save-excursion
+ (goto-char (point-min))
+ ;; Skip headers.
+ (re-search-forward "^\r\n" nil)
+ (decode-coding-region (point) (point-max) coding-system)))
;;;; Footer
diff --git a/tests/test-plz.el b/tests/test-plz.el
index 7551a91e41..74f400fa30 100644
--- a/tests/test-plz.el
+++ b/tests/test-plz.el
@@ -55,18 +55,77 @@
;;;; Tests
-(ert-deftest plz-get-async nil
- (let* ((test-response)
- (process (plz-get "https://httpbin.org/get"
- :success (lambda (response)
- (setf test-response response)))))
- (cl-loop for i upto 100 ;; 10 seconds
- while (equal 'run (process-status process))
- do (sleep-for 0.1))
- (plz-test-get-response test-response)))
-
-(ert-deftest plz-get-sync nil
- (plz-test-get-response (plz-get "https://httpbin.org/get" :sync t)))
+;;;;; Async
+
+(ert-deftest plz-get-string nil
+ (should (let* ((test-string)
+ (process (plz-get "https://httpbin.org/get"
+ :as 'string
+ :then (lambda (string)
+ (setf test-string string)))))
+ (cl-loop for i upto 100 ;; 10 seconds
+ while (equal 'run (process-status process))
+ do (sleep-for 0.1))
+ (string-match "curl" test-string))))
+
+(ert-deftest plz-get-buffer nil
+ ;; The sentinel kills the buffer, so we get the buffer as a string.
+ (should (let* ((test-buffer-string)
+ (process (plz-get "https://httpbin.org/get"
+ :as 'buffer
+ :then (lambda (buffer)
+ (with-current-buffer buffer
+ (setf test-buffer-string
(buffer-string)))))))
+ (cl-loop for i upto 100 ;; 10 seconds
+ while (equal 'run (process-status process))
+ do (sleep-for 0.1))
+ (string-match "curl" test-buffer-string))))
+
+(ert-deftest plz-get-response nil
+ (should (let* ((test-response)
+ (process (plz-get "https://httpbin.org/get"
+ :as 'response
+ :then (lambda (response)
+ (setf test-response response)))))
+ (cl-loop for i upto 100 ;; 10 seconds
+ while (equal 'run (process-status process))
+ do (sleep-for 0.1))
+ (plz-test-get-response test-response))))
+
+(ert-deftest plz-get-json nil
+ (should (let* ((test-json)
+ (process (plz-get "https://httpbin.org/get"
+ :as #'json-read
+ :then (lambda (json)
+ (setf test-json json)))))
+ (cl-loop for i upto 100 ;; 10 seconds
+ while (equal 'run (process-status process))
+ do (sleep-for 0.1))
+ (let* ((headers (alist-get 'headers test-json))
+ (user-agent (alist-get 'User-Agent headers nil nil
#'equal)))
+ (string-match "curl" user-agent)))))
+
+;;;;; Sync
+
+(ert-deftest plz-get-sync-string nil
+ (should (string-match "curl" (plz-get-sync "https://httpbin.org/get"
+ :as 'string))))
+
+(ert-deftest plz-get-sync-response nil
+ (should (plz-test-get-response (plz-get-sync "https://httpbin.org/get"
+ :as 'response))))
+
+(ert-deftest plz-get-sync-json nil
+ (should (let* ((test-json (plz-get-sync "https://httpbin.org/get"
+ :as #'json-read))
+ (headers (alist-get 'headers test-json))
+ (user-agent (alist-get 'User-Agent headers nil nil #'equal)))
+ (string-match "curl" user-agent))))
+
+(ert-deftest plz-get-sync-buffer nil
+ ;; `buffer' is not a valid type for `plz-get-sync'.
+ (should-error (plz-get-sync "https://httpbin.org/get"
+ :as 'buffer)))
;;;; Footer
- [elpa] externals/plz 69d5bf74f6 77/81: Fix: (plz--coding-system) Alist key, (continued)
- [elpa] externals/plz 69d5bf74f6 77/81: Fix: (plz--coding-system) Alist key, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 1d3efc036c 78/81: Add: (plz) DELETE method, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 9e308f1833 80/81: Meta: Tidy headers, etc. for ELPA, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 9156de9c59 81/81: Release: 0.1, ELPA Syncer, 2022/05/11
- [elpa] externals/plz bca0b3112b 20/81: Tidy, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 2b51ecd6be 27/81: Meta: Add test.yml for GitHub Actions, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 4f74be6565 30/81: Notes: Add idea, ELPA Syncer, 2022/05/11
- [elpa] externals/plz fbdcd69950 09/81: Changes, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 4588884c11 10/81: Changes, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 2c19b7f11d 11/81: Changes, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 6a21c7e809 12/81: Significant changes,
ELPA Syncer <=
- [elpa] externals/plz 5b00a61ce3 15/81: Rename functions, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 370d0bbc1a 21/81: Tidy, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 971077e1d3 23/81: Tests, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 30e48b1e6a 22/81: Tidy, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 05f93b0b6b 25/81: Meta: Update Makefile, makem.sh, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 19a0110109 33/81: Notes: Add ToC, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 9a1b119eff 38/81: Meta: Ignore sandbox/, ELPA Syncer, 2022/05/11
- [elpa] externals/plz 0301272d8d 40/81: Add: plz-put, ELPA Syncer, 2022/05/11
- [elpa] externals/plz a5f22b23e1 42/81: Add: (plz), ELPA Syncer, 2022/05/11
- [elpa] externals/plz 430ceffd1d 43/81: Change: Handle killed processes, ELPA Syncer, 2022/05/11