Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/lucky/routable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ module Lucky::Routable
end
{% end %}

# Define a route that responds to a WebSocket request
macro ws(path, &block)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would need wss too

{% unless path.starts_with?("/") %}
{% path.raise "Path must start with a slash. Example: '/#{path}'" %}
{% end %}

{% if block.args.size != 1 %}
{% raise "ws takes a block with 1 arg." %}
{% end %}

add_route({{method}}, {{ path }}, {{ @type.name.id }})
Comment thread
jwoertink marked this conversation as resolved.
Outdated

setup_ws_call_method(block)
end

# Define a route with a custom HTTP method.
#
# Use this method if you need to match a route with a custom HTTP method (verb).
Expand Down Expand Up @@ -85,6 +100,35 @@ module Lucky::Routable
setup_call_method({{ yield }})
end

# :nodoc:
macro setup_ws_call_method(&block)

abstract def on_message(message)
abstract def on_close

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought was by having these abstract, the websocket can proxy to them, and forces you to set these up. Websockets have a few other methods though.... are any of them as necessary? Is there ever a time you'd use a websocket and not use these?


def call(socket : Lucky::WebSocket)
Comment thread
jwoertink marked this conversation as resolved.
Outdated
# Ensure clients_desired_format is cached by calling it
clients_desired_format

%pipe_result = run_before_pipes

%response = if %pipe_result.is_a?(Lucky::Response)
%pipe_result
else
{{ block.args.first }} = socket
{{ block.body }}
end

%pipe_result = run_after_pipes

if %pipe_result.is_a?(Lucky::Response)
%pipe_result
else
%response
end
end
end

# :nodoc:
macro setup_call_method(body)
def call
Expand Down
8 changes: 8 additions & 0 deletions src/lucky/web_socket.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Lucky
class WebSocket < HTTP::WebSocketHandler
getter proc

Comment thread
jwoertink marked this conversation as resolved.
Outdated
def initialize(@action : Lucky::Action.class, &@proc : HTTP::WebSocket, HTTP::Server::Context -> Void)
Comment thread
jwoertink marked this conversation as resolved.
Outdated
end
end
end
19 changes: 19 additions & 0 deletions src/lucky/web_socket_action.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
abstract class Lucky::WebSocketAction < Lucky::Action
getter websocket : Lucky::WebSocket

def initialize(@context : HTTP::Server::Context, @route_params : Hash(String, String))
@websocket = Lucky::WebSocket.new(self.class) do |ws|
ws.on_message { |message| on_message(message) }
ws.on_close { on_close }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't make on_message and on_close abstract, then we can't guarantee they will exist which means we can't do this proxy. You'd just have to use them on your own. Not bad, but is this the better path?

call(ws)
end
Comment on lines +6 to +10

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would create a new handler for every websocket action you added. I'm not sure how many a person would add, but it seems like you should only have 1, and the handler would just route to each action based on where it connected. This looks like what Kemal seems to be doing too. A single instance that just adds each route and the handler.

end

abstract def call(socket : Lucky::WebSocket)

def call
raise <<-ERROR
WebSocketAction must define `call(socket)`
Comment thread
jwoertink marked this conversation as resolved.
Outdated
ERROR
end
end
26 changes: 26 additions & 0 deletions src/lucky/web_socket_handler.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class WebSocketHandler
Comment thread
jwoertink marked this conversation as resolved.
Outdated
include HTTP::Handler

def call(context : HTTP::Server::Context)
if ws_route_found?(context) && websocket_upgrade_request?(context)
websocket_action.payload.new(context, websocket_action.params).websocket.call(context)
Comment thread
jwoertink marked this conversation as resolved.
Outdated
else
call_next(context)
end
end

private def ws_route_found?(context)
!!websocket_action
end

memoize def websocket_action : LuckyRouter::Match(Lucky::Action.class)?
Lucky::Router.find_action(:ws, context.request.path)
end

private def websocket_upgrade_request?(context)
Comment thread
jwoertink marked this conversation as resolved.
Outdated
return unless upgrade = context.request.headers["Upgrade"]?
return unless upgrade.compare("websocket", case_insensitive: true) == 0

context.request.headers.includes_word?("Connection", "Upgrade")
end
end