diff --git a/components/rest-linear/deps.edn b/components/rest-linear/deps.edn new file mode 100644 index 0000000..d34e348 --- /dev/null +++ b/components/rest-linear/deps.edn @@ -0,0 +1,4 @@ +{:paths ["src" "resources"] + :deps {} + :aliases {:test {:extra-paths ["test"] + :extra-deps {}}}} diff --git a/components/rest-linear/resources/rest_linear/.gitkeep b/components/rest-linear/resources/rest_linear/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/components/rest-linear/src/org/aplex/rest_linear/core.cljc b/components/rest-linear/src/org/aplex/rest_linear/core.cljc new file mode 100644 index 0000000..0e32bc1 --- /dev/null +++ b/components/rest-linear/src/org/aplex/rest_linear/core.cljc @@ -0,0 +1,59 @@ +(ns org.aplex.rest-linear.core + (:require [org.aplex.helpers.interface :as h])) + +(def default-config {:api-host "https://api.linear.app"}) +(def api-config (atom default-config)) + +(defn init-config [cfg] + (reset! api-config (merge default-config cfg))) + +(defn- api-key [] + (or (-> @api-config :api-key) + (throw (ex-info "Linear api_key is not configured; call init-config with :api-key" + {:component "org.aplex.rest-linear"})))) + +(defn- request [query variables] + (let [envelope (cond-> {:query query} + variables (assoc :variables variables))] + {:method :post + :base-url (-> @api-config :api-host) + :path "/graphql" + :path-params [] + :query-params [] + :headers [{:name "Authorization" :value (api-key)}] + :auth nil + :body (when envelope (h/encode-json envelope))})) + +(defn fetch-teams [] + (request "query{ teams{ nodes{ id name key } } }" nil)) + +(defn fetch-issue [issue-id] + (request "query($id: String!){ issue(id: $id){ id identifier title description state{ name } } }" + {:id issue-id})) + +(defn fetch-my-issues [] + (request "query{ viewer{ assignedIssues{ nodes{ id identifier title state{ name } } } } }" nil)) + +(defn fetch-team-issues [team-id] + (request "query($id: String!){ team(id: $id){ id name key issues{ nodes{ id identifier title state{ name } } } } }" + {:id team-id})) + +(defn fetch-active-cycle-issues [team-id] + (request "query($id: String!){ team(id: $id){ activeCycle{ id number issues{ nodes{ id identifier title state{ name } assignee{ name } } } } } }" + {:id team-id})) + +(defn create-issue + ([title team-id] + (create-issue title team-id nil)) + ([title team-id description] + (request "mutation($input: IssueCreateInput!){ issueCreate(input: $input){ success issue{ id identifier } } }" + (cond-> {:input {:title title :teamId team-id}} + description (assoc-in [:input :description] description))))) + +(defn update-issue [issue-id updates] + (request "mutation($id: String!, $input: IssueUpdateInput!){ issueUpdate(id: $id, input: $input){ success issue{ id identifier } } }" + {:id issue-id :input updates})) + +(defn fetch-issues [filter] + (request "query($filter: IssueFilter){ issues(filter: $filter){ nodes{ id identifier title state{ name } assignee{ name email } } } }" + {:filter filter})) diff --git a/components/rest-linear/src/org/aplex/rest_linear/interface.clj b/components/rest-linear/src/org/aplex/rest_linear/interface.clj new file mode 100644 index 0000000..b24a14a --- /dev/null +++ b/components/rest-linear/src/org/aplex/rest_linear/interface.clj @@ -0,0 +1,33 @@ +(ns org.aplex.rest-linear.interface + (:require + [org.aplex.rest-linear.core :as c])) + +(defn init-config [cfg] + (c/init-config cfg)) + +(defn fetch-teams [] + (c/fetch-teams)) + +(defn fetch-issue [issue-id] + (c/fetch-issue issue-id)) + +(defn fetch-my-issues [] + (c/fetch-my-issues)) + +(defn fetch-team-issues [team-id] + (c/fetch-team-issues team-id)) + +(defn fetch-active-cycle-issues [team-id] + (c/fetch-active-cycle-issues team-id)) + +(defn create-issue + ([title team-id] + (c/create-issue title team-id)) + ([title team-id description] + (c/create-issue title team-id description))) + +(defn update-issue [issue-id updates] + (c/update-issue issue-id updates)) + +(defn fetch-issues [filter] + (c/fetch-issues filter)) diff --git a/components/rest-linear/test/org/aplex/rest_linear/interface_test.clj b/components/rest-linear/test/org/aplex/rest_linear/interface_test.clj new file mode 100644 index 0000000..9ef28b7 --- /dev/null +++ b/components/rest-linear/test/org/aplex/rest_linear/interface_test.clj @@ -0,0 +1,137 @@ +(ns org.aplex.rest-linear.interface-test + (:require [clojure.test :as test :refer :all] + [org.aplex.rest-linear.interface :as rest-linear])) + +(def abstract-keys + #{:method :base-url :path :path-params :query-params :headers :auth :body}) + +(defn reset-config [cfg] + (rest-linear/init-config cfg)) + +(defn test-config [] + {:api-key "lin_api_test"}) + +(defn api-key-header [] + [{:name "Authorization" :value "lin_api_test"}]) + +(deftest api-host-defaults-to-linear + (reset-config (test-config)) + (is (= "https://api.linear.app" + (:base-url (rest-linear/fetch-teams))))) + +(deftest api-host-is-overridable + (reset-config (merge (test-config) {:api-host "https://linear.example.com"})) + (is (= "https://linear.example.com" + (:base-url (rest-linear/fetch-teams))))) + +(deftest api-key-is-mandatory + (reset-config {:api-key nil}) + (is (thrown? Exception (rest-linear/fetch-teams)))) + +(deftest fetch-teams-builds-abstract-request + (reset-config (test-config)) + (is (= {:method :post + :base-url "https://api.linear.app" + :path "/graphql" + :path-params [] + :query-params [] + :headers (api-key-header) + :auth nil + :body "{\"query\":\"query{ teams{ nodes{ id name key } } }\"}"} + (rest-linear/fetch-teams)))) + +(deftest fetch-issue-builds-abstract-request + (reset-config (test-config)) + (is (= {:method :post + :base-url "https://api.linear.app" + :path "/graphql" + :path-params [] + :query-params [] + :headers (api-key-header) + :auth nil + :body "{\"query\":\"query($id: String!){ issue(id: $id){ id identifier title description state{ name } } }\",\"variables\":{\"id\":\"ABC-123\"}}"} + (rest-linear/fetch-issue "ABC-123")))) + +(deftest fetch-my-issues-builds-abstract-request + (reset-config (test-config)) + (is (= {:method :post + :base-url "https://api.linear.app" + :path "/graphql" + :path-params [] + :query-params [] + :headers (api-key-header) + :auth nil + :body "{\"query\":\"query{ viewer{ assignedIssues{ nodes{ id identifier title state{ name } } } } }\"}"} + (rest-linear/fetch-my-issues)))) + +(deftest fetch-team-issues-builds-abstract-request + (reset-config (test-config)) + (is (= {:method :post + :base-url "https://api.linear.app" + :path "/graphql" + :path-params [] + :query-params [] + :headers (api-key-header) + :auth nil + :body "{\"query\":\"query($id: String!){ team(id: $id){ id name key issues{ nodes{ id identifier title state{ name } } } } }\",\"variables\":{\"id\":\"team-1\"}}"} + (rest-linear/fetch-team-issues "team-1")))) + +(deftest create-issue-builds-abstract-request + (reset-config (test-config)) + (is (= {:method :post + :base-url "https://api.linear.app" + :path "/graphql" + :path-params [] + :query-params [] + :headers (api-key-header) + :auth nil + :body "{\"query\":\"mutation($input: IssueCreateInput!){ issueCreate(input: $input){ success issue{ id identifier } } }\",\"variables\":{\"input\":{\"title\":\"My title\",\"teamId\":\"team-1\"}}}"} + (rest-linear/create-issue "My title" "team-1")))) + +(deftest create-issue-includes-description-when-given + (reset-config (test-config)) + (is (= {:method :post + :base-url "https://api.linear.app" + :path "/graphql" + :path-params [] + :query-params [] + :headers (api-key-header) + :auth nil + :body "{\"query\":\"mutation($input: IssueCreateInput!){ issueCreate(input: $input){ success issue{ id identifier } } }\",\"variables\":{\"input\":{\"title\":\"My title\",\"teamId\":\"team-1\",\"description\":\"A description\"}}}"} + (rest-linear/create-issue "My title" "team-1" "A description")))) + +(deftest update-issue-builds-abstract-request + (reset-config (test-config)) + (is (= {:method :post + :base-url "https://api.linear.app" + :path "/graphql" + :path-params [] + :query-params [] + :headers (api-key-header) + :auth nil + :body "{\"query\":\"mutation($id: String!, $input: IssueUpdateInput!){ issueUpdate(id: $id, input: $input){ success issue{ id identifier } } }\",\"variables\":{\"id\":\"issue-1\",\"input\":{\"title\":\"New title\"}}}"} + (rest-linear/update-issue "issue-1" {:title "New title"})))) + +(deftest fetch-active-cycle-issues-builds-abstract-request + (reset-config (test-config)) + (is (= {:method :post + :base-url "https://api.linear.app" + :path "/graphql" + :path-params [] + :query-params [] + :headers (api-key-header) + :auth nil + :body "{\"query\":\"query($id: String!){ team(id: $id){ activeCycle{ id number issues{ nodes{ id identifier title state{ name } assignee{ name } } } } } }\",\"variables\":{\"id\":\"team-1\"}}"} + (rest-linear/fetch-active-cycle-issues "team-1")))) + +(deftest requests-are-transport-agnostic + (reset-config (test-config)) + (doseq [request [(rest-linear/fetch-teams) + (rest-linear/fetch-issue "ABC-123") + (rest-linear/fetch-my-issues) + (rest-linear/fetch-team-issues "team-1") + (rest-linear/fetch-active-cycle-issues "team-1") + (rest-linear/create-issue "My title" "team-1") + (rest-linear/update-issue "issue-1" {:title "New title"})]] + (is (= abstract-keys (set (keys request))) + (str "request carries only abstract fields: " request))))