diff options
author | Artur Malabarba <bruce.connor.am@gmail.com> | 2014-11-13 02:07:43 +0000 |
---|---|---|
committer | Artur Malabarba <bruce.connor.am@gmail.com> | 2014-11-13 02:07:43 +0000 |
commit | 6355c676f48506ecb730acb200085f7b211e08c4 (patch) | |
tree | 8173b74b21d1445997159adc9a5cca261d9c8892 | |
parent | 4cf7825918bfb60e2d1d1ce1dd342665f1161fa2 (diff) | |
parent | cfd909e8524c37ac3bc94bec56c4e577b2c33d2d (diff) |
Merge branch 'master' into sx-question-mode
-rw-r--r-- | .travis.yml | 1 | ||||
-rw-r--r-- | Makefile | 38 | ||||
-rw-r--r-- | stack-exchange.el | 30 | ||||
-rw-r--r-- | sx-auth.el | 23 | ||||
-rw-r--r-- | sx-cache.el | 12 | ||||
-rw-r--r-- | sx-encoding.el | 32 | ||||
-rw-r--r-- | sx-filter.el | 60 | ||||
-rw-r--r-- | sx-lto.el | 4 | ||||
-rw-r--r-- | sx-method.el | 47 | ||||
-rw-r--r-- | sx-network.el | 2 | ||||
-rw-r--r-- | sx-question-list.el | 31 | ||||
-rw-r--r-- | sx-question.el | 14 | ||||
-rw-r--r-- | sx-request.el | 182 | ||||
-rw-r--r-- | sx-time.el | 3 | ||||
-rw-r--r-- | sx.el | 82 | ||||
-rw-r--r-- | test/tests.el | 84 |
16 files changed, 379 insertions, 266 deletions
diff --git a/.travis.yml b/.travis.yml index 63f0cb6..ae882b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ env: - EVM_EMACS=emacs-24.1-bin - EVM_EMACS=emacs-24.2-bin - EVM_EMACS=emacs-24.3-bin + - EVM_EMACS=emacs-24.4-bin before_install: - sudo mkdir /usr/local/evm diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7b0b698 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +# This makefile runs the tests as Travis runs them. Be sure to test +# locally before you push if you are under the impression that the +# patch should work. This will cut down on the number of commits in +# the repository that, essentially, patch patches. +# +# To test Emacs 24.1, for example, use +# +# make 1 +# +# To test on all versions, of course, simply use +# +# make +# +# or +# +# make all +# + +VERSIONS = 1 2 3 4 + +all :: $(VERSIONS) + +$(VERSIONS) :: + evm install emacs-24.$@-bin --skip || true + evm use emacs-24.$@-bin + emacs --version + cask install + emacs --batch -L . -l ert -l test/tests.el -f ert-run-tests-batch-and-exit + +install_cask: + curl -fsSkL https://raw.github.com/cask/cask/master/go | python + +install_evm: + curl -fsSkL https://raw.github.com/rejeep/evm/master/go | bash + +# Local Variables: +# indent-tabs-mode: t +# End: diff --git a/stack-exchange.el b/stack-exchange.el new file mode 100644 index 0000000..bca777b --- /dev/null +++ b/stack-exchange.el @@ -0,0 +1,30 @@ +;;; stack-exchange.el --- A StackExchange Mode -*- lexical-binding: t; -*- + +;; Copyright (C) 2014 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: help, hypermedia, mail, news, tools + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; + +;;; Code: + +(mapc #'load (file-expand-wildcards "sx*.el")) + +(provide 'stack-exchange) +;;; stack-exchange.el ends here @@ -50,25 +50,24 @@ questions)." (interactive) (setq sx-auth-access-token - (let* ((sx-request-api-root sx-auth-root) - (url (sx-request--build - "dialog" - `((client_id . ,sx-auth-client-id) - (scope . (read_inbox - no_expiry - write_access)) - (redirect_uri . ,(url-hexify-string - sx-auth-redirect-uri))) - ","))) + (let ((url (sx-request-build + "dialog" + `((client_id . ,sx-auth-client-id) + (scope . (read_inbox + no_expiry + write_access)) + (redirect_uri . ,(url-hexify-string + sx-auth-redirect-uri))) + "," sx-auth-root))) (browse-url url) (read-string "Enter the access token displayed on the webpage: "))) (if (string-equal "" sx-auth-access-token) (progn (setq sx-auth-access-token nil) (error "You must enter this code to use this client fully")) - (sx-cache-set "auth.el" `((access-token . ,sx-auth-access-token))))) + (sx-cache-set 'auth `((access-token . ,sx-auth-access-token))))) (provide 'sx-auth) -;;; stack-auth.el ends here +;;; sx-auth.el ends here ;; Local Variables: ;; indent-tabs-mode: nil diff --git a/sx-cache.el b/sx-cache.el index a090982..098c292 100644 --- a/sx-cache.el +++ b/sx-cache.el @@ -2,8 +2,7 @@ ;; Copyright (C) 2014 Sean Allred -;; Author: Sean Allred <sallred@calamity.tcs.com> -;; Keywords: help +;; Author: Sean Allred <code@seanallred.com> ;; 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 @@ -20,7 +19,12 @@ ;;; Commentary: +;; All caches are retrieved and set using symbols. The symbol should +;; be the sub-subpackage that is using the cache. For example, +;; `sx-pkg' would use `(sx-cache-get 'pkg)'. ;; +;; This symbol is then converted into a filename within +;; `sx-cache-directory'. ;;; Code: @@ -30,7 +34,9 @@ (defun sx-cache-get-file-name (filename) "Expands FILENAME in the context of `sx-cache-directory'." - (expand-file-name filename sx-cache-directory)) + (expand-file-name + (concat (symbol-name filename) ".el") + sx-cache-directory)) (defun sx-cache-get (cache) "Return the data within CACHE. diff --git a/sx-encoding.el b/sx-encoding.el index efb333e..0b72365 100644 --- a/sx-encoding.el +++ b/sx-encoding.el @@ -2,8 +2,7 @@ ;; Copyright (C) 2014 Sean Allred -;; Author: Sean Allred <sallred@calamity.tcs.com> -;; Keywords: help +;; Author: Sean Allred <code@seanallred.com> ;; 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 @@ -75,6 +74,35 @@ (substring ss 1)))))))) (replace-regexp-in-string "&[^; ]*;" get-function string))) +(defun sx-encoding-gzipped-p (data) + "Checks for magic bytes in DATA. + +Check if the first two bytes of a string in DATA match magic +numbers identifying the gzip file format. See [1] for the file +format description. + +http://www.gzip.org/zlib/rfc-gzip.html + +http://emacs.stackexchange.com/a/2978" + (equal (substring (string-as-unibyte data) 0 2) + (unibyte-string 31 139))) + +(defun sx-encoding-gzipped-buffer-p (filename) + "Check if the BUFFER is gzip-compressed. + +See `gzip-check-magic' for details." + (sx-encoding-gzip-check-magic (buffer-string))) + +(defun sx-encoding-gzipped-file-p (file) + "Check if the FILE is gzip-compressed. + +See `gzip-check-magic' for details." + (let ((first-two-bytes (with-temp-buffer + (set-buffer-multibyte nil) + (insert-file-contents-literally file nil 0 2) + (buffer-string)))) + (sx-encoding-gzipped-p first-two-bytes))) + (provide 'sx-encoding) ;;; sx-encoding.el ends here diff --git a/sx-filter.el b/sx-filter.el index 7178259..acd8fc1 100644 --- a/sx-filter.el +++ b/sx-filter.el @@ -28,21 +28,14 @@ (require 'sx) (require 'sx-cache) +(require 'sx-request) ;;; Customizations -(defconst sx-filter-cache-file - "filters.el") - -(defvar sx-filter - 'default - "The current filter. -To customize the filter for the next call to `sx-request-make', -let-bind this variable to the output of a call to -`sx-filter-compile'. Be careful! If you're going to be using -this new filter a lot, create a variable for it. Creation -requests count against `sx-request-remaining-api-requests'!") +(defvar sx--filter-alist + (sx-cache-get 'filter) + "") ;;; Compilation @@ -60,39 +53,26 @@ or string." (let ((response (sx-request-make "filter/create" keyword-arguments))) - (url-hexify-string - (cdr (assoc 'filter - (elt response 0))))))) + (sx-assoc-let (elt response 0) + (url-hexify-string .filter))))) ;;; Storage and Retrieval -(defun sx-filter-get (filter) - "Retrieve named FILTER from `sx-filter-cache-file'." - (cdr (assoc filter (sx-cache-get sx-filter-cache-file)))) - -(defun sx-filter-store (name &optional filter) - "Store NAME as FILTER in `sx-filter-cache-file'. - -NAME should be a symbol and FILTER is a string as compiled by -`sx-filter-compile'. - -If NAME is a cons cell, (car NAME) is taken to be the actual NAME -and (cdr NAME) is taken to be the actual FILTER. In this case, -the second argument is simply ignored." - (let ((name (if (consp name) (car name) name)) - (filter (if (consp name) (cdr name) filter))) - (unless (symbolp name) - (error "Name must be a symbol: %S" name)) - (let* ((dict (sx-cache-get sx-filter-cache-file)) - (entry (assoc name dict))) - (if entry (setcdr entry filter) - (setq dict (cons (cons name filter) dict))) - - (sx-cache-set sx-filter-cache-file dict)))) - -(defun sx-filter-store-all (name-filter-alist) - (mapc #'sx-filter-store name-filter-alist)) +(defun sx-filter-get-var (filter-variable) + "Return the string representation of FILTER-VARIABLE." + (apply #'sx-filter-get filter-variable)) + +(defun sx-filter-get (&optional include exclude base) + "Return the string representation of the given filter." + ;; Maybe we alreay have this filter + (or (cdr (assoc (list include exclude base) sx--filter-alist)) + ;; If we don't, build it, save it, and return it. + (let ((filter (sx-filter-compile include exclude base))) + (when filter + (push (cons (list include exclude base) filter) sx--filter-alist) + (sx-cache-set 'filter sx--filter-alist) + filter)))) (provide 'sx-filter) ;;; sx-filter.el ends here @@ -66,14 +66,14 @@ by the API and read by `json-read'." '((((background light)) :background "Grey90") (((background dark)) :background "Grey10")) "Face used on the body content of questions and answers." - :group 'stack-mode-faces) + :group 'sx-faces) ;;; This is not used ATM since we got rid of HTML. But it can be used ;;; once we start extending markdown mode. (defcustom sx-lto-bullet (if (char-displayable-p ?•) " •" " -") "Bullet used on the display of lists." :type 'string - :group 'stack-mode) + :group 'sx) (defun sx-lto--body (data) "Get and cleanup `body_markdown' from DATA." diff --git a/sx-method.el b/sx-method.el new file mode 100644 index 0000000..6f0a36b --- /dev/null +++ b/sx-method.el @@ -0,0 +1,47 @@ +;;; sx-method.el --- method calls + +;; Copyright (C) 2014 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; + +;;; Code: +(require 'json) +(require 'url) +(require 'sx) +(require 'sx-request) +(require 'sx-filter) + +(defun sx-method-call + (method &optional keyword-arguments filter silent) + "Call METHOD with KEYWORD-ARGUMENTS using FILTER. + +If SILENT is non-nil, no messages will be printed. + +Return the entire response as a complex alist." + (sx-request-make + method + (cons (cons 'filter + (sx-filter-get-var + (cond (filter filter) + ((boundp 'stack-filter) stack-filter)))) + keyword-arguments))) + +(provide 'sx-method) +;;; sx-method.el ends here diff --git a/sx-network.el b/sx-network.el index dcd2349..f756a26 100644 --- a/sx-network.el +++ b/sx-network.el @@ -29,7 +29,7 @@ (sx-request-make "sites")) (provide 'sx-network) -;;; stack-network.el ends here +;;; sx-network.el ends here ;; Local Variables: ;; indent-tabs-mode: nil diff --git a/sx-question-list.el b/sx-question-list.el index f305306..726c58d 100644 --- a/sx-question-list.el +++ b/sx-question-list.el @@ -20,11 +20,13 @@ ;;; Commentary: ;;; Code: -(require 'sx-question) -(require 'sx-time) (require 'tabulated-list) (require 'cl-lib) +(require 'sx) +(require 'sx-time) +(require 'sx-question) + ;;; Customization (defcustom sx-question-list-height 12 @@ -130,6 +132,7 @@ Letters do not insert themselves; instead, they are commands. ("j" sx-question-list-view-next) ("k" sx-question-list-view-previous) ("g" sx-question-list-refresh) + ("v" sx-question-list-visit) ([?\r] sx-question-list-display-question))) (defvar sx-question-list--current-page "Latest" @@ -198,6 +201,14 @@ a new list before redisplaying." (mapcar #'sx-question-list--print-info question-list))) (when redisplay (tabulated-list-print 'remember))) +(defun sx-question-list-visit (&optional data) + "Visits question under point (or from DATA) using `browse-url'." + (interactive) + (unless data (setq data (tabulated-list-get-id))) + (unless data (error "No question here!")) + (sx-assoc-let data + (browse-url .link))) + (defcustom sx-question-list-ago-string " ago" "String appended to descriptions of the time since something happened. Used in the questions list to indicate a question was updated \"4d ago\"." @@ -210,26 +221,26 @@ Used in the questions list to indicate a question was updated \"4d ago\"." (list data (vector - (list (int-to-string score) - 'face (if upvoted 'sx-question-list-score-upvoted + (list (int-to-string .score) + 'face (if .upvoted 'sx-question-list-score-upvoted 'sx-question-list-score)) - (list (int-to-string answer_count) - 'face (if (sx-question--accepted-answer data) + (list (int-to-string .answer_count) + 'face (if (sx-question--accepted-answer .data) 'sx-question-list-answers-accepted 'sx-question-list-answers)) (concat (propertize - title - 'face (if (sx-question--read-p data) + .title + 'face (if (sx-question--read-p .data) 'sx-question-list-read-question ;; Increment `sx-question-list--unread-count' for the mode-line. (cl-incf sx-question-list--unread-count) 'sx-question-list-unread-question)) (propertize " " 'display "\n ") - (propertize (concat (sx-time-since last_activity_date) + (propertize (concat (sx-time-since .last_activity_date) sx-question-list-ago-string) 'face 'sx-question-list-date) - (propertize (concat " [" (mapconcat #'identity tags "] [") "]") + (propertize (concat " [" (mapconcat #'identity .tags "] [") "]") 'face 'sx-question-list-tags) (propertize " " 'display "\n")))))) diff --git a/sx-question.el b/sx-question.el index 2d65af3..d3fd79f 100644 --- a/sx-question.el +++ b/sx-question.el @@ -27,20 +27,14 @@ (require 'sx) (require 'sx-filter) (require 'sx-lto) -(require 'sx-request) +(require 'sx-method) -;; I don't know why this is here, but it was causing an API request on require. -(defvar sx-question-browse-filter nil - ;; (stack-filter-compile - ;; nil - ;; '(user.profile_image shallow_user.profile_image)) - ) - -;; (stack-filter-store 'question-browse sx-question-browse-filter) +(defvar sx-question-browse-filter + '(nil (user.profile_image shallow_user.profile_image))) (defun sx-question-get-questions (site &optional page) "Get the page PAGE of questions from SITE." - (sx-request-make + (sx-method-call "questions" `((site . ,site) (page . ,page)) diff --git a/sx-request.el b/sx-request.el index a62ee0e..dd98ead 100644 --- a/sx-request.el +++ b/sx-request.el @@ -1,9 +1,8 @@ -;;; sx-request.el --- requests for stack-mode +;;; sx-request.el --- requests and url manipulation -*- lexical-binding: t; -*- ;; Copyright (C) 2014 Sean Allred -;; Author: Sean Allred <sallred@calamity.tcs.com> -;; Keywords: +;; Author: Sean Allred <code@seanallred.com> ;; 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 @@ -23,9 +22,27 @@ ;; ;;; Code: -(require 'json) + (require 'url) +(require 'json) + (require 'sx) +(require 'sx-encoding) + + +;;; Variables + +(defconst sx-request-api-key + "0TE6s1tveCpP9K5r5JNDNQ((" + "When passed, this key provides a higher request quota.") + +(defconst sx-request-api-version + "2.2" + "The current version of the API.") + +(defconst sx-request-api-root + (format "http://api.stackexchange.com/%s/" sx-request-api-version) + "The base URL to make requests from.") (defcustom sx-request-silent-p t @@ -50,111 +67,63 @@ recent call. Set by `sx-request-make'.") number, `sx-request-make' will begin printing out the number of requests left every time it finishes a call.") -(defcustom sx-request-default-keyword-arguments-alist - '(("filters/create") - ("sites") - ("questions" (site . emacs)) - (t nil)) - "Keywords to use as the default for a given method. - -The first element of each list is the method call the keywords -apply to. The remaining cons cells (and they must be conses) are -the values for each keyword. - -For each list, if no keywords are provided, the method's -arguments are forced to the default as determined by the API. - -For each cons cell, if the cdr is `nil', then the keyword will be -forced to the default as determined by the API. - -See `sx-request-get-default-keyword-arguments' and -`sx-request-build-keyword-arguments'. -") - -(defconst sx-request-api-version - "2.2" - "The current version of the API.") - -(defconst sx-request-api-root - (format "http://api.stackexchange.com/%s/" sx-request-api-version) - "The base URL to make requests from.") - -(defconst sx-request-api-key - "0TE6s1tveCpP9K5r5JNDNQ((" - "When passed, this key provides a higher request quota.") + +;;; Making Requests (defun sx-request-make - (method &optional keyword-arguments filter silent) - "Make a request to the StackExchange API using METHOD and -optional KEYWORD-ARGUMENTS. If no KEYWORD-ARGUMENTS are given, -`sx-default-keyword-arguments-alist' is used. Return the -entire response as a complex alist." + (method &optional args silent) (let ((url-automatic-caching sx-request-cache-p) - (url-inhibit-uncompression t) - (silent (or silent sx-request-silent-p)) - (call - (sx-request--build - method - (append `((filter . ,(cond (filter filter) - ((boundp 'stack-filter) stack-filter))) - (key . ,sx-request-api-key)) - (if keyword-arguments keyword-arguments - (sx-request--get-default-keyword-arguments method)))))) - ;; TODO: url-retrieve-synchronously can return nil if the call is - ;; unsuccessful should handle this case + (url-inhibit-uncompression t) + (silent (or silent sx-request-silent-p)) + (call (sx-request-build + method + (cons (cons 'key sx-request-api-key) + args)))) (unless silent (sx-message "Request: %S" call)) (let ((response-buffer (cond - ((= emacs-minor-version 4) - (url-retrieve-synchronously call silent)) - (t (url-retrieve-synchronously call))))) + ((equal '(24 . 4) (cons emacs-major-version emacs-minor-version)) + (url-retrieve-synchronously call silent)) + (t (url-retrieve-synchronously call))))) (if (not response-buffer) - (error "Something went wrong in `url-retrieve-synchronously'") - (with-current-buffer response-buffer - (let* ((data (progn - (goto-char (point-min)) - (if (not (search-forward "\n\n" nil t)) - (error "Response headers missing") - (delete-region (point-min) (point)) - (buffer-string)))) - (response (ignore-errors - (json-read-from-string data)))) - ;; if response isn't nil, the response was in plain text - (unless response - ;; try to decompress the response - (setq response - (with-demoted-errors "JSON Error: %s" - (shell-command-on-region - (point-min) (point-max) - sx-request-unzip-program - nil t) - (json-read-from-string - (buffer-substring - (point-min) (point-max))))) - ;; If it still fails, error out - (unless response - (sx-message "Unable to parse response") - (sx-message "Printing response as message") - (message "%S" response) - (error "Response could not be read by json-read-string"))) - ;; At this point, either response is a valid data structure - ;; or we have already thrown an error - (when (assoc 'error_id response) - (error "Request failed: (%s) [%i %s] %s" - method - (cdr (assoc 'error_id response)) - (cdr (assoc 'error_name response)) - (cdr (assoc 'error_message response)))) - (when (< (setq sx-request-remaining-api-requests - (cdr (assoc 'quota_remaining response))) - sx-request-remaining-api-requests-message-threshold) - (sx-message "%d API requests remaining" - sx-request-remaining-api-requests)) - (cdr (assoc 'items response)))))))) - -(defun sx-request--build (method keyword-arguments &optional kv-value-sep) + (error "Something went wrong in `url-retrieve-synchronously'") + (with-current-buffer response-buffer + (let* ((data (progn + (goto-char (point-min)) + (if (not (search-forward "\n\n" nil t)) + (error "Response headers missing; response corrupt") + (delete-region (point-min) (point)) + (buffer-string)))) + (response-zipped-p (sx-encoding-gzipped-p data)) + (data (if (not response-zipped-p) data + (shell-command-on-region + (point-min) (point-max) + sx-request-unzip-program + nil t) + (buffer-string))) + (response (with-demoted-errors "`json' error: %S" + (json-read-from-string data)))) + (when (and (not response) (string-equal data "{}")) + (sx-message "Unable to parse response: %S" response) + (error "Response could not be read by `json-read-from-string'")) + ;; If we get here, the response is a valid data structure + (sx-assoc-let response + (when .error_id + (error "Request failed: (%s) [%i %s] %S" + .method .error_id .error_name .error_message)) + (when (< (setq sx-request-remaining-api-requests + .quota_remaining) + sx-request-remaining-api-requests-message-threshold) + (sx-message "%d API requests reamining" + sx-request-remaining-api-requests)) + .items))))))) + + +;;; Support Functions + +(defun sx-request-build (method keyword-arguments &optional kv-value-sep root) "Build the request string that will be used to process REQUEST with the given KEYWORD-ARGUMENTS." - (let ((base (concat sx-request-api-root method)) + (let ((base (concat (or root sx-request-api-root) method)) (args (sx-request--build-keyword-arguments keyword-arguments kv-value-sep))) (if (string-equal "" args) @@ -179,16 +148,5 @@ false, use the symbol `false'. Each element is processed with alist)) "&")) -(defun sx-request--get-default-keyword-arguments (method) - "Gets the correct keyword arguments for METHOD." - (let ((entry (assoc method sx-request-default-keyword-arguments-alist))) - (cdr (or entry (assoc t sx-request-default-keyword-arguments-alist))))) - -;;; @todo sx-request-change-default-keyword-arguments -;;; (method new-keyword-arguments) -;;; @todo sx-request-change-default-keyword-arguments-for-key -;;; (method key new-value) - - (provide 'sx-request) ;;; sx-request.el ends here @@ -2,8 +2,7 @@ ;; Copyright (C) 2014 Sean Allred -;; Author: Sean Allred <sallred@calamity.tcs.com> -;; Keywords: help +;; Author: Sean Allred <code@seanallred.com> ;; 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 @@ -25,7 +25,8 @@ ;;; Code: -;;; Requirements +;;; Utility Functions + (defun sx-message (format-string &rest args) "Display a message" (message "[stack] %s" (apply #'format format-string args))) @@ -70,48 +71,71 @@ a string, just return it." ;;; Interpreting request data -(defvar sx--api-symbols - '(accept_rate answer_count answer_id answers body body_markdown close_vote_count upvoted downvoted - comment_count comment_id creation_date delete_vote_count display_name - edited favorite_count is_accepted is_answered last_activity_date - last_edit_date last_editor link owner profile_image question_id - reopen_vote_count reputation score tags title user_id user_type view_count) - "") - -(defun sx--deep-search (symbol list) - "Non-nil if SYMBOL is contained somewhere inside LIST." +(defun sx--deep-dot-search (data) + "Find symbols somewhere inside DATA which start with a `.'. +Returns a list where each element is a cons cell. The car is the +symbol, the cdr is the symbol without the `.'." (cond - ((symbolp list) - (eq symbol list)) - ((not (listp list)) - nil) - (t - (remove nil (mapcar (lambda (x) (sx--deep-search symbol x)) list))))) + ((symbolp data) + (let ((name (symbol-name data))) + (when (string-match "\\`\\." name) + ;; Return the cons cell inside a list, so it can be appended + ;; with other results in the clause below. + (list (cons data (intern (replace-match "" nil nil name))))))) + ((not (listp data)) nil) + (t (apply + #'append + (remove nil (mapcar #'sx--deep-dot-search data)))))) (defmacro sx-assoc-let (alist &rest body) - "Execute BODY while let-binding api symbols to their values in ALIST. -Any api symbol is any symbol listed in `sx--api-symbols'. Only -those present in BODY are letbound, which leads to optimal -performance. + "Execute BODY while let-binding dotted symbols to their values in ALIST. +Dotted symbol is any symbol starting with a `.'. Only those +present in BODY are letbound, which leads to optimal performance. For instance the following code (stack-core-with-data alist - (list title body)) + (list .title .body)) is equivalent to - (let ((title (cdr (assoc 'title alist))) - (body (cdr (assoc 'body alist)))) - (list title body))" + (let ((.title (cdr (assoc 'title alist))) + (.body (cdr (assoc 'body alist)))) + (list .title .body))" (declare (indent 1) (debug t)) - (let ((symbols (cl-member-if - (lambda (x) (sx--deep-search x body)) - sx--api-symbols))) - `(let ,(mapcar (lambda (x) `(,x (cdr (assoc ',x ,alist)))) symbols) + (let ((symbol-alist (sx--deep-dot-search body))) + `(let ,(mapcar (lambda (x) `(,(car x) (cdr (assoc ',(cdr x) ,alist)))) + symbol-alist) ,@body))) +(defcustom sx-init-hook nil + "Hook run when stack-mode initializes. + +Run after `sx-init--internal-hook'.") + +(defvar sx-init--internal-hook nil + "Hook run when stack-mode initializes. + +This is used internally to set initial values for variables such +as filters.") + +(defmacro sx-init-variable (variable value &optional setter) + "Set VARIABLE to VALUE using SETTER. +SETTER should be a function of two arguments. If SETTER is nil, +`set' is used." + (eval + `(add-hook + 'sx-init--internal-hook + (lambda () + (,(or setter #'setq) ,variable ,value)))) + nil) + +(defun stack-initialize () + (run-hooks + 'sx-init--internal-hook + 'sx-init-hook)) + (provide 'sx) ;;; sx.el ends here diff --git a/test/tests.el b/test/tests.el index 2864428..6a48257 100644 --- a/test/tests.el +++ b/test/tests.el @@ -1,41 +1,49 @@ -(defun -stack--nuke () +(defun -sx--nuke () (interactive) (mapatoms (lambda (symbol) - (if (string-prefix-p "stack-" (symbol-name symbol)) + (if (string-prefix-p "sx-" (symbol-name symbol)) (unintern symbol))))) ;;; Tests -(defvar stack-test-data-dir +(defvar sx-test-data-dir (expand-file-name "data-samples/" - (or (file-name-directory load-file-name) "./")) - "") + (or (file-name-directory load-file-name) "./"))) -(defun stack-test-sample-data (method &optional directory) +(defun sx-test-sample-data (method &optional directory) (let ((file (concat (when directory (concat directory "/")) - stack-test-data-dir + sx-test-data-dir method ".el"))) (when (file-exists-p file) (with-temp-buffer (insert-file-contents file) (read (buffer-string)))))) +(defmacro line-should-match (regexp) + "" + `(let ((line (buffer-substring-no-properties + (line-beginning-position) + (line-end-position)))) + (message "Line here is: %S" line) + (should (string-match ,regexp line)))) + (setq sx-request-remaining-api-requests-message-threshold 50000 debug-on-error t sx-request-silent-p nil user-emacs-directory "." - stack-test-data-questions - (stack-test-sample-data "questions") - stack-test-data-sites - (stack-test-sample-data "sites")) + sx-test-data-questions + (sx-test-sample-data "questions") + sx-test-data-sites + (sx-test-sample-data "sites")) (setq package-user-dir (expand-file-name (format "../../.cask/%s/elpa" emacs-version) - stack-test-data-dir)) + sx-test-data-dir)) (package-initialize) + (require 'cl-lib) (require 'sx) (require 'sx-question) @@ -55,7 +63,7 @@ (sx-request-make "questions" '(())))) (ert-deftest test-tree-filter () - "`stack-core-filter-data'" + "`sx-core-filter-data'" ;; flat (should (equal @@ -89,35 +97,9 @@ ((1 . alpha) (2 . beta))] '(1 2 3))))) -(ert-deftest test-filters () - (let ((stack-cache-directory (make-temp-file "stack-test" t))) - (should-error (sx-filter-store "names must be symbols" - "this is a filter")) - ;; basic use - (should (equal '((test . "filter")) - (sx-filter-store 'test "filter"))) - ;; aggregation - (should (equal '((test2 . "filter2") (test . "filter")) - (sx-filter-store 'test2 "filter2"))) - ;; mutation - (should (equal '((test2 . "filter2") (test . "filter-test")) - (sx-filter-store 'test "filter-test"))) - ;; clean up (note: the file should exist) - (delete-file - (sx-cache-get-file-name - sx-filter-cache-file)))) - -(defmacro line-should-match (regexp) - "" - `(let ((line (buffer-substring-no-properties - (line-beginning-position) - (line-end-position)))) - (message "Line here is: %S" line) - (should (string-match ,regexp line)))) - (ert-deftest question-list-display () (cl-letf (((symbol-function #'sx-request-make) - (lambda (&rest _) stack-test-data-questions))) + (lambda (&rest _) sx-test-data-questions))) (list-questions nil) (switch-to-buffer "*question-list*") (goto-char (point-min)) @@ -127,10 +109,26 @@ (sx-question-list-next 5) (line-should-match "^\\s-+0\\s-+1\\s-+Babel doesn't wrap results in verbatim [ 0-9]+[ydhms] ago\\s-+\\[org-mode\\]") - ;; ;; Use this when we have a real stack-question buffer. - ;; (call-interactively 'stack-question-list-display-question) - ;; (should (equal (buffer-name) "*stack-question*")) + ;; ;; Use this when we have a real sx-question buffer. + ;; (call-interactively 'sx-question-list-display-question) + ;; (should (equal (buffer-name) "*sx-question*")) (switch-to-buffer "*question-list*") (sx-question-list-previous 4) (line-should-match "^\\s-+2\\s-+1\\s-+"Making tag completion table" Freezes/Blocks -- how to disable [ 0-9]+[ydhms] ago\\s-+\\[autocomplete\\]"))) + +(ert-deftest macro-test--sx-assoc-let () + "Tests macro expansion for `sx-assoc-let'" + (should + (equal '(let ((.test (cdr (assoc 'test data)))) + .test) + (macroexpand + '(sx-assoc-let data + .test)))) + (should + (equal '(let ((.test-one (cdr (assoc 'test-one data))) + (.test-two (cdr (assoc 'test-two data)))) + (cons .test-one .test-two)) + (macroexpand + '(sx-assoc-let data + (cons .test-one .test-two)))))) |