-
Notifications
You must be signed in to change notification settings - Fork 11
/
channel.cr
161 lines (142 loc) Β· 3.95 KB
/
channel.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
require "http/web_socket"
require "./endpoint"
# A websocket HTTP Channel.
#
# Channel instance is bind to a websocket instance, calling `#on_open`, `#on_message`,
# `#on_binary`, `#on_ping`, `#on_pong` and `#on_close` callbacks
# on according socket event. You are expected to re-define these methods.
#
# Channel includes the `Endpoint` module.
#
# ## Params
#
# Channel params can be defined with the `Endpoint.params` macro. The params are
# checked **before** the request is upgraded to a websocket, raising a default 400
# HTTP error if something is wrong.
#
# ## Errors
#
# Channel errors can be defined with the `Endpoint.errors` macro. They can be raised
# when the request is not upgraded yet (by overriding default `#call` method or in callbacks),
# or when it is already a websocket.
#
# Some considertations when raising when already upgraded:
#
# * Error codes must be in 4000-4999 range to conform with [standards](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Properties)
# * Error message length must be less than or equal to 123 characters
# * `HTTP::Error`s are rescued and handled internally in a `Channel`,
# properly closing the socket, so you do not need a rescuer there
#
# ## Example
#
# ```
# class Channels::Echo
# include Onyx::HTTP::Channel
#
# params do
# query do
# # Would raise 400 HTTP error before upgrading if username is missing
# type username : String
#
# # Would raise 400 HTTP error before upgrading if secret is missing or of invalid type
# type secret : Int32
# end
# end
#
# errors do
# # Expected to be raised before the request is upgraded
# type UsernameTaken(403)
#
# # Expected to be raised when the request is already upgraded to a websocket
# type InvalidSecret(4003)
# end
#
# before do
# # Return 403 HTTP error without upgrading to a websocket
# raise UsernameTaken.new if params.query.username == "Vlad"
# end
#
# def on_open
# unless params.query.secret == 42
# # Close websocket with 4003 code and "Invalid Secret" reason
# raise InvalidSecret.new
# end
# end
#
# def on_message(message)
# socket.send(message)
# end
# end
# ```
#
# Router example:
#
# ```
# router = Onyx::HTTP::Router.new do |r|
# r.ws "/", Channels::Echo
# # Equivalent of
# r.ws "/" do |context|
# channel.call(context)
# end
# end
# ```
module Onyx::HTTP::Channel
include Endpoint
# TODO: Refactor this mess.
protected def call
{% raise "An Onyx::HTTP::Channel must be `#call`'ed with WebSocket argument" %}
end
# Call `#bind`.
def call(socket)
bind(socket)
end
macro included
include Onyx::HTTP::Endpoint
def self.call(socket, context)
instance = new(context)
instance.with_callbacks { instance.call(socket) }
end
end
protected getter! socket : ::HTTP::WebSocket
# Called once when a new socket is opened.
protected def on_open
end
# Called when the socket receives a message from client.
protected def on_message(message)
end
# Called when the socket receives a binary message from client.
def on_binary(binary)
end
# Called when the socket receives a PING message from client.
# Sends `"PONG"` by default.
protected def on_ping
socket.send("PONG")
end
# Called when the socket receives a PONG message from client.
protected def on_pong
end
# Called once when the socket closes.
protected def on_close
end
# Call `#on_open` and bind to the `socket`'s events. Read more in [Crystal API docs](https://crystal-lang.org/api/latest/HTTP/WebSocket.html).
# Rescues errors, gracefully closing the websocket with according error code.
protected def bind(socket)
@socket = socket
on_open
socket.on_message do |message|
on_message(message)
end
socket.on_binary do |binary|
on_binary(binary)
end
socket.on_ping do
on_ping
end
socket.on_pong do
on_pong
end
socket.on_close do
on_close
end
end
end