aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pearl.el36
-rw-r--r--tests/test-pearl-filter.el26
-rw-r--r--tests/test-pearl-reverse-compile.el18
3 files changed, 74 insertions, 6 deletions
diff --git a/pearl.el b/pearl.el
index 0ecc03a..9aebcef 100644
--- a/pearl.el
+++ b/pearl.el
@@ -849,13 +849,15 @@ PRIORITY is an integer 0-4 or a symbol in `pearl--priority-symbols'."
(defun pearl--compile-state-filter (plist)
"Return the state sub-filter alist for PLIST, or nil when no state key is set.
-An explicit `:state' (name) or `:state-type' (one type or a list) takes
-precedence over `:open', the broad not-closed predicate."
+An explicit `:state' (a name or a list of names) or `:state-type' (one type or
+a list) takes precedence over `:open', the broad not-closed predicate."
(let ((state (plist-get plist :state))
(state-type (plist-get plist :state-type))
(open (plist-get plist :open)))
(cond
- (state (list (cons "name" (pearl--eq state))))
+ (state (list (cons "name" (if (listp state)
+ (pearl--in state)
+ (pearl--eq state)))))
(state-type (list (cons "type"
(pearl--in (if (listp state-type)
state-type
@@ -984,8 +986,18 @@ multi-value filter would change which issues match."
((or (assq 'and node) (assq 'or node))
(pearl--rc-refuse "state uses OR logic Pearl can't represent"))
((assq 'name node)
- (let ((r (pearl--rc-scalar (cdr (assq 'name node)) "state.name")))
- (if (eq (car r) 'ok) (cons 'ok (list :state (cdr r))) r)))
+ ;; :state is plural-capable (a name or a list), like :state-type:
+ ;; name.eq / name.in[one] -> scalar, name.in[many] -> list.
+ (let ((op (cdr (assq 'name node))))
+ (cond
+ ((assq 'eq op) (cons 'ok (list :state (cdr (assq 'eq op)))))
+ ((assq 'in op)
+ (let ((vals (append (cdr (assq 'in op)) nil)))
+ (cond
+ ((null vals) (pearl--rc-refuse "state.name has an empty `in'"))
+ ((= 1 (length vals)) (cons 'ok (list :state (car vals))))
+ (t (cons 'ok (list :state vals))))))
+ (t (pearl--rc-refuse "state.name uses an operator Pearl can't represent")))))
((assq 'type node)
(let ((op (cdr (assq 'type node))))
(cond
@@ -1126,7 +1138,7 @@ needs team context) is resolved upstream, not here."
(let ((order (plist-get plist :order)))
(when (and order (not (memq order '(asc desc))))
(user-error ":order must be `asc' or `desc', got %S" order)))
- (dolist (key '(:state :project :team :cycle :assignee-id :label-id))
+ (dolist (key '(:project :team :cycle :assignee-id :label-id))
(let ((val (plist-get plist key)))
(when (and (stringp val) (string-empty-p val))
(user-error "%s must not be an empty string" key))))
@@ -1136,6 +1148,18 @@ needs team context) is resolved upstream, not here."
(cl-some (lambda (x) (not (and (stringp x) (not (string-empty-p x)))))
labels)))
(user-error ":labels must be a list of non-empty strings")))
+ ;; :state is a single name OR a list of non-empty names (mirrors :labels).
+ (let ((state (plist-get plist :state)))
+ (cond
+ ((null state))
+ ((stringp state)
+ (when (string-empty-p state)
+ (user-error ":state must not be an empty string")))
+ ((listp state)
+ (when (cl-some (lambda (x) (not (and (stringp x) (not (string-empty-p x)))))
+ state)
+ (user-error ":state must be a name or a list of non-empty names")))
+ (t (user-error ":state must be a name or a list of names, got %S" state))))
t)
;;; Issue Model Normalization
diff --git a/tests/test-pearl-filter.el b/tests/test-pearl-filter.el
index 38d254e..d3fca88 100644
--- a/tests/test-pearl-filter.el
+++ b/tests/test-pearl-filter.el
@@ -83,6 +83,16 @@
(should (equal (pearl--build-issue-filter '(:state-type "started"))
'(("state" ("type" ("in" . ["started"])))))))
+(ert-deftest test-pearl-filter-state-name-list ()
+ ":state with a list of names compiles to state.name.in."
+ (should (equal (pearl--build-issue-filter '(:state ("Todo" "In Review")))
+ '(("state" ("name" ("in" . ["Todo" "In Review"])))))))
+
+(ert-deftest test-pearl-filter-state-list-beats-open ()
+ "A list-valued :state keeps precedence over :open."
+ (should (equal (pearl--build-issue-filter '(:state ("Todo" "Done") :open t))
+ '(("state" ("name" ("in" . ["Todo" "Done"])))))))
+
(ert-deftest test-pearl-filter-project-team-cycle ()
":project / :cycle compile to id.eq; :team to key.eq."
(should (equal (pearl--build-issue-filter '(:project "p-1"))
@@ -189,6 +199,22 @@
"A non-string entry in :labels is a user-error."
(should-error (pearl--validate-issue-filter '(:labels ("bug" 7))) :type 'user-error))
+(ert-deftest test-pearl-filter-validate-accepts-state-list ()
+ "A :state list of non-empty names validates."
+ (should (eq t (pearl--validate-issue-filter '(:state ("Todo" "In Review"))))))
+
+(ert-deftest test-pearl-filter-validate-accepts-state-string ()
+ "A single-string :state still validates."
+ (should (eq t (pearl--validate-issue-filter '(:state "In Progress")))))
+
+(ert-deftest test-pearl-filter-validate-rejects-empty-name-in-state-list ()
+ "An empty name anywhere in a :state list is a user-error."
+ (should-error (pearl--validate-issue-filter '(:state ("" "Todo"))) :type 'user-error))
+
+(ert-deftest test-pearl-filter-validate-rejects-empty-state-string ()
+ "An empty single :state string is a user-error."
+ (should-error (pearl--validate-issue-filter '(:state "")) :type 'user-error))
+
;;; id-based forms (rename-proof; used by favorite-derived filter sources)
(ert-deftest test-pearl-filter-assignee-id ()
diff --git a/tests/test-pearl-reverse-compile.el b/tests/test-pearl-reverse-compile.el
index ec67391..382c72a 100644
--- a/tests/test-pearl-reverse-compile.el
+++ b/tests/test-pearl-reverse-compile.el
@@ -57,6 +57,24 @@
(ert-deftest test-pearl-rc-roundtrip-state-name ()
(test-pearl-rc--should-roundtrip '(:state "In Progress")))
+(ert-deftest test-pearl-rc-roundtrip-state-name-list ()
+ "A multi-state :state round-trips (state.name.in with several names)."
+ (let ((r (test-pearl-rc--roundtrip '(:state ("Todo" "In Review")))))
+ (should (eq 'ok (car r)))
+ (should (equal (cdr r) '(:state ("Todo" "In Review"))))))
+
+(ert-deftest test-pearl-rc-state-name-in-many-to-list ()
+ "state.name.in with several names reverse-compiles to a list :state."
+ (should (equal (test-pearl-rc--from-json
+ "{\"state\":{\"name\":{\"in\":[\"Todo\",\"In Review\"]}}}")
+ '(ok :state ("Todo" "In Review")))))
+
+(ert-deftest test-pearl-rc-state-name-in-one-to-scalar ()
+ "state.name.in with one name normalizes to a scalar :state."
+ (should (equal (test-pearl-rc--from-json
+ "{\"state\":{\"name\":{\"in\":[\"Todo\"]}}}")
+ '(ok :state "Todo"))))
+
(ert-deftest test-pearl-rc-roundtrip-state-type-single ()
(let ((r (test-pearl-rc--roundtrip '(:state-type "started"))))
(should (eq 'ok (car r)))