Skip to content

Commit

Permalink
Move websocket protocols to separate library
Browse files Browse the repository at this point in the history
This allows adapters to support Ring websockets without requiring a
dependency on ring-core.
  • Loading branch information
weavejester committed Oct 12, 2023
1 parent 4a69cda commit a19c97b
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 96 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
key: cljdeps-${{ hashFiles('project.clj', 'ring-*/project.clj') }}
restore-keys: cljdeps-

- name: Install websocket protocols project locally
run: lein install
working-directory: ./ring-websocket-protocols

- name: Install core project locally
run: lein install
working-directory: ./ring-core
Expand Down
20 changes: 11 additions & 9 deletions SPEC-alpha.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ A websocket response contains the following keys. Any key not marked as

#### :ring.websocket/listener

An event listener that satisfies the `ring.websocket/Listener` protocol,
as described in section 3.2.
An event listener that satisfies the `ring.websocket.protocols/Listener`
protocol, as described in section 3.2.

#### :ring.websocket/protocol

Expand All @@ -264,8 +264,8 @@ the `Sec-Websocket-Protocol` header on the request.

### 3.2. Websocket Listeners

A websocket listener must satisfy the `ring.websocket/Listener`
protocol:
A websocket listener must satisfy the
`ring.websocket.protocols/Listener` protocol:

```clojure
(defprotocol Listener
Expand All @@ -276,7 +276,8 @@ protocol:
(on-close [listener socket code reason]))
```

It *may* optionally satisfy the `ring.websocket/PingListener` protocol:
It *may* optionally satisfy the `ring.websocket.protocols/PingListener`
protocol:

```clojure
(defprotocol PingListener
Expand All @@ -290,8 +291,8 @@ message that has the same data.
#### on-open

Called once when the websocket is first opened. Supplies a `socket`
argument that satisfies `ring.websocket/Socket`, described in section
3.3.
argument that satisfies `ring.websocket.protools/Socket`, described in
section 3.3.

#### on-message

Expand Down Expand Up @@ -328,7 +329,7 @@ an integer `code` and a string `reason` as arguments.

### 3.3. Websocket Sockets

A socket must satisfy the `ring.websocket/Socket` protocol:
A socket must satisfy the `ring.websocket.protocols/Socket` protocol:

```clojure
(defprotocol Socket
Expand All @@ -339,7 +340,8 @@ A socket must satisfy the `ring.websocket/Socket` protocol:
(-close [socket code reason]))
```

It *may* optionally satisfy the `ring.websocket/AsyncSocket` protocol:
It *may* optionally satisfy the `ring.websocket.protocols/AsyncSocket`
protocol:

```clojure
(defprotocol AsyncSocket
Expand Down
1 change: 1 addition & 0 deletions ring-core/project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:license {:name "The MIT License"
:url "http://opensource.org/licenses/MIT"}
:dependencies [[org.clojure/clojure "1.7.0"]
[org.ring-clojure/ring-websocket-protocols "1.11.0-alpha4"]
[ring/ring-codec "1.2.0"]
[commons-io "2.13.0"]
[org.apache.commons/commons-fileupload2-core "2.0.0-M1"]
Expand Down
74 changes: 13 additions & 61 deletions ring-core/src/ring/websocket.clj
Original file line number Diff line number Diff line change
@@ -1,37 +1,12 @@
(ns ring.websocket
"Protocols and utility functions for websocket support."
(:refer-clojure :exclude [send])
(:require [clojure.string :as str])
(:require [clojure.string :as str]
[ring.websocket.protocols :as p])
(:import [java.nio ByteBuffer]))

(defprotocol Listener
"A protocol for handling websocket events. The second argument is always an
object that satisfies the Socket protocol."
(on-open [listener socket]
"Called when the websocket is opened.")
(on-message [listener socket message]
"Called when a message is received. The message may be a String or a
ByteBuffer.")
(on-pong [listener socket data]
"Called when a pong is received in response to an earlier ping. The client
may provide additional binary data, represented by the data ByteBuffer.")
(on-error [listener socket throwable]
"Called when a Throwable error is thrown.")
(on-close [listener socket code reason]
"Called when the websocket is closed, along with an integer code and a
plaintext string reason for being closed."))

(defprotocol PingListener
"A protocol for handling ping websocket events. The second argument is always
always an object that satisfies the Socket protocol. This is separate from
the Listener protocol as some APIs (for example Jakarta) don't support
listening for ping events."
(on-ping [listener socket data]
"Called when a ping is received from the client. The client may provide
additional binary data, represented by the data ByteBuffer."))

(extend-type clojure.lang.IPersistentMap
Listener
p/Listener
(on-open [m socket]
(when-let [kv (find m :on-open)] ((val kv) socket)))
(on-message [m socket message]
Expand All @@ -42,33 +17,10 @@
(when-let [kv (find m :on-error)] ((val kv) socket throwable)))
(on-close [m socket code reason]
(when-let [kv (find m :on-close)] ((val kv) socket code reason)))
PingListener
p/PingListener
(on-ping [m socket data]
(when-let [kv (find m :on-ping)] ((val kv) socket data))))

(defprotocol Socket
"A protocol for sending data via websocket."
(-open? [socket]
"Returns true if the socket is open; false otherwise.")
(-send [socket message]
"Sends a String or ByteBuffer to the client via the websocket.")
(-ping [socket data]
"Sends a ping message to the client with a ByteBuffer of extra data.")
(-pong [socket data]
"Sends an unsolicited pong message to the client, with a ByteBuffer of extra
data.")
(-close [socket code reason]
"Closes the socket with an integer status code, and a String reason."))

(defprotocol AsyncSocket
"A protocol for sending data asynchronously via websocket. Intended for use
with the Socket protocol."
(-send-async [socket message succeed fail]
"Sends a String or ByteBuffer to the client via the websocket. If it
succeeds, the 'succeed' callback function is called with zero arguments. If
it fails, the 'fail' callback function is called with the exception that was
thrown."))

(defprotocol TextData
"A protocol for converting text data into a String."
(->char-sequence [data]
Expand Down Expand Up @@ -101,42 +53,42 @@
(defn open?
"Returns true if the Socket is open, false otherwise."
[socket]
(boolean (-open? socket)))
(boolean (p/-open? socket)))

(defn send
"Sends text or binary data via a websocket, either synchronously or
asynchronously with callback functions. A convenient wrapper for the -send and
-send-async protocol methods."
([socket message]
(-send socket (encode-message message)))
(p/-send socket (encode-message message)))
([socket message succeed fail]
(-send-async socket (encode-message message) succeed fail)))
(p/-send-async socket (encode-message message) succeed fail)))

(defn ping
"Sends a ping message via a websocket, with an optional byte array or
ByteBuffer that may contain custom session data. A convenient wrapper for the
-ping protocol method."
([socket]
(-ping socket (ByteBuffer/allocate 0)))
(p/-ping socket (ByteBuffer/allocate 0)))
([socket data]
(-ping socket (->byte-buffer data))))
(p/-ping socket (->byte-buffer data))))

(defn pong
"Sends an unsolicited pong message via a websocket, with an optional byte
array or ByteBuffer that may contain custom session data. A convenient wrapper
for the -pong protocol method."
([socket]
(-pong socket (ByteBuffer/allocate 0)))
(p/-pong socket (ByteBuffer/allocate 0)))
([socket data]
(-pong socket (->byte-buffer data))))
(p/-pong socket (->byte-buffer data))))

(defn close
"Closes the websocket, with an optional custom integer status code and reason
string."
([socket]
(-close socket 1000 "Normal Closure"))
(p/-close socket 1000 "Normal Closure"))
([socket code reason]
(-close socket code reason)))
(p/-close socket code reason)))

(defn upgrade-request?
"Returns true if the request map is a websocket upgrade request."
Expand Down
23 changes: 12 additions & 11 deletions ring-jetty-adapter/src/ring/adapter/jetty.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
Adapters are used to convert Ring handlers into running web servers."
(:require [ring.util.jakarta.servlet :as servlet]
[ring.websocket :as ws])
[ring.websocket :as ws]
[ring.websocket.protocols :as wsp])
(:import [java.nio ByteBuffer]
[java.util ArrayList]
[org.eclipse.jetty.server
Expand Down Expand Up @@ -39,7 +40,7 @@
(defn- websocket-socket [^Session session]
(let [remote (.getRemote session)]
(reify
ws/Socket
wsp/Socket
(-open? [_]
(.isOpen session))
(-send [_ message]
Expand All @@ -52,7 +53,7 @@
(.sendPong remote data))
(-close [_ status reason]
(.close session status reason))
ws/AsyncSocket
wsp/AsyncSocket
(-send-async [_ message succeed fail]
(let [callback (reify WriteCallback
(writeSuccess [_] (succeed))
Expand All @@ -67,24 +68,24 @@
WebSocketConnectionListener
(onWebSocketConnect [_ session]
(vreset! socket (websocket-socket session))
(ws/on-open listener @socket))
(wsp/on-open listener @socket))
(onWebSocketClose [_ status reason]
(ws/on-close listener @socket status reason))
(wsp/on-close listener @socket status reason))
(onWebSocketError [_ throwable]
(ws/on-error listener @socket throwable))
(wsp/on-error listener @socket throwable))
WebSocketListener
(onWebSocketText [_ message]
(ws/on-message listener @socket message))
(wsp/on-message listener @socket message))
(onWebSocketBinary [_ payload offset length]
(let [buffer (ByteBuffer/wrap payload offset length)]
(ws/on-message listener @socket buffer)))
(wsp/on-message listener @socket buffer)))
WebSocketPingPongListener
(onWebSocketPing [_ payload]
(if (satisfies? ws/PingListener listener)
(ws/on-ping listener @socket payload)
(if (satisfies? wsp/PingListener listener)
(wsp/on-ping listener @socket payload)
(ws/pong @socket payload)))
(onWebSocketPong [_ payload]
(ws/on-pong listener @socket payload)))))
(wsp/on-pong listener @socket payload)))))

(defn- ^JettyWebSocketCreator websocket-creator
[{:keys [::ws/listener ::ws/protocol]}]
Expand Down
31 changes: 16 additions & 15 deletions ring-jetty-adapter/test/ring/adapter/test/jetty.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
[hato.websocket :as hato]
[less.awful.ssl :as less-ssl]
[ring.core.protocols :as p]
[ring.websocket :as ws])
[ring.websocket :as ws]
[ring.websocket.protocols :as wsp])
(:import [java.nio ByteBuffer]
[org.eclipse.jetty.util.thread QueuedThreadPool]
[org.eclipse.jetty.util BlockingArrayQueue]
Expand Down Expand Up @@ -644,7 +645,7 @@
(let [log (atom [])
handler (constantly
{::ws/listener
(reify ws/Listener
(reify wsp/Listener
(on-open [_ _] (swap! log conj [:open]))
(on-message [_ _ msg] (swap! log conj [:message msg]))
(on-pong [_ _ data]
Expand All @@ -668,7 +669,7 @@
(let [log (atom [])
handler (constantly
{::ws/listener
(reify ws/Listener
(reify wsp/Listener
(on-open [_ sock]
(ws/send sock "Hello")
(ws/send sock (.getBytes "World")))
Expand Down Expand Up @@ -701,7 +702,7 @@
(let [log (atom [])
handler (constantly
{::ws/listener
(reify ws/Listener
(reify wsp/Listener
(on-open [_ sock]
(ws/ping sock (ByteBuffer/wrap (.getBytes "foo")))
(swap! log conj [:ping "foo"]))
Expand All @@ -722,7 +723,7 @@
(let [log (atom [])
handler (constantly
{::ws/listener
(reify ws/Listener
(reify wsp/Listener
(on-open [_ _])
(on-message [_ _ _])
(on-pong [_ _ _])
Expand All @@ -743,13 +744,13 @@
(let [log (atom [])
handler (constantly
{::ws/listener
(reify ws/Listener
(reify wsp/Listener
(on-open [_ _])
(on-message [_ _ _])
(on-pong [_ _ _])
(on-error [_ _ _])
(on-close [_ _ _ _])
ws/PingListener
wsp/PingListener
(on-ping [_ sock data]
(ws/pong sock data)
(swap! log conj [:ping (buf->str data)])))})]
Expand All @@ -769,7 +770,7 @@
(let [log (atom [])
handler (constantly
{::ws/listener
(reify ws/Listener
(reify wsp/Listener
(on-open [_ sock]
(swap! log conj [:open? (ws/open? sock)])
(ws/close sock)
Expand All @@ -790,7 +791,7 @@
handler (constantly
{::ws/protocol "mqtt"
::ws/listener
(reify ws/Listener
(reify wsp/Listener
(on-open [_ _])
(on-message [_ _ _])
(on-pong [_ _ _])
Expand All @@ -807,7 +808,7 @@
(let [log (atom [])
handler (constantly
{::ws/listener
(reify ws/Listener
(reify wsp/Listener
(on-open [_ sock]
(ws/send sock "Hello"
(fn [] (ws/send sock "World" (fn []) (fn [_])))
Expand All @@ -832,12 +833,12 @@
:on-error (fn [s e] [:on-error s e])
:on-close (fn [s c r] [:on-close s c r])}]
(is (= [:on-open :sock]
(ws/on-open listener :sock)))
(wsp/on-open listener :sock)))
(is (= [:on-message :sock "foo"]
(ws/on-message listener :sock "foo")))
(wsp/on-message listener :sock "foo")))
(is (= [:on-pong :sock "data"]
(ws/on-pong listener :sock "data")))
(wsp/on-pong listener :sock "data")))
(is (= [:on-error :sock "err"]
(ws/on-error listener :sock "err")))
(wsp/on-error listener :sock "err")))
(is (= [:on-close :sock 1000 "closed"]
(ws/on-close listener :sock 1000 "closed"))))))
(wsp/on-close listener :sock 1000 "closed"))))))
9 changes: 9 additions & 0 deletions ring-websocket-protocols/project.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
(defproject org.ring-clojure/ring-websocket-protocols "1.11.0-alpha4"
:description "Ring protocols for websockets."
:url "https://github.com/ring-clojure/ring"
:scm {:dir ".."}
:license {:name "The MIT License"
:url "http://opensource.org/licenses/MIT"}
:dependencies []
:profiles
{:dev {:dependencies [[org.clojure/clojure "1.7.0"]]}})
Loading

0 comments on commit a19c97b

Please sign in to comment.