branch: externals/llm commit c81b44943b2bfeac5114566ab5ad66640bad1e3e Author: Leo Gaskin <leo.gas...@le0.gs> Commit: GitHub <nore...@github.com>
Unvendor plz-event-source and plz-media-type (#87) This PR aims to unvendor plz-event-source and plz-media-type. According to <https://github.com/ahyatt/llm/pull/46#issuecomment-2387962790>, this should not cause any problems. I chose to define `0.1.0` as the minimum required versions, because the vendored packages are seemingly older, and it seems like established Elisp practice is to specify the "lowest version that is expected to reasonably function". --- llm.el | 2 +- plz-event-source.el | 485 ---------------------------------- plz-media-type.el | 728 ---------------------------------------------------- 3 files changed, 1 insertion(+), 1214 deletions(-) diff --git a/llm.el b/llm.el index 16c16baa90..0de9c695d2 100644 --- a/llm.el +++ b/llm.el @@ -4,7 +4,7 @@ ;; Author: Andrew Hyatt <ahy...@gmail.com> ;; Homepage: https://github.com/ahyatt/llm -;; Package-Requires: ((emacs "28.1") (plz "0.8")) +;; Package-Requires: ((emacs "28.1") (plz "0.8") (plz-event-source "0.1.1") (plz-media-type "0.2.1")) ;; Package-Version: 0.17.4 ;; SPDX-License-Identifier: GPL-3.0-or-later ;; diff --git a/plz-event-source.el b/plz-event-source.el deleted file mode 100644 index f5268fb4ff..0000000000 --- a/plz-event-source.el +++ /dev/null @@ -1,485 +0,0 @@ -;;; plz-event-source.el --- Plz Event Source -*- lexical-binding: t; -*- - -;; Copyright (C) 2019-2023 Free Software Foundation, Inc. - -;; Author: r0man <ro...@burningswell.com> -;; Maintainer: r0man <ro...@burningswell.com> -;; URL: https://github.com/r0man/plz-event-source.el - -;; This file is part of GNU Emacs. - -;; It is temporarily vendored within the llm library. Please DO NOT -;; depend on it! It is subject to change. Once we think this package -;; is stable, we will release it to GNU ELPA. If no serious issues -;; are found, we plan to do this in Q4 of 2024. - -;;; License: - -;; This program 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 of the License, or -;; (at your option) any later version. - -;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>. - -;;; Commentary: - -;; This library provides a parser and an event source implementation -;; for the Server Sent Event (SSE) protocol. - -;; See: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events - -;;; Code: - -(require 'cl-lib) -(require 'eieio) -(require 'plz) -(require 'plz-media-type) -(require 'rx) - -;; Event - -(defclass plz-event-source-event () - ((data - :accessor plz-event-source-event-data - :initarg :data - :initform nil - :documentation "The event data.") - (last-event-id - :accessor plz-event-source-event-last-event-id - :initarg :last-event-id - :initform nil - :documentation "The last event id." - :type (or null string)) - (origin - :accessor plz-event-source-event-origin - :initarg :origin - :initform nil - :documentation "The event origin." - :type (or null string)) - (type - :accessor plz-event-source-event-type - :initarg :type - :initform 'message - :documentation "The event type." - :type symbol)) - "The server sent event class.") - -;; Parser - -(defclass plz-event-source-parser () - ((buffer - :documentation "The name of the buffer to read events from." - :initarg :buffer - :type string) - (events - :initarg :events - :initform nil - :documentation "The queue of events to dispatch." - :type (list-of plz-event-source-event)) - (data-buffer - :initarg :data-buffer - :initform "" - :documentation "Data buffer." - :type string) - (event-type-buffer - :initarg :event-type-buffer - :initform "" - :documentation "Event type buffer." - :type string) - (last-event-id - :initarg :last-event-id - :initform "" - :documentation "Last event id." - :type string) - (last-event-id-buffer - :initarg :last-event-id-buffer - :initform "" - :documentation "Last event id buffer." - :type string) - (position - :initarg :position - :initform 0 - :type integer - :documentation "The position in the buffer." - :type integer)) - "The server sent event stream parser.") - -(defconst plz-event-source-parser--end-of-line-regexp - (rx (or "\r\n" "\n" "\r")) - "Regular expression matching the end of a line.") - -(defconst plz-event-source-parser--line-regexp - (rx (* not-newline) (or "\r\n" "\n" "\r")) - "Regular expression matching a line of the event source stream.") - -(defun plz-event-source-parser--parse-bom (line) - "Parse the Byte Order Mark (BOM) from LINE." - (if (string-prefix-p "\uFEFF" line) - (substring line 1) - line)) - -(defun plz-event-source-parser--looking-at-line-p () - "Return non-nil if the current line matches the event source line regexp." - (looking-at plz-event-source-parser--line-regexp)) - -(defun plz-event-source-parser--parse-line () - "Return non-nil if the current line matches the event source line regexp." - (when (looking-at plz-event-source-parser--line-regexp) - (string-trim-right (delete-and-extract-region (match-beginning 0) (match-end 0)) - plz-event-source-parser--end-of-line-regexp))) - -(defun plz-event-source-parser--dispatch-event (parser) - "Dispatch an event from PARSER to registered listeners." - (with-slots (data-buffer event-type-buffer events last-event-id last-event-id-buffer) parser - (setf last-event-id last-event-id-buffer) - (if (string-empty-p data-buffer) - (setf data-buffer "" - event-type-buffer "") - (progn - (setf data-buffer (string-trim-right data-buffer "\n")) - (let ((event (plz-event-source-event - :data data-buffer - :last-event-id (unless (string-blank-p last-event-id) - last-event-id) - :origin (buffer-name) - :type (if (string-blank-p event-type-buffer) - 'message - (intern event-type-buffer))))) - (setf data-buffer "" - event-type-buffer "") - (setf events (cons event events)) - event))))) - -(defun plz-event-source-parser--process-event (parser field value) - "Process the FIELD and VALUE from PARSER as a event." - (ignore field) - (with-slots (event-type-buffer) parser - (setf event-type-buffer value))) - -(defun plz-event-source-parser--process-data (parser field value) - "Process the FIELD and VALUE from PARSER as data." - (ignore field) - (with-slots (data-buffer) parser - (setf data-buffer (concat data-buffer value "\n")))) - -(defun plz-event-source-parser--process-id (parser field value) - "Process the FIELD and VALUE from PARSER as event id." - (ignore field) - (unless (string-match "\u0000" value) - (with-slots (last-event-id-buffer) parser - (setf last-event-id-buffer value)))) - -(defun plz-event-source--process-field (parser field value) - "Process the FIELD and VALUE from PARSER." - (cond ((equal "event" field) - (plz-event-source-parser--process-event parser field value)) - ((equal "data" field) - (plz-event-source-parser--process-data parser field value)) - ((equal "id" field) - (plz-event-source-parser--process-id parser field value)))) - -(defun plz-event-source-parser--process-line (parser line) - "Parse a LINE of the event stream PARSER and dispatch events." - (cond ((string-prefix-p ":" line)) - ((string-blank-p line) - (plz-event-source-parser--dispatch-event parser)) - ((string-match ":" line) - (let ((field (substring line 0 (match-beginning 0))) - (value (substring line (match-end 0)))) - (plz-event-source--process-field parser field - (if (string-prefix-p " " value) - (substring value 1) - value)))) - (t (plz-event-source--process-field parser line "")))) - -(defun plz-event-source-parser--insert (parser string) - "Insert STRING into the buffer of the event PARSER." - (with-slots (buffer events position) parser - (with-current-buffer (get-buffer buffer) - (insert string) - (while (plz-event-source-parser-parse-line parser)) - events))) - -(defun plz-event-source-parser--end-of-headers () - "Return the end of headers position in the current buffer." - (save-excursion - (goto-char (point-min)) - (re-search-forward plz-http-end-of-headers-regexp nil t) - (point))) - -(defun plz-event-source-parser-parse-line (parser) - "Parse a line from the event stream in the PARSER buffer." - (with-slots (buffer position) parser - (with-current-buffer buffer - (save-excursion - (goto-char position) - (when-let (line (plz-event-source-parser--parse-line)) - (setf position (point)) - (plz-event-source-parser--process-line parser line) - line))))) - -(defun plz-event-source-parser-parse (parser) - "Parse the event stream in the the PARSER buffer." - (with-slots (buffer handlers) parser - (with-current-buffer (get-buffer buffer) - (goto-char (point-min)) - (while (not (eobp)) - (when-let (line (plz-event-source-parser--parse-line)) - (plz-event-source-parser--process-line parser line)))))) - -;; Event Source - -(defclass plz-event-source () - ((errors - :initarg :errors - :documentation "The errors of the event source.") - (handlers - :initarg :handlers - :initform nil - :documentation "Registered event handlers.") - (last-event-id - :initarg :last-event-id - :initform "" - :documentation "Last event id.") - (options - :initarg :options - :documentation "The url of the event source." - :type list) - (ready-state - :documentation "The ready state of the event source." - :initarg :ready-state - :initform 'closed - :type (member closed connecting open)) - (url - :initarg :url - :documentation "The url of the event source." - :type (or null string))) - "The server sent event source class.") - -(cl-defgeneric plz-event-source-open (source) - "Open the event SOURCE.") - -(cl-defgeneric plz-event-source-close (source) - "Close the event SOURCE.") - -(cl-defgeneric plz-event-source--insert (source data) - "Insert DATA into the event SOURCE buffer, parse and dispatch events.") - -(defun plz-event-source-add-listener (source type listener) - "Add an event LISTENER for event TYPE to the event SOURCE." - (with-slots (handlers) source - (setf handlers (append handlers (list (cons type listener)))) - source)) - -(defun plz-event-source-remove-listener (source type listener) - "Remove an event LISTENER for event TYPE from the event SOURCE." - (with-slots (handlers) source - (setf handlers (cl-remove-if (lambda (pair) - (and (eq (car pair) type) - (eq (cdr pair) listener))) - handlers)) - source)) - -(defun plz-event-source--dispatch-event (source event) - "Dispatch the EVENT to the listeners of event SOURCE." - (with-slots (handlers) source - (dolist (pair handlers) - (when (equal (car pair) (oref event type)) - (let ((timer (timer-create))) - (timer-set-time timer (current-time)) - (timer-set-function timer - (lambda (handler event) - (with-temp-buffer - (funcall handler event))) - (list (cdr pair) event)) - (timer-activate timer)))))) - -(defun plz-event-source--dispatch-events (source events) - "Dispatch the EVENTS to the listeners of event SOURCE." - (dolist (event (reverse events)) - (plz-event-source--dispatch-event source event))) - -;; Buffer event source - -(defclass plz-event-source-buffer (plz-event-source) - ((buffer - :initarg :buffer - :documentation "The event source buffer." - :type string) - (parser - :initarg :parser - :documentation "The event source parser." - :type (or null plz-event-source-parser))) - "A server sent event source using curl for HTTP.") - -(cl-defmethod plz-event-source--insert ((source plz-event-source-buffer) data) - "Insert DATA into the event SOURCE buffer, parse and dispatch events." - (with-slots (parser) source - (plz-event-source-parser--insert parser data) - (with-slots (events) parser - (plz-event-source--dispatch-events source events) - (setf events nil)))) - -(defun plz-event-source--skip-proxy-headers () - "Skip proxy headers in current buffer." - (when (looking-at plz-http-response-status-line-regexp) - (let* ((status-code (string-to-number (match-string 2))) - (reason-phrase (match-string 3))) - (when (and (equal 200 status-code) - (equal "Connection established" reason-phrase)) - (re-search-forward "\r\n\r\n" nil t))))) - -(defun plz-event-source--skip-redirect-headers () - "Skip HTTP redirect headers in current buffer." - (when (and (looking-at plz-http-response-status-line-regexp) - (member (string-to-number (match-string 2)) '(301 302 303 307 308))) - (re-search-forward "\r\n\r\n" nil t))) - -(defun plz-event-source--buffer-start-position () - "Return the start position of the current buffer." - (save-excursion - (goto-char (point-min)) - (plz-event-source--skip-proxy-headers) - (while (plz-event-source--skip-redirect-headers)) - (re-search-forward plz-http-end-of-headers-regexp nil t) - (point))) - -(cl-defmethod plz-event-source-open ((source plz-event-source-buffer)) - "Open a connection to the URL of the event SOURCE." - (with-slots (buffer errors options ready-state parser) source - (with-current-buffer (get-buffer-create buffer) - (let ((event (plz-event-source-event :type 'open))) - (setf ready-state 'connecting) - (setf parser (plz-event-source-parser - :buffer buffer - :position (plz-event-source--buffer-start-position))) - (setf ready-state 'open) - (plz-event-source--dispatch-event source event) - source)))) - -(cl-defmethod plz-event-source-close ((source plz-event-source-buffer)) - "Close the connection of the event SOURCE." - (with-slots (buffer ready-state) source - (let ((event (plz-event-source-event :type 'close))) - (setf ready-state 'closed) - (plz-event-source--dispatch-event source event) - source))) - -(defclass plz-event-source-http (plz-event-source) - ((process - :initarg :process - :documentation "The process of the event source." - :type (or null process)) - (response - :initarg :response - :documentation "The plz HTTP response." - :type (or null plz-response))) - "A server sent event source using curl for HTTP.") - -(defun plz-event-source--media-types (source) - "Return the media types of the event SOURCE." - (with-slots (handlers) source - (let ((media-type (plz-event-source:text/event-stream :events handlers))) - (cons (cons 'text/event-stream media-type) plz-media-types)))) - -(cl-defmethod plz-event-source-open ((source plz-event-source-http)) - "Open a connection to the URL of the event SOURCE." - (with-slots (errors options process ready-state response url) source - (setf ready-state 'connecting) - (setf response nil) - (setf process (plz-media-type-request - (or (alist-get 'method options) 'get) url - :as `(media-types ,(plz-event-source--media-types source)) - :body (alist-get 'body options) - :headers (alist-get 'headers options) - :then (lambda (object) - (setf response object)) - :else (lambda (object) - (setf errors (push object errors)) - (setf response (plz-error-response object))) - :finally (lambda () - (setf ready-state 'closed)))) - source)) - -(cl-defmethod plz-event-source-close ((source plz-event-source-http)) - "Close the connection of the event SOURCE." - (with-slots (process ready-state) source - (delete-process process) - (setf ready-state 'closed))) - -;; Content Type: text/event-stream - -(defclass plz-event-source:text/event-stream (plz-media-type:application/octet-stream) - ((coding-system :initform 'utf-8) - (type :initform 'text) - (subtype :initform 'event-stream) - (events :documentation "Association list from event type to handler." - :initarg :events - :initform nil - :type list)) - "Media type class that handles the processing of HTTP responses -in the server sent events format. The HTTP response is processed -in a streaming way. The :events slot of the class can be set to -an association list from event type symbol to a handler function. -Whenever a new event is parsed and emitted the handler for the -corresponding event type will be called with two arguments, an -instance of the underlying event source class and an event. The -body slot of the plz-response structure passed to the THEN and -ELSE callbacks will always be set to nil.") - -(defvar-local plz-event-source--current nil - "The event source of the current buffer.") - -(cl-defmethod plz-media-type-else ((_ plz-event-source:text/event-stream) error) - "Transform the ERROR into a format suitable for MEDIA-TYPE." - (let* ((source plz-event-source--current) - (event (plz-event-source-event :type 'error :data error))) - (plz-event-source-close source) - (plz-event-source--dispatch-event source event) - error)) - -(cl-defmethod plz-media-type-process ((media-type plz-event-source:text/event-stream) process chunk) - "Process the CHUNK according to MEDIA-TYPE using PROCESS." - (unless plz-event-source--current - (let* ((response (make-plz-response - :status (plz-response-status chunk) - :headers (plz-response-headers chunk))) - (source (plz-event-source-open - (plz-event-source-buffer - :buffer (buffer-name (process-buffer process)) - :handlers (seq-map - (lambda (pair) - (let ((type (car pair)) - (handler (cdr pair))) - (cond - ((equal 'open type) - (cons type (lambda (event) - (setf (oref event data) response) - (funcall handler event)))) - ((equal 'close type) - (cons type (lambda (event) - (setf (oref event data) response) - (funcall handler event)))) - (t pair)))) - (oref media-type events)))))) - (setq-local plz-event-source--current source))) - (let ((body (plz-media-type-decode-coding-string media-type (plz-response-body chunk)))) - (plz-event-source--insert plz-event-source--current body) - (set-marker (process-mark process) (point)))) - -(cl-defmethod plz-media-type-then ((media-type plz-event-source:text/event-stream) response) - "Transform the RESPONSE into a format suitable for MEDIA-TYPE." - (plz-event-source-close plz-event-source--current) - (cl-call-next-method media-type response) - (setf (plz-response-body response) nil) - response) - -(provide 'plz-event-source) -;;; plz-event-source.el ends here diff --git a/plz-media-type.el b/plz-media-type.el deleted file mode 100644 index 1ff4d3012a..0000000000 --- a/plz-media-type.el +++ /dev/null @@ -1,728 +0,0 @@ -;;; plz-media-type.el --- Plz Media Types -*- lexical-binding: t; -*- - -;; Copyright (C) 2019-2023 Free Software Foundation, Inc. - -;; Author: r0man <ro...@burningswell.com> -;; Maintainer: r0man <ro...@burningswell.com> -;; URL: https://github.com/r0man/plz-media-type.el - -;; This file is part of GNU Emacs. - -;; It is temporarily vendored within the llm library. Please DO NOT -;; depend on it! It is subject to change. Once we think this package -;; is stable, we will release it to GNU ELPA. If no serious issues -;; are found, we plan to do this in Q4 of 2024. - -;;; License: - -;; This program 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 of the License, or -;; (at your option) any later version. - -;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>. - -;;; Commentary: - -;; This library provides enhanced handling of MIME types for HTTP -;; requests within Emacs. It utilizes the 'plz' library for -;; networking calls, extending it to process responses based on the -;; Content-Type header. This library defines various classes and -;; methods for parsing and processing standard MIME types, including -;; JSON, XML, HTML, and binary data. It allows for extensible -;; processing of additional types through subclassing. - -;;; Code: - -;;;; Requirements - -(require 'cl-lib) -(require 'eieio) -(require 'plz) - -(defclass plz-media-type () - ((coding-system - :documentation "The coding system to use for the media type." - :initarg :coding-system - :initform nil - :type (or null symbol)) - (type - :documentation "The media type." - :initarg :type - :type symbol) - (subtype - :documentation "The media subtype." - :initarg :subtype - :type symbol) - (parameters - :documentation "The parameters of the media type." - :initarg :parameters - :initform nil - :type list)) - "A class that hold information about the type, subtype and -parameters of a media type. It is meant to be sub-classed to -handle the processing of different media types and supports the -processing of streaming and non-streaming HTTP responses. The -response will be decoded with the coding-system of the charset -parameter in the content type header, or the coding-sytem of the -media type. If the coding system of a media type is nil, the -response will not be decoded.") - -(cl-defgeneric plz-media-type-else (media-type error) - "Transform and handle the ERROR according to MEDIA-TYPE.") - -(cl-defgeneric plz-media-type-then (media-type response) - "Transform and handle the RESPONSE according to MEDIA-TYPE.") - -(cl-defgeneric plz-media-type-process (media-type process chunk) - "Process the CHUNK according to MEDIA-TYPE using PROCESS.") - -(cl-defmethod plz-media-type-else ((_ (eql nil)) error) - "Transform and handle the ERROR according to MEDIA-TYPE." - error) - -(defun plz-media-type-charset (media-type) - "Return the character set of the MEDIA-TYPE." - (with-slots (parameters) media-type - (alist-get "charset" parameters nil nil #'equal))) - -(defun plz-media-type-coding-system (media-type) - "Return the coding system of the MEDIA-TYPE." - (if-let (charset (plz-media-type-charset media-type)) - (coding-system-from-name charset) - (oref media-type coding-system))) - -(defun plz-media-type-decode-coding-string (media-type string) - "Decode STRING which is encoded in the coding system of MEDIA-TYPE." - (if-let (coding-system (plz-media-type-coding-system media-type)) - (decode-coding-string string coding-system) - string)) - -(defun plz-media-type-name (media-type) - "Return the name of the MEDIA-TYPE as a string." - (with-slots (type subtype) media-type - (format "%s/%s" type subtype))) - -(defun plz-media-type-symbol (media-type) - "Return the name of the MEDIA-TYPE as a symbol." - (intern (plz-media-type-name media-type))) - -(defun plz-media-type-of-response (media-types response) - "Lookup the content type of RESPONSE in MEDIA-TYPES." - (let ((media-type (plz-media-type--content-type response))) - (clone (plz-media-type--find media-types media-type) - :parameters (oref media-type parameters)))) - -(defun plz-media-type--parse (header) - "Parse the Content-Type HEADER and return a `plz-media-type' instance." - (unless (or (null header) (string-blank-p header)) - (let* ((components (split-string header ";")) - (mime-type (string-trim (car components))) - (parameters-list (cdr components)) - (parameters-alist '())) - (dolist (param parameters-list parameters-alist) - (let* ((key-value (split-string param "=")) - (key (string-trim (car key-value))) - (value (string-trim (cadr key-value) "\""))) - (setq parameters-alist (cons (cons key value) parameters-alist)))) - (let ((parts (split-string mime-type "/"))) - (plz-media-type - :type (intern (car parts)) - :subtype (intern (cadr parts)) - :parameters (nreverse parameters-alist)))))) - -(defun plz-media-type--content-type (response) - "Return the content type header of RESPONSE, or nil if it's not set." - (let ((headers (plz-response-headers response))) - (when-let (header (cdr (assoc 'content-type headers))) - (plz-media-type--parse header)))) - -(defun plz-media-type--find (media-types media-type) - "Lookup the MEDIA-TYPE in MEDIA-TYPES." - (or (alist-get (plz-media-type-symbol media-type) media-types) - (alist-get t media-types) - (plz-media-type:application/octet-stream))) - -(defvar-local plz-media-type--current nil - "The media type of the process buffer.") - -(defvar-local plz-media-type--position nil - "The position in the process buffer.") - -(defvar-local plz-media-type--response nil - "The response of the process buffer.") - -(defun plz-media-type--schedule (handler messages) - "Schedule MESSAGES to be processed with the HANDLER on a timer." - (cl-loop with time = (current-time) - for msg = (pop messages) while msg - do (let ((timer (timer-create))) - (timer-set-time timer time) - (timer-set-function timer - (lambda (handler msg) - (with-temp-buffer (funcall handler msg))) - (list handler msg)) - (timer-activate timer)))) - -(defun plz-media-type--skip-proxy-headers () - "Skip proxy headers in current buffer." - (when (looking-at plz-http-response-status-line-regexp) - (let* ((status-code (string-to-number (match-string 2))) - (reason-phrase (match-string 3))) - (when (and (equal 200 status-code) - (equal "Connection established" reason-phrase)) - (re-search-forward "\r\n\r\n" nil t))))) - -(defun plz-media-type--skip-redirect-headers () - "Skip HTTP redirect headers in current buffer." - (when (and (looking-at plz-http-response-status-line-regexp) - (member (string-to-number (match-string 2)) '(301 302 303 307 308))) - (re-search-forward "\r\n\r\n" nil t))) - -(defun plz-media-type--parse-headers () - "Parse the HTTP response headers in the current buffer." - (forward-line 1) - (let ((limit (save-excursion - (re-search-forward plz-http-end-of-headers-regexp 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 (intern (downcase (match-string 1))) (match-string 2))))) - -(cl-defun plz-media-type--parse-response () - "Parse the response in the current buffer." - (when (re-search-forward plz-http-end-of-headers-regexp nil t) - (goto-char (point-min)) - (plz-media-type--skip-proxy-headers) - (while (plz-media-type--skip-redirect-headers)) - (let ((start-of-response (point))) - (when (re-search-forward plz-http-end-of-headers-regexp nil t) - (let ((end-of-headers (point))) - (goto-char start-of-response) - (when (looking-at plz-http-response-status-line-regexp) - (prog1 (make-plz-response - :version (string-to-number (match-string 1)) - :status (string-to-number (match-string 2)) - :headers (plz-media-type--parse-headers) - :body (buffer-substring end-of-headers (point-max))) - (goto-char end-of-headers)))))))) - -(defun plz-media-type-process-filter (process media-types string) - "The process filter that handles different content types. - -PROCESS is the process. - -MEDIA-TYPES is an association list from media type to an -instance of a content type class. - -STRING which is output just received from the process." - (when (buffer-live-p (process-buffer process)) - (with-current-buffer (process-buffer process) - (let ((moving (= (point) (process-mark process)))) - (if-let (media-type plz-media-type--current) - (let ((response plz-media-type--response)) - (setf (plz-response-body response) string) - (plz-media-type-process media-type process response)) - (progn - (save-excursion - (goto-char (process-mark process)) - (insert string) - (set-marker (process-mark process) (point))) - (goto-char (point-min)) - (when-let (chunk (plz-media-type--parse-response)) - (delete-region (point) (point-max)) - (let ((media-type (plz-media-type-of-response media-types chunk))) - (setq-local plz-media-type--current media-type) - (setq-local plz-media-type--response - (make-plz-response - :headers (plz-response-headers chunk) - :status (plz-response-status chunk) - :version (plz-response-version chunk))) - (when-let (body (plz-response-body chunk)) - (when (> (length body) 0) - (setf (plz-response-body chunk) body) - (set-marker (process-mark process) (point)) - (plz-media-type-process media-type process chunk))))))) - (when moving - (goto-char (process-mark process))))))) - -;; Content Type: application/octet-stream - -(defclass plz-media-type:application/octet-stream (plz-media-type) - ((type :initform 'application) - (subtype :initform 'octet-stream)) - "Media type class that handles the processing of octet stream -HTTP responses. The media type sets the body slot of the -plz-response structure to the unmodified value of the HTTP response -body. It is used as the default media type processor.") - -(cl-defmethod plz-media-type-else - ((media-type plz-media-type:application/octet-stream) error) - "Transform the ERROR into a format suitable for MEDIA-TYPE." - (when-let (response (plz-error-response error)) - (setf (plz-error-response error) (plz-media-type-then media-type response))) - error) - -(cl-defmethod plz-media-type-then - ((media-type plz-media-type:application/octet-stream) response) - "Transform the RESPONSE into a format suitable for MEDIA-TYPE." - (ignore media-type) - (setf (plz-response-body response) (buffer-string)) - response) - -(cl-defmethod plz-media-type-process - ((media-type plz-media-type:application/octet-stream) process chunk) - "Process the CHUNK according to MEDIA-TYPE using PROCESS." - (ignore media-type) - (save-excursion - (goto-char (process-mark process)) - (insert (plz-media-type-decode-coding-string media-type (plz-response-body chunk))) - (set-marker (process-mark process) (point)))) - -;; Content Type: application/json - -(defclass plz-media-type:application/json (plz-media-type:application/octet-stream) - ((coding-system :initform 'utf-8) - (subtype :initform 'json) - (array-type - :documentation "Specifies which Lisp type is used to represent arrays. It can be -`array' (the default) or `list'." - :initarg :array-type - :initform 'array - :type symbol) - (false-object - :documentation "Specifies which object to use to represent a JSON false value. It -defaults to `:json-false'." - :initarg :false-object - :initform :json-false) - (null-object - :documentation "Specifies which object to use to represent a JSON null value. It -defaults to `nil`." - :initarg :null-object - :initform nil) - (object-type - :documentation "Specifies which Lisp type is used to represent objects. It can -be `hash-table', `alist' (the default) or `plist'." - :initarg :object-type - :initform 'alist - :type symbol)) - "Media type class that handles the processing of HTTP responses -in the JSON format. The HTTP response is processed in a -non-streaming way. After the response has been received, the -body of the plz-response structure is set to the result of parsing -the HTTP response body with the `json-parse-buffer' function. -The arguments to the `json-parse-buffer' can be customized by -making an instance of this class and setting its slots -accordingly.") - -(defun plz-media-type--parse-json-object (media-type) - "Parse the JSON object in the current buffer according to MEDIA-TYPE." - (with-slots (array-type false-object null-object object-type) media-type - (json-parse-buffer :array-type array-type - :false-object false-object - :null-object null-object - :object-type object-type)) ) - -(cl-defmethod plz-media-type-then - ((media-type plz-media-type:application/json) response) - "Transform the RESPONSE into a format suitable for MEDIA-TYPE." - (setf (plz-response-body response) (plz-media-type--parse-json-object media-type)) - response) - -;; Content Type: application/json (array of objects) - -(defclass plz-media-type:application/json-array (plz-media-type:application/json) - ((handler - :documentation "Function that will be called for each object in the JSON array." - :initarg :handler - :type (or function symbol))) - "Media type class that handles the processing of HTTP responses -in a JSON format that assumes that the object at the top level is -an array. The HTTP response is processed in a streaming way. -Each object in the top level array will be parsed with the -`json-parse-buffer' function. The function in the :handler slot -will be called each time a new object arrives. The body slot of -the plz-response structure passed to the THEN and ELSE callbacks -will always be set to nil.") - -(defun plz-media-type:application/json-array--parse-next (media-type) - "Parse a single line of the newline delimited JSON MEDIA-TYPE." - (let ((begin (point))) - (cond ((looking-at "\\[") - (forward-char 1) - (cons :array-start (buffer-substring begin (point)))) - ((looking-at ",") - (forward-char 1) - (cons :comma (buffer-substring begin (point)))) - ((looking-at "\n") - (forward-char 1) - (cons :line-feed (buffer-substring begin (point)))) - ((looking-at "\r") - (forward-char 1) - (cons :carriage-return (buffer-substring begin (point)))) - ((looking-at "\\]") - (forward-char 1) - (cons :array-end (buffer-substring begin (point)))) - ((not (eobp)) - (condition-case nil - (cons :array-element (plz-media-type--parse-json-object media-type)) - (json-error)))))) - -(defun plz-media-type:application/json-array--consume-next (media-type) - "Parse a single line of the newline delimited JSON MEDIA-TYPE." - (let ((begin (point))) - (prog1 (plz-media-type:application/json-array--parse-next media-type) - (delete-region begin (point)) - (setq-local plz-media-type--position (point))))) - -(defun plz-media-type:application/json-array--parse-stream (media-type) - "Parse all lines of the newline delimited JSON MEDIA-TYPE in the PROCESS buffer." - (let ((objects)) - (unless plz-media-type--position - (setq-local plz-media-type--position (point))) - (goto-char plz-media-type--position) - (when-let (result (plz-media-type:application/json-array--consume-next media-type)) - (while result - (when (equal :array-element (car result)) - (push (cdr result) objects)) - (setq result (plz-media-type:application/json-array--consume-next media-type)))) - objects)) - -(cl-defmethod plz-media-type-process - ((media-type plz-media-type:application/json-array) process chunk) - "Process the CHUNK according to MEDIA-TYPE using PROCESS." - (cl-call-next-method media-type process chunk) - (with-slots (handler) media-type - (let ((objects (plz-media-type:application/json-array--parse-stream media-type))) - (set-marker (process-mark process) (point-max)) - (plz-media-type--schedule handler objects)))) - -(cl-defmethod plz-media-type-then - ((media-type plz-media-type:application/json-array) response) - "Transform the RESPONSE into a format suitable for MEDIA-TYPE." - (ignore media-type) - (setf (plz-response-body response) nil) - response) - -;; Content Type: application/x-ndjson - -(defclass plz-media-type:application/x-ndjson (plz-media-type:application/json) - ((subtype :initform 'x-ndjson) - (handler - :documentation "Function that will be called for each line that contains a JSON object." - :initarg :handler - :initform nil - :type (or function null symbol))) - "Media type class that handles the processing of HTTP responses -in a JSON format that assumes that the object at the top level is -an array. The HTTP response is processed in a streaming way. -Each object in the top level array will be parsed with the -`json-parse-buffer' function. The function in the :handler slot -will be called each time a new object arrives. The body slot of -the plz-response structure passed to the THEN and ELSE callbacks -will always be set to nil.") - -(defconst plz-media-type:application/x-ndjson--line-regexp - (rx (* not-newline) (or "\r\n" "\n" "\r")) - "Regular expression matching a JSON Object line.") - -(defun plz-media-type:application/x-ndjson--parse-line (media-type) - "Parse a single line of the newline delimited JSON MEDIA-TYPE." - (when (looking-at plz-media-type:application/x-ndjson--line-regexp) - (prog1 (plz-media-type--parse-json-object media-type) - (delete-region (match-beginning 0) (match-end 0))))) - -(defun plz-media-type:application/x-ndjson--parse-stream (media-type) - "Parse all lines of the newline delimited JSON MEDIA-TYPE in the PROCESS buffer." - (with-slots (handler) media-type - (let (objects) - (unless plz-media-type--position - (setq-local plz-media-type--position (point))) - (goto-char plz-media-type--position) - (when-let (object (plz-media-type:application/x-ndjson--parse-line media-type)) - (while object - (setq-local plz-media-type--position (point)) - (push object objects) - (setq object (plz-media-type:application/x-ndjson--parse-line media-type)))) - objects))) - -(cl-defmethod plz-media-type-process - ((media-type plz-media-type:application/x-ndjson) process chunk) - "Process the CHUNK according to MEDIA-TYPE using PROCESS." - (cl-call-next-method media-type process chunk) - (with-slots (handler) media-type - (let ((objects (plz-media-type:application/x-ndjson--parse-stream media-type))) - (plz-media-type--schedule handler objects)))) - -(cl-defmethod plz-media-type-then - ((media-type plz-media-type:application/x-ndjson) response) - "Transform the RESPONSE into a format suitable for MEDIA-TYPE." - (ignore media-type) - (setf (plz-response-body response) nil) - response) - -;; Content Type: application/xml - -(defclass plz-media-type:application/xml (plz-media-type:application/octet-stream) - ((coding-system :initform 'utf-8) - (subtype :initform 'xml)) - "Media type class that handles the processing of HTTP responses -in the XML format. The HTTP response is processed in a -non-streaming way. After the response has been received, the -body of the plz-response structure is set to the result of parsing -the HTTP response body with the `libxml-parse-html-region' -function.") - -(cl-defmethod plz-media-type-then - ((media-type plz-media-type:application/xml) response) - "Transform the RESPONSE into a format suitable for MEDIA-TYPE." - (with-slots (array-type false-object null-object object-type) media-type - (setf (plz-response-body response) - (libxml-parse-html-region (point-min) (point-max) nil)) - response)) - -;; Content Type: text/html - -(defclass plz-media-type:text/html (plz-media-type:application/xml) - ((type :initform 'text) - (subtype :initform 'html)) - "Media type class that handles the processing of HTTP responses -in the HTML format. The HTTP response is processed in a -non-streaming way. After the response has been received, the -body of the plz-response structure is set to the result of parsing -the HTTP response body with the `libxml-parse-html-region' -function.") - -(defclass plz-media-type:text/xml (plz-media-type:application/xml) - ((coding-system :initform 'us-ascii) - (type :initform 'text) - (subtype :initform 'xml)) - "Media type class that handles the processing of HTTP responses -in the HTML format. The HTTP response is processed in a -non-streaming way. After the response has been received, the -body of the plz-response structure is set to the result of -parsing the HTTP response body with the -`libxml-parse-html-region' function.") - -(defvar plz-media-types - `((application/json . ,(plz-media-type:application/json)) - (application/octet-stream . ,(plz-media-type:application/octet-stream)) - (application/xml . ,(plz-media-type:application/xml)) - (text/html . ,(plz-media-type:text/html)) - (text/xml . ,(plz-media-type:text/xml)) - (t . ,(plz-media-type:application/octet-stream))) - "Association list from media type to content type.") - -(defun plz-media-type--handle-sync-http-error (error media-types) - "Handle the synchronous HTTP ERROR using MEDIA-TYPES." - (let* ((msg (cadr error)) - (plzerror (caddr error))) - (signal (car error) - (cond - ((plz-error-response plzerror) - (let ((response (plz-error-response plzerror))) - (if-let (media-type (plz-media-type-of-response media-types response)) - (list msg (with-temp-buffer - (when-let (body (plz-response-body response)) - (insert body) - (goto-char (point-min))) - (plz-media-type-else media-type plzerror))) - (cdr error)))))))) - -(defun plz-media-type--handle-sync-error (error media-types) - "Handle the synchronous ERROR using MEDIA-TYPES." - (cond - ((eq 'plz-http-error (car error)) - (plz-media-type--handle-sync-http-error error media-types)) - (t (signal (car error) (cdr error))))) - -(defun plz-media-type--handle-sync-response (buffer) - "Handle a successful synchronous response in BUFFER." - (unwind-protect - (with-current-buffer buffer - (plz-media-type-then plz-media-type--current plz-media-type--response)) - (when (buffer-live-p buffer) - (kill-buffer buffer)))) - -(cl-defun plz-media-type-request - (method - url - &rest rest &key headers body else finally noquery timeout - (as 'string) - (body-type 'text) - (connect-timeout plz-connect-timeout) - (decode t decode-s) - (then 'sync)) - "Request METHOD from URL with curl. - -This function works in a similar way as the `plz' function, with -the additional functionality of handling streaming and -non-streaming media types with the :as (media-types MEDIA-TYPES) -option. Setting a process :filter by the user is not supported. -Instead this function will always install its own process filter -that will process the response until the HTTP headers arrived. -Once the headers arrived it will hand over control to a media -type based on the content type header of the response. The media -type is responsible for processing the HTTP body. - -Return the curl process object or, for a synchronous request, the -selected result. - -HEADERS may be an alist of extra headers to send with the -request. - -BODY may be a string, a buffer, or a list like `(file FILENAME)' -to upload a file from disk. - -BODY-TYPE may be `text' to send BODY as text, or `binary' to send -it as binary. - -AS selects the kind of result to pass to the callback function -THEN, or the kind of result to return for synchronous requests. -It may be: - -- `buffer' to pass the response buffer, which will be narrowed to - the response body and decoded according to DECODE. - -- `binary' to pass the response body as an un-decoded string. - -- `string' to pass the response body as a decoded string. - -- `response' to pass a `plz-response' structure. - -- `file' to pass a temporary filename to which the response body - has been saved without decoding. - -- `(file FILENAME)' to pass FILENAME after having saved the - response body to it without decoding. FILENAME must be a - non-existent file; if it exists, it will not be overwritten, - and an error will be signaled. - -- `(media-types MEDIA-TYPES)' to handle the processing of the - response based on the Content-Type header. MEDIA-TYPES is an - association list from a content type symbol to an instance of a - `plz-media-type' class. The `plz-media-types' variable is - bound to an association list and can be used to handle some - commonly used formats such as JSON, HTML, XML. This list can - be used as a basis and is meant to be extended by users. If no - media type was found for a content type, it will be handled by - the default octet stream media type. When this option is used, - the THEN callback will always receive a plz-response structure as - argument, and the ELSE callback always a plz-error structure. The - plz-response structure will always have the status and header - slots set. The body slot depends on the media type - implementation. In the case for JSON, HTML, XML it will - contain the decoded response body. When receiving JSON for - example, it will be an Emacs Lisp association list. For - streaming responses like text/event-stream it will be set to - nil, and the events of the server sent events specification - will be dispatched to the handlers registered with the media - type instance. - -- A function, which is called in the response buffer with it - narrowed to the response body (suitable for, e.g. `json-read'). - -If DECODE is non-nil, the response body is decoded automatically. -For binary content, it should be nil. When AS is `binary', -DECODE is automatically set to nil. - -THEN is a callback function, whose sole argument is selected -above with AS; if the request fails and no ELSE function is -given (see below), the argument will be a `plz-error' structure -describing the error. Or THEN may be `sync' to make a -synchronous request, in which case the result is returned -directly from this function. - -ELSE is an optional callback function called when the request -fails (i.e. if curl fails, or if the HTTP response has a non-2xx -status code). It is called with one argument, a `plz-error' -structure. If ELSE is nil, a `plz-curl-error' or -`plz-http-error' is signaled when the request fails, with a -`plz-error' structure as the error data. For synchronous -requests, this argument is ignored. - -NOTE: In v0.8 of `plz', only one error will be signaled: -`plz-error'. The existing errors, `plz-curl-error' and -`plz-http-error', inherit from `plz-error' to allow applications -to update their code while using v0.7 (i.e. any `condition-case' -forms should now handle only `plz-error', not the other two). - -FINALLY is an optional function called without argument after -THEN or ELSE, as appropriate. For synchronous requests, this -argument is ignored. - -CONNECT-TIMEOUT and TIMEOUT are a number of seconds that limit -how long it takes to connect to a host and to receive a complete -response from a host, respectively. - -NOQUERY is passed to `make-process', which see. - -When the HTTP response is streamed, the buffering in the curl -output stream is turned off and the PROCESS-FILTER may be called -multiple times, depending on the size of the HTTP body. It is -the user's responsibility to understand and process each chunk, -and to construct the finalized response if necessary. There are -no guarantees regarding the chunk, such as being line-based or -not. -\(To silence checkdoc, we mention the internal argument REST.)" - ;; FIXME(v0.8): Remove the note about error changes from the docstring. - ;; FIXME(v0.8): Update error signals in docstring. - (declare (indent defun)) - (if-let (media-types (pcase as - (`(media-types ,media-types) - media-types))) - (let ((buffer)) - (condition-case error - (let* ((plz-curl-default-args (cons "--no-buffer" plz-curl-default-args)) - (result (plz method url - :as 'buffer - :body body - :body-type body-type - :connect-timeout connect-timeout - :decode decode - :else (lambda (error) - (setq buffer (current-buffer)) - (when (or (functionp else) (symbolp else)) - (funcall else (plz-media-type-else - plz-media-type--current - error)))) - :finally (lambda () - (unwind-protect - (when (functionp finally) - (funcall finally)) - (when (buffer-live-p buffer) - (kill-buffer buffer)))) - :headers headers - :noquery noquery - :filter (lambda (process chunk) - (plz-media-type-process-filter process media-types chunk)) - :timeout timeout - :then (if (symbolp then) - then - (lambda (_) - (setq buffer (current-buffer)) - (when (or (functionp then) (symbolp then)) - (funcall then (plz-media-type-then - plz-media-type--current - plz-media-type--response)))))))) - (cond ((bufferp result) - (plz-media-type--handle-sync-response result)) - ((processp result) - result) - (t (user-error "Unexpected response: %s" result)))) - ;; TODO: How to kill the buffer for sync requests that raise an error? - (plz-error (plz-media-type--handle-sync-error error media-types)))) - (apply #'plz (append (list method url) rest)))) - -;;;; Footer - -(provide 'plz-media-type) - -;;; plz-media-type.el ends here