aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArtur Malabarba <bruce.connor.am@gmail.com>2014-11-13 02:07:43 +0000
committerArtur Malabarba <bruce.connor.am@gmail.com>2014-11-13 02:07:43 +0000
commit6355c676f48506ecb730acb200085f7b211e08c4 (patch)
tree8173b74b21d1445997159adc9a5cca261d9c8892
parent4cf7825918bfb60e2d1d1ce1dd342665f1161fa2 (diff)
parentcfd909e8524c37ac3bc94bec56c4e577b2c33d2d (diff)
Merge branch 'master' into sx-question-mode
-rw-r--r--.travis.yml1
-rw-r--r--Makefile38
-rw-r--r--stack-exchange.el30
-rw-r--r--sx-auth.el23
-rw-r--r--sx-cache.el12
-rw-r--r--sx-encoding.el32
-rw-r--r--sx-filter.el60
-rw-r--r--sx-lto.el4
-rw-r--r--sx-method.el47
-rw-r--r--sx-network.el2
-rw-r--r--sx-question-list.el31
-rw-r--r--sx-question.el14
-rw-r--r--sx-request.el182
-rw-r--r--sx-time.el3
-rw-r--r--sx.el82
-rw-r--r--test/tests.el84
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
diff --git a/sx-auth.el b/sx-auth.el
index 59be452..7912508 100644
--- a/sx-auth.el
+++ b/sx-auth.el
@@ -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
diff --git a/sx-lto.el b/sx-lto.el
index 6bdd5d0..ad58570 100644
--- a/sx-lto.el
+++ b/sx-lto.el
@@ -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
diff --git a/sx-time.el b/sx-time.el
index b1a8f80..9c4dfaa 100644
--- a/sx-time.el
+++ b/sx-time.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
diff --git a/sx.el b/sx.el
index a1e504e..7ed56d3 100644
--- a/sx.el
+++ b/sx.el
@@ -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&#39;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-+&quot;Making tag completion table&quot; 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))))))