summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-10-07 17:04:04 +0800
committereltrac <hi@eltr.ac>2026-10-07 17:04:04 +0800
commite6ca4cbecff78a0f26086178c48e8fc66914ab86 (patch)
tree489bd51e684a3972734aa5b79efef8413dc307bf
parent89d5bd3a26619e30dcf8b7382432180a399d25c9 (diff)
handler: pluggable services in API routes/handlers
-rw-r--r--src/weepinbell/webserver/facade.clj22
-rw-r--r--src/weepinbell/webserver/feed.clj2
-rw-r--r--src/weepinbell/webserver/handler.clj43
-rw-r--r--src/weepinbell/webserver/receiver.clj3
-rw-r--r--src/weepinbell/webserver/retriever.clj3
-rw-r--r--test/weepinbell/handler_test.clj29
6 files changed, 65 insertions, 37 deletions
diff --git a/src/weepinbell/webserver/facade.clj b/src/weepinbell/webserver/facade.clj
index ba9b8b0..23d5689 100644
--- a/src/weepinbell/webserver/facade.clj
+++ b/src/weepinbell/webserver/facade.clj
@@ -1,19 +1,33 @@
(ns weepinbell.webserver.facade
(:require [org.httpkit.server :refer [run-server]]
- [weepinbell.webserver.handler :refer [app]]
[taoensso.timbre :as timbre]
- [clojure.spec.alpha :as s]))
+ [clojure.spec.alpha :as s]
+ [weepinbell.webserver.handler :refer [app]]
+ ;; services
+ [weepinbell.webserver.receiver :as receiver]
+ [weepinbell.webserver.retriever :as retriever]
+ [weepinbell.webserver.feed :as feed]))
(s/def ::server-meta
(s/keys :req-un [::wmc ::store ::config]))
+(defn make-services []
+ {:post [#(s/valid? :weepinbell.webserver.handler/services %)]}
+ {:gen-feed feed/rss
+ :receive receiver/receive
+ :retrieve-by-domain retriever/by-domain
+ :retrieve-by-url retriever/by-url
+ :retrieve-by-id retriever/by-id
+ :retrieve-by-pair retriever/by-st-pair})
+
(defn start-webserver
"Starts web server. Returns a function that stops the server."
[metadata]
{:pre [(s/valid? ::server-meta metadata)]
:post [fn?]}
- (let [{:keys [wmc store config]} metadata
+ (let [{:keys [config]} metadata
+ api-meta (assoc metadata :services (make-services))
port (Integer/parseInt (or (:port config) (System/getenv "PORT") "3000"))]
(timbre/info "starting web server, listening on" port)
- (run-server (app wmc store config) {:port port})))
+ (run-server (app api-meta) {:port port})))
diff --git a/src/weepinbell/webserver/feed.clj b/src/weepinbell/webserver/feed.clj
index 12a39ec..7a78a9a 100644
--- a/src/weepinbell/webserver/feed.clj
+++ b/src/weepinbell/webserver/feed.clj
@@ -1,5 +1,5 @@
(ns weepinbell.webserver.feed
- "Handler of route /feed/:domain. Generating RSS feed for each domain."
+ "Service generating RSS feed for each domain."
(:require [clj-rss.core :as rss]
[weepinbell.utils.url :refer [get-domain]]
[weepinbell.utils.digest :refer [digest]])
diff --git a/src/weepinbell/webserver/handler.clj b/src/weepinbell/webserver/handler.clj
index b6fac9e..b1d6990 100644
--- a/src/weepinbell/webserver/handler.clj
+++ b/src/weepinbell/webserver/handler.clj
@@ -1,20 +1,31 @@
(ns weepinbell.webserver.handler
"Handler define routes and wrap responses. It pass request to other specific handlers.
Other handlers are receiver, retriever and feed."
- (:require [compojure.core :refer [routes POST GET context]]
+ (:require [clojure.spec.alpha :as s]
+ [compojure.core :refer [routes POST GET context]]
[compojure.route :as route]
[ring.middleware.defaults :refer [wrap-defaults api-defaults]]
[ring.middleware.resource :refer [wrap-resource]]
- [ring.middleware.json :refer [wrap-json-response]]
- [weepinbell.webserver.receiver :refer [receive]]
- [weepinbell.webserver.retriever :as retriever]
- [weepinbell.webserver.feed :as feed]))
+ [ring.middleware.json :refer [wrap-json-response]]))
+
+(s/def ::services
+ (s/and (s/keys :req-un [::receive ::retrieve-by-url ::retrieve-by-domain
+ ::retrieve-by-id ::retrieve-by-pair ::gen-feed])
+ #(every? fn? (vals %))))
+(s/def ::api-meta
+ (s/keys :req-un [::wmc ::store ::config ::services]))
(defn- make-api
"Receives webmention channel, storage vtable and config map.
Returns a vector of compojure route handlers. Some may be nil."
- [wmc storage config]
- [;; receiver endpoint
+ [api-meta]
+ {:pre [(s/valid? ::api-meta api-meta)]
+ :post [sequential?]}
+ (let [{:keys [wmc store config services]} api-meta
+ {:keys [receive retrieve-by-url retrieve-by-domain
+ retrieve-by-id retrieve-by-pair
+ gen-feed]} services]
+ [;; receiver endpoint
(POST (:endpoint config) [source target]
(receive {:s source :t target} wmc config))
;; query API routes
@@ -22,21 +33,21 @@
;; query webmention list
(context "/mentions" [page microformats mftype]
(GET "/" [url]
- (retriever/by-url url page storage
+ (retrieve-by-url url page store
{:microformats microformats
:type mftype}))
(GET "/:domain" [domain]
- (retriever/by-domain domain page storage
+ (retrieve-by-domain domain page store
{:microformats microformats
:type mftype})))
;; query one specific webmention
(context "/mention" [microformats]
- (GET "/:id" [id] (retriever/by-id id storage
+ (GET "/:id" [id] (retrieve-by-id id store
{:microformats microformats}))
- (GET "/" [source target] (retriever/by-st-pair source target storage
+ (GET "/" [source target] (retrieve-by-pair source target store
{:microformats microformats}))))
;; RSS feed
- (GET "/feed/:domain" [domain] (feed/rss domain storage config))
+ (GET "/feed/:domain" [domain] (gen-feed domain store config))
;; host proxied avatars (microformats photos)
(route/files "/avatars/" {:root "data/avatars"})
;; frontend
@@ -44,7 +55,7 @@
(when (= (:source fe) :disk)
(route/files "/" {:root (:dir fe)})))
;; 404
- (route/not-found "Not Found")])
+ (route/not-found "Not Found")]))
(defn- make-handler
"Receive a vector of handlers. Remove nil elements.
@@ -55,11 +66,11 @@
(defn app
"Receive webmention channel, storage vtable and config map.
Returns an app handler."
- [wmc storage config]
- (when (= :clojure.spec.alpha/invalid storage)
+ [api-meta]
+ (when (= :clojure.spec.alpha/invalid (:store api-meta))
(println "Invalid storage. Check your database configuration or submit an issue.")
(System/exit 1))
- (-> (make-api wmc storage config)
+ (-> (make-api api-meta)
make-handler
(wrap-resource "static")
(wrap-defaults api-defaults)
diff --git a/src/weepinbell/webserver/receiver.clj b/src/weepinbell/webserver/receiver.clj
index 144f3db..9d14a10 100644
--- a/src/weepinbell/webserver/receiver.clj
+++ b/src/weepinbell/webserver/receiver.clj
@@ -1,6 +1,5 @@
(ns weepinbell.webserver.receiver
- "Handler of route /endpoint or any other route the user configured.
- Receiver receives Webmention request (a source and a target); verify the request; return error message if any; push Webmention into webbmention channel for processor to take."
+ "Service receiving Webmention request (a source and a target); verify the request; return error message if any; push Webmention into webbmention channel for processor to take."
(:require [clojure.core.async :refer [put!]]
[weepinbell.utils.url :refer [get-domain valid-http-url? normalize url=]]
[weepinbell.utils.digest :refer [digest]]))
diff --git a/src/weepinbell/webserver/retriever.clj b/src/weepinbell/webserver/retriever.clj
index a8c175d..b597daf 100644
--- a/src/weepinbell/webserver/retriever.clj
+++ b/src/weepinbell/webserver/retriever.clj
@@ -1,6 +1,5 @@
(ns weepinbell.webserver.retriever
- "Handler of routes /api/mention/:id, /api/mentions/...
- Retriever gets webmention form storage and wraps it in a response."
+ "Service getting webmention form storage and wrapping it in a response."
(:require [weepinbell.utils.url :refer [get-domain url=]]
[weepinbell.microformats.reducer :refer [reduce-mf]]))
diff --git a/test/weepinbell/handler_test.clj b/test/weepinbell/handler_test.clj
index a59dd5e..0e46a22 100644
--- a/test/weepinbell/handler_test.clj
+++ b/test/weepinbell/handler_test.clj
@@ -1,12 +1,17 @@
(ns weepinbell.handler-test
(:require [clojure.test :refer [testing is deftest]]
[ring.mock.request :as mock]
- [weepinbell.handler :refer [app]]
+ [weepinbell.webserver.handler :refer [app]]
+ [weepinbell.webserver.facade :refer [make-services]]
[cheshire.core :refer [parse-string]]))
+(defn test-app [wmc store config]
+ (app {:wmc wmc :store store :config config
+ :services (make-services)}))
+
(deftest api-routes-test
(testing "/api/mentions/:domain route"
- (let [response ((app nil {:get-by-domain (fn [_]
+ (let [response ((test-app nil {:get-by-domain (fn [_]
[{:webmention {:source ""
:target ""
:id ""
@@ -17,7 +22,7 @@
(is (some? (:body response)))))
(testing "/api/mentions?url= route"
- (let [response ((app nil
+ (let [response ((test-app nil
{:get-by-domain
(fn [_]
[{:webmention {:source "https://source.net"
@@ -33,7 +38,7 @@
(is (-> response :body parse-string first (get "webmention") some?)))))
(testing "/api/mention/:id"
- (let [response ((app nil {:get (fn [_]
+ (let [response ((test-app nil {:get (fn [_]
{:webmention {:source ""
:target ""
:id "xxx"
@@ -43,7 +48,7 @@
(is (= (:status response) 200))))
(testing "/api/mention/?source=&target="
- (let [response ((app nil {:get (fn [_ _]
+ (let [response ((test-app nil {:get (fn [_ _]
{:webmention {:source "1"
:target "2"
:id "xxx"
@@ -54,34 +59,34 @@
(deftest feed-route-test
(testing "RSS feed for allowed domains and has items"
- (let [response ((app nil {:get-by-domain (fn [_] [{:webmention nil}])}
+ (let [response ((test-app nil {:get-by-domain (fn [_] [{:webmention nil}])}
{:allowed-target-domains "valid.com"})
(mock/request :get "/feed/valid.com"))]
(is (= (:status response) 404))))
(testing "RSS feed for allowed domains, but with no webmentions"
- (let [response ((app nil nil {:allowed-target-domains "valid.com"})
+ (let [response ((test-app nil nil {:allowed-target-domains "valid.com"})
(mock/request :get "/feed/valid.com"))]
(is (= (:status response) 404))))
(testing "not found for disallowed domains"
- (let [response ((app nil nil {}) (mock/request :get "/feed/invalid.com"))]
+ (let [response ((test-app nil nil {}) (mock/request :get "/feed/invalid.com"))]
(is (= (:status response) 404)))))
(deftest not-found-route-test
(testing "not-found route"
- (let [response ((app nil nil {}) (mock/request :get "/invalid"))]
+ (let [response ((test-app nil nil {}) (mock/request :get "/invalid"))]
(is (= (:status response) 404))))
(testing "root route is not found when frontend is disabled"
- (let [response ((app nil nil {:frontend nil}) (mock/request :get "/"))]
+ (let [response ((test-app nil nil {:frontend nil}) (mock/request :get "/"))]
(is (= (:status response) 404)))))
(deftest frontend-route-test
(testing "serve frontend resources from disk when enabled"
(let [dir (System/getProperty "java.io.tmpdir")
- response ((app nil nil {:frontend {:source :disk :dir dir}})
+ response ((test-app nil nil {:frontend {:source :disk :dir dir}})
(mock/request :get "/"))]
(is (not= (:status response) 500))))
(testing "serve nothing when disabled"
- (let [response ((app nil nil {:frontend nil})
+ (let [response ((test-app nil nil {:frontend nil})
(mock/request :get "/"))]
(is (= (:status response) 404)))))