aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sx-auth.el23
-rw-r--r--sx-cache.el9
-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.el6
-rw-r--r--sx-question.el14
-rw-r--r--sx-request.el184
-rw-r--r--sx.el76
-rw-r--r--test/tests.el52
11 files changed, 258 insertions, 219 deletions
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 ed4895a..098c292 100644
--- a/sx-cache.el
+++ b/sx-cache.el
@@ -19,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:
@@ -29,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-filter.el b/sx-filter.el
index 7178259..aa815a2 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..f69c381
--- /dev/null
+++ b/sx-method.el
@@ -0,0 +1,47 @@
+;;; sx-request.el --- requests for stack-mode
+
+;; Copyright (C) 2014 Sean Allred
+
+;; Author: Sean Allred <sallred@calamity.tcs.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-request.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 86e9194..caf24b1 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
diff --git a/sx-question.el b/sx-question.el
index e9634f7..20a71cc 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 e9f52f3..9d9dca4 100644
--- a/sx-request.el
+++ b/sx-request.el
@@ -1,4 +1,4 @@
-;;; sx-request.el --- requests for stack-mode
+;;; sx-request.el --- requests and url manipulation -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -22,10 +22,27 @@
;;
;;; Code:
-(require 'json)
+
(require 'url)
+(require 'json)
+
(require 'sx)
+
+;;; 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
"When `t', requests default to being silent.")
@@ -49,111 +66,69 @@ 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 (ignore-errors
+ (json-read-from-string data))))
+ ;; If the response isn't nil, the response was in plain text
+ (unless response
+ ;; try to decompress the response
+ (setq response
+ (with-demoted-errors "`json-read' 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 outline
+ (unless response
+ (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)
@@ -178,16 +153,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.el b/sx.el
index a1e504e..6165714 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)))
@@ -71,11 +72,47 @@ 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)
+ '(
+ accept_rate
+ answer_count
+ answer_id
+ answers
+ body
+ body_markdown
+ close_vote_count
+ comment_count
+ comment_id
+ creation_date
+ delete_vote_count
+ display_name
+ downvoted
+ edited
+ error_id
+ error_name
+ error_message
+ favorite_count
+ filter
+ items
+ is_accepted
+ is_answered
+ last_activity_date
+ last_edit_date
+ last_editor
+ link
+ owner
+ profile_image
+ question_id
+ quota_remaining
+ reopen_vote_count
+ reputation
+ score
+ tags
+ title
+ upvoted
+ user_id
+ user_type
+ view_count
+ )
"")
(defun sx--deep-search (symbol list)
@@ -112,6 +149,33 @@ is equivalent to
`(let ,(mapcar (lambda (x) `(,x (cdr (assoc ',x ,alist)))) symbols)
,@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..a66394c 100644
--- a/test/tests.el
+++ b/test/tests.el
@@ -1,20 +1,19 @@
-(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
@@ -27,15 +26,16 @@
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 +55,7 @@
(sx-request-make "questions" '(()))))
(ert-deftest test-tree-filter ()
- "`stack-core-filter-data'"
+ "`sx-core-filter-data'"
;; flat
(should
(equal
@@ -89,24 +89,6 @@
((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
@@ -117,7 +99,7 @@
(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,9 +109,9 @@
(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